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.
package/dist/index.mjs CHANGED
@@ -26,6 +26,16 @@ function parseConfig(configContent) {
26
26
  rootFolderId: parsed.google_drive.root_folder_id || process.env.HAZO_GOOGLE_DRIVE_ROOT_FOLDER_ID
27
27
  };
28
28
  }
29
+ if (parsed.dropbox) {
30
+ config.dropbox = {
31
+ clientId: parsed.dropbox.client_id || process.env.HAZO_DROPBOX_CLIENT_ID || "",
32
+ clientSecret: parsed.dropbox.client_secret || process.env.HAZO_DROPBOX_CLIENT_SECRET || "",
33
+ redirectUri: parsed.dropbox.redirect_uri || process.env.HAZO_DROPBOX_REDIRECT_URI || "",
34
+ refreshToken: parsed.dropbox.refresh_token || process.env.HAZO_DROPBOX_REFRESH_TOKEN,
35
+ accessToken: parsed.dropbox.access_token || process.env.HAZO_DROPBOX_ACCESS_TOKEN,
36
+ rootPath: parsed.dropbox.root_path || process.env.HAZO_DROPBOX_ROOT_PATH
37
+ };
38
+ }
29
39
  return config;
30
40
  }
31
41
  function loadConfig(configPath) {
@@ -87,6 +97,18 @@ refresh_token =
87
97
  access_token =
88
98
  ; Optional: Root folder ID to use as base (empty = root of Drive)
89
99
  root_folder_id =
100
+
101
+ [dropbox]
102
+ ; Dropbox OAuth credentials
103
+ ; These can also be set via environment variables:
104
+ ; HAZO_DROPBOX_CLIENT_ID, HAZO_DROPBOX_CLIENT_SECRET, etc.
105
+ client_id =
106
+ client_secret =
107
+ redirect_uri = http://localhost:3000/api/auth/dropbox/callback
108
+ refresh_token =
109
+ access_token =
110
+ ; Optional: Root path to use as base (empty = root of Dropbox)
111
+ root_path =
90
112
  `;
91
113
  }
92
114
  async function saveConfig(config, configPath) {
@@ -113,6 +135,16 @@ async function saveConfig(config, configPath) {
113
135
  root_folder_id: config.google_drive.rootFolderId || ""
114
136
  };
115
137
  }
138
+ if (config.dropbox) {
139
+ iniConfig.dropbox = {
140
+ client_id: config.dropbox.clientId || "",
141
+ client_secret: config.dropbox.clientSecret || "",
142
+ redirect_uri: config.dropbox.redirectUri || "",
143
+ refresh_token: config.dropbox.refreshToken || "",
144
+ access_token: config.dropbox.accessToken || "",
145
+ root_path: config.dropbox.rootPath || ""
146
+ };
147
+ }
116
148
  const content = ini.stringify(iniConfig);
117
149
  await fs.promises.writeFile(resolvedPath, content, "utf-8");
118
150
  }
@@ -1650,10 +1682,641 @@ function createGoogleDriveModule() {
1650
1682
  return new GoogleDriveModule();
1651
1683
  }
1652
1684
 
1685
+ // src/modules/dropbox/index.ts
1686
+ import { Dropbox } from "dropbox";
1687
+
1688
+ // src/modules/dropbox/auth.ts
1689
+ var DROPBOX_AUTH_URL = "https://www.dropbox.com/oauth2/authorize";
1690
+ var DROPBOX_TOKEN_URL = "https://api.dropboxapi.com/oauth2/token";
1691
+ var DROPBOX_REVOKE_URL = "https://api.dropboxapi.com/2/auth/token/revoke";
1692
+ var DropboxAuth = class {
1693
+ constructor(config, callbacks = {}) {
1694
+ this.tokens = null;
1695
+ this.config = config;
1696
+ this.callbacks = callbacks;
1697
+ }
1698
+ /**
1699
+ * Generate the authorization URL for OAuth consent
1700
+ */
1701
+ getAuthUrl(state) {
1702
+ const params = new URLSearchParams({
1703
+ client_id: this.config.clientId,
1704
+ redirect_uri: this.config.redirectUri,
1705
+ response_type: "code",
1706
+ token_access_type: "offline"
1707
+ });
1708
+ if (state) {
1709
+ params.set("state", state);
1710
+ }
1711
+ return `${DROPBOX_AUTH_URL}?${params.toString()}`;
1712
+ }
1713
+ /**
1714
+ * Exchange authorization code for tokens
1715
+ */
1716
+ async exchangeCodeForTokens(code) {
1717
+ const response = await fetch(DROPBOX_TOKEN_URL, {
1718
+ method: "POST",
1719
+ headers: {
1720
+ "Content-Type": "application/x-www-form-urlencoded"
1721
+ },
1722
+ body: new URLSearchParams({
1723
+ code,
1724
+ grant_type: "authorization_code",
1725
+ client_id: this.config.clientId,
1726
+ client_secret: this.config.clientSecret,
1727
+ redirect_uri: this.config.redirectUri
1728
+ })
1729
+ });
1730
+ if (!response.ok) {
1731
+ const errorData = await response.text();
1732
+ throw new Error(`Failed to exchange code for tokens: ${errorData}`);
1733
+ }
1734
+ const data = await response.json();
1735
+ this.tokens = {
1736
+ accessToken: data.access_token,
1737
+ refreshToken: data.refresh_token,
1738
+ expiryDate: data.expires_in ? Date.now() + data.expires_in * 1e3 : void 0
1739
+ };
1740
+ if (this.callbacks.onTokensUpdated) {
1741
+ await this.callbacks.onTokensUpdated(this.tokens);
1742
+ }
1743
+ return this.tokens;
1744
+ }
1745
+ /**
1746
+ * Set tokens directly (e.g., from stored tokens)
1747
+ */
1748
+ async setTokens(tokens) {
1749
+ this.tokens = tokens;
1750
+ }
1751
+ /**
1752
+ * Load tokens from storage using callback
1753
+ */
1754
+ async loadStoredTokens() {
1755
+ if (!this.callbacks.getStoredTokens) {
1756
+ return false;
1757
+ }
1758
+ const tokens = await this.callbacks.getStoredTokens();
1759
+ if (tokens) {
1760
+ await this.setTokens(tokens);
1761
+ return true;
1762
+ }
1763
+ return false;
1764
+ }
1765
+ /**
1766
+ * Check if authenticated
1767
+ */
1768
+ isAuthenticated() {
1769
+ return this.tokens !== null && !!this.tokens.accessToken;
1770
+ }
1771
+ /**
1772
+ * Get current tokens
1773
+ */
1774
+ getTokens() {
1775
+ return this.tokens;
1776
+ }
1777
+ /**
1778
+ * Get current access token
1779
+ */
1780
+ getAccessToken() {
1781
+ return this.tokens?.accessToken ?? null;
1782
+ }
1783
+ /**
1784
+ * Refresh the access token
1785
+ */
1786
+ async refreshAccessToken() {
1787
+ if (!this.tokens?.refreshToken) {
1788
+ throw new Error("No refresh token available");
1789
+ }
1790
+ const response = await fetch(DROPBOX_TOKEN_URL, {
1791
+ method: "POST",
1792
+ headers: {
1793
+ "Content-Type": "application/x-www-form-urlencoded"
1794
+ },
1795
+ body: new URLSearchParams({
1796
+ grant_type: "refresh_token",
1797
+ refresh_token: this.tokens.refreshToken,
1798
+ client_id: this.config.clientId,
1799
+ client_secret: this.config.clientSecret
1800
+ })
1801
+ });
1802
+ if (!response.ok) {
1803
+ const errorData = await response.text();
1804
+ throw new Error(`Failed to refresh token: ${errorData}`);
1805
+ }
1806
+ const data = await response.json();
1807
+ this.tokens = {
1808
+ ...this.tokens,
1809
+ accessToken: data.access_token,
1810
+ expiryDate: data.expires_in ? Date.now() + data.expires_in * 1e3 : void 0
1811
+ };
1812
+ if (this.callbacks.onTokensUpdated) {
1813
+ await this.callbacks.onTokensUpdated(this.tokens);
1814
+ }
1815
+ return this.tokens;
1816
+ }
1817
+ /**
1818
+ * Revoke access (disconnect)
1819
+ */
1820
+ async revokeAccess() {
1821
+ if (this.tokens?.accessToken) {
1822
+ try {
1823
+ await fetch(DROPBOX_REVOKE_URL, {
1824
+ method: "POST",
1825
+ headers: {
1826
+ Authorization: `Bearer ${this.tokens.accessToken}`
1827
+ }
1828
+ });
1829
+ } catch {
1830
+ }
1831
+ }
1832
+ this.tokens = null;
1833
+ }
1834
+ /**
1835
+ * Check if token is expired or will expire soon
1836
+ */
1837
+ isTokenExpired(bufferSeconds = 300) {
1838
+ if (!this.tokens?.expiryDate) {
1839
+ return false;
1840
+ }
1841
+ const now = Date.now();
1842
+ const expiry = this.tokens.expiryDate;
1843
+ return now >= expiry - bufferSeconds * 1e3;
1844
+ }
1845
+ /**
1846
+ * Ensure valid access token (refresh if needed)
1847
+ */
1848
+ async ensureValidToken() {
1849
+ if (this.isTokenExpired()) {
1850
+ await this.refreshAccessToken();
1851
+ }
1852
+ }
1853
+ };
1854
+ function createDropboxAuth(config, callbacks) {
1855
+ return new DropboxAuth(config, callbacks);
1856
+ }
1857
+
1858
+ // src/modules/dropbox/index.ts
1859
+ var MAX_UPLOAD_SIZE = 150 * 1024 * 1024;
1860
+ var DropboxModule = class extends BaseStorageModule {
1861
+ constructor() {
1862
+ super(...arguments);
1863
+ this.provider = "dropbox";
1864
+ this.auth = null;
1865
+ this.dbx = null;
1866
+ this.rootPath = "";
1867
+ this.authCallbacks = {};
1868
+ }
1869
+ /**
1870
+ * Set authentication callbacks for token persistence
1871
+ */
1872
+ setAuthCallbacks(callbacks) {
1873
+ this.authCallbacks = callbacks;
1874
+ }
1875
+ async initialize(config) {
1876
+ await super.initialize(config);
1877
+ const dropboxConfig = this.getProviderConfig();
1878
+ if (!dropboxConfig.clientId || !dropboxConfig.clientSecret) {
1879
+ throw new AuthenticationError("dropbox", "Missing client ID or client secret");
1880
+ }
1881
+ this.auth = createDropboxAuth(
1882
+ {
1883
+ clientId: dropboxConfig.clientId,
1884
+ clientSecret: dropboxConfig.clientSecret,
1885
+ redirectUri: dropboxConfig.redirectUri
1886
+ },
1887
+ this.authCallbacks
1888
+ );
1889
+ if (dropboxConfig.refreshToken || dropboxConfig.accessToken) {
1890
+ await this.auth.setTokens({
1891
+ accessToken: dropboxConfig.accessToken || "",
1892
+ refreshToken: dropboxConfig.refreshToken || ""
1893
+ });
1894
+ }
1895
+ this.rootPath = dropboxConfig.rootPath || "";
1896
+ this.createDropboxClient();
1897
+ }
1898
+ /**
1899
+ * Create/recreate the Dropbox SDK client with the current access token
1900
+ */
1901
+ createDropboxClient() {
1902
+ const accessToken = this.auth?.getAccessToken();
1903
+ if (accessToken) {
1904
+ this.dbx = new Dropbox({ accessToken });
1905
+ }
1906
+ }
1907
+ /**
1908
+ * Get the auth instance for OAuth flow
1909
+ */
1910
+ getAuth() {
1911
+ if (!this.auth) {
1912
+ throw new AuthenticationError("dropbox", "Module not initialized");
1913
+ }
1914
+ return this.auth;
1915
+ }
1916
+ /**
1917
+ * Check if user is authenticated
1918
+ */
1919
+ isAuthenticated() {
1920
+ return this.auth?.isAuthenticated() ?? false;
1921
+ }
1922
+ /**
1923
+ * Authenticate with provided tokens
1924
+ */
1925
+ async authenticate(tokens) {
1926
+ if (!this.auth) {
1927
+ throw new AuthenticationError("dropbox", "Module not initialized");
1928
+ }
1929
+ await this.auth.setTokens(tokens);
1930
+ this.createDropboxClient();
1931
+ }
1932
+ /**
1933
+ * Ensure authenticated before operations
1934
+ */
1935
+ async ensureAuthenticated() {
1936
+ this.ensureInitialized();
1937
+ if (!this.isAuthenticated()) {
1938
+ throw new AuthenticationError("dropbox", "Not authenticated. Please connect your Dropbox.");
1939
+ }
1940
+ await this.auth.ensureValidToken();
1941
+ this.createDropboxClient();
1942
+ }
1943
+ /**
1944
+ * Convert virtual path to Dropbox path
1945
+ * Virtual: /folder/file.txt -> Dropbox: /folder/file.txt (or /rootPath/folder/file.txt)
1946
+ * Dropbox root is empty string "", not "/"
1947
+ */
1948
+ toDropboxPath(virtualPath) {
1949
+ const normalized = this.normalizePath(virtualPath);
1950
+ if (this.rootPath) {
1951
+ if (normalized === "/") {
1952
+ return `/${this.rootPath}`;
1953
+ }
1954
+ return `/${this.rootPath}${normalized}`;
1955
+ }
1956
+ if (normalized === "/") {
1957
+ return "";
1958
+ }
1959
+ return normalized;
1960
+ }
1961
+ /**
1962
+ * Convert Dropbox metadata to FileSystemItem
1963
+ */
1964
+ metadataToItem(entry, virtualPath) {
1965
+ const isFolder2 = entry[".tag"] === "folder";
1966
+ const path3 = virtualPath || this.toVirtualPath(entry.path_display || entry.name);
1967
+ if (isFolder2) {
1968
+ return createFolderItem({
1969
+ id: entry.id,
1970
+ name: entry.name,
1971
+ path: path3,
1972
+ createdAt: /* @__PURE__ */ new Date(),
1973
+ modifiedAt: /* @__PURE__ */ new Date(),
1974
+ metadata: {
1975
+ dropboxId: entry.id,
1976
+ pathDisplay: entry.path_display
1977
+ }
1978
+ });
1979
+ }
1980
+ const fileEntry = entry;
1981
+ return createFileItem({
1982
+ id: fileEntry.id,
1983
+ name: fileEntry.name,
1984
+ path: path3,
1985
+ size: fileEntry.size,
1986
+ mimeType: getMimeType(fileEntry.name),
1987
+ createdAt: new Date(fileEntry.client_modified),
1988
+ modifiedAt: new Date(fileEntry.server_modified),
1989
+ metadata: {
1990
+ dropboxId: fileEntry.id,
1991
+ pathDisplay: fileEntry.path_display
1992
+ }
1993
+ });
1994
+ }
1995
+ /**
1996
+ * Convert a Dropbox path_display to virtual path
1997
+ */
1998
+ toVirtualPath(dropboxPath) {
1999
+ if (this.rootPath && dropboxPath.toLowerCase().startsWith(`/${this.rootPath.toLowerCase()}`)) {
2000
+ const stripped = dropboxPath.substring(this.rootPath.length + 1);
2001
+ return stripped || "/";
2002
+ }
2003
+ return dropboxPath || "/";
2004
+ }
2005
+ async createDirectory(virtualPath) {
2006
+ try {
2007
+ await this.ensureAuthenticated();
2008
+ const dbxPath = this.toDropboxPath(virtualPath);
2009
+ const response = await this.dbx.filesCreateFolderV2({
2010
+ path: dbxPath,
2011
+ autorename: false
2012
+ });
2013
+ const metadata = response.result.metadata;
2014
+ const item = this.metadataToItem(
2015
+ { ".tag": "folder", id: metadata.id, name: metadata.name, path_display: metadata.path_display, path_lower: metadata.path_lower },
2016
+ this.normalizePath(virtualPath)
2017
+ );
2018
+ return this.successResult(item);
2019
+ } catch (error) {
2020
+ if (error instanceof AuthenticationError) {
2021
+ return this.errorResult(error.message);
2022
+ }
2023
+ const errMsg = error.message || String(error);
2024
+ if (errMsg.includes("path/conflict")) {
2025
+ return this.errorResult(`Directory already exists: ${virtualPath}`);
2026
+ }
2027
+ return this.errorResult(`Failed to create directory: ${errMsg}`);
2028
+ }
2029
+ }
2030
+ async removeDirectory(virtualPath, recursive = false) {
2031
+ try {
2032
+ await this.ensureAuthenticated();
2033
+ const dbxPath = this.toDropboxPath(virtualPath);
2034
+ if (!recursive) {
2035
+ const listResponse = await this.dbx.filesListFolder({
2036
+ path: dbxPath,
2037
+ limit: 1
2038
+ });
2039
+ if (listResponse.result.entries.length > 0) {
2040
+ return this.errorResult(`Directory is not empty: ${virtualPath}`);
2041
+ }
2042
+ }
2043
+ await this.dbx.filesDeleteV2({ path: dbxPath });
2044
+ return this.successResult();
2045
+ } catch (error) {
2046
+ const errMsg = error.message || String(error);
2047
+ if (errMsg.includes("path_lookup/not_found") || errMsg.includes("not_found")) {
2048
+ return this.errorResult(`Directory not found: ${virtualPath}`);
2049
+ }
2050
+ return this.errorResult(`Failed to remove directory: ${errMsg}`);
2051
+ }
2052
+ }
2053
+ async uploadFile(source, remotePath, options = {}) {
2054
+ try {
2055
+ await this.ensureAuthenticated();
2056
+ const normalized = this.normalizePath(remotePath);
2057
+ const dbxPath = this.toDropboxPath(remotePath);
2058
+ let contents;
2059
+ if (typeof source === "string") {
2060
+ const fs3 = await import("fs");
2061
+ contents = await fs3.promises.readFile(source);
2062
+ } else if (Buffer.isBuffer(source)) {
2063
+ contents = source;
2064
+ } else {
2065
+ const chunks = [];
2066
+ const reader = source.getReader();
2067
+ let done = false;
2068
+ while (!done) {
2069
+ const result = await reader.read();
2070
+ done = result.done;
2071
+ if (result.value) {
2072
+ chunks.push(result.value);
2073
+ }
2074
+ }
2075
+ contents = Buffer.concat(chunks);
2076
+ }
2077
+ if (contents.length > MAX_UPLOAD_SIZE) {
2078
+ throw new FileTooLargeError(this.getBaseName(remotePath), contents.length, MAX_UPLOAD_SIZE);
2079
+ }
2080
+ if (!options.overwrite) {
2081
+ try {
2082
+ await this.dbx.filesGetMetadata({ path: dbxPath });
2083
+ throw new FileExistsError(remotePath);
2084
+ } catch (err) {
2085
+ if (err instanceof FileExistsError) throw err;
2086
+ }
2087
+ }
2088
+ const response = await this.dbx.filesUpload({
2089
+ path: dbxPath,
2090
+ contents,
2091
+ mode: options.overwrite ? { ".tag": "overwrite" } : { ".tag": "add" },
2092
+ autorename: false
2093
+ });
2094
+ if (options.onProgress) {
2095
+ options.onProgress(100, contents.length, contents.length);
2096
+ }
2097
+ const metadata = response.result;
2098
+ const item = this.metadataToItem(
2099
+ {
2100
+ ".tag": "file",
2101
+ id: metadata.id,
2102
+ name: metadata.name,
2103
+ path_display: metadata.path_display,
2104
+ path_lower: metadata.path_lower,
2105
+ size: metadata.size,
2106
+ client_modified: metadata.client_modified,
2107
+ server_modified: metadata.server_modified
2108
+ },
2109
+ normalized
2110
+ );
2111
+ return this.successResult(item);
2112
+ } catch (error) {
2113
+ if (error instanceof AuthenticationError || error instanceof FileExistsError || error instanceof FileTooLargeError) {
2114
+ return this.errorResult(error.message);
2115
+ }
2116
+ return this.errorResult(`Failed to upload file: ${error.message}`);
2117
+ }
2118
+ }
2119
+ async downloadFile(remotePath, localPath, options = {}) {
2120
+ try {
2121
+ await this.ensureAuthenticated();
2122
+ const dbxPath = this.toDropboxPath(remotePath);
2123
+ const response = await this.dbx.filesDownload({ path: dbxPath });
2124
+ const result = response.result;
2125
+ const buffer = Buffer.from(result.fileBinary);
2126
+ if (options.onProgress) {
2127
+ options.onProgress(100, buffer.length, buffer.length);
2128
+ }
2129
+ if (localPath) {
2130
+ const fs3 = await import("fs");
2131
+ const path3 = await import("path");
2132
+ await fs3.promises.mkdir(path3.dirname(localPath), { recursive: true });
2133
+ await fs3.promises.writeFile(localPath, buffer);
2134
+ return this.successResult(localPath);
2135
+ }
2136
+ return this.successResult(buffer);
2137
+ } catch (error) {
2138
+ const errMsg = error.message || String(error);
2139
+ if (errMsg.includes("path/not_found") || errMsg.includes("path_lookup/not_found")) {
2140
+ return this.errorResult(`File not found: ${remotePath}`);
2141
+ }
2142
+ return this.errorResult(`Failed to download file: ${errMsg}`);
2143
+ }
2144
+ }
2145
+ async moveItem(sourcePath, destinationPath, _options = {}) {
2146
+ try {
2147
+ await this.ensureAuthenticated();
2148
+ const fromPath = this.toDropboxPath(sourcePath);
2149
+ const toPath = this.toDropboxPath(destinationPath);
2150
+ const response = await this.dbx.filesMoveV2({
2151
+ from_path: fromPath,
2152
+ to_path: toPath,
2153
+ autorename: false
2154
+ });
2155
+ const metadata = response.result.metadata;
2156
+ const item = this.metadataToItem(metadata, this.normalizePath(destinationPath));
2157
+ return this.successResult(item);
2158
+ } catch (error) {
2159
+ const errMsg = error.message || String(error);
2160
+ if (errMsg.includes("not_found")) {
2161
+ return this.errorResult(`Item not found: ${sourcePath}`);
2162
+ }
2163
+ return this.errorResult(`Failed to move item: ${errMsg}`);
2164
+ }
2165
+ }
2166
+ async deleteFile(virtualPath) {
2167
+ try {
2168
+ await this.ensureAuthenticated();
2169
+ const dbxPath = this.toDropboxPath(virtualPath);
2170
+ await this.dbx.filesDeleteV2({ path: dbxPath });
2171
+ return this.successResult();
2172
+ } catch (error) {
2173
+ const errMsg = error.message || String(error);
2174
+ if (errMsg.includes("not_found")) {
2175
+ return this.errorResult(`File not found: ${virtualPath}`);
2176
+ }
2177
+ return this.errorResult(`Failed to delete file: ${errMsg}`);
2178
+ }
2179
+ }
2180
+ async renameFile(virtualPath, newName, _options = {}) {
2181
+ try {
2182
+ await this.ensureAuthenticated();
2183
+ const parentPath = this.getParentPath(virtualPath);
2184
+ const newVirtualPath = this.joinPath(parentPath, newName);
2185
+ const fromPath = this.toDropboxPath(virtualPath);
2186
+ const toPath = this.toDropboxPath(newVirtualPath);
2187
+ const response = await this.dbx.filesMoveV2({
2188
+ from_path: fromPath,
2189
+ to_path: toPath,
2190
+ autorename: false
2191
+ });
2192
+ const metadata = response.result.metadata;
2193
+ const item = this.metadataToItem(metadata, newVirtualPath);
2194
+ return this.successResult(item);
2195
+ } catch (error) {
2196
+ const errMsg = error.message || String(error);
2197
+ if (errMsg.includes("not_found")) {
2198
+ return this.errorResult(`File not found: ${virtualPath}`);
2199
+ }
2200
+ return this.errorResult(`Failed to rename file: ${errMsg}`);
2201
+ }
2202
+ }
2203
+ async renameFolder(virtualPath, newName, _options = {}) {
2204
+ try {
2205
+ await this.ensureAuthenticated();
2206
+ const parentPath = this.getParentPath(virtualPath);
2207
+ const newVirtualPath = this.joinPath(parentPath, newName);
2208
+ const fromPath = this.toDropboxPath(virtualPath);
2209
+ const toPath = this.toDropboxPath(newVirtualPath);
2210
+ const response = await this.dbx.filesMoveV2({
2211
+ from_path: fromPath,
2212
+ to_path: toPath,
2213
+ autorename: false
2214
+ });
2215
+ const metadata = response.result.metadata;
2216
+ const item = this.metadataToItem(metadata, newVirtualPath);
2217
+ return this.successResult(item);
2218
+ } catch (error) {
2219
+ const errMsg = error.message || String(error);
2220
+ if (errMsg.includes("not_found")) {
2221
+ return this.errorResult(`Folder not found: ${virtualPath}`);
2222
+ }
2223
+ return this.errorResult(`Failed to rename folder: ${errMsg}`);
2224
+ }
2225
+ }
2226
+ async listDirectory(virtualPath, options = {}) {
2227
+ try {
2228
+ await this.ensureAuthenticated();
2229
+ const dbxPath = this.toDropboxPath(virtualPath);
2230
+ const items = [];
2231
+ let hasMore = true;
2232
+ let cursor;
2233
+ const firstResponse = await this.dbx.filesListFolder({
2234
+ path: dbxPath,
2235
+ limit: 100
2236
+ });
2237
+ let entries = firstResponse.result.entries;
2238
+ hasMore = firstResponse.result.has_more;
2239
+ cursor = firstResponse.result.cursor;
2240
+ const processEntries = async (entryList) => {
2241
+ for (const entry of entryList) {
2242
+ if (!options.includeHidden && entry.name.startsWith(".")) {
2243
+ continue;
2244
+ }
2245
+ const itemPath = this.joinPath(virtualPath, entry.name);
2246
+ const item = this.metadataToItem(entry, itemPath);
2247
+ if (options.filter && !options.filter(item)) {
2248
+ continue;
2249
+ }
2250
+ items.push(item);
2251
+ if (options.recursive && entry[".tag"] === "folder") {
2252
+ const subResult = await this.listDirectory(itemPath, options);
2253
+ if (subResult.success && subResult.data) {
2254
+ items.push(...subResult.data);
2255
+ }
2256
+ }
2257
+ }
2258
+ };
2259
+ await processEntries(entries);
2260
+ while (hasMore && cursor) {
2261
+ const continueResponse = await this.dbx.filesListFolderContinue({ cursor });
2262
+ entries = continueResponse.result.entries;
2263
+ hasMore = continueResponse.result.has_more;
2264
+ cursor = continueResponse.result.cursor;
2265
+ await processEntries(entries);
2266
+ }
2267
+ return this.successResult(items);
2268
+ } catch (error) {
2269
+ const errMsg = error.message || String(error);
2270
+ if (errMsg.includes("path/not_found") || errMsg.includes("not_found")) {
2271
+ return this.errorResult(`Directory not found: ${virtualPath}`);
2272
+ }
2273
+ return this.errorResult(`Failed to list directory: ${errMsg}`);
2274
+ }
2275
+ }
2276
+ async getItem(virtualPath) {
2277
+ try {
2278
+ await this.ensureAuthenticated();
2279
+ const dbxPath = this.toDropboxPath(virtualPath);
2280
+ const response = await this.dbx.filesGetMetadata({ path: dbxPath });
2281
+ const metadata = response.result;
2282
+ const item = this.metadataToItem(metadata, this.normalizePath(virtualPath));
2283
+ return this.successResult(item);
2284
+ } catch (error) {
2285
+ const errMsg = error.message || String(error);
2286
+ if (errMsg.includes("not_found")) {
2287
+ return this.errorResult(`Item not found: ${virtualPath}`);
2288
+ }
2289
+ return this.errorResult(`Failed to get item: ${errMsg}`);
2290
+ }
2291
+ }
2292
+ async exists(virtualPath) {
2293
+ try {
2294
+ await this.ensureAuthenticated();
2295
+ const dbxPath = this.toDropboxPath(virtualPath);
2296
+ await this.dbx.filesGetMetadata({ path: dbxPath });
2297
+ return true;
2298
+ } catch {
2299
+ return false;
2300
+ }
2301
+ }
2302
+ async getFolderTree(path3 = "/", depth = 3) {
2303
+ try {
2304
+ await this.ensureAuthenticated();
2305
+ return super.getFolderTree(path3, depth);
2306
+ } catch (error) {
2307
+ return this.errorResult(`Failed to get folder tree: ${error.message}`);
2308
+ }
2309
+ }
2310
+ };
2311
+ function createDropboxModule() {
2312
+ return new DropboxModule();
2313
+ }
2314
+
1653
2315
  // src/modules/index.ts
1654
2316
  var moduleRegistry = {
1655
2317
  local: createLocalModule,
1656
- google_drive: createGoogleDriveModule
2318
+ google_drive: createGoogleDriveModule,
2319
+ dropbox: createDropboxModule
1657
2320
  };
1658
2321
  function getRegisteredProviders() {
1659
2322
  return Object.keys(moduleRegistry);
@@ -2219,6 +2882,7 @@ var FileMetadataService = class {
2219
2882
  if (input.scope_id !== void 0) record.scope_id = input.scope_id;
2220
2883
  if (input.uploaded_by !== void 0) record.uploaded_by = input.uploaded_by;
2221
2884
  if (input.original_filename !== void 0) record.original_filename = input.original_filename;
2885
+ if (input.content_tag !== void 0) record.content_tag = input.content_tag;
2222
2886
  const results = await this.crud.insert(record);
2223
2887
  this.logger?.debug?.("Recorded file upload", { path: input.file_path });
2224
2888
  return results[0] || null;
@@ -3596,10 +4260,18 @@ function createNamingConventionService(crudService, options) {
3596
4260
 
3597
4261
  // src/services/llm-extraction-service.ts
3598
4262
  var LLMExtractionService = class {
3599
- constructor(llmFactory, defaultProvider = "gemini") {
3600
- this.llmFactory = llmFactory;
4263
+ constructor(factoryConfig, defaultProvider = "gemini") {
4264
+ this.llmFactory = factoryConfig.create;
4265
+ this.cacheInvalidator = factoryConfig.invalidateCache;
3601
4266
  this.defaultProvider = defaultProvider;
3602
4267
  }
4268
+ /**
4269
+ * Invalidate the LLM prompt cache
4270
+ * Passthrough to hazo_llm_api's invalidate_prompt_cache when configured
4271
+ */
4272
+ invalidatePromptCache(area, key) {
4273
+ this.cacheInvalidator?.(area, key);
4274
+ }
3603
4275
  /**
3604
4276
  * Extract data from a document
3605
4277
  *
@@ -3726,8 +4398,8 @@ var LLMExtractionService = class {
3726
4398
  };
3727
4399
  }
3728
4400
  };
3729
- function createLLMExtractionService(llmFactory, defaultProvider) {
3730
- return new LLMExtractionService(llmFactory, defaultProvider);
4401
+ function createLLMExtractionService(factoryConfig, defaultProvider) {
4402
+ return new LLMExtractionService(factoryConfig, defaultProvider);
3731
4403
  }
3732
4404
 
3733
4405
  // src/common/naming-utils.ts
@@ -4053,10 +4725,11 @@ function generatePreviewName(pattern, userVariables, options = {}) {
4053
4725
 
4054
4726
  // src/services/upload-extract-service.ts
4055
4727
  var UploadExtractService = class {
4056
- constructor(fileManager, namingService, extractionService) {
4728
+ constructor(fileManager, namingService, extractionService, defaultContentTagConfig) {
4057
4729
  this.fileManager = fileManager;
4058
4730
  this.namingService = namingService;
4059
4731
  this.extractionService = extractionService;
4732
+ this.defaultContentTagConfig = defaultContentTagConfig;
4060
4733
  }
4061
4734
  /**
4062
4735
  * Upload a file with optional extraction and naming convention
@@ -4133,11 +4806,12 @@ var UploadExtractService = class {
4133
4806
  metadata.extraction_id = extractionData.id;
4134
4807
  metadata.extraction_source = extractionData.source;
4135
4808
  }
4809
+ const effectiveContentTagConfig = options.contentTagConfig ?? this.defaultContentTagConfig;
4810
+ const needsContentTagging = effectiveContentTagConfig?.content_tag_set_by_llm && this.extractionService && this.fileManager.isTrackingActive();
4136
4811
  const uploadResult = await this.fileManager.uploadFile(source, fullPath, {
4137
4812
  ...options,
4138
4813
  metadata,
4139
- awaitRecording: !!extractionData
4140
- // Await recording when extraction needs to be added
4814
+ awaitRecording: !!extractionData || !!needsContentTagging
4141
4815
  });
4142
4816
  if (!uploadResult.success) {
4143
4817
  return {
@@ -4161,13 +4835,23 @@ var UploadExtractService = class {
4161
4835
  );
4162
4836
  }
4163
4837
  }
4838
+ let contentTag;
4839
+ if (needsContentTagging && effectiveContentTagConfig) {
4840
+ contentTag = await this.performContentTagging(
4841
+ source,
4842
+ mimeType,
4843
+ effectiveContentTagConfig,
4844
+ fullPath
4845
+ );
4846
+ }
4164
4847
  return {
4165
4848
  success: true,
4166
4849
  file: uploadResult.data,
4167
4850
  extraction: extractionData,
4168
4851
  generatedPath: fullPath,
4169
4852
  generatedFolderPath: generatedFolderPath || void 0,
4170
- originalFileName
4853
+ originalFileName,
4854
+ contentTag
4171
4855
  };
4172
4856
  } catch (error) {
4173
4857
  const message = error instanceof Error ? error.message : String(error);
@@ -4265,6 +4949,76 @@ var UploadExtractService = class {
4265
4949
  folderPath: folderPath || void 0
4266
4950
  };
4267
4951
  }
4952
+ /**
4953
+ * Perform content tagging via LLM extraction.
4954
+ * Calls the LLM with the configured prompt, extracts the specified field,
4955
+ * and writes it to the content_tag column.
4956
+ */
4957
+ async performContentTagging(buffer, mimeType, config, filePath) {
4958
+ try {
4959
+ if (!this.extractionService) return void 0;
4960
+ const result = await this.extractionService.extract(buffer, mimeType, {
4961
+ promptArea: config.content_tag_prompt_area,
4962
+ promptKey: config.content_tag_prompt_key,
4963
+ promptVariables: config.content_tag_prompt_variables
4964
+ });
4965
+ if (!result.success || !result.data) return void 0;
4966
+ const tagValue = result.data[config.content_tag_prompt_return_fieldname];
4967
+ if (typeof tagValue !== "string" || !tagValue) return void 0;
4968
+ const metadataService = this.fileManager.getMetadataService();
4969
+ if (metadataService) {
4970
+ const storageType = this.fileManager.getProvider() || "local";
4971
+ const record = await metadataService.findByPath(filePath, storageType);
4972
+ if (record) {
4973
+ await metadataService.updateFields(record.id, { content_tag: tagValue });
4974
+ }
4975
+ }
4976
+ return tagValue;
4977
+ } catch {
4978
+ return void 0;
4979
+ }
4980
+ }
4981
+ /**
4982
+ * Manually tag a file's content via LLM.
4983
+ * Works with existing DB records, resolving the file path internally.
4984
+ *
4985
+ * @param fileId - Database record ID of the file
4986
+ * @param config - Content tag config (falls back to default if not provided)
4987
+ * @returns OperationResult with the tag value
4988
+ */
4989
+ async tagFileContent(fileId, config) {
4990
+ const effectiveConfig = config ?? this.defaultContentTagConfig;
4991
+ if (!effectiveConfig || !effectiveConfig.content_tag_set_by_llm) {
4992
+ return { success: false, error: "Content tagging is not configured or disabled" };
4993
+ }
4994
+ if (!this.extractionService) {
4995
+ return { success: false, error: "Extraction service not available" };
4996
+ }
4997
+ const metadataService = this.fileManager.getMetadataService();
4998
+ if (!metadataService) {
4999
+ return { success: false, error: "Metadata service not available (tracking not enabled)" };
5000
+ }
5001
+ const record = await metadataService.findById(fileId);
5002
+ if (!record) {
5003
+ return { success: false, error: `File record not found: ${fileId}` };
5004
+ }
5005
+ const downloadResult = await this.fileManager.downloadFile(record.file_path);
5006
+ if (!downloadResult.success || !downloadResult.data) {
5007
+ return { success: false, error: `Failed to download file: ${downloadResult.error}` };
5008
+ }
5009
+ const buffer = Buffer.isBuffer(downloadResult.data) ? downloadResult.data : Buffer.from(downloadResult.data);
5010
+ const mimeType = getMimeType(record.filename);
5011
+ const tagValue = await this.performContentTagging(
5012
+ buffer,
5013
+ mimeType,
5014
+ effectiveConfig,
5015
+ record.file_path
5016
+ );
5017
+ if (!tagValue) {
5018
+ return { success: false, error: "Content tagging did not produce a result" };
5019
+ }
5020
+ return { success: true, data: tagValue };
5021
+ }
4268
5022
  /**
4269
5023
  * Get the file manager
4270
5024
  */
@@ -4284,8 +5038,8 @@ var UploadExtractService = class {
4284
5038
  return this.extractionService;
4285
5039
  }
4286
5040
  };
4287
- function createUploadExtractService(fileManager, namingService, extractionService) {
4288
- return new UploadExtractService(fileManager, namingService, extractionService);
5041
+ function createUploadExtractService(fileManager, namingService, extractionService, defaultContentTagConfig) {
5042
+ return new UploadExtractService(fileManager, namingService, extractionService, defaultContentTagConfig);
4289
5043
  }
4290
5044
 
4291
5045
  // src/schema/index.ts
@@ -4312,7 +5066,8 @@ var HAZO_FILES_TABLE_SCHEMA = {
4312
5066
  uploaded_by TEXT,
4313
5067
  storage_verified_at TEXT,
4314
5068
  deleted_at TEXT,
4315
- original_filename TEXT
5069
+ original_filename TEXT,
5070
+ content_tag TEXT
4316
5071
  )`,
4317
5072
  indexes: [
4318
5073
  "CREATE INDEX IF NOT EXISTS idx_hazo_files_path ON hazo_files (file_path)",
@@ -4322,7 +5077,8 @@ var HAZO_FILES_TABLE_SCHEMA = {
4322
5077
  "CREATE INDEX IF NOT EXISTS idx_hazo_files_status ON hazo_files (status)",
4323
5078
  "CREATE INDEX IF NOT EXISTS idx_hazo_files_scope ON hazo_files (scope_id)",
4324
5079
  "CREATE INDEX IF NOT EXISTS idx_hazo_files_ref_count ON hazo_files (ref_count)",
4325
- "CREATE INDEX IF NOT EXISTS idx_hazo_files_deleted ON hazo_files (deleted_at)"
5080
+ "CREATE INDEX IF NOT EXISTS idx_hazo_files_deleted ON hazo_files (deleted_at)",
5081
+ "CREATE INDEX IF NOT EXISTS idx_hazo_files_content_tag ON hazo_files (content_tag)"
4326
5082
  ]
4327
5083
  },
4328
5084
  postgres: {
@@ -4345,7 +5101,8 @@ var HAZO_FILES_TABLE_SCHEMA = {
4345
5101
  uploaded_by UUID,
4346
5102
  storage_verified_at TIMESTAMP WITH TIME ZONE,
4347
5103
  deleted_at TIMESTAMP WITH TIME ZONE,
4348
- original_filename TEXT
5104
+ original_filename TEXT,
5105
+ content_tag TEXT
4349
5106
  )`,
4350
5107
  indexes: [
4351
5108
  "CREATE INDEX IF NOT EXISTS idx_hazo_files_path ON hazo_files (file_path)",
@@ -4355,7 +5112,8 @@ var HAZO_FILES_TABLE_SCHEMA = {
4355
5112
  "CREATE INDEX IF NOT EXISTS idx_hazo_files_status ON hazo_files (status)",
4356
5113
  "CREATE INDEX IF NOT EXISTS idx_hazo_files_scope ON hazo_files (scope_id)",
4357
5114
  "CREATE INDEX IF NOT EXISTS idx_hazo_files_ref_count ON hazo_files (ref_count)",
4358
- "CREATE INDEX IF NOT EXISTS idx_hazo_files_deleted ON hazo_files (deleted_at)"
5115
+ "CREATE INDEX IF NOT EXISTS idx_hazo_files_deleted ON hazo_files (deleted_at)",
5116
+ "CREATE INDEX IF NOT EXISTS idx_hazo_files_content_tag ON hazo_files (content_tag)"
4359
5117
  ]
4360
5118
  },
4361
5119
  columns: [
@@ -4377,7 +5135,8 @@ var HAZO_FILES_TABLE_SCHEMA = {
4377
5135
  "uploaded_by",
4378
5136
  "storage_verified_at",
4379
5137
  "deleted_at",
4380
- "original_filename"
5138
+ "original_filename",
5139
+ "content_tag"
4381
5140
  ]
4382
5141
  };
4383
5142
  function getSchemaForTable(tableName, dbType) {
@@ -4518,6 +5277,45 @@ function getNamingSchemaForTable(tableName, dbType) {
4518
5277
  )
4519
5278
  };
4520
5279
  }
5280
+ var HAZO_FILES_MIGRATION_V3 = {
5281
+ tableName: HAZO_FILES_DEFAULT_TABLE_NAME,
5282
+ sqlite: {
5283
+ alterStatements: [
5284
+ "ALTER TABLE hazo_files ADD COLUMN content_tag TEXT"
5285
+ ],
5286
+ indexes: [
5287
+ "CREATE INDEX IF NOT EXISTS idx_hazo_files_content_tag ON hazo_files (content_tag)"
5288
+ ],
5289
+ backfill: ""
5290
+ // No backfill needed — column is nullable, defaults to NULL
5291
+ },
5292
+ postgres: {
5293
+ alterStatements: [
5294
+ "ALTER TABLE hazo_files ADD COLUMN IF NOT EXISTS content_tag TEXT"
5295
+ ],
5296
+ indexes: [
5297
+ "CREATE INDEX IF NOT EXISTS idx_hazo_files_content_tag ON hazo_files (content_tag)"
5298
+ ],
5299
+ backfill: ""
5300
+ // No backfill needed — column is nullable, defaults to NULL
5301
+ },
5302
+ newColumns: [
5303
+ "content_tag"
5304
+ ]
5305
+ };
5306
+ function getMigrationV3ForTable(tableName, dbType) {
5307
+ const migration = HAZO_FILES_MIGRATION_V3[dbType];
5308
+ const defaultName = HAZO_FILES_MIGRATION_V3.tableName;
5309
+ return {
5310
+ alterStatements: migration.alterStatements.map(
5311
+ (stmt) => stmt.replace(new RegExp(defaultName, "g"), tableName)
5312
+ ),
5313
+ indexes: migration.indexes.map(
5314
+ (idx) => idx.replace(new RegExp(defaultName, "g"), tableName)
5315
+ ),
5316
+ backfill: migration.backfill
5317
+ };
5318
+ }
4521
5319
 
4522
5320
  // src/migrations/add-reference-tracking.ts
4523
5321
  async function migrateToV2(executor, dbType, tableName) {
@@ -4536,6 +5334,20 @@ async function backfillV2Defaults(executor, dbType, tableName) {
4536
5334
  const migration = tableName ? getMigrationForTable(tableName, dbType) : HAZO_FILES_MIGRATION_V2[dbType];
4537
5335
  await executor.run(migration.backfill);
4538
5336
  }
5337
+
5338
+ // src/migrations/add-content-tag.ts
5339
+ async function migrateToV3(executor, dbType, tableName) {
5340
+ const migration = tableName ? getMigrationV3ForTable(tableName, dbType) : HAZO_FILES_MIGRATION_V3[dbType];
5341
+ for (const stmt of migration.alterStatements) {
5342
+ try {
5343
+ await executor.run(stmt);
5344
+ } catch {
5345
+ }
5346
+ }
5347
+ for (const idx of migration.indexes) {
5348
+ await executor.run(idx);
5349
+ }
5350
+ }
4539
5351
  export {
4540
5352
  ALL_SYSTEM_VARIABLES,
4541
5353
  AuthenticationError,
@@ -4544,6 +5356,8 @@ export {
4544
5356
  DirectoryExistsError,
4545
5357
  DirectoryNotEmptyError,
4546
5358
  DirectoryNotFoundError,
5359
+ DropboxAuth,
5360
+ DropboxModule,
4547
5361
  FileExistsError,
4548
5362
  FileManager,
4549
5363
  FileMetadataService,
@@ -4553,6 +5367,7 @@ export {
4553
5367
  GoogleDriveModule,
4554
5368
  HAZO_FILES_DEFAULT_TABLE_NAME,
4555
5369
  HAZO_FILES_MIGRATION_V2,
5370
+ HAZO_FILES_MIGRATION_V3,
4556
5371
  HAZO_FILES_NAMING_DEFAULT_TABLE_NAME,
4557
5372
  HAZO_FILES_NAMING_TABLE_SCHEMA,
4558
5373
  HAZO_FILES_TABLE_SCHEMA,
@@ -4579,6 +5394,8 @@ export {
4579
5394
  computeFileHashSync,
4580
5395
  computeFileInfo,
4581
5396
  createAndInitializeModule,
5397
+ createDropboxAuth,
5398
+ createDropboxModule,
4582
5399
  createEmptyFileDataStructure,
4583
5400
  createEmptyNamingRuleSchema,
4584
5401
  createFileItem,
@@ -4622,6 +5439,7 @@ export {
4622
5439
  getFileMetadataValues,
4623
5440
  getMergedData,
4624
5441
  getMigrationForTable,
5442
+ getMigrationV3ForTable,
4625
5443
  getMimeType,
4626
5444
  getNameWithoutExtension,
4627
5445
  getNamingSchemaForTable,
@@ -4654,6 +5472,7 @@ export {
4654
5472
  loadConfig,
4655
5473
  loadConfigAsync,
4656
5474
  migrateToV2,
5475
+ migrateToV3,
4657
5476
  normalizePath,
4658
5477
  parseConfig,
4659
5478
  parseFileData,