Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 11 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1397,18 +1397,19 @@ Edit the configuration of the Internxt CLI WebDav server as the port or the prot
```
USAGE
$ internxt webdav-config [--json] [-x] [--debug] [-l <value>] [-p <value>] [-s | -h] [-t <value>] [-c] [-a] [-u
<value>] [-w <value>]
<value>] [-w <value>] [-d]

FLAGS
-a, --[no-]customAuth Configures the WebDAV server to use custom authentication.
-c, --[no-]createFullPath Auto-create missing parent directories during file uploads.
-h, --http Configures the WebDAV server to use insecure plain HTTP.
-l, --host=<value> The listening host for the WebDAV server.
-p, --port=<value> The new port for the WebDAV server.
-s, --https Configures the WebDAV server to use HTTPS with self-signed certificates.
-t, --timeout=<value> Configures the WebDAV server to use this timeout in minutes.
-u, --username=<value> Configures the WebDAV server to use this username for custom authentication.
-w, --password=<value> Configures the WebDAV server to use this password for custom authentication.
-a, --[no-]customAuth Configures the WebDAV server to use custom authentication.
-c, --[no-]createFullPath Auto-create missing parent directories during file uploads.
-d, --[no-]deleteFilesPermanently Configures the WebDAV server to delete files permanently instead of trashing them.
-h, --http Configures the WebDAV server to use insecure plain HTTP.
-l, --host=<value> The listening host for the WebDAV server.
-p, --port=<value> The new port for the WebDAV server.
-s, --https Configures the WebDAV server to use HTTPS with self-signed certificates.
-t, --timeout=<value> Configures the WebDAV server to use this timeout in minutes.
-u, --username=<value> Configures the WebDAV server to use this username for custom authentication.
-w, --password=<value> Configures the WebDAV server to use this password for custom authentication.

HELPER FLAGS
-x, --non-interactive [env: INXT_NONINTERACTIVE] Prevents the CLI from being interactive. When enabled, the CLI will
Expand Down
56 changes: 32 additions & 24 deletions docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,17 @@ services:
container_name: internxt-webdav
restart: unless-stopped
environment:
INXT_USER: "" # Your Internxt account email
INXT_PASSWORD: "" # Your Internxt account password
INXT_TWOFACTORCODE: "" # (Optional) Current 2FA one-time code
INXT_OTPTOKEN: "" # (Optional) OTP secret for auto-generating 2FA codes
INXT_WORKSPACE_ID: "" # (Optional) Workspace ID to use for WebDAV server
WEBDAV_PORT: "" # (Optional) WebDAV port. Defaults to 3005 if empty
WEBDAV_PROTOCOL: "" # (Optional) WebDAV protocol. Accepts 'http' or 'https'. Defaults to 'https' if empty
WEBDAV_CUSTOM_AUTH: "" # (Optional) Enable custom authentication. Set to 'true' to enable
WEBDAV_USERNAME: "" # (Optional) Custom username for WebDAV authentication
WEBDAV_PASSWORD: "" # (Optional) Custom password for WebDAV authentication
INXT_USER: "" # Your Internxt account email
INXT_PASSWORD: "" # Your Internxt account password
INXT_TWOFACTORCODE: "" # (Optional) Current 2FA one-time code
INXT_OTPTOKEN: "" # (Optional) OTP secret for auto-generating 2FA codes
INXT_WORKSPACE_ID: "" # (Optional) Workspace ID to use for WebDAV server
WEBDAV_PORT: "" # (Optional) WebDAV port. Defaults to 3005 if empty
WEBDAV_PROTOCOL: "" # (Optional) WebDAV protocol. Accepts 'http' or 'https'. Defaults to 'https' if empty
WEBDAV_CUSTOM_AUTH: "" # (Optional) Enable custom authentication. Set to 'true' to enable
WEBDAV_USERNAME: "" # (Optional) Custom username for WebDAV authentication
WEBDAV_PASSWORD: "" # (Optional) Custom password for WebDAV authentication
WEBDAV_DELETE_FILES_PERMANENTLY: "" # (Optional) Delete files permanently. Set to 'true' to enable
ports:
- "127.0.0.1:3005:3005" # Map container port to host. Change if WEBDAV_PORT is customized
```
Expand All @@ -49,9 +50,10 @@ docker run -d \
-e INXT_WORKSPACE_ID="" \
-e WEBDAV_PORT="" \
-e WEBDAV_PROTOCOL="" \
-e WEBDAV_CUSTOM_AUTH="false" \
-e WEBDAV_CUSTOM_AUTH="" \
-e WEBDAV_USERNAME="" \
-e WEBDAV_PASSWORD="" \
-e WEBDAV_DELETE_FILES_PERMANENTLY="" \
-p 127.0.0.1:3005:3005 \
internxt/webdav:latest
```
Expand Down Expand Up @@ -79,19 +81,19 @@ You can also run the `internxt/webdav` image directly on popular NAS devices lik

## 🔑 Authentication & Environment Variables

| Variable | Required | Description |
|----------------------|----------|------------------------------------------------------------------------------------------------|
| `INXT_USER` | ✅ Yes | Your Internxt account email. |
| `INXT_PASSWORD` | ✅ Yes | Your Internxt account password. |
| `INXT_TWOFACTORCODE` | ❌ No | Temporary one-time code from your 2FA app. Must be refreshed every startup. |
| `INXT_OTPTOKEN` | ❌ No | OTP secret key (base32). Used to auto-generate fresh codes at runtime. |
| `INXT_WORKSPACE_ID` | ❌ No | Workspace ID to use. If set, the WebDAV server will operate within this workspace. |
| `WEBDAV_PORT` | ❌ No | Port for the WebDAV server. Defaults to `3005` if left empty. |
| `WEBDAV_PROTOCOL` | ❌ No | Protocol for the WebDAV server. Accepts `http` or `https`. Defaults to `https` if left empty. |
| `WEBDAV_CUSTOM_AUTH` | ❌ No | Enable custom Basic Authentication for WebDAV. Set to `true` to enable. |
| `WEBDAV_USERNAME` | ❌ No | Username for custom WebDAV authentication. Required if `WEBDAV_CUSTOM_AUTH` is enabled. |
| `WEBDAV_PASSWORD` | ❌ No | Password for custom WebDAV authentication. Required if `WEBDAV_CUSTOM_AUTH` is enabled. |

| Variable | Required | Description |
|-----------------------------------|----------|------------------------------------------------------------------------------------------------|
| `INXT_USER` | ✅ Yes | Your Internxt account email. |
| `INXT_PASSWORD` | ✅ Yes | Your Internxt account password. |
| `INXT_TWOFACTORCODE` | ❌ No | Temporary one-time code from your 2FA app. Must be refreshed every startup. |
| `INXT_OTPTOKEN` | ❌ No | OTP secret key (base32). Used to auto-generate fresh codes at runtime. |
| `INXT_WORKSPACE_ID` | ❌ No | Workspace ID to use. If set, the WebDAV server will operate within this workspace. |
| `WEBDAV_PORT` | ❌ No | Port for the WebDAV server. Defaults to `3005` if left empty. |
| `WEBDAV_PROTOCOL` | ❌ No | Protocol for the WebDAV server. Accepts `http` or `https`. Defaults to `https` if left empty. |
| `WEBDAV_CUSTOM_AUTH` | ❌ No | Enable custom Basic Authentication for WebDAV. Set to `true` to enable. |
| `WEBDAV_USERNAME` | ❌ No | Username for custom WebDAV authentication. Required if `WEBDAV_CUSTOM_AUTH` is enabled. |
| `WEBDAV_PASSWORD` | ❌ No | Password for custom WebDAV authentication. Required if `WEBDAV_CUSTOM_AUTH` is enabled. |
| `WEBDAV_DELETE_FILES_PERMANENTLY` | ❌ No | Delete files permanently instead of moving them to trash. Set to `true`to enable. |

---

Expand Down Expand Up @@ -122,6 +124,12 @@ If your Internxt account has **two-factor authentication enabled**, you can choo
### Using Workspaces
If you have access to Internxt Workspaces and want to use the WebDAV server with a specific workspace instead of your personal drive, you can set the INXT_WORKSPACE_ID environment variable.


### Permanent File Deletion
By default, when you delete files through the WebDAV server, they are moved to your Internxt trash and can be recovered. However, you can configure the server to delete files permanently by enabling `WEBDAV_DELETE_FILES_PERMANENTLY`.
⚠️ WARNING: When this option is enabled, deleted files CANNOT be recovered. They will be permanently deleted from your Internxt account, bypassing the trash entirely. Use this option with extreme caution and only if you understand the consequences.


## 🌐 Accessing WebDAV

Once running, your Internxt WebDAV server will be available at:
Expand Down
6 changes: 6 additions & 0 deletions docker/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ if [ "$customAuth" = "true" ] || [ "$customAuth" = "1" ] || [ "$customAuth" = "y
WEBDAV_ARGS="$WEBDAV_ARGS --customAuth -u=$WEBDAV_USERNAME -w=$WEBDAV_PASSWORD"
fi

deleteFilesPermanently=$(echo "$WEBDAV_DELETE_FILES_PERMANENTLY" | tr '[:upper:]' '[:lower:]')
if [ "$deleteFilesPermanently" = "true" ] || [ "$deleteFilesPermanently" = "1" ] || [ "$deleteFilesPermanently" = "yes" ] || [ "$deleteFilesPermanently" = "y" ]; then
echo "WARNING: Permanent file deletion is enabled. Deleted files will NOT be recoverable."
WEBDAV_ARGS="$WEBDAV_ARGS -d"
fi

internxt webdav-config $WEBDAV_ARGS

internxt webdav enable
Expand Down
14 changes: 7 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@
"dependencies": {
"@dashlane/pqc-kem-kyber512-node": "1.0.0",
"@inquirer/prompts": "8.3.0",
"@internxt/inxt-js": "2.3.0",
"@internxt/inxt-js": "2.3.1",
"@internxt/lib": "1.4.1",
"@internxt/sdk": "1.15.1",
"@oclif/core": "4.8.1",
"@oclif/core": "4.8.3",
"@oclif/plugin-autocomplete": "3.2.40",
"axios": "1.13.6",
"better-sqlite3": "12.6.2",
Expand All @@ -52,7 +52,7 @@
"dotenv": "17.3.1",
"express": "5.2.1",
"express-async-handler": "1.2.0",
"fast-xml-parser": "5.4.1",
"fast-xml-parser": "5.4.2",
"hash-wasm": "4.12.0",
"mime-types": "3.0.2",
"open": "11.0.0",
Expand All @@ -72,18 +72,18 @@
"@types/cli-progress": "3.11.6",
"@types/express": "5.0.6",
"@types/mime-types": "3.0.1",
"@types/node": "25.3.2",
"@types/node": "25.3.3",
"@types/range-parser": "1.2.7",
"@vitest/coverage-istanbul": "4.0.18",
"@vitest/spy": "4.0.18",
"eslint": "9.39.3",
"husky": "9.1.7",
"lint-staged": "16.2.7",
"lint-staged": "16.3.2",
"nodemon": "3.1.14",
"oclif": "4.22.81",
"oclif": "4.22.85",
"prettier": "3.8.1",
"rimraf": "6.1.3",
"sql.js": "1.14.0",
"sql.js": "1.14.1",
"ts-node": "10.9.2",
"typescript": "5.9.3",
"vitest": "4.0.18",
Expand Down
14 changes: 13 additions & 1 deletion src/commands/webdav-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,20 @@ export default class WebDAVConfig extends Command {
description: 'Configures the WebDAV server to use this password for custom authentication.',
required: false,
}),
deleteFilesPermanently: Flags.boolean({
char: 'd',
description: 'Configures the WebDAV server to delete files permanently instead of trashing them.',
required: false,
default: undefined,
allowNo: true,
}),
};
static readonly enableJsonFlag = true;

public run = async () => {
const { flags } = await this.parse(WebDAVConfig);
const { host, port, https, http, timeout, createFullPath, customAuth, username, password } = flags;
const { host, port, https, http, timeout, createFullPath, customAuth, username, password, deleteFilesPermanently } =
flags;
const nonInteractive = flags['non-interactive'];

const webdavConfig = await ConfigService.instance.readWebdavConfig();
Expand Down Expand Up @@ -118,6 +126,10 @@ export default class WebDAVConfig extends Command {
throw new MissingCredentialsWhenUsingAuthError();
}

if (deleteFilesPermanently !== undefined) {
webdavConfig['deleteFilesPermanently'] = deleteFilesPermanently;
}

await ConfigService.instance.saveWebdavConfig(webdavConfig);

const printWebdavConfig = {
Expand Down
1 change: 1 addition & 0 deletions src/commands/whoami.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export default class Whoami extends Command {
const refreshedCreds = await AuthService.instance.refreshUserToken(
userCredentials.token,
userCredentials.user.mnemonic,
userCredentials.user.keys.ecc.privateKey,
);
await ConfigService.instance.saveUser(refreshedCreds);
} catch {
Expand Down
1 change: 1 addition & 0 deletions src/constants/configs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ export const WEBDAV_DEFAULT_PROTOCOL = 'https';
export const WEBDAV_DEFAULT_TIMEOUT = 0;
export const WEBDAV_DEFAULT_CREATE_FULL_PATH = true;
export const WEBDAV_DEFAULT_CUSTOM_AUTH = false;
export const WEBDAV_DEFAULT_DELETE_FILES_PERMANENTLY = false;
32 changes: 30 additions & 2 deletions src/services/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,20 @@ export class AuthService {
const { user, newToken } = data;

const clearMnemonic = CryptoService.instance.decryptTextWithKey(user.mnemonic, password);
const clearPrivateKey = Buffer.from(
CryptoService.instance.decryptPrivateKey(user.keys.ecc.privateKey, password),
).toString('base64');
user.keys.ecc.privateKey = clearPrivateKey;
if (user.keys?.kyber?.privateKey) {
user.keys.kyber.privateKey = Buffer.from(
CryptoService.instance.decryptPrivateKey(user.keys.kyber.privateKey, password),
).toString('base64');
}
const clearUser: LoginCredentials['user'] = {
...user,
mnemonic: clearMnemonic,
};

return {
user: clearUser,
token: newToken,
Expand Down Expand Up @@ -86,7 +96,11 @@ export class AuthService {

if (tokenDetails.expiration.refreshRequired) {
try {
loginCreds = await this.refreshUserToken(loginCreds.token, loginCreds.user.mnemonic);
loginCreds = await this.refreshUserToken(
loginCreds.token,
loginCreds.user.mnemonic,
loginCreds.user.keys.ecc.privateKey,
);
} catch (error) {
await ConfigService.instance.clearUser();
throw error;
Expand All @@ -107,7 +121,11 @@ export class AuthService {
* @returns The user details and the renewed auth token
* @throws {InvalidCredentialsError} When the mnemonic is invalid
*/
public refreshUserToken = async (oldToken: string, mnemonic: string): Promise<LoginCredentials> => {
public refreshUserToken = async (
oldToken: string,
mnemonic: string,
privateKey: string,
): Promise<LoginCredentials> => {
SdkManager.init({ token: oldToken });

const isValidMnemonic = ValidationService.instance.validateMnemonic(mnemonic);
Expand All @@ -124,6 +142,16 @@ export class AuthService {
user: {
...newCreds.user,
mnemonic: mnemonic,
keys: {
ecc: {
privateKey: privateKey,
publicKey: newCreds.user.keys.ecc.publicKey,
},
kyber: {
privateKey: newCreds.user.keys.kyber.privateKey,
publicKey: newCreds.user.keys.kyber.publicKey,
},
},
},
token: newCreds.newToken,
};
Expand Down
3 changes: 3 additions & 0 deletions src/services/config.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
WEBDAV_DEFAULT_TIMEOUT,
WEBDAV_DEFAULT_CUSTOM_AUTH,
WEBDAV_SSL_CERTS_DIR,
WEBDAV_DEFAULT_DELETE_FILES_PERMANENTLY,
} from '../constants/configs';

export class ConfigService {
Expand Down Expand Up @@ -96,6 +97,7 @@ export class ConfigService {
customAuth: configs?.customAuth ?? WEBDAV_DEFAULT_CUSTOM_AUTH,
username: configs?.username ?? '',
password: configs?.password ?? '',
deleteFilesPermanently: configs?.deleteFilesPermanently ?? WEBDAV_DEFAULT_DELETE_FILES_PERMANENTLY,
};
} catch {
return {
Expand All @@ -107,6 +109,7 @@ export class ConfigService {
customAuth: WEBDAV_DEFAULT_CUSTOM_AUTH,
username: '',
password: '',
deleteFilesPermanently: WEBDAV_DEFAULT_DELETE_FILES_PERMANENTLY,
};
}
};
Expand Down
25 changes: 21 additions & 4 deletions src/services/crypto.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ConfigService } from '../services/config.service';
import { StreamUtils } from '../utils/stream.utils';
import { LoginCredentials } from '../types/command.types';
import { WorkspaceData } from '@internxt/sdk/dist/workspaces';
import { aes } from '@internxt/lib';

export class CryptoService {
public static readonly instance: CryptoService = new CryptoService();
Expand Down Expand Up @@ -191,12 +192,15 @@ export class CryptoService {
return { key, iv };
};

private readonly decryptMnemonic = async (encryptionKey: string, user: LoginCredentials['user']): Promise<string> => {
private readonly decryptWorkspaceMnemonic = async (
encryptionKey: string,
user: LoginCredentials['user'],
): Promise<string> => {
const privateKeyInBase64 = user.keys?.ecc?.privateKey;
const privateKyberKeyInBase64 = user.keys?.kyber?.privateKey;

if (!privateKeyInBase64) {
return user.mnemonic;
throw new Error('Missing privateKey in user keys');
}

try {
Expand All @@ -206,7 +210,7 @@ export class CryptoService {
privateKyberKeyInBase64,
});
} catch {
return user.mnemonic;
throw new Error('Failed to decrypt workspace mnemonic');
}
};

Expand All @@ -220,10 +224,23 @@ export class CryptoService {
...workspace,
workspaceUser: {
...workspace.workspaceUser,
key: await this.decryptMnemonic(workspace.workspaceUser.key, user),
key: await this.decryptWorkspaceMnemonic(workspace.workspaceUser.key, user),
},
};
}),
);
};

public decryptPrivateKey = (privateKey: string, password: string): string => {
const MINIMAL_ENCRYPTED_KEY_LEN = 129;
if (!privateKey || privateKey.length <= MINIMAL_ENCRYPTED_KEY_LEN) return '';
else {
try {
const result = aes.decrypt(privateKey, password);
return result;
} catch {
throw new Error('Private key is corrupted');
}
}
};
}
8 changes: 8 additions & 0 deletions src/services/drive/trash.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ export class TrashService {
return storageClient.addItemsToTrash(payload);
};

public deleteItemPermanently = (itemType: 'file' | 'folder', id: string) => {
if (itemType === 'file') {
return this.deleteFile(id);
} else {
return this.deleteFolder(id);
}
};

public deleteFile = (fileId: string) => {
const storageClient = SdkManager.instance.getStorage();
return storageClient.deleteFileByUuid(fileId);
Expand Down
9 changes: 6 additions & 3 deletions src/services/sdk-manager.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,13 @@ export class SdkManager {
* @param apiSecurity Security properties to be setted
**/
public static readonly init = (apiSecurity: SdkManagerApiSecurity) => {
apiSecurity.retryOptions = {
maxRetries: MAX_RETRIES,
const newApiSecurity: SdkManagerApiSecurity = {
...apiSecurity,
retryOptions: {
maxRetries: MAX_RETRIES,
},
};
SdkManager.apiSecurity = apiSecurity;
SdkManager.apiSecurity = newApiSecurity;
};

/**
Expand Down
Loading