diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..35be8f05 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,18 @@ +.git +.github +.husky +.vscode +.idea +node_modules +dist +lib +coverage +.nyc_output +tmp +*.log +*.tsbuildinfo +oclif.lock +oclif.manifest.json +.env +.npmrc +cli.inxt diff --git a/Dockerfile b/Dockerfile index 8f4671ff..7213d028 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,6 +9,8 @@ COPY .env.template .env COPY .npmrc.template .npmrc RUN npm install +RUN npm approve-scripts --all +RUN npm rebuild RUN npm run build RUN chmod +x /app/docker/entrypoint.sh diff --git a/README.md b/README.md index bc87c738..892af69d 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ $ npm install -g @internxt/cli $ internxt COMMAND running command... $ internxt (--version) -@internxt/cli/1.6.6 win32-x64 node-v24.15.0 +@internxt/cli/1.6.7 win32-x64 node-v24.15.0 $ internxt --help [COMMAND] USAGE $ internxt COMMAND @@ -145,7 +145,7 @@ EXAMPLES $ internxt add-cert ``` -_See code: [src/commands/add-cert.ts](https://github.com/internxt/cli/blob/v1.6.6/src/commands/add-cert.ts)_ +_See code: [src/commands/add-cert.ts](https://github.com/internxt/cli/blob/v1.6.7/src/commands/add-cert.ts)_ ## `internxt add cert` @@ -203,7 +203,7 @@ EXAMPLES $ internxt autocomplete --refresh-cache ``` -_See code: [@oclif/plugin-autocomplete](https://github.com/oclif/plugin-autocomplete/blob/v3.2.52/src/commands/autocomplete/index.ts)_ +_See code: [@oclif/plugin-autocomplete](https://github.com/oclif/plugin-autocomplete/blob/v3.2.53/src/commands/autocomplete/index.ts)_ ## `internxt config` @@ -232,7 +232,7 @@ EXAMPLES $ internxt config ``` -_See code: [src/commands/config.ts](https://github.com/internxt/cli/blob/v1.6.6/src/commands/config.ts)_ +_See code: [src/commands/config.ts](https://github.com/internxt/cli/blob/v1.6.7/src/commands/config.ts)_ ## `internxt create-folder` @@ -266,7 +266,7 @@ EXAMPLES $ internxt create-folder ``` -_See code: [src/commands/create-folder.ts](https://github.com/internxt/cli/blob/v1.6.6/src/commands/create-folder.ts)_ +_See code: [src/commands/create-folder.ts](https://github.com/internxt/cli/blob/v1.6.7/src/commands/create-folder.ts)_ ## `internxt create folder` @@ -330,7 +330,7 @@ EXAMPLES $ internxt delete-permanently-file ``` -_See code: [src/commands/delete-permanently-file.ts](https://github.com/internxt/cli/blob/v1.6.6/src/commands/delete-permanently-file.ts)_ +_See code: [src/commands/delete-permanently-file.ts](https://github.com/internxt/cli/blob/v1.6.7/src/commands/delete-permanently-file.ts)_ ## `internxt delete-permanently-folder` @@ -362,7 +362,7 @@ EXAMPLES $ internxt delete-permanently-folder ``` -_See code: [src/commands/delete-permanently-folder.ts](https://github.com/internxt/cli/blob/v1.6.6/src/commands/delete-permanently-folder.ts)_ +_See code: [src/commands/delete-permanently-folder.ts](https://github.com/internxt/cli/blob/v1.6.7/src/commands/delete-permanently-folder.ts)_ ## `internxt delete permanently file` @@ -457,7 +457,7 @@ EXAMPLES $ internxt download-file ``` -_See code: [src/commands/download-file.ts](https://github.com/internxt/cli/blob/v1.6.6/src/commands/download-file.ts)_ +_See code: [src/commands/download-file.ts](https://github.com/internxt/cli/blob/v1.6.7/src/commands/download-file.ts)_ ## `internxt download file` @@ -520,7 +520,7 @@ EXAMPLES $ internxt list ``` -_See code: [src/commands/list.ts](https://github.com/internxt/cli/blob/v1.6.6/src/commands/list.ts)_ +_See code: [src/commands/list.ts](https://github.com/internxt/cli/blob/v1.6.7/src/commands/list.ts)_ ## `internxt login` @@ -554,7 +554,7 @@ EXAMPLES $ internxt login ``` -_See code: [src/commands/login.ts](https://github.com/internxt/cli/blob/v1.6.6/src/commands/login.ts)_ +_See code: [src/commands/login.ts](https://github.com/internxt/cli/blob/v1.6.7/src/commands/login.ts)_ ## `internxt login-legacy` @@ -588,7 +588,7 @@ EXAMPLES $ internxt login-legacy ``` -_See code: [src/commands/login-legacy.ts](https://github.com/internxt/cli/blob/v1.6.6/src/commands/login-legacy.ts)_ +_See code: [src/commands/login-legacy.ts](https://github.com/internxt/cli/blob/v1.6.7/src/commands/login-legacy.ts)_ ## `internxt logout` @@ -614,7 +614,7 @@ EXAMPLES $ internxt logout ``` -_See code: [src/commands/logout.ts](https://github.com/internxt/cli/blob/v1.6.6/src/commands/logout.ts)_ +_See code: [src/commands/logout.ts](https://github.com/internxt/cli/blob/v1.6.7/src/commands/logout.ts)_ ## `internxt logs` @@ -640,7 +640,7 @@ EXAMPLES $ internxt logs ``` -_See code: [src/commands/logs.ts](https://github.com/internxt/cli/blob/v1.6.6/src/commands/logs.ts)_ +_See code: [src/commands/logs.ts](https://github.com/internxt/cli/blob/v1.6.7/src/commands/logs.ts)_ ## `internxt move-file` @@ -674,7 +674,7 @@ EXAMPLES $ internxt move-file ``` -_See code: [src/commands/move-file.ts](https://github.com/internxt/cli/blob/v1.6.6/src/commands/move-file.ts)_ +_See code: [src/commands/move-file.ts](https://github.com/internxt/cli/blob/v1.6.7/src/commands/move-file.ts)_ ## `internxt move-folder` @@ -708,7 +708,7 @@ EXAMPLES $ internxt move-folder ``` -_See code: [src/commands/move-folder.ts](https://github.com/internxt/cli/blob/v1.6.6/src/commands/move-folder.ts)_ +_See code: [src/commands/move-folder.ts](https://github.com/internxt/cli/blob/v1.6.7/src/commands/move-folder.ts)_ ## `internxt move file` @@ -805,7 +805,7 @@ EXAMPLES $ internxt rename-file ``` -_See code: [src/commands/rename-file.ts](https://github.com/internxt/cli/blob/v1.6.6/src/commands/rename-file.ts)_ +_See code: [src/commands/rename-file.ts](https://github.com/internxt/cli/blob/v1.6.7/src/commands/rename-file.ts)_ ## `internxt rename-folder` @@ -838,7 +838,7 @@ EXAMPLES $ internxt rename-folder ``` -_See code: [src/commands/rename-folder.ts](https://github.com/internxt/cli/blob/v1.6.6/src/commands/rename-folder.ts)_ +_See code: [src/commands/rename-folder.ts](https://github.com/internxt/cli/blob/v1.6.7/src/commands/rename-folder.ts)_ ## `internxt rename file` @@ -932,7 +932,7 @@ EXAMPLES $ internxt trash-clear ``` -_See code: [src/commands/trash-clear.ts](https://github.com/internxt/cli/blob/v1.6.6/src/commands/trash-clear.ts)_ +_See code: [src/commands/trash-clear.ts](https://github.com/internxt/cli/blob/v1.6.7/src/commands/trash-clear.ts)_ ## `internxt trash-file` @@ -964,7 +964,7 @@ EXAMPLES $ internxt trash-file ``` -_See code: [src/commands/trash-file.ts](https://github.com/internxt/cli/blob/v1.6.6/src/commands/trash-file.ts)_ +_See code: [src/commands/trash-file.ts](https://github.com/internxt/cli/blob/v1.6.7/src/commands/trash-file.ts)_ ## `internxt trash-folder` @@ -996,7 +996,7 @@ EXAMPLES $ internxt trash-folder ``` -_See code: [src/commands/trash-folder.ts](https://github.com/internxt/cli/blob/v1.6.6/src/commands/trash-folder.ts)_ +_See code: [src/commands/trash-folder.ts](https://github.com/internxt/cli/blob/v1.6.7/src/commands/trash-folder.ts)_ ## `internxt trash-list` @@ -1028,7 +1028,7 @@ EXAMPLES $ internxt trash-list ``` -_See code: [src/commands/trash-list.ts](https://github.com/internxt/cli/blob/v1.6.6/src/commands/trash-list.ts)_ +_See code: [src/commands/trash-list.ts](https://github.com/internxt/cli/blob/v1.6.7/src/commands/trash-list.ts)_ ## `internxt trash-restore-file` @@ -1061,7 +1061,7 @@ EXAMPLES $ internxt trash-restore-file ``` -_See code: [src/commands/trash-restore-file.ts](https://github.com/internxt/cli/blob/v1.6.6/src/commands/trash-restore-file.ts)_ +_See code: [src/commands/trash-restore-file.ts](https://github.com/internxt/cli/blob/v1.6.7/src/commands/trash-restore-file.ts)_ ## `internxt trash-restore-folder` @@ -1094,7 +1094,7 @@ EXAMPLES $ internxt trash-restore-folder ``` -_See code: [src/commands/trash-restore-folder.ts](https://github.com/internxt/cli/blob/v1.6.6/src/commands/trash-restore-folder.ts)_ +_See code: [src/commands/trash-restore-folder.ts](https://github.com/internxt/cli/blob/v1.6.7/src/commands/trash-restore-folder.ts)_ ## `internxt trash clear` @@ -1309,7 +1309,7 @@ EXAMPLES $ internxt upload-file ``` -_See code: [src/commands/upload-file.ts](https://github.com/internxt/cli/blob/v1.6.6/src/commands/upload-file.ts)_ +_See code: [src/commands/upload-file.ts](https://github.com/internxt/cli/blob/v1.6.7/src/commands/upload-file.ts)_ ## `internxt upload-folder` @@ -1342,7 +1342,7 @@ EXAMPLES $ internxt upload-folder ``` -_See code: [src/commands/upload-folder.ts](https://github.com/internxt/cli/blob/v1.6.6/src/commands/upload-folder.ts)_ +_See code: [src/commands/upload-folder.ts](https://github.com/internxt/cli/blob/v1.6.7/src/commands/upload-folder.ts)_ ## `internxt upload file` @@ -1436,7 +1436,7 @@ EXAMPLES $ internxt webdav status ``` -_See code: [src/commands/webdav.ts](https://github.com/internxt/cli/blob/v1.6.6/src/commands/webdav.ts)_ +_See code: [src/commands/webdav.ts](https://github.com/internxt/cli/blob/v1.6.7/src/commands/webdav.ts)_ ## `internxt webdav-config` @@ -1475,7 +1475,7 @@ EXAMPLES $ internxt webdav-config ``` -_See code: [src/commands/webdav-config.ts](https://github.com/internxt/cli/blob/v1.6.6/src/commands/webdav-config.ts)_ +_See code: [src/commands/webdav-config.ts](https://github.com/internxt/cli/blob/v1.6.7/src/commands/webdav-config.ts)_ ## `internxt whoami` @@ -1501,7 +1501,7 @@ EXAMPLES $ internxt whoami ``` -_See code: [src/commands/whoami.ts](https://github.com/internxt/cli/blob/v1.6.6/src/commands/whoami.ts)_ +_See code: [src/commands/whoami.ts](https://github.com/internxt/cli/blob/v1.6.7/src/commands/whoami.ts)_ ## `internxt workspaces-list` @@ -1533,7 +1533,7 @@ EXAMPLES $ internxt workspaces-list ``` -_See code: [src/commands/workspaces-list.ts](https://github.com/internxt/cli/blob/v1.6.6/src/commands/workspaces-list.ts)_ +_See code: [src/commands/workspaces-list.ts](https://github.com/internxt/cli/blob/v1.6.7/src/commands/workspaces-list.ts)_ ## `internxt workspaces-unset` @@ -1563,7 +1563,7 @@ EXAMPLES $ internxt workspaces-unset ``` -_See code: [src/commands/workspaces-unset.ts](https://github.com/internxt/cli/blob/v1.6.6/src/commands/workspaces-unset.ts)_ +_See code: [src/commands/workspaces-unset.ts](https://github.com/internxt/cli/blob/v1.6.7/src/commands/workspaces-unset.ts)_ ## `internxt workspaces-use` @@ -1599,7 +1599,7 @@ EXAMPLES $ internxt workspaces-use ``` -_See code: [src/commands/workspaces-use.ts](https://github.com/internxt/cli/blob/v1.6.6/src/commands/workspaces-use.ts)_ +_See code: [src/commands/workspaces-use.ts](https://github.com/internxt/cli/blob/v1.6.7/src/commands/workspaces-use.ts)_ ## `internxt workspaces list` diff --git a/docker/README.md b/docker/README.md index 0ee96db1..56548ad1 100644 --- a/docker/README.md +++ b/docker/README.md @@ -27,6 +27,7 @@ services: 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 + WEBDAV_KEEPALIVE_ENABLED: "" # (Optional) Auto-renew the session/WebDAV server on failure. Defaults to 'true'. Set to 'false' to stop the container on failure instead ports: - "127.0.0.1:3005:3005" # Map container port to host. Change if WEBDAV_PORT is customized ``` @@ -54,6 +55,7 @@ docker run -d \ -e WEBDAV_USERNAME="" \ -e WEBDAV_PASSWORD="" \ -e WEBDAV_DELETE_FILES_PERMANENTLY="" \ + -e WEBDAV_KEEPALIVE_ENABLED="" \ -p 127.0.0.1:3005:3005 \ internxt/webdav:latest ``` @@ -94,6 +96,7 @@ You can also run the `internxt/webdav` image directly on popular NAS devices lik | `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. | +| `WEBDAV_KEEPALIVE_ENABLED` | ❌ No | Whether the session/WebDAV server is auto-renewed on failure. Session and WebDAV status are checked every 30 min. Defaults to `true`. Set to `false` to stop the container instead of re-authenticating, so it can be recovered by Docker's restart policy (e.g. `restart: unless-stopped`). | --- diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 4c81ecf3..d325be31 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -2,14 +2,38 @@ set -e +login() { + echo "[login] Logging into your account [$INXT_USER]..." + internxt login-legacy $LOGIN_ARGS + + if [ -n "$INXT_WORKSPACE_ID" ]; then + echo "[login] Switching to workspace: $INXT_WORKSPACE_ID" + internxt workspaces use -i="$INXT_WORKSPACE_ID" + fi +} + +webdav_enable() { + internxt webdav enable +} + +session_alive() { + WHOAMI_OUTPUT=$(internxt whoami --json 2>/dev/null || true) + WHOAMI_EMAIL=$(echo "$WHOAMI_OUTPUT" | jq -r '.login.user.email // empty') + [ "$WHOAMI_EMAIL" = "$INXT_USER" ] +} + +webdav_online() { + STATUS_OUTPUT=$(internxt webdav status --json 2>/dev/null || true) + WEBDAV_STATUS=$(echo "$STATUS_OUTPUT" | jq -r '.message | split(" ") | last // empty') + [ "$WEBDAV_STATUS" = "online" ] +} + + if [ -z "$INXT_USER" ] || [ -z "$INXT_PASSWORD" ]; then echo "Error: INXT_USER and INXT_PASSWORD environment variables must be set." exit 1 fi - -echo "Logging into your account [$INXT_USER] using legacy authentication..." - LOGIN_ARGS="-x -e=$INXT_USER -p=$INXT_PASSWORD" if [ -n "$INXT_OTPTOKEN" ]; then @@ -20,13 +44,7 @@ elif [ -n "$INXT_TWOFACTORCODE" ]; then LOGIN_ARGS="$LOGIN_ARGS -w=$INXT_TWOFACTORCODE" fi -internxt login-legacy $LOGIN_ARGS - -if [ -n "$INXT_WORKSPACE_ID" ]; then - echo "Switching to workspace: $INXT_WORKSPACE_ID" - internxt workspaces use -i="$INXT_WORKSPACE_ID" -fi - +login WEBDAV_ARGS="-l=0.0.0.0" @@ -59,8 +77,54 @@ fi internxt webdav-config $WEBDAV_ARGS -internxt webdav enable +webdav_enable mkdir -p /root/.internxt-cli/logs touch /root/.internxt-cli/logs/internxt-webdav-combined.log -tail -f /root/.internxt-cli/logs/internxt-webdav-combined.log + +# Stream the WebDAV server log to the container's stdout in the background. +tail -f /root/.internxt-cli/logs/internxt-webdav-combined.log & + +# Keep-alive loop (runs in the foreground, as PID 1): periodically verify the +# session and WebDAV server, and re-authenticate if needed. +# WEBDAV_KEEPALIVE_ENABLED controls what happens on failure: when enabled (default), +# it self-heals by re-authenticating. +# When disabled, checks still run, but instead of self-healing, this process +# exits on failure so that Docker's restart policy (e.g. --restart unless-stopped) +# can recover the container with a clean login. +set +e +KEEPALIVE_INTERVAL=1800 + +keepaliveEnabled=$(echo "$WEBDAV_KEEPALIVE_ENABLED" | tr '[:upper:]' '[:lower:]') +if [ -z "$WEBDAV_KEEPALIVE_ENABLED" ] || [ "$keepaliveEnabled" = "true" ] || [ "$keepaliveEnabled" = "1" ] || [ "$keepaliveEnabled" = "yes" ] || [ "$keepaliveEnabled" = "y" ]; then + AUTO_HEAL=true +else + AUTO_HEAL=false +fi + +while true; do + sleep "$KEEPALIVE_INTERVAL" + + if ! session_alive; then + if [ "$AUTO_HEAL" = true ]; then + echo "[keepalive] Session expired. Re-authenticating..." + login + webdav_enable + echo "[keepalive] Session restored." + else + echo "[keepalive] ERROR: Session expired and WEBDAV_KEEPALIVE_ENABLED=false (auto-renewal disabled). Stopping container so it can be restarted." + exit 1 + fi + fi + + if ! webdav_online; then + if [ "$AUTO_HEAL" = true ]; then + echo "[keepalive] WebDAV server is not online. Re-enabling..." + webdav_enable + echo "[keepalive] WebDAV server re-enabled." + else + echo "[keepalive] ERROR: WebDAV server is not online and WEBDAV_KEEPALIVE_ENABLED=false (auto-renewal disabled). Stopping container so it can be restarted." + exit 1 + fi + fi +done diff --git a/package-lock.json b/package-lock.json index b982af0f..cd05dd0c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,22 +1,22 @@ { "name": "@internxt/cli", - "version": "1.6.6", + "version": "1.6.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@internxt/cli", - "version": "1.6.6", + "version": "1.6.7", "hasInstallScript": true, "license": "MIT", "dependencies": { "@dashlane/pqc-kem-kyber512-node": "1.0.0", "@inquirer/prompts": "8.5.2", "@internxt/inxt-js": "3.3.1", - "@internxt/lib": "1.4.2", - "@internxt/sdk": "1.17.12", - "@oclif/core": "4.11.10", - "@oclif/plugin-autocomplete": "3.2.52", + "@internxt/lib": "1.5.1", + "@internxt/sdk": "1.17.13", + "@oclif/core": "4.11.14", + "@oclif/plugin-autocomplete": "3.2.53", "axios": "1.18.1", "better-sqlite3": "12.11.1", "bip39": "3.1.0", @@ -26,7 +26,7 @@ "dotenv": "17.4.2", "express": "5.2.1", "express-async-handler": "1.2.0", - "fast-xml-builder": "1.2.0", + "fast-xml-builder": "1.2.1", "fast-xml-parser": "5.9.3", "hash-wasm": "4.12.0", "mime-types": "3.0.2", @@ -34,7 +34,7 @@ "openpgp": "6.3.1", "otpauth": "9.5.1", "pm2": "7.0.3", - "range-parser": "1.2.1", + "range-parser": "1.3.0", "selfsigned": "5.5.0", "tty-table": "5.0.0", "typeorm": "1.0.0", @@ -50,10 +50,10 @@ "@types/cli-progress": "^3.11.6", "@types/express": "^5.0.6", "@types/mime-types": "^3.0.1", - "@types/node": "^26.0.0", + "@types/node": "^26.1.1", "@types/range-parser": "^1.2.7", - "@vitest/coverage-istanbul": "^4.1.9", - "@vitest/spy": "^4.1.9", + "@vitest/coverage-istanbul": "^4.1.10", + "@vitest/spy": "^4.1.10", "eslint": "^10.6.0", "husky": "^9.1.7", "lint-staged": "^17.0.8", @@ -64,7 +64,7 @@ "sql.js": "^1.14.1", "ts-node": "^10.9.2", "typescript": "^6.0.3", - "vitest": "^4.1.9", + "vitest": "^4.1.10", "vitest-mock-express": "^2.2.0" }, "engines": { @@ -734,14 +734,14 @@ } }, "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.2.1", + "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, @@ -756,9 +756,9 @@ } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", "dev": true, "license": "MIT", "optional": true, @@ -1881,9 +1881,9 @@ } }, "node_modules/@internxt/lib": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@internxt/lib/-/lib-1.4.2.tgz", - "integrity": "sha512-3eYwSSl3NMwCyq+W8QkOrV0V0tCAcDfzmo8JH6BwU+rd4fMmJvriOnbFu9cKLJmXLk4GhCDEL5cr8rpFt4cAZQ==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@internxt/lib/-/lib-1.5.1.tgz", + "integrity": "sha512-Zt6ZRJ4B3N6zgYE7kBtVSiBXNWLV2F6QgK27IvWCg+6C2qoWDwy6uN1niMzHgSi3KUSTKOB4jiu42wJUYj4LXw==", "license": "MIT" }, "node_modules/@internxt/prettier-config": { @@ -1894,9 +1894,9 @@ "license": "MIT" }, "node_modules/@internxt/sdk": { - "version": "1.17.12", - "resolved": "https://registry.npmjs.org/@internxt/sdk/-/sdk-1.17.12.tgz", - "integrity": "sha512-HDo+NR+k1sUify2XyiwnAwEUuejJF1fJYg4DUFPHp/0YIvrrEecKe+oSY1HY+hTce8UMMvFnlLLkk1/wA95PSg==", + "version": "1.17.13", + "resolved": "https://registry.npmjs.org/@internxt/sdk/-/sdk-1.17.13.tgz", + "integrity": "sha512-PZm384nnkGKbDBsqsaik9xQ7zP1RPZUWwSobUH3PgF6pLx8Yz9SJLbWl43No/HHATrIkz0V2Tss4OAfZaJr7cg==", "license": "MIT", "dependencies": { "axios": "^1.16.0" @@ -1963,14 +1963,14 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", - "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@tybys/wasm-util": "^0.10.2" + "@tybys/wasm-util": "^0.10.3" }, "funding": { "type": "github", @@ -2006,9 +2006,9 @@ "license": "MIT" }, "node_modules/@oclif/core": { - "version": "4.11.10", - "resolved": "https://registry.npmjs.org/@oclif/core/-/core-4.11.10.tgz", - "integrity": "sha512-kbzi5ZfWKYXZzUldAiJMoxVyXaBnMZqoIVDdHJs4DD7T9wg6ADWU5Ale+9XYfysScAt4Og9psyCEPCzIe10sEQ==", + "version": "4.11.14", + "resolved": "https://registry.npmjs.org/@oclif/core/-/core-4.11.14.tgz", + "integrity": "sha512-cZ5Ktd+rT0PO+o7KBH4vRFTgg+xMLf8F41WK39p8MkXEViZA/Qqe+4lzZT6102zgUxMORET1HtF9t5w8CB3tnQ==", "license": "MIT", "dependencies": { "ansi-escapes": "^4.3.2", @@ -2035,9 +2035,9 @@ } }, "node_modules/@oclif/plugin-autocomplete": { - "version": "3.2.52", - "resolved": "https://registry.npmjs.org/@oclif/plugin-autocomplete/-/plugin-autocomplete-3.2.52.tgz", - "integrity": "sha512-SZaTawQ5ekM4KZCHNxZ0aUNcZ81q4+zyMFKnWeKqpXOIL1ypqvFwRNdxP3Rj3YGEXYoLxrNNW+XOuoqriaBRTg==", + "version": "3.2.53", + "resolved": "https://registry.npmjs.org/@oclif/plugin-autocomplete/-/plugin-autocomplete-3.2.53.tgz", + "integrity": "sha512-cGAgN9ujTDxa6C84d6XNVsmoFnDFfHwdiykAnAfym3oBLxoXBNYMZhmfnZ8KBWDy/r/DYf11BrGJwqKl2P0iSA==", "license": "MIT", "dependencies": { "@oclif/core": "^4", @@ -2517,9 +2517,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.133.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", - "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "version": "0.138.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.138.0.tgz", + "integrity": "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==", "dev": true, "license": "MIT", "funding": { @@ -2792,9 +2792,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", - "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.4.tgz", + "integrity": "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==", "cpu": [ "arm64" ], @@ -2809,9 +2809,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", - "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.4.tgz", + "integrity": "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==", "cpu": [ "arm64" ], @@ -2826,9 +2826,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", - "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.4.tgz", + "integrity": "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==", "cpu": [ "x64" ], @@ -2843,9 +2843,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", - "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.4.tgz", + "integrity": "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==", "cpu": [ "x64" ], @@ -2860,9 +2860,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", - "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.4.tgz", + "integrity": "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==", "cpu": [ "arm" ], @@ -2877,9 +2877,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", - "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.4.tgz", + "integrity": "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==", "cpu": [ "arm64" ], @@ -2897,9 +2897,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", - "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.4.tgz", + "integrity": "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==", "cpu": [ "arm64" ], @@ -2917,9 +2917,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", - "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.4.tgz", + "integrity": "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==", "cpu": [ "ppc64" ], @@ -2937,9 +2937,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", - "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.4.tgz", + "integrity": "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==", "cpu": [ "s390x" ], @@ -2957,9 +2957,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", - "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.4.tgz", + "integrity": "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==", "cpu": [ "x64" ], @@ -2977,9 +2977,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", - "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.4.tgz", + "integrity": "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==", "cpu": [ "x64" ], @@ -2997,9 +2997,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", - "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.4.tgz", + "integrity": "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==", "cpu": [ "arm64" ], @@ -3014,9 +3014,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", - "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.4.tgz", + "integrity": "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==", "cpu": [ "wasm32" ], @@ -3024,29 +3024,18 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "1.10.0", - "@emnapi/runtime": "1.10.0", - "@napi-rs/wasm-runtime": "^1.1.4" + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" }, "engines": { "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", - "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.4.tgz", + "integrity": "sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==", "cpu": [ "arm64" ], @@ -3061,9 +3050,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", - "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.4.tgz", + "integrity": "sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==", "cpu": [ "x64" ], @@ -3255,9 +3244,9 @@ "license": "MIT" }, "node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", - "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", "dev": true, "license": "MIT", "optional": true, @@ -3399,9 +3388,9 @@ } }, "node_modules/@types/node": { - "version": "26.0.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.0.tgz", - "integrity": "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==", + "version": "26.1.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", + "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", "devOptional": true, "license": "MIT", "dependencies": { @@ -3687,9 +3676,9 @@ } }, "node_modules/@vitest/coverage-istanbul": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/coverage-istanbul/-/coverage-istanbul-4.1.9.tgz", - "integrity": "sha512-4a7DsIwycTf4eYwEDtnMfMV8H80KSKH9PuMHhqL5SwPZzDyUKq2X/TPCVZ7NqIuSz7UbZckmEmkip6iZBI/gEA==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/coverage-istanbul/-/coverage-istanbul-4.1.10.tgz", + "integrity": "sha512-AyNJ5pQRFqCX7pwB9PSTmoVKPaZ4H5IEVJfJsT+q1DYkXvZMEFYgJlyk5sfStmt9rVYRyYYRRsuBeImCOc39ww==", "dev": true, "license": "MIT", "dependencies": { @@ -3708,20 +3697,20 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "4.1.9" + "vitest": "4.1.10" } }, "node_modules/@vitest/expect": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", - "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.9", - "@vitest/utils": "4.1.9", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -3730,13 +3719,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", - "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.9", + "@vitest/spy": "4.1.10", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -3757,9 +3746,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", - "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", "dev": true, "license": "MIT", "dependencies": { @@ -3770,13 +3759,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", - "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.9", + "@vitest/utils": "4.1.10", "pathe": "^2.0.3" }, "funding": { @@ -3784,14 +3773,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", - "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.9", - "@vitest/utils": "4.1.9", + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -3800,9 +3789,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", - "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", "dev": true, "license": "MIT", "funding": { @@ -3810,13 +3799,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", - "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.9", + "@vitest/pretty-format": "4.1.10", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -5559,9 +5548,9 @@ } }, "node_modules/es-module-lexer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", - "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.0.tgz", + "integrity": "sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==", "dev": true, "license": "MIT" }, @@ -5895,9 +5884,9 @@ } }, "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -6026,9 +6015,9 @@ } }, "node_modules/fast-xml-builder": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", - "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.1.tgz", + "integrity": "sha512-tPb5TTWfgfVx5BNSi2xV0eLr89POeXXn0dXIsCJ9m1narrWxeIyx6je9d7Rce/3NyXLbvuQmLkxq+RuxMWejvw==", "funding": [ { "type": "github", @@ -8531,9 +8520,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", "dev": true, "funding": [ { @@ -8900,54 +8889,6 @@ "node": ">=18" } }, - "node_modules/oclif/node_modules/@oclif/core": { - "version": "4.11.14", - "resolved": "https://registry.npmjs.org/@oclif/core/-/core-4.11.14.tgz", - "integrity": "sha512-cZ5Ktd+rT0PO+o7KBH4vRFTgg+xMLf8F41WK39p8MkXEViZA/Qqe+4lzZT6102zgUxMORET1HtF9t5w8CB3tnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-escapes": "^4.3.2", - "ansis": "^3.17.0", - "clean-stack": "^3.0.1", - "cli-spinners": "^2.9.2", - "debug": "^4.4.3", - "ejs": "^3.1.10", - "get-package-type": "^0.1.0", - "indent-string": "^4.0.0", - "is-wsl": "^2.2.0", - "lilconfig": "^3.1.3", - "minimatch": "^10.2.5", - "semver": "^7.8.1", - "string-width": "^4.2.3", - "supports-color": "^8", - "tinyglobby": "^0.2.17", - "widest-line": "^3.1.0", - "wordwrap": "^1.0.0", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/oclif/node_modules/@oclif/core/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/oclif/node_modules/@types/node": { "version": "22.19.21", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.21.tgz", @@ -9557,9 +9498,9 @@ } }, "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", "dev": true, "funding": [ { @@ -9809,12 +9750,16 @@ } }, "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", "license": "MIT", "engines": { "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/raw-body": { @@ -10039,13 +9984,13 @@ } }, "node_modules/rolldown": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", - "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz", + "integrity": "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.133.0", + "@oxc-project/types": "=0.138.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -10055,21 +10000,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.3", - "@rolldown/binding-darwin-arm64": "1.0.3", - "@rolldown/binding-darwin-x64": "1.0.3", - "@rolldown/binding-freebsd-x64": "1.0.3", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", - "@rolldown/binding-linux-arm64-gnu": "1.0.3", - "@rolldown/binding-linux-arm64-musl": "1.0.3", - "@rolldown/binding-linux-ppc64-gnu": "1.0.3", - "@rolldown/binding-linux-s390x-gnu": "1.0.3", - "@rolldown/binding-linux-x64-gnu": "1.0.3", - "@rolldown/binding-linux-x64-musl": "1.0.3", - "@rolldown/binding-openharmony-arm64": "1.0.3", - "@rolldown/binding-wasm32-wasi": "1.0.3", - "@rolldown/binding-win32-arm64-msvc": "1.0.3", - "@rolldown/binding-win32-x64-msvc": "1.0.3" + "@rolldown/binding-android-arm64": "1.1.4", + "@rolldown/binding-darwin-arm64": "1.1.4", + "@rolldown/binding-darwin-x64": "1.1.4", + "@rolldown/binding-freebsd-x64": "1.1.4", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.4", + "@rolldown/binding-linux-arm64-gnu": "1.1.4", + "@rolldown/binding-linux-arm64-musl": "1.1.4", + "@rolldown/binding-linux-ppc64-gnu": "1.1.4", + "@rolldown/binding-linux-s390x-gnu": "1.1.4", + "@rolldown/binding-linux-x64-gnu": "1.1.4", + "@rolldown/binding-linux-x64-musl": "1.1.4", + "@rolldown/binding-openharmony-arm64": "1.1.4", + "@rolldown/binding-wasm32-wasi": "1.1.4", + "@rolldown/binding-win32-arm64-msvc": "1.1.4", + "@rolldown/binding-win32-x64-msvc": "1.1.4" } }, "node_modules/router": { @@ -10916,9 +10861,9 @@ } }, "node_modules/std-env": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", - "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", "dev": true, "license": "MIT" }, @@ -11946,16 +11891,16 @@ } }, "node_modules/vite": { - "version": "8.0.16", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", - "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz", + "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", - "postcss": "^8.5.15", - "rolldown": "1.0.3", + "postcss": "^8.5.16", + "rolldown": "~1.1.3", "tinyglobby": "^0.2.17" }, "bin": { @@ -11972,7 +11917,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.18", + "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", @@ -12024,19 +11969,19 @@ } }, "node_modules/vitest": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", - "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.9", - "@vitest/mocker": "4.1.9", - "@vitest/pretty-format": "4.1.9", - "@vitest/runner": "4.1.9", - "@vitest/snapshot": "4.1.9", - "@vitest/spy": "4.1.9", - "@vitest/utils": "4.1.9", + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -12064,12 +12009,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.9", - "@vitest/browser-preview": "4.1.9", - "@vitest/browser-webdriverio": "4.1.9", - "@vitest/coverage-istanbul": "4.1.9", - "@vitest/coverage-v8": "4.1.9", - "@vitest/ui": "4.1.9", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" diff --git a/package.json b/package.json index 2d82c333..6c4ac375 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "author": "Internxt ", - "version": "1.6.6", + "version": "1.6.7", "description": "Internxt CLI to manage your encrypted storage", "scripts": { "build": "npm run clean && tsc", @@ -39,10 +39,10 @@ "@dashlane/pqc-kem-kyber512-node": "1.0.0", "@inquirer/prompts": "8.5.2", "@internxt/inxt-js": "3.3.1", - "@internxt/lib": "1.4.2", - "@internxt/sdk": "1.17.12", - "@oclif/core": "4.11.10", - "@oclif/plugin-autocomplete": "3.2.52", + "@internxt/lib": "1.5.1", + "@internxt/sdk": "1.17.13", + "@oclif/core": "4.11.14", + "@oclif/plugin-autocomplete": "3.2.53", "axios": "1.18.1", "better-sqlite3": "12.11.1", "bip39": "3.1.0", @@ -52,7 +52,7 @@ "dotenv": "17.4.2", "express": "5.2.1", "express-async-handler": "1.2.0", - "fast-xml-builder": "1.2.0", + "fast-xml-builder": "1.2.1", "fast-xml-parser": "5.9.3", "hash-wasm": "4.12.0", "mime-types": "3.0.2", @@ -60,7 +60,7 @@ "openpgp": "6.3.1", "otpauth": "9.5.1", "pm2": "7.0.3", - "range-parser": "1.2.1", + "range-parser": "1.3.0", "selfsigned": "5.5.0", "tty-table": "5.0.0", "typeorm": "1.0.0", @@ -73,10 +73,10 @@ "@types/cli-progress": "^3.11.6", "@types/express": "^5.0.6", "@types/mime-types": "^3.0.1", - "@types/node": "^26.0.0", + "@types/node": "^26.1.1", "@types/range-parser": "^1.2.7", - "@vitest/coverage-istanbul": "^4.1.9", - "@vitest/spy": "^4.1.9", + "@vitest/coverage-istanbul": "^4.1.10", + "@vitest/spy": "^4.1.10", "eslint": "^10.6.0", "husky": "^9.1.7", "lint-staged": "^17.0.8", @@ -87,7 +87,7 @@ "sql.js": "^1.14.1", "ts-node": "^10.9.2", "typescript": "^6.0.3", - "vitest": "^4.1.9", + "vitest": "^4.1.10", "vitest-mock-express": "^2.2.0" }, "optionalDependencies": { diff --git a/src/commands/list.ts b/src/commands/list.ts index 3c3bf437..7db0978f 100644 --- a/src/commands/list.ts +++ b/src/commands/list.ts @@ -47,7 +47,8 @@ export default class List extends Command { name: folder.plainName, id: folder.uuid, size: '-', - modified: FormatUtils.formatDate(folder.updatedAt), + created: FormatUtils.formatDate(folder.creationTime ?? folder.createdAt), + modified: FormatUtils.formatDate(folder.modificationTime ?? folder.updatedAt), }; }), ...files.map((file) => { @@ -56,7 +57,8 @@ export default class List extends Command { name: file.type && file.type.length > 0 ? `${file.plainName}.${file.type}` : file.plainName, id: file.uuid, size: FormatUtils.humanFileSize(Number(file.size)), - modified: FormatUtils.formatDate(file.updatedAt), + created: FormatUtils.formatDate(file.creationTime ?? file.createdAt), + modified: FormatUtils.formatDate(file.modificationTime ?? file.updatedAt), }; }), ]; @@ -66,7 +68,11 @@ export default class List extends Command { { value: 'id', alias: 'Id' }, ]; if (flags.extended) { - headers.push({ value: 'modified', alias: 'Modified' }, { value: 'size', alias: 'Size' }); + headers.push( + { value: 'created', alias: 'Created' }, + { value: 'modified', alias: 'Modified' }, + { value: 'size', alias: 'Size' }, + ); } CLIUtils.table(this.log.bind(this), headers, allItems); diff --git a/src/commands/trash-list.ts b/src/commands/trash-list.ts index 67e05e1d..faa7d8ce 100644 --- a/src/commands/trash-list.ts +++ b/src/commands/trash-list.ts @@ -36,7 +36,8 @@ export default class TrashList extends Command { name: folder.plainName, id: folder.uuid, size: '-', - modified: FormatUtils.formatDate(folder.updatedAt), + created: FormatUtils.formatDate(folder.creationTime ?? folder.createdAt), + modified: FormatUtils.formatDate(folder.modificationTime ?? folder.updatedAt), }; }), ...files.map((file) => { @@ -45,7 +46,8 @@ export default class TrashList extends Command { name: file.type && file.type.length > 0 ? `${file.plainName}.${file.type}` : file.plainName, id: file.uuid, size: FormatUtils.humanFileSize(Number(file.size)), - modified: FormatUtils.formatDate(file.updatedAt), + created: FormatUtils.formatDate(file.creationTime ?? file.createdAt), + modified: FormatUtils.formatDate(file.modificationTime ?? file.updatedAt), }; }), ]; @@ -55,7 +57,11 @@ export default class TrashList extends Command { { value: 'id', alias: 'Id' }, ]; if (flags.extended) { - headers.push({ value: 'modified', alias: 'Modified' }, { value: 'size', alias: 'Size' }); + headers.push( + { value: 'created', alias: 'Created' }, + { value: 'modified', alias: 'Modified' }, + { value: 'size', alias: 'Size' }, + ); } CLIUtils.table(this.log.bind(this), headers, allItems); diff --git a/src/commands/whoami.ts b/src/commands/whoami.ts index b5c441b1..2670412c 100644 --- a/src/commands/whoami.ts +++ b/src/commands/whoami.ts @@ -4,6 +4,7 @@ import { ConfigService } from '../services/config.service'; import { ValidationService } from '../services/validation.service'; import { LoginCredentials } from '../types/command.types'; import { AuthService } from '../services/auth.service'; +import { TokenStatus } from '@internxt/lib'; export default class Whoami extends Command { static readonly args = {}; @@ -64,10 +65,12 @@ export default class Whoami extends Command { if (!(loginCreds?.token && loginCreds?.user?.mnemonic)) { return { valid: false, refreshRequired: false }; } - const tokenDetails = ValidationService.instance.validateTokenAndCheckExpiration(loginCreds.token); + const tokenStatus = ValidationService.instance.validateTokenAndCheckExpiration(loginCreds.token); const goodMnemonic = ValidationService.instance.validateMnemonic(loginCreds.user.mnemonic); - const goodToken = tokenDetails.isValid && !tokenDetails.expiration.expired; - return { valid: goodToken && goodMnemonic, refreshRequired: tokenDetails.expiration.refreshRequired }; + const isRefreshRequired = tokenStatus === TokenStatus.REFRESH_REQUIRED; + const isTokenValid = tokenStatus === TokenStatus.VALID || isRefreshRequired; + + return { valid: isTokenValid && goodMnemonic, refreshRequired: isRefreshRequired }; }; } diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 01320c12..4e449a26 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -12,6 +12,7 @@ import { } from '../types/command.types'; import { ValidationService } from './validation.service'; import { WorkspaceService } from './drive/workspace.service'; +import { TokenStatus } from '@internxt/lib'; export class AuthService { public static readonly instance: AuthService = new AuthService(); @@ -85,19 +86,19 @@ export class AuthService { throw new MissingCredentialsError(); } - const tokenDetails = ValidationService.instance.validateTokenAndCheckExpiration(loginCreds.token); + const tokenStatus = ValidationService.instance.validateTokenAndCheckExpiration(loginCreds.token); const isValidMnemonic = ValidationService.instance.validateMnemonic(loginCreds.user.mnemonic); - if (!tokenDetails.isValid || !isValidMnemonic) { + if (tokenStatus === TokenStatus.INVALID || !isValidMnemonic) { throw new InvalidCredentialsError(); } - if (tokenDetails.expiration.expired) { + if (tokenStatus === TokenStatus.EXPIRED) { throw new ExpiredCredentialsError(); } let credentialsChanged = false; - if (tokenDetails.expiration.refreshRequired) { + if (tokenStatus === TokenStatus.REFRESH_REQUIRED) { try { loginCreds = await this.refreshUserToken( loginCreds.token, @@ -180,16 +181,15 @@ export class AuthService { if (loginCreds.workspace?.workspaceCredentials && loginCreds.workspace?.workspaceData) { const workspaceToken = loginCreds.workspace.workspaceCredentials.token; const workspaceUuid = loginCreds.workspace.workspaceCredentials.id; - const workspaceTokenDetails = ValidationService.instance.validateTokenAndCheckExpiration(workspaceToken); + const workspaceTokenStatus = ValidationService.instance.validateTokenAndCheckExpiration(workspaceToken); - if (!workspaceTokenDetails.isValid) { + if (workspaceTokenStatus === TokenStatus.VALID) { + return loginCreds.workspace; + } else if (workspaceTokenStatus === TokenStatus.INVALID) { throw new InvalidCredentialsError(); - } - if (workspaceTokenDetails.expiration.expired) { + } else if (workspaceTokenStatus === TokenStatus.EXPIRED) { throw new ExpiredCredentialsError(); - } - - if (workspaceTokenDetails.expiration.refreshRequired) { + } else if (workspaceTokenStatus === TokenStatus.REFRESH_REQUIRED) { SdkManager.init({ token: loginCreds.token, workspaceToken: loginCreds.workspace.workspaceCredentials.token }); const workspaceCredentials = await WorkspaceService.instance.getWorkspaceCredentials(workspaceUuid); // TODO [PB-5788] Refresh workspace data when workspace token requires refresh @@ -197,8 +197,6 @@ export class AuthService { workspaceCredentials, workspaceData: loginCreds.workspace.workspaceData, }; - } else { - return loginCreds.workspace; } } }; diff --git a/src/services/validation.service.ts b/src/services/validation.service.ts index 0b228625..42f2df1b 100644 --- a/src/services/validation.service.ts +++ b/src/services/validation.service.ts @@ -1,4 +1,4 @@ -import { auth } from '@internxt/lib'; +import { auth, TokenStatus } from '@internxt/lib'; import { validateMnemonic } from 'bip39'; import fs from 'node:fs/promises'; @@ -51,63 +51,7 @@ export class ValidationService { } }; - /** - * Validates JWT token structure and parses the expiration claim. - * Does not verify signature or issuer. - * @returns Expiration timestamp in seconds, or null if invalid structure - */ - public validateJwtAndCheckExpiration = (token?: string): number | null => { - if (!token || typeof token !== 'string' || token.split('.').length !== 3) { - return null; - } - - try { - const payload = JSON.parse(atob(token.split('.')[1])); - return typeof payload.exp === 'number' ? payload.exp : null; - } catch { - return null; - } - }; - - /** - * Checks token expiration status. - * @param expirationTimestamp - Unix timestamp in seconds - * @returns Object indicating if token is expired or needs refresh (within 2 days) - */ - public checkTokenExpiration = ( - expirationTimestamp: number, - ): { - expired: boolean; - refreshRequired: boolean; - } => { - const TWO_DAYS_IN_SECONDS = 2 * 24 * 60 * 60; - const currentTime = Math.floor(Date.now() / 1000); - const remainingSeconds = expirationTimestamp - currentTime; - - return { - expired: remainingSeconds <= 0, - refreshRequired: remainingSeconds > 0 && remainingSeconds <= TWO_DAYS_IN_SECONDS, - }; - }; - - /** - * Combined validation and expiration check for convenience. - * For the original combined behavior, use this method. - * For more granular control, use parseJwtExpiration + checkTokenExpiration separately. - */ - public validateTokenAndCheckExpiration = ( - token?: string, - ): { - isValid: boolean; - expiration: { - expired: boolean; - refreshRequired: boolean; - }; - } => { - const expiration = this.validateJwtAndCheckExpiration(token); - return { - isValid: expiration !== null, - expiration: expiration ? this.checkTokenExpiration(expiration) : { expired: true, refreshRequired: false }, - }; + public validateTokenAndCheckExpiration = (token: string): TokenStatus => { + return auth.validateTokenAndCheckExpiration(token); }; } diff --git a/src/types/command.types.ts b/src/types/command.types.ts index 259b47fd..9b7111a6 100644 --- a/src/types/command.types.ts +++ b/src/types/command.types.ts @@ -215,6 +215,7 @@ export interface PaginatedItem { type: string; id: string; size: string; + created: string; modified: string; } diff --git a/src/utils/drive.utils.ts b/src/utils/drive.utils.ts index 1278f48c..fef2d105 100644 --- a/src/utils/drive.utils.ts +++ b/src/utils/drive.utils.ts @@ -2,13 +2,20 @@ import { FileMeta, FolderMeta, CreateFolderResponse, FileStatus } from '@internx import { DriveFileItem, DriveFolderItem } from '../types/drive.types'; export class DriveUtils { + // WebDAV clients parse getcontentlength/Content-Length as an integer, a literal + // "NaN" in the response breaks them, so any non-numeric size degrades to 0. + static parseFileSize(size: string | number): number { + const parsed = Number(size); + return Number.isFinite(parsed) ? parsed : 0; + } + static driveFileMetaToItem(fileMeta: FileMeta): DriveFileItem { return { itemType: 'file', uuid: fileMeta.uuid ?? '', status: fileMeta.status, folderUuid: fileMeta.folderUuid, - size: Number(fileMeta.size), + size: DriveUtils.parseFileSize(fileMeta.size), name: fileMeta.plainName ?? fileMeta.name, bucket: fileMeta.bucket, createdAt: new Date(fileMeta.createdAt), diff --git a/src/utils/format.utils.ts b/src/utils/format.utils.ts index a976ac5e..cfeca3f7 100644 --- a/src/utils/format.utils.ts +++ b/src/utils/format.utils.ts @@ -11,11 +11,15 @@ export class FormatUtils { }; static readonly formatDate = (date: string | Date) => { - return dayjs(date).format('D MMMM, YYYY [at] HH:mm'); + const d = dayjs(date); + return d.isValid() ? d.format('D MMMM, YYYY [at] HH:mm') : '-'; }; static readonly formatDateForWebDav = (date: string | Date) => { - return dayjs.utc(date).format('ddd, DD MMM YYYY HH:mm:ss [GMT]'); + const d = dayjs.utc(date); + return d.isValid() + ? d.format('ddd, DD MMM YYYY HH:mm:ss [GMT]') + : dayjs.utc().format('ddd, DD MMM YYYY HH:mm:ss [GMT]'); }; static readonly formatLimit = (limit: number): string => { diff --git a/src/utils/network.utils.ts b/src/utils/network.utils.ts index 0edcc765..88049ea9 100644 --- a/src/utils/network.utils.ts +++ b/src/utils/network.utils.ts @@ -102,9 +102,9 @@ export class NetworkUtils { throw new Error(`Unkwnown Range-Request type "${parsed.type}". ${JSON.stringify(rangeOptions)}`); } } else if (parsed === -1) { - throw new Error(`Malformed Range-Request. ${JSON.stringify(rangeOptions)}`); - } else if (parsed === -2) { throw new Error(`Unsatisfiable Range-Request. ${JSON.stringify(rangeOptions)}`); + } else if (parsed === -2) { + throw new Error(`Malformed Range-Request. ${JSON.stringify(rangeOptions)}`); } else { throw new Error(`Unknown error from Range-Request. ${JSON.stringify(rangeOptions)}`); } diff --git a/src/utils/pm2.utils.ts b/src/utils/pm2.utils.ts index 85ad0190..7585995c 100644 --- a/src/utils/pm2.utils.ts +++ b/src/utils/pm2.utils.ts @@ -2,14 +2,7 @@ import path from 'node:path'; import pm2 from 'pm2'; export type WebDavProcessStatus = - | 'online' - | 'stopping' - | 'stopped' - | 'launching' - | 'errored' - | 'one-launch-status' - | 'offline' - | 'unknown'; + 'online' | 'stopping' | 'stopped' | 'launching' | 'errored' | 'one-launch-status' | 'offline' | 'unknown'; export class PM2Utils { private static readonly WEBDAV_APP_NAME = 'Internxt CLI WebDav'; diff --git a/src/webdav/handlers/GET.handler.ts b/src/webdav/handlers/GET.handler.ts index fddd7063..1e28fdf0 100644 --- a/src/webdav/handlers/GET.handler.ts +++ b/src/webdav/handlers/GET.handler.ts @@ -27,6 +27,7 @@ export class GETRequestHandler implements WebDavMethodHandler { webdavLogger.info(`[GET] [${driveFile.uuid}] Network ready for download`); res.header('Content-Type', 'application/octet-stream'); + res.header('Accept-Ranges', 'bytes'); const fileSize = driveFile.size ?? 0; diff --git a/src/webdav/handlers/HEAD.handler.ts b/src/webdav/handlers/HEAD.handler.ts index 2f78a73b..52c997be 100644 --- a/src/webdav/handlers/HEAD.handler.ts +++ b/src/webdav/handlers/HEAD.handler.ts @@ -32,6 +32,7 @@ export class HEADRequestHandler implements WebDavMethodHandler { } res.header('Content-Type', 'application/octet-stream'); + res.header('Accept-Ranges', 'bytes'); res.header('Content-length', contentLength.toString()); } diff --git a/src/webdav/handlers/MKCOL.handler.ts b/src/webdav/handlers/MKCOL.handler.ts index 6e7d017b..904d9713 100644 --- a/src/webdav/handlers/MKCOL.handler.ts +++ b/src/webdav/handlers/MKCOL.handler.ts @@ -6,6 +6,7 @@ import { webdavLogger } from '../../utils/logger.utils'; import { XMLUtils } from '../../utils/xml.utils'; import { WebDavFolderService } from '../../services/webdav/webdav-folder.service'; import { AsyncUtils } from '../../utils/async.utils'; +import { MethodNotAllowed } from '../../utils/errors.utils'; export class MKCOLRequestHandler implements WebDavMethodHandler { handle = async (req: Request, res: Response) => { @@ -22,9 +23,10 @@ export class MKCOLRequestHandler implements WebDavMethodHandler { const folderAlreadyExists = !!driveFolderItem; if (folderAlreadyExists) { - webdavLogger.info(`[MKCOL] Folder '${resource.url}' already exists, ignoring the creation request`); - res.status(200).send(XMLUtils.toWebDavXML({}, {})); - return; + webdavLogger.info(`[MKCOL] Folder '${resource.url}' already exists, throwing 405 error`); + // RFC 4918: MKCOL on an existing resource must fail with 405. Clients like + // QNAP HBS may abort the whole job when they receive a 2xx here. + throw new MethodNotAllowed('Folder already exists'); } const newFolder = await WebDavFolderService.instance.createFolder({ diff --git a/src/webdav/handlers/PROPFIND.handler.ts b/src/webdav/handlers/PROPFIND.handler.ts index 3802fae9..7db8dcac 100644 --- a/src/webdav/handlers/PROPFIND.handler.ts +++ b/src/webdav/handlers/PROPFIND.handler.ts @@ -4,6 +4,7 @@ import { DriveFileItem, DriveFolderItem } from '../../types/drive.types'; import { DriveItemBD } from '../../services/database/drive-item/drive-item.domain'; import { DriveItemRepository } from '../../services/database/drive-item/drive-item.repository'; import { DriveFolderService } from '../../services/drive/drive-folder.service'; +import { DriveUtils } from '../../utils/drive.utils'; import { FormatUtils } from '../../utils/format.utils'; import { Request, Response } from 'express'; import { randomUUID } from 'node:crypto'; @@ -24,6 +25,8 @@ export class PROPFINDRequestHandler implements WebDavMethodHandler { return; } + res.set('Content-Type', 'application/xml; charset="utf-8"'); + switch (driveItem.itemType) { case 'file': { const fileMetaXML = await this.getFileMetaXML(resource, driveItem); @@ -144,7 +147,7 @@ export class PROPFINDRequestHandler implements WebDavMethodHandler { type: file.type, status: file.status, folderUuid: file.folderUuid, - size: Number(file.size), + size: DriveUtils.parseFileSize(file.size), creationTime: new Date(file.creationTime), modificationTime: new Date(file.modificationTime), createdAt: new Date(file.createdAt), @@ -184,7 +187,8 @@ export class PROPFINDRequestHandler implements WebDavMethodHandler { [XMLUtils.addDefaultNamespace('propstat')]: { [XMLUtils.addDefaultNamespace('status')]: 'HTTP/1.1 200 OK', [XMLUtils.addDefaultNamespace('prop')]: { - [XMLUtils.addDefaultNamespace('getcontenttype')]: 'application/octet-stream', + [XMLUtils.addDefaultNamespace('getcontenttype')]: 'httpd/unix-directory', + [XMLUtils.addDefaultNamespace('getetag')]: '"' + randomUUID().replaceAll('-', '') + '"', 'x1:lastmodified': { '#text': FormatUtils.formatDateForWebDav(driveFolderItem.updatedAt), '@_xmlns:x1': 'SAR:', @@ -219,6 +223,8 @@ export class PROPFINDRequestHandler implements WebDavMethodHandler { [XMLUtils.addDefaultNamespace('displayname')]: displayName, [XMLUtils.addDefaultNamespace('getlastmodified')]: FormatUtils.formatDateForWebDav(driveFolderItem.updatedAt), [XMLUtils.addDefaultNamespace('getcontentlength')]: 0, + [XMLUtils.addDefaultNamespace('getcontenttype')]: 'httpd/unix-directory', + [XMLUtils.addDefaultNamespace('getetag')]: '"' + randomUUID().replaceAll('-', '') + '"', [XMLUtils.addDefaultNamespace('resourcetype')]: { [XMLUtils.addDefaultNamespace('collection')]: '', }, diff --git a/src/webdav/middewares/auth.middleware.ts b/src/webdav/middewares/auth.middleware.ts index 75751af4..0185aca4 100644 --- a/src/webdav/middewares/auth.middleware.ts +++ b/src/webdav/middewares/auth.middleware.ts @@ -39,6 +39,7 @@ export const AuthMiddleware = (): RequestHandler => { {}, 'error', ); + res.set('WWW-Authenticate', 'Basic realm="Internxt WebDAV"'); res.status(401).send(errorBodyXML); } })(); diff --git a/src/webdav/middewares/webdav-auth.middleware.ts b/src/webdav/middewares/webdav-auth.middleware.ts index 53bf4682..202fb1aa 100644 --- a/src/webdav/middewares/webdav-auth.middleware.ts +++ b/src/webdav/middewares/webdav-auth.middleware.ts @@ -49,5 +49,6 @@ const sendUnauthorizedError = (res: Response, message: string) => { 'error', ); + res.set('WWW-Authenticate', 'Basic realm="Internxt WebDAV"'); res.status(401).send(errorBodyXML); }; diff --git a/src/webdav/webdav-server.ts b/src/webdav/webdav-server.ts index f67af166..598220f9 100644 --- a/src/webdav/webdav-server.ts +++ b/src/webdav/webdav-server.ts @@ -30,13 +30,15 @@ export class WebDavServer { constructor(private readonly app: Express) {} private readonly registerStartMiddlewares = (configs: WebdavConfig) => { - this.app.use(WebDAVAuthMiddleware(configs)); - this.app.use(AuthMiddleware()); + // The logger must run before the auth middlewares, otherwise rejected + // requests (401) may leave no trace in the WebDAV log. this.app.use( RequestLoggerMiddleware({ enable: true, }), ); + this.app.use(WebDAVAuthMiddleware(configs)); + this.app.use(AuthMiddleware()); this.app.use(bodyParser.text({ type: ['application/xml', 'text/xml'] })); this.app.use(MkcolMiddleware); }; diff --git a/test/commands/whoami.test.ts b/test/commands/whoami.test.ts index 0bba1141..e15619c8 100644 --- a/test/commands/whoami.test.ts +++ b/test/commands/whoami.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test, vi } from 'vitest'; +import { TokenStatus } from '@internxt/lib'; import { ConfigService } from '../../src/services/config.service'; import { UserCredentialsFixture } from '../fixtures/login.fixture'; import Whoami from '../../src/commands/whoami'; @@ -30,7 +31,7 @@ describe('Whoami Command', () => { const clearUserSpy = vi.spyOn(ConfigService.instance, 'clearUser').mockResolvedValue(); const validateTokensSpy = vi .spyOn(ValidationService.instance, 'validateTokenAndCheckExpiration') - .mockReturnValueOnce({ expiration: { expired: true, refreshRequired: false }, isValid: true }); + .mockReturnValueOnce(TokenStatus.EXPIRED); const validateMnemonicSpy = vi.spyOn(ValidationService.instance, 'validateMnemonic').mockReturnValue(true); const message = 'Your session has expired. You have been logged out. Please log in again.'; @@ -50,7 +51,7 @@ describe('Whoami Command', () => { const clearUserSpy = vi.spyOn(ConfigService.instance, 'clearUser').mockResolvedValue(); const validateTokensSpy = vi .spyOn(ValidationService.instance, 'validateTokenAndCheckExpiration') - .mockReturnValueOnce({ expiration: { expired: false, refreshRequired: false }, isValid: true }); + .mockReturnValueOnce(TokenStatus.VALID); const validateMnemonicSpy = vi.spyOn(ValidationService.instance, 'validateMnemonic').mockReturnValue(true); const message = `You are logged in as: ${UserCredentialsFixture.user.email}.`; diff --git a/test/services/auth.service.test.ts b/test/services/auth.service.test.ts index be1877f5..bea76496 100644 --- a/test/services/auth.service.test.ts +++ b/test/services/auth.service.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; import crypto from 'node:crypto'; +import { TokenStatus } from '@internxt/lib'; import { Auth, LoginDetails, SecurityDetails } from '@internxt/sdk'; import { AuthService } from '../../src/services/auth.service'; import { CryptoService } from '../../src/services/crypto.service'; @@ -104,19 +105,13 @@ describe('Auth service', () => { const sut = AuthService.instance; const loginCreds: LoginCredentials = UserCredentialsFixture; - const mockToken = { - isValid: true, - expiration: { - expired: false, - refreshRequired: false, - }, - }; + const mockTokenStatus = TokenStatus.VALID; vi.spyOn(ConfigService.instance, 'readUser').mockResolvedValue(loginCreds); const validateTokensStub = vi .spyOn(ValidationService.instance, 'validateTokenAndCheckExpiration') - .mockImplementationOnce(() => mockToken); + .mockImplementationOnce(() => mockTokenStatus); const validateMnemonicStub = vi.spyOn(ValidationService.instance, 'validateMnemonic').mockReturnValue(true); const result = await sut.getAuthDetails(); @@ -162,18 +157,12 @@ describe('Auth service', () => { test('when the recovery phrase is invalid, then an error is thrown', async () => { const sut = AuthService.instance; - const mockToken = { - isValid: true, - expiration: { - expired: false, - refreshRequired: false, - }, - }; + const mockTokenStatus = TokenStatus.VALID; const authStub = vi.spyOn(ConfigService.instance, 'readUser').mockResolvedValue(UserCredentialsFixture); const validateTokensStub = vi .spyOn(ValidationService.instance, 'validateTokenAndCheckExpiration') - .mockImplementationOnce(() => mockToken); + .mockImplementationOnce(() => mockTokenStatus); const validateMnemonicStub = vi.spyOn(ValidationService.instance, 'validateMnemonic').mockReturnValue(false); try { @@ -190,18 +179,12 @@ describe('Auth service', () => { test('when the session token has expired, then an error is thrown', async () => { const sut = AuthService.instance; - const mockToken = { - isValid: true, - expiration: { - expired: true, - refreshRequired: false, - }, - }; + const mockTokenStatus = TokenStatus.EXPIRED; const authStub = vi.spyOn(ConfigService.instance, 'readUser').mockResolvedValue(UserCredentialsFixture); const validateTokensStub = vi .spyOn(ValidationService.instance, 'validateTokenAndCheckExpiration') - .mockImplementationOnce(() => mockToken); + .mockImplementationOnce(() => mockTokenStatus); const validateMnemonicStub = vi.spyOn(ValidationService.instance, 'validateMnemonic').mockReturnValue(true); try { @@ -218,18 +201,12 @@ describe('Auth service', () => { test('when the session token is about to expire, then it is refreshed automatically', async () => { const sut = AuthService.instance; - const mockToken = { - isValid: true, - expiration: { - expired: false, - refreshRequired: true, - }, - }; + const mockTokenStatus = TokenStatus.REFRESH_REQUIRED; const authStub = vi.spyOn(ConfigService.instance, 'readUser').mockResolvedValue(UserCredentialsFixture); const validateTokensStub = vi .spyOn(ValidationService.instance, 'validateTokenAndCheckExpiration') - .mockImplementationOnce(() => mockToken); + .mockImplementationOnce(() => mockTokenStatus); const validateMnemonicStub = vi.spyOn(ValidationService.instance, 'validateMnemonic').mockReturnValue(true); const refreshTokensStub = vi.spyOn(sut, 'refreshUserToken').mockResolvedValue(UserCredentialsFixture); @@ -243,18 +220,14 @@ describe('Auth service', () => { test('when the token refresh fails, then stored credentials are cleared and an error is thrown', async () => { const sut = AuthService.instance; - const mockToken = { - isValid: true, - expiration: { - expired: false, - refreshRequired: true, - }, - }; + const mockTokenStatus = TokenStatus.REFRESH_REQUIRED; const oldTokenError = new Error('Old token version detected'); vi.spyOn(ConfigService.instance, 'readUser').mockResolvedValue(UserCredentialsFixture); - vi.spyOn(ValidationService.instance, 'validateTokenAndCheckExpiration').mockImplementationOnce(() => mockToken); + vi.spyOn(ValidationService.instance, 'validateTokenAndCheckExpiration').mockImplementationOnce( + () => mockTokenStatus, + ); vi.spyOn(ValidationService.instance, 'validateMnemonic').mockReturnValue(true); const refreshTokenStub = vi.spyOn(sut, 'refreshUserToken').mockRejectedValue(oldTokenError); const clearUserStub = vi.spyOn(ConfigService.instance, 'clearUser').mockResolvedValue(); diff --git a/test/services/validation.service.test.ts b/test/services/validation.service.test.ts index fb5ba356..4d9d0079 100644 --- a/test/services/validation.service.test.ts +++ b/test/services/validation.service.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test, vi } from 'vitest'; -import { auth } from '@internxt/lib'; +import { auth, TokenStatus } from '@internxt/lib'; import { randomInt, randomUUID } from 'node:crypto'; import { UserFixture } from '../fixtures/auth.fixture'; import { ValidationService } from '../../src/services/validation.service'; @@ -69,142 +69,82 @@ describe('Validation Service', () => { expect(ValidationService.instance.validateStringIsNotEmpty('\t')).to.be.equal(false); expect(ValidationService.instance.validateStringIsNotEmpty('\t\n')).to.be.equal(false); }); - describe('parseJwtExpiration', () => { - test('when the token is not provided, then null is returned', () => { - expect(ValidationService.instance.validateJwtAndCheckExpiration(undefined)).to.be.equal(null); - }); - - test('when the token is an empty string, then null is returned', () => { - expect(ValidationService.instance.validateJwtAndCheckExpiration('')).to.be.equal(null); - }); - - test('when the token does not have the expected format, then null is returned', () => { - expect(ValidationService.instance.validateJwtAndCheckExpiration('invalid')).to.be.equal(null); - expect(ValidationService.instance.validateJwtAndCheckExpiration('invalid.token')).to.be.equal(null); - }); - - test('when the token payload is not valid base64 encoding, then null is returned', () => { - const invalidToken = 'header.!!!invalid_base64!!!.signature'; - expect(ValidationService.instance.validateJwtAndCheckExpiration(invalidToken)).to.be.equal(null); - }); - - test('when the token does not contain an expiration claim, then null is returned', () => { - const payload = btoa(JSON.stringify({ sub: 'user123' })); - const token = `header.${payload}.signature`; - expect(ValidationService.instance.validateJwtAndCheckExpiration(token)).to.be.equal(null); - }); - - test('when the token expiration value is not a number, then null is returned', () => { - const payload = btoa(JSON.stringify({ exp: 'not-a-number' })); - const token = `header.${payload}.signature`; - expect(ValidationService.instance.validateJwtAndCheckExpiration(token)).to.be.equal(null); - }); - - test('when the token has a valid structure with expiration, then the expiration timestamp is returned', () => { - const expiration = Math.floor(Date.now() / 1000) + 3600; // 1 hour from now - const payload = btoa(JSON.stringify({ exp: expiration, sub: 'user123' })); - const token = `header.${payload}.signature`; - expect(ValidationService.instance.validateJwtAndCheckExpiration(token)).to.be.equal(expiration); - }); - }); - - describe('checkTokenExpiration', () => { - test('when the token expired more than two days ago, then it is expired and cannot be refreshed', () => { - const threeDaysAgo = Math.floor(Date.now() / 1000) - 3 * 24 * 60 * 60; - const result = ValidationService.instance.checkTokenExpiration(threeDaysAgo); - expect(result.expired).to.be.equal(true); - expect(result.refreshRequired).to.be.equal(false); - }); - - test('when the token expired one second ago, then it is expired and cannot be refreshed', () => { - const oneSecondAgo = Math.floor(Date.now() / 1000) - 1; - const result = ValidationService.instance.checkTokenExpiration(oneSecondAgo); - expect(result.expired).to.be.equal(true); - expect(result.refreshRequired).to.be.equal(false); - }); - - test('when the token expires at the current moment, then it is considered expired', () => { - const now = Math.floor(Date.now() / 1000); - const result = ValidationService.instance.checkTokenExpiration(now); - expect(result.expired).to.be.equal(true); - expect(result.refreshRequired).to.be.equal(false); - }); - - test('when the token expires within one day, then it is not yet expired but requires refresh', () => { - const oneDayFromNow = Math.floor(Date.now() / 1000) + 24 * 60 * 60; - const result = ValidationService.instance.checkTokenExpiration(oneDayFromNow); - expect(result.expired).to.be.equal(false); - expect(result.refreshRequired).to.be.equal(true); - }); - - test('when the token expires in exactly two days, then it is not yet expired but requires refresh', () => { - const twoDaysFromNow = Math.floor(Date.now() / 1000) + 2 * 24 * 60 * 60; - const result = ValidationService.instance.checkTokenExpiration(twoDaysFromNow); - expect(result.expired).to.be.equal(false); - expect(result.refreshRequired).to.be.equal(true); - }); - - test('when the token expires in more than two days, then it is not expired and does not require refresh', () => { - const twoDaysPlusOneSecond = Math.floor(Date.now() / 1000) + 2 * 24 * 60 * 60 + 1; - const result = ValidationService.instance.checkTokenExpiration(twoDaysPlusOneSecond); - expect(result.expired).to.be.equal(false); - expect(result.refreshRequired).to.be.equal(false); - }); - - test('when the token expires in thirty days, then it is not expired and does not require refresh', () => { - const thirtyDaysFromNow = Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60; - const result = ValidationService.instance.checkTokenExpiration(thirtyDaysFromNow); - expect(result.expired).to.be.equal(false); - expect(result.refreshRequired).to.be.equal(false); - }); - }); describe('validateTokenAndCheckExpiration', () => { - test('when the token is not provided, then it is invalid and marked as expired', () => { - const result = ValidationService.instance.validateTokenAndCheckExpiration(undefined); - expect(result.isValid).to.be.equal(false); - expect(result.expiration.expired).to.be.equal(true); - expect(result.expiration.refreshRequired).to.be.equal(false); - }); - - test('when the token is malformed, then it is invalid and marked as expired', () => { - const result = ValidationService.instance.validateTokenAndCheckExpiration('invalid.token'); - expect(result.isValid).to.be.equal(false); - expect(result.expiration.expired).to.be.equal(true); - expect(result.expiration.refreshRequired).to.be.equal(false); - }); - - test('when the token is structurally valid but has expired, then it is marked as valid but expired', () => { - const expiration = Math.floor(Date.now() / 1000) - 3600; // 1 hour ago - const payload = btoa(JSON.stringify({ exp: expiration })); - const token = `header.${payload}.signature`; - - const result = ValidationService.instance.validateTokenAndCheckExpiration(token); - expect(result.isValid).to.be.equal(true); - expect(result.expiration.expired).to.be.equal(true); - expect(result.expiration.refreshRequired).to.be.equal(false); - }); - - test('when the token is valid and expires within one day, then it is valid and requires refresh', () => { - const expiration = Math.floor(Date.now() / 1000) + 24 * 60 * 60; // 1 day from now - const payload = btoa(JSON.stringify({ exp: expiration })); - const token = `header.${payload}.signature`; - - const result = ValidationService.instance.validateTokenAndCheckExpiration(token); - expect(result.isValid).to.be.equal(true); - expect(result.expiration.expired).to.be.equal(false); - expect(result.expiration.refreshRequired).to.be.equal(true); - }); - - test('when the token is valid and expires in thirty days, then it is valid with no action needed', () => { - const expiration = Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60; // 30 days from now - const payload = btoa(JSON.stringify({ exp: expiration })); - const token = `header.${payload}.signature`; - - const result = ValidationService.instance.validateTokenAndCheckExpiration(token); - expect(result.isValid).to.be.equal(true); - expect(result.expiration.expired).to.be.equal(false); - expect(result.expiration.refreshRequired).to.be.equal(false); + const nowInSeconds = () => Math.floor(Date.now() / 1000); + const ONE_HOUR_IN_SECONDS = 60 * 60; + // The library decodes the payload with Buffer.from(..., 'base64') which + // handles both standard and URL-safe base64 after character substitution. + const encodeSegment = (payload: object): string => Buffer.from(JSON.stringify(payload)).toString('base64'); + const createToken = (payload: object): string => + [encodeSegment({ alg: 'RS256', typ: 'JWT' }), encodeSegment(payload), 'signature'].join('.'); + + test('when a token expires far in the future and most of its lifetime remains, then it is VALID', () => { + const token = createToken({ exp: nowInSeconds() + 24 * ONE_HOUR_IN_SECONDS, iat: nowInSeconds() }); + expect(ValidationService.instance.validateTokenAndCheckExpiration(token)).to.be.equal(TokenStatus.VALID); + }); + + test('when more than half of the token lifetime has elapsed, then it is REFRESH_REQUIRED', () => { + const token = createToken({ + exp: nowInSeconds() + ONE_HOUR_IN_SECONDS, + iat: nowInSeconds() - 2 * ONE_HOUR_IN_SECONDS, + }); + expect(ValidationService.instance.validateTokenAndCheckExpiration(token)).to.be.equal( + TokenStatus.REFRESH_REQUIRED, + ); + }); + + test('when the expiration timestamp is in the past, then it is EXPIRED', () => { + const token = createToken({ + exp: nowInSeconds() - ONE_HOUR_IN_SECONDS, + iat: nowInSeconds() - 3 * ONE_HOUR_IN_SECONDS, + }); + expect(ValidationService.instance.validateTokenAndCheckExpiration(token)).to.be.equal(TokenStatus.EXPIRED); + }); + + test('when the token has no iat claim, then a fixed six-hour refresh margin applies', () => { + const farFromExpiring = createToken({ exp: nowInSeconds() + 7 * ONE_HOUR_IN_SECONDS }); + expect(ValidationService.instance.validateTokenAndCheckExpiration(farFromExpiring)).to.be.equal( + TokenStatus.VALID, + ); + + const closeToExpiring = createToken({ exp: nowInSeconds() + 5 * ONE_HOUR_IN_SECONDS }); + expect(ValidationService.instance.validateTokenAndCheckExpiration(closeToExpiring)).to.be.equal( + TokenStatus.REFRESH_REQUIRED, + ); + }); + + test('when a token is malformed or unparseable, then it is INVALID', () => { + expect(ValidationService.instance.validateTokenAndCheckExpiration('')).to.be.equal(TokenStatus.INVALID); + expect(ValidationService.instance.validateTokenAndCheckExpiration('not-a-token')).to.be.equal( + TokenStatus.INVALID, + ); + expect(ValidationService.instance.validateTokenAndCheckExpiration('only.twosegments')).to.be.equal( + TokenStatus.INVALID, + ); + expect( + ValidationService.instance.validateTokenAndCheckExpiration('header.!!!not-base64!!!.signature'), + ).to.be.equal(TokenStatus.INVALID); + const notJsonPayload = ['header', Buffer.from('plain text').toString('base64'), 'signature'].join('.'); + expect(ValidationService.instance.validateTokenAndCheckExpiration(notJsonPayload)).to.be.equal( + TokenStatus.INVALID, + ); + }); + + test('when the payload has no numeric exp claim, then it is INVALID', () => { + const missingExp = createToken({ iat: nowInSeconds() }); + expect(ValidationService.instance.validateTokenAndCheckExpiration(missingExp)).to.be.equal(TokenStatus.INVALID); + + const nonNumericExp = createToken({ exp: 'tomorrow', iat: nowInSeconds() }); + expect(ValidationService.instance.validateTokenAndCheckExpiration(nonNumericExp)).to.be.equal( + TokenStatus.INVALID, + ); + }); + + test('when the payload encoding contains base64url-specific characters, then it is correctly decoded', () => { + const base64UrlToken = + 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjQxMDI0NDQ4MDAsImlhdCI6MTU3NzgzNjgwMCwic3ViIjoiw7_DvsO9In0.signature'; + expect(ValidationService.instance.validateTokenAndCheckExpiration(base64UrlToken)).to.be.equal(TokenStatus.VALID); }); }); }); diff --git a/test/utils/network.utils.test.ts b/test/utils/network.utils.test.ts index 3298df17..6d16f6ae 100644 --- a/test/utils/network.utils.test.ts +++ b/test/utils/network.utils.test.ts @@ -170,14 +170,14 @@ describe('Network utils', () => { NetworkUtils.parseRangeHeader({ range: 'range', totalFileSize }); fail('Expected function to throw an error, but it did not.'); } catch (error) { - expect((error as Error).message).to.contain('Unsatisfiable Range-Request.'); + expect((error as Error).message).to.contain('Malformed Range-Request.'); } try { NetworkUtils.parseRangeHeader({ range: 'whatever-range', totalFileSize }); fail('Expected function to throw an error, but it did not.'); } catch (error) { - expect((error as Error).message).to.contain('Unsatisfiable Range-Request.'); + expect((error as Error).message).to.contain('Malformed Range-Request.'); } try { @@ -187,6 +187,13 @@ describe('Network utils', () => { expect((error as Error).message).to.contain('Malformed Range-Request.'); } + try { + NetworkUtils.parseRangeHeader({ range: 'bytes=999999-1000005', totalFileSize }); + fail('Expected function to throw an error, but it did not.'); + } catch (error) { + expect((error as Error).message).to.contain('Unsatisfiable Range-Request.'); + } + try { NetworkUtils.parseRangeHeader({ range: 'megabytes=50-55', totalFileSize }); fail('Expected function to throw an error, but it did not.'); diff --git a/test/webdav/handlers/MKCOL.handler.test.ts b/test/webdav/handlers/MKCOL.handler.test.ts index cff468e4..95d10eca 100644 --- a/test/webdav/handlers/MKCOL.handler.test.ts +++ b/test/webdav/handlers/MKCOL.handler.test.ts @@ -12,6 +12,7 @@ import { WebDavUtils } from '../../../src/utils/webdav.utils'; import { AuthService } from '../../../src/services/auth.service'; import { UserCredentialsFixture } from '../../fixtures/login.fixture'; import { WebDavFolderService } from '../../../src/services/webdav/webdav-folder.service'; +import { MethodNotAllowed } from '../../../src/utils/errors.utils'; describe('MKCOL request handler', () => { let sut: MKCOLRequestHandler; @@ -92,4 +93,31 @@ describe('MKCOL request handler', () => { parentFolderUuid: parentFolder.uuid, }); }); + + test('when the folder already exists, then the server rejects the request with a 405 error', async () => { + const requestedFolderResource: WebDavRequestedResource = getRequestedFolderResource({ + parentFolder: '/test', + folderName: 'FolderA', + }); + const request = createWebDavRequestFixture({ + method: 'MKCOL', + url: requestedFolderResource.url, + user: UserSettingsFixture, + }); + const response = createWebDavResponseFixture({ + status: vi.fn().mockReturnValue({ send: vi.fn() }), + }); + + const parentFolder = newFolderItem({ name: 'test', uuid: 'parent-uuid' }); + const existingFolder = newFolderItem({ name: 'FolderA', uuid: 'existing-folder-uuid' }); + + vi.spyOn(WebDavUtils, 'getRequestedResource').mockResolvedValue(requestedFolderResource); + vi.spyOn(WebDavFolderService.instance, 'getDriveFolderItemFromPath').mockResolvedValue(parentFolder); + vi.spyOn(WebDavUtils, 'getDriveFolderFromResource').mockResolvedValue(existingFolder); + const createFolderStub = vi.spyOn(WebDavFolderService.instance, 'createFolder'); + + await expect(sut.handle(request, response)).rejects.toThrow(MethodNotAllowed); + expect(createFolderStub).not.toHaveBeenCalled(); + expect(response.status).not.toHaveBeenCalled(); + }); }); diff --git a/test/webdav/handlers/PROPFIND.handler.test.ts b/test/webdav/handlers/PROPFIND.handler.test.ts index 728b86ee..176de18f 100644 --- a/test/webdav/handlers/PROPFIND.handler.test.ts +++ b/test/webdav/handlers/PROPFIND.handler.test.ts @@ -57,7 +57,11 @@ describe('PROPFIND request handler', () => { }); const usageFixture = crypto.randomInt(2000000000); const spaceLimitFixture = crypto.randomInt(2000000000); + const uuidFixture = 'test-test-test-test-test'; + const etagFixture = uuidFixture.replaceAll('-', ''); + randomUUIDStub.mockClear(); + randomUUIDStub.mockImplementation(() => uuidFixture); const getRequestedResourceStub = vi .spyOn(WebDavUtils, 'getRequestedResource') .mockResolvedValue(requestedFolderResource); @@ -73,7 +77,7 @@ describe('PROPFIND request handler', () => { await sut.handle(request, response); expect(response.status).toHaveBeenCalledWith(207); expect(response.send).toHaveBeenCalledWith( - `${XMLUtils.encodeWebDavUri('/')}HTTP/1.1 200 OKapplication/octet-stream${FormatUtils.formatDateForWebDav(folderFixture.updatedAt)}F00000030${spaceLimitFixture - usageFixture}${usageFixture}`, + `${XMLUtils.encodeWebDavUri('/')}HTTP/1.1 200 OKhttpd/unix-directory"${etagFixture}"${FormatUtils.formatDateForWebDav(folderFixture.updatedAt)}F00000030${spaceLimitFixture - usageFixture}${usageFixture}`, ); expect(getRequestedResourceStub).toHaveBeenCalledOnce(); expect(getFolderMetadataStub).toHaveBeenCalledOnce(); @@ -137,6 +141,11 @@ describe('PROPFIND request handler', () => { }); const usageFixture = crypto.randomInt(2000000000); const spaceLimitFixture = crypto.randomInt(2000000000); + const uuidFixture = 'test-test-test-test-test'; + const etagFixture = uuidFixture.replaceAll('-', ''); + + randomUUIDStub.mockClear(); + randomUUIDStub.mockImplementation(() => uuidFixture); const getRequestedResourceStub = vi .spyOn(WebDavUtils, 'getRequestedResource') @@ -154,7 +163,7 @@ describe('PROPFIND request handler', () => { await sut.handle(request, response); expect(response.status).toHaveBeenCalledWith(207); expect(response.send).toHaveBeenCalledWith( - `${XMLUtils.encodeWebDavUri('/')}HTTP/1.1 200 OKapplication/octet-stream${FormatUtils.formatDateForWebDav(folderFixture.updatedAt)}F00000030${spaceLimitFixture - usageFixture}${usageFixture}${XMLUtils.encodeWebDavUri(`/${paginatedFolder1.plainName}/`)}HTTP/1.1 200 OK${paginatedFolder1.plainName}${FormatUtils.formatDateForWebDav(paginatedFolder1.updatedAt)}0`, + `${XMLUtils.encodeWebDavUri('/')}HTTP/1.1 200 OKhttpd/unix-directory"${etagFixture}"${FormatUtils.formatDateForWebDav(folderFixture.updatedAt)}F00000030${spaceLimitFixture - usageFixture}${usageFixture}${XMLUtils.encodeWebDavUri(`/${paginatedFolder1.plainName}/`)}HTTP/1.1 200 OK${paginatedFolder1.plainName}${FormatUtils.formatDateForWebDav(paginatedFolder1.updatedAt)}0httpd/unix-directory"${etagFixture}"`, ); expect(getRequestedResourceStub).toHaveBeenCalledOnce(); expect(getAndSearchItemFromResourceStub).toHaveBeenCalledOnce(); @@ -225,8 +234,8 @@ describe('PROPFIND request handler', () => { const getAndSearchItemFromResourceStub = vi .spyOn(WebDavUtils, 'getDriveItemFromResource') .mockResolvedValue(fileFixture); - randomUUIDStub.mockImplementation(() => 'test-test-test-test-test'); randomUUIDStub.mockClear(); + randomUUIDStub.mockImplementation(() => uuidFixture); const mimeLookupStub = vi.spyOn(mime, 'lookup').mockReturnValue(mimeFixture); await sut.handle(request, response);