From 60219e042d3cdd089c90bdce211cf3efee4175f2 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Sat, 21 Feb 2026 06:29:36 +0100 Subject: [PATCH] feat: add .npmrc support for private npm registries (#8039) * feat: add .npmrc support for private npm registries Add a new `npmrc` instance setting that accepts full .npmrc file content for configuring private npm registries. Works with bun (native .npmrc support since 1.1.18), deno (native .npmrc support in 2.x), and the npm proxy (parses default registry + auth token from .npmrc). Legacy `npm_config_registry` and `bunfig_install_scopes` fields are now hidden when empty, so new users only see the .npmrc field. Also fixes a pre-existing race condition where gen_bunfig was called after start_child_process. Co-Authored-By: Claude Opus 4.6 * all --------- Co-authored-by: Claude Opus 4.6 --- .github/workflows/backend-test.yml | 6 + backend/Cargo.lock | 1 + backend/src/monitor.rs | 13 +- backend/tests/bun_jobs.rs | 217 +++++++++++++----- backend/tests/worker.rs | 60 +++++ backend/windmill-api-npm-proxy/Cargo.toml | 1 + backend/windmill-api-npm-proxy/src/lib.rs | 116 +++++----- .../windmill-common/src/global_settings.rs | 1 + .../windmill-common/src/instance_config.rs | 20 +- backend/windmill-common/src/utils.rs | 93 ++++++++ backend/windmill-worker/src/bun_executor.rs | 98 +++++--- backend/windmill-worker/src/deno_executor.rs | 51 ++-- backend/windmill-worker/src/worker.rs | 1 + frontend/package-lock.json | 46 +--- .../src/lib/components/instanceSettings.ts | 30 ++- 15 files changed, 527 insertions(+), 227 deletions(-) diff --git a/.github/workflows/backend-test.yml b/.github/workflows/backend-test.yml index a2eeaaa044..48bd1e11ec 100644 --- a/.github/workflows/backend-test.yml +++ b/.github/workflows/backend-test.yml @@ -165,6 +165,12 @@ jobs: fi echo "NPM_TOKEN=${NPM_TOKEN}" >> $GITHUB_ENV + { + echo "TEST_NPMRC<> $GITHUB_ENV echo "Got NPM token successfully: ${NPM_TOKEN:0:10}..." # Configure npm globally with the auth token diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 513e32ed2f..a221bb6fa9 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -16200,6 +16200,7 @@ version = "1.640.0" dependencies = [ "axum 0.7.9", "flate2", + "reqwest 0.13.1", "serde", "serde_json", "sqlx", diff --git a/backend/src/monitor.rs b/backend/src/monitor.rs index 75cff8971f..17e79c3393 100644 --- a/backend/src/monitor.rs +++ b/backend/src/monitor.rs @@ -54,7 +54,7 @@ use windmill_common::{ HUB_API_SECRET_SETTING, HUB_BASE_URL_SETTING, INSTANCE_PYTHON_VERSION_SETTING, JOB_DEFAULT_TIMEOUT_SECS_SETTING, JOB_ISOLATION_SETTING, JWT_SECRET_SETTING, KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, MONITOR_LOGS_ON_OBJECT_STORE_SETTING, - NPM_CONFIG_REGISTRY_SETTING, NUGET_CONFIG_SETTING, OTEL_SETTING, + NPMRC_SETTING, NPM_CONFIG_REGISTRY_SETTING, NUGET_CONFIG_SETTING, OTEL_SETTING, OTEL_TRACING_PROXY_SETTING, PIP_INDEX_URL_SETTING, POWERSHELL_REPO_PAT_SETTING, POWERSHELL_REPO_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING, REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING, RETENTION_PERIOD_SECS_SETTING, @@ -89,9 +89,9 @@ use windmill_worker::{ result_processor::handle_job_error, JobCompletedSender, JobIsolationLevel, OtelTracingProxySettings, SameWorkerSender, BUNFIG_INSTALL_SCOPES, CARGO_REGISTRIES, INSTANCE_PYTHON_VERSION, JAVA_HOME_DIR, JOB_DEFAULT_TIMEOUT, JOB_ISOLATION, KEEP_JOB_DIR, - MAVEN_REPOS, MAVEN_SETTINGS_XML, NO_DEFAULT_MAVEN, NPM_CONFIG_REGISTRY, NSJAIL_AVAILABLE, - NUGET_CONFIG, OTEL_TRACING_PROXY_SETTINGS, PIP_EXTRA_INDEX_URL, PIP_INDEX_URL, - POWERSHELL_REPO_PAT, POWERSHELL_REPO_URL, UV_INDEX_STRATEGY, + MAVEN_REPOS, MAVEN_SETTINGS_XML, NO_DEFAULT_MAVEN, NPMRC, NPM_CONFIG_REGISTRY, + NSJAIL_AVAILABLE, NUGET_CONFIG, OTEL_TRACING_PROXY_SETTINGS, PIP_EXTRA_INDEX_URL, + PIP_INDEX_URL, POWERSHELL_REPO_PAT, POWERSHELL_REPO_URL, UV_INDEX_STRATEGY, }; #[cfg(feature = "parquet")] @@ -330,6 +330,7 @@ pub async fn initial_load( reload_uv_index_strategy_setting(&conn).await; reload_npm_config_registry_setting(&conn).await; reload_bunfig_install_scopes_setting(&conn).await; + reload_npmrc_setting(&conn).await; reload_instance_python_version_setting(&conn).await; reload_nuget_config_setting(&conn).await; reload_powershell_repo_url_setting(&conn).await; @@ -1306,6 +1307,10 @@ pub async fn reload_bunfig_install_scopes_setting(conn: &Connection) { .await; } +pub async fn reload_npmrc_setting(conn: &Connection) { + reload_option_setting_with_tracing(conn, NPMRC_SETTING, "NPMRC", NPMRC.clone()).await; +} + pub async fn reload_nuget_config_setting(conn: &Connection) { reload_option_setting_with_tracing( conn, diff --git a/backend/tests/bun_jobs.rs b/backend/tests/bun_jobs.rs index 534d530095..15a2b3c27c 100644 --- a/backend/tests/bun_jobs.rs +++ b/backend/tests/bun_jobs.rs @@ -1,8 +1,8 @@ -use windmill_test_utils::*; use sqlx::postgres::Postgres; use sqlx::Pool; use windmill_common::jobs::{JobPayload, RawCode}; use windmill_common::scripts::ScriptLang; +use windmill_test_utils::*; // ============================================================================ // Basic Execution Tests @@ -27,8 +27,8 @@ export function main() { path: None, language: ScriptLang::Bun, lock: None, - concurrency_settings: - windmill_common::runnable_settings::ConcurrencySettings::default().into(), + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() + .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), cache_ttl: None, cache_ignore_s3_path: None, @@ -63,8 +63,8 @@ export function main(name: string, count: number) { path: None, language: ScriptLang::Bun, lock: None, - concurrency_settings: - windmill_common::runnable_settings::ConcurrencySettings::default().into(), + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() + .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), cache_ttl: None, cache_ignore_s3_path: None, @@ -104,8 +104,9 @@ export function main() { path: None, language: ScriptLang::Bun, lock: None, - concurrency_settings: - windmill_common::runnable_settings::ConcurrencySettings::default().into(), + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default( + ) + .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), cache_ttl: None, cache_ignore_s3_path: None, @@ -135,8 +136,9 @@ export function main() { path: None, language: ScriptLang::Bun, lock: None, - concurrency_settings: - windmill_common::runnable_settings::ConcurrencySettings::default().into(), + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default( + ) + .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), cache_ttl: None, cache_ignore_s3_path: None, @@ -167,8 +169,9 @@ export function main() { path: None, language: ScriptLang::Bun, lock: None, - concurrency_settings: - windmill_common::runnable_settings::ConcurrencySettings::default().into(), + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default( + ) + .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), cache_ttl: None, cache_ignore_s3_path: None, @@ -207,8 +210,8 @@ export async function main() { path: None, language: ScriptLang::Bun, lock: None, - concurrency_settings: - windmill_common::runnable_settings::ConcurrencySettings::default().into(), + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() + .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), cache_ttl: None, cache_ignore_s3_path: None, @@ -245,8 +248,9 @@ export function main() { path: None, language: ScriptLang::Bun, lock: None, - concurrency_settings: - windmill_common::runnable_settings::ConcurrencySettings::default().into(), + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default( + ) + .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), cache_ttl: None, cache_ignore_s3_path: None, @@ -276,8 +280,9 @@ export function main() { path: None, language: ScriptLang::Bun, lock: None, - concurrency_settings: - windmill_common::runnable_settings::ConcurrencySettings::default().into(), + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default( + ) + .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), cache_ttl: None, cache_ignore_s3_path: None, @@ -318,8 +323,8 @@ export function main() { path: None, language: ScriptLang::Bun, lock: None, - concurrency_settings: - windmill_common::runnable_settings::ConcurrencySettings::default().into(), + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() + .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), cache_ttl: None, cache_ignore_s3_path: None, @@ -358,8 +363,8 @@ export function notMain() { path: None, language: ScriptLang::Bun, lock: None, - concurrency_settings: - windmill_common::runnable_settings::ConcurrencySettings::default().into(), + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() + .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), cache_ttl: None, cache_ignore_s3_path: None, @@ -398,8 +403,8 @@ export function main() { path: None, language: ScriptLang::Bun, lock: None, - concurrency_settings: - windmill_common::runnable_settings::ConcurrencySettings::default().into(), + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() + .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), cache_ttl: None, cache_ignore_s3_path: None, @@ -437,8 +442,8 @@ export function main() { path: None, language: ScriptLang::Bun, lock: None, - concurrency_settings: - windmill_common::runnable_settings::ConcurrencySettings::default().into(), + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() + .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), cache_ttl: None, cache_ignore_s3_path: None, @@ -474,8 +479,8 @@ export function main() { path: None, language: ScriptLang::Bun, lock: None, - concurrency_settings: - windmill_common::runnable_settings::ConcurrencySettings::default().into(), + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() + .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), cache_ttl: None, cache_ignore_s3_path: None, @@ -516,8 +521,8 @@ export function main() { path: None, language: ScriptLang::Bun, lock: None, - concurrency_settings: - windmill_common::runnable_settings::ConcurrencySettings::default().into(), + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() + .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), cache_ttl: None, cache_ignore_s3_path: None, @@ -613,8 +618,9 @@ export function main() { path: Some("f/nested/test_deep".to_string()), language: ScriptLang::Bun, lock: None, - concurrency_settings: - windmill_common::runnable_settings::ConcurrencySettings::default().into(), + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default( + ) + .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), cache_ttl: None, cache_ignore_s3_path: None, @@ -647,8 +653,9 @@ export function main() { path: Some("f/nested/test_deep_relative".to_string()), language: ScriptLang::Bun, lock: None, - concurrency_settings: - windmill_common::runnable_settings::ConcurrencySettings::default().into(), + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default( + ) + .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), cache_ttl: None, cache_ignore_s3_path: None, @@ -693,8 +700,8 @@ export function main() { path: Some("f/circular/test_both".to_string()), language: ScriptLang::Bun, lock: None, - concurrency_settings: - windmill_common::runnable_settings::ConcurrencySettings::default().into(), + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() + .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), cache_ttl: None, cache_ignore_s3_path: None, @@ -741,8 +748,8 @@ export function main(x: number) { path: None, language: ScriptLang::Bun, lock: None, - concurrency_settings: - windmill_common::runnable_settings::ConcurrencySettings::default().into(), + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() + .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), cache_ttl: None, cache_ignore_s3_path: None, @@ -791,8 +798,8 @@ export function main() { path: None, language: ScriptLang::Bun, lock: None, - concurrency_settings: - windmill_common::runnable_settings::ConcurrencySettings::default().into(), + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() + .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), cache_ttl: None, cache_ignore_s3_path: None, @@ -836,8 +843,8 @@ export function main() { path: None, language: ScriptLang::Bun, lock: None, - concurrency_settings: - windmill_common::runnable_settings::ConcurrencySettings::default().into(), + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() + .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), cache_ttl: None, cache_ignore_s3_path: None, @@ -859,11 +866,11 @@ export function main() { // ============================================================================ mod dedicated_worker_protocol { - use windmill_test_utils::{parse_dedicated_worker_line, DedicatedWorkerResult}; use std::io::{BufRead, BufReader, Write}; use std::process::{Command, Stdio}; + use windmill_test_utils::{parse_dedicated_worker_line, DedicatedWorkerResult}; use windmill_worker::{ - build_loader, generate_dedicated_worker_wrapper, BUN_DEDICATED_WORKER_ARGS, LoaderMode, + build_loader, generate_dedicated_worker_wrapper, LoaderMode, BUN_DEDICATED_WORKER_ARGS, BUN_PATH, NODE_BIN_PATH, }; @@ -934,12 +941,8 @@ mod dedicated_worker_protocol { let temp_dir = tempfile::tempdir().unwrap(); // Create files and get the wrapper path (bundled for node, raw for bun) - let wrapper_path = create_test_worker_files( - temp_dir.path(), - script, - arg_names, - runtime == "node", - ); + let wrapper_path = + create_test_worker_files(temp_dir.path(), script, arg_names, runtime == "node"); let wrapper_str = wrapper_path.to_str().unwrap(); // Build args matching production behavior @@ -992,7 +995,10 @@ mod dedicated_worker_protocol { match parse_dedicated_worker_line(response.trim()) { DedicatedWorkerResult::Success(value) => results.push(Ok(value)), DedicatedWorkerResult::Error(err) => { - let msg = err["message"].as_str().unwrap_or("Unknown error").to_string(); + let msg = err["message"] + .as_str() + .unwrap_or("Unknown error") + .to_string(); results.push(Err(msg)); } other => panic!("Unexpected response: {:?}", other), @@ -1162,8 +1168,8 @@ export function main(name: string) { path: None, language: ScriptLang::Bun, lock: None, - concurrency_settings: - windmill_common::runnable_settings::ConcurrencySettings::default().into(), + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() + .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), cache_ttl: None, cache_ignore_s3_path: None, @@ -1190,6 +1196,68 @@ export function main(name: string) { Ok(()) } +/// Test that full .npmrc content works for bun jobs with private registries. +/// Requires: +/// - `TEST_NPMRC` environment variable set to the full .npmrc content +#[cfg(feature = "private_registry_test")] +#[sqlx::test(fixtures("base"))] +async fn test_bun_job_private_npmrc(db: Pool) -> anyhow::Result<()> { + use windmill_worker::NPMRC; + + let npmrc_content = std::env::var("TEST_NPMRC") + .expect("TEST_NPMRC must be set when running private_registry_test"); + + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + + { + let mut npmrc = NPMRC.write().await; + *npmrc = Some(npmrc_content.clone()); + } + + let content = r#" +import { greet } from "@windmill-test/private-pkg"; + +export function main(name: string) { + return greet(name); +} +"# + .to_owned(); + + let job = JobPayload::Code(RawCode { + hash: None, + content, + path: None, + language: ScriptLang::Bun, + lock: None, + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() + .into(), + debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + cache_ttl: None, + cache_ignore_s3_path: None, + dedicated_worker: None, + }); + + let result = RunJob::from(job) + .arg("name", serde_json::json!("World")) + .run_until_complete(&db, false, port) + .await + .json_result() + .unwrap(); + + { + let mut npmrc = NPMRC.write().await; + *npmrc = None; + } + + assert_eq!( + result, + serde_json::json!("Hello from private package, World!") + ); + Ok(()) +} + /// Tests for RELATIVE_BUN_BUILDER (loader_builder.bun.js) /// These tests verify Bun's behavior for import scanning and package.json generation. /// Purpose: Catch regressions when upgrading Bun versions. @@ -1241,8 +1309,8 @@ mod bun_builder_tests { } // Read generated package.json - let package_json = std::fs::read_to_string(dir.join("package.json")) - .expect("package.json not generated"); + let package_json = + std::fs::read_to_string(dir.join("package.json")).expect("package.json not generated"); serde_json::from_str(&package_json).expect("Invalid JSON in package.json") } @@ -1257,7 +1325,10 @@ export function main() { return lodash; } let pkg = run_builder(main_ts); let deps = pkg["dependencies"].as_object().unwrap(); - assert!(deps.contains_key("lodash"), "lodash should be in dependencies"); + assert!( + deps.contains_key("lodash"), + "lodash should be in dependencies" + ); assert_eq!(deps["lodash"], "latest"); } @@ -1271,7 +1342,10 @@ export function main() { return _; } let pkg = run_builder(main_ts); let deps = pkg["dependencies"].as_object().unwrap(); - assert!(deps.contains_key("lodash"), "lodash should be in dependencies"); + assert!( + deps.contains_key("lodash"), + "lodash should be in dependencies" + ); assert_eq!(deps["lodash"], "4.17.21"); } @@ -1304,9 +1378,18 @@ export function main() { return { lodash, axios, dayjs }; } let pkg = run_builder(main_ts); let deps = pkg["dependencies"].as_object().unwrap(); - assert!(deps.contains_key("lodash"), "lodash should be in dependencies"); - assert!(deps.contains_key("axios"), "axios should be in dependencies"); - assert!(deps.contains_key("dayjs"), "dayjs should be in dependencies"); + assert!( + deps.contains_key("lodash"), + "lodash should be in dependencies" + ); + assert!( + deps.contains_key("axios"), + "axios should be in dependencies" + ); + assert!( + deps.contains_key("dayjs"), + "dayjs should be in dependencies" + ); assert_eq!(deps.len(), 3, "Should have exactly 3 dependencies"); } @@ -1330,8 +1413,15 @@ export function main() { return { fs, path, lodash }; } !deps.contains_key("path"), "path (builtin) should NOT be in dependencies" ); - assert!(deps.contains_key("lodash"), "lodash should be in dependencies"); - assert_eq!(deps.len(), 1, "Should have exactly 1 dependency (lodash only)"); + assert!( + deps.contains_key("lodash"), + "lodash should be in dependencies" + ); + assert_eq!( + deps.len(), + 1, + "Should have exactly 1 dependency (lodash only)" + ); } /// Test: semver.order() resolves version conflicts (picks lowest version) @@ -1347,7 +1437,10 @@ export function main() { return { a, b }; } let pkg = run_builder(main_ts); let deps = pkg["dependencies"].as_object().unwrap(); - assert!(deps.contains_key("lodash"), "lodash should be in dependencies"); + assert!( + deps.contains_key("lodash"), + "lodash should be in dependencies" + ); // The builder sorts by semver and picks the first (lowest) version assert_eq!( deps["lodash"], "4.17.10", diff --git a/backend/tests/worker.rs b/backend/tests/worker.rs index d084a0177c..647647604f 100644 --- a/backend/tests/worker.rs +++ b/backend/tests/worker.rs @@ -1592,6 +1592,66 @@ export async function main(a: Date) { Ok(()) } +/// Test that full .npmrc content works for deno jobs with private registries. +/// Requires: +/// - `TEST_NPMRC` environment variable set to the full .npmrc content +#[cfg(feature = "private_registry_test")] +#[sqlx::test(fixtures("base"))] +async fn test_deno_job_private_npmrc(db: Pool) -> anyhow::Result<()> { + use windmill_worker::NPMRC; + + let npmrc_content = std::env::var("TEST_NPMRC") + .expect("TEST_NPMRC must be set when running private_registry_test"); + + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + + { + let mut npmrc = NPMRC.write().await; + *npmrc = Some(npmrc_content.clone()); + } + + let content = r#" +import { greet } from "npm:@windmill-test/private-pkg"; + +export function main(name: string) { + return greet(name); +} +"# + .to_owned(); + + let result = RunJob::from(JobPayload::Code(RawCode { + hash: None, + content, + path: None, + language: ScriptLang::Deno, + lock: None, + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() + .into(), + debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + cache_ttl: None, + cache_ignore_s3_path: None, + dedicated_worker: None, + })) + .arg("name", json!("World")) + .run_until_complete(&db, false, port) + .await + .json_result() + .unwrap(); + + { + let mut npmrc = NPMRC.write().await; + *npmrc = None; + } + + assert_eq!( + result, + serde_json::json!("Hello from private package, World!") + ); + Ok(()) +} + #[cfg(feature = "python")] #[sqlx::test(fixtures("base"))] async fn test_python_job_datetime_and_bytes(db: Pool) -> anyhow::Result<()> { diff --git a/backend/windmill-api-npm-proxy/Cargo.toml b/backend/windmill-api-npm-proxy/Cargo.toml index c5852090ce..4cdae773fe 100644 --- a/backend/windmill-api-npm-proxy/Cargo.toml +++ b/backend/windmill-api-npm-proxy/Cargo.toml @@ -13,6 +13,7 @@ windmill-api-auth.workspace = true windmill-common = { workspace = true, default-features = false } axum.workspace = true flate2.workspace = true +reqwest.workspace = true serde.workspace = true serde_json.workspace = true sqlx.workspace = true diff --git a/backend/windmill-api-npm-proxy/src/lib.rs b/backend/windmill-api-npm-proxy/src/lib.rs index 6bcffe0d0a..310142598f 100644 --- a/backend/windmill-api-npm-proxy/src/lib.rs +++ b/backend/windmill-api-npm-proxy/src/lib.rs @@ -14,8 +14,10 @@ use std::collections::HashMap; use tower_http::cors::{Any, CorsLayer}; use windmill_common::{ error::{Error, JsonResult, Result}, - global_settings::{load_value_from_global_settings, NPM_CONFIG_REGISTRY_SETTING}, - utils::StripPath, + global_settings::{ + load_value_from_global_settings, NPMRC_SETTING, NPM_CONFIG_REGISTRY_SETTING, + }, + utils::{parse_npmrc_registry, StripPath}, }; use windmill_api_auth::ApiAuthed; @@ -129,6 +131,14 @@ pub fn workspaced_service() -> Router { ) } +fn build_registry_request(url: &str, auth_token: &Option) -> reqwest::RequestBuilder { + let mut req = HTTP_CLIENT.get(url); + if let Some(token) = auth_token { + req = req.bearer_auth(token); + } + req +} + /// Get package metadata (versions and tags) from the private registry async fn get_package_metadata( _authed: ApiAuthed, @@ -136,21 +146,14 @@ async fn get_package_metadata( Extension(db): Extension>, ) -> JsonResult { let package = parse_package_name(package_path.to_path()); - let npm_registry = get_npm_registry(&db).await?; - - if npm_registry.is_none() { - return Err(Error::BadRequest( - "No private npm registry configured".to_string(), - )); - } - - let registry_url = npm_registry.unwrap(); + let (registry_url, auth_token) = get_npm_registry(&db) + .await? + .ok_or_else(|| Error::BadRequest("No private npm registry configured".to_string()))?; let package_url = format_registry_url(®istry_url, &package, None, None); tracing::info!("Fetching package metadata from: {}", package_url); - let response = HTTP_CLIENT - .get(&package_url) + let response = build_registry_request(&package_url, &auth_token) .send() .await .map_err(|e| Error::InternalErr(format!("Failed to fetch package metadata: {}", e)))?; @@ -167,7 +170,6 @@ async fn get_package_metadata( .await .map_err(|e| Error::InternalErr(format!("Failed to parse package metadata: {}", e)))?; - // Extract versions and dist-tags from the package metadata let mut versions = Vec::new(); let mut tags = HashMap::new(); @@ -194,22 +196,15 @@ async fn resolve_package_version( Extension(db): Extension>, ) -> JsonResult { let package = parse_package_name(package_path.to_path()); - let npm_registry = get_npm_registry(&db).await?; - - if npm_registry.is_none() { - return Err(Error::BadRequest( - "No private npm registry configured".to_string(), - )); - } - - let registry_url = npm_registry.unwrap(); + let (registry_url, auth_token) = get_npm_registry(&db) + .await? + .ok_or_else(|| Error::BadRequest("No private npm registry configured".to_string()))?; let reference = query.tag.unwrap_or_else(|| "latest".to_string()); let package_url = format_registry_url(®istry_url, &package, None, None); tracing::info!("Resolving package version from: {}", package_url); - let response = HTTP_CLIENT - .get(&package_url) + let response = build_registry_request(&package_url, &auth_token) .send() .await .map_err(|e| Error::InternalErr(format!("Failed to fetch package metadata: {}", e)))?; @@ -256,21 +251,14 @@ async fn get_package_filetree( Extension(db): Extension>, ) -> JsonResult { let (package, version) = parse_package_and_version(package_version_path.to_path())?; - let npm_registry = get_npm_registry(&db).await?; - - if npm_registry.is_none() { - return Err(Error::BadRequest( - "No private npm registry configured".to_string(), - )); - } - - let registry_url = npm_registry.unwrap(); + let (registry_url, auth_token) = get_npm_registry(&db) + .await? + .ok_or_else(|| Error::BadRequest("No private npm registry configured".to_string()))?; let package_url = format_registry_url(®istry_url, &package, None, None); tracing::info!("Fetching package filetree from: {}", package_url); - let response = HTTP_CLIENT - .get(&package_url) + let response = build_registry_request(&package_url, &auth_token) .send() .await .map_err(|e| Error::InternalErr(format!("Failed to fetch package metadata: {}", e)))?; @@ -287,7 +275,6 @@ async fn get_package_filetree( .await .map_err(|e| Error::InternalErr(format!("Failed to parse package metadata: {}", e)))?; - // Get the tarball URL for this version let tarball_url = package_json .get("versions") .and_then(|v| v.get(&version)) @@ -296,9 +283,7 @@ async fn get_package_filetree( .and_then(|t| t.as_str()) .ok_or_else(|| Error::NotFound(format!("Tarball not found for {}@{}", package, version)))?; - // Download and extract tarball to get file list - let tarball_response = HTTP_CLIENT - .get(tarball_url) + let tarball_response = build_registry_request(tarball_url, &auth_token) .send() .await .map_err(|e| Error::InternalErr(format!("Failed to download tarball: {}", e)))?; @@ -337,21 +322,14 @@ async fn get_package_file( Extension(db): Extension>, ) -> Result { let (package, version, filepath) = parse_package_version_and_file(full_path.to_path())?; - let npm_registry = get_npm_registry(&db).await?; - - if npm_registry.is_none() { - return Err(Error::BadRequest( - "No private npm registry configured".to_string(), - )); - } - - let registry_url = npm_registry.unwrap(); + let (registry_url, auth_token) = get_npm_registry(&db) + .await? + .ok_or_else(|| Error::BadRequest("No private npm registry configured".to_string()))?; let package_url = format_registry_url(®istry_url, &package, None, None); tracing::info!("Fetching package file from: {}", package_url); - let response = HTTP_CLIENT - .get(&package_url) + let response = build_registry_request(&package_url, &auth_token) .send() .await .map_err(|e| Error::InternalErr(format!("Failed to fetch package metadata: {}", e)))?; @@ -368,7 +346,6 @@ async fn get_package_file( .await .map_err(|e| Error::InternalErr(format!("Failed to parse package metadata: {}", e)))?; - // Get the tarball URL for this version let tarball_url = package_json .get("versions") .and_then(|v| v.get(&version)) @@ -377,9 +354,7 @@ async fn get_package_file( .and_then(|t| t.as_str()) .ok_or_else(|| Error::NotFound(format!("Tarball not found for {}@{}", package, version)))?; - // Download tarball - let tarball_response = HTTP_CLIENT - .get(tarball_url) + let tarball_response = build_registry_request(tarball_url, &auth_token) .send() .await .map_err(|e| Error::InternalErr(format!("Failed to download tarball: {}", e)))?; @@ -402,13 +377,38 @@ async fn get_package_file( Ok(file_content) } -/// Get the npm registry URL from global settings -async fn get_npm_registry(db: &sqlx::Pool) -> Result> { +/// Get the npm registry URL and optional auth token from global settings. +/// Checks the `npmrc` setting first, then falls back to `npm_config_registry`. +async fn get_npm_registry( + db: &sqlx::Pool, +) -> Result)>> { + let npmrc = load_value_from_global_settings(db, NPMRC_SETTING) + .await? + .and_then(|v| v.as_str().map(|s| s.to_string())); + + if let Some(ref npmrc_content) = npmrc { + if let Some(parsed) = parse_npmrc_registry(npmrc_content) { + return Ok(Some(parsed)); + } + } + let registry = load_value_from_global_settings(db, NPM_CONFIG_REGISTRY_SETTING) .await? .and_then(|v| v.as_str().map(|s| s.to_string())); - Ok(registry) + if let Some(ref s) = registry { + let (url, token) = if s.contains(":_authToken=") { + let parts: Vec<&str> = s.split(":_authToken=").collect(); + let url = parts[0].to_string(); + let token = parts.get(1).map(|t| t.to_string()); + (url, token) + } else { + (s.clone(), None) + }; + return Ok(Some((url, token))); + } + + Ok(None) } /// Format a registry URL for a package diff --git a/backend/windmill-common/src/global_settings.rs b/backend/windmill-common/src/global_settings.rs index 712ae50098..3347127303 100644 --- a/backend/windmill-common/src/global_settings.rs +++ b/backend/windmill-common/src/global_settings.rs @@ -10,6 +10,7 @@ pub const REQUEST_SIZE_LIMIT_SETTING: &str = "request_size_limit_mb"; pub const LICENSE_KEY_SETTING: &str = "license_key"; pub const NPM_CONFIG_REGISTRY_SETTING: &str = "npm_config_registry"; pub const BUNFIG_INSTALL_SCOPES_SETTING: &str = "bunfig_install_scopes"; +pub const NPMRC_SETTING: &str = "npmrc"; pub const NUGET_CONFIG_SETTING: &str = "nuget_config"; pub const POWERSHELL_REPO_URL_SETTING: &str = "powershell_repo_url"; pub const POWERSHELL_REPO_PAT_SETTING: &str = "powershell_repo_pat"; diff --git a/backend/windmill-common/src/instance_config.rs b/backend/windmill-common/src/instance_config.rs index 7270577e88..89df71824f 100644 --- a/backend/windmill-common/src/instance_config.rs +++ b/backend/windmill-common/src/instance_config.rs @@ -261,6 +261,8 @@ pub struct GlobalSettings { #[serde(skip_serializing_if = "Option::is_none")] pub bunfig_install_scopes: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub npmrc: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub nuget_config: Option, #[serde(skip_serializing_if = "Option::is_none")] pub maven_repos: Option, @@ -774,7 +776,11 @@ pub const PROTECTED_SETTINGS: &[&str] = &[ /// Note: jwt_secret is intentionally NOT hidden — it is included in YAML exports so that /// operators can set it via ConfigMap. It is protected from deletion (PROTECTED_SETTINGS) /// and from being set to empty/null, and its value is partially redacted in log output. -pub const HIDDEN_SETTINGS: &[&str] = &["uid", "min_keep_alive_version", "automate_username_creation"]; +pub const HIDDEN_SETTINGS: &[&str] = &[ + "uid", + "min_keep_alive_version", + "automate_username_creation", +]; /// Top-level settings whose entire value is sensitive and must be fully redacted in logs. const SENSITIVE_SETTINGS: &[&str] = &[ @@ -788,6 +794,7 @@ const SENSITIVE_SETTINGS: &[&str] = &[ "pip_extra_index_url", "npm_config_registry", "bunfig_install_scopes", + "npmrc", "maven_repos", "ruby_repos", "powershell_repo_pat", @@ -798,7 +805,10 @@ const SENSITIVE_SETTINGS: &[&str] = &[ const NESTED_SENSITIVE_FIELDS: &[(&str, &[&str])] = &[ ("smtp_settings", &["smtp_password"]), ("secret_backend", &["token"]), - ("object_store_cache_config", &["secret_key", "serviceAccountKey"]), + ( + "object_store_cache_config", + &["secret_key", "serviceAccountKey"], + ), ]; fn redact_json_value(value: &serde_json::Value) -> serde_json::Value { @@ -2353,7 +2363,11 @@ mod tests { ); let diff = diff_global_settings(¤t, &desired, ApplyMode::Merge); - assert_eq!(diff.upserts.len(), 1, "Same client with newer expiry should update even with different signature"); + assert_eq!( + diff.upserts.len(), + 1, + "Same client with newer expiry should update even with different signature" + ); } #[test] diff --git a/backend/windmill-common/src/utils.rs b/backend/windmill-common/src/utils.rs index 0c5f4b4a23..2fec2f9e94 100644 --- a/backend/windmill-common/src/utils.rs +++ b/backend/windmill-common/src/utils.rs @@ -1281,3 +1281,96 @@ mod tests { assert_eq!(parsed, serde_json::json!([[1], [2], [3], [4], [5]])); } } + +/// Parse .npmrc content to extract the default registry URL and its auth token. +/// Returns `Some((registry_url, Option))` if a default registry is found. +pub fn parse_npmrc_registry(npmrc_content: &str) -> Option<(String, Option)> { + let mut registry_url: Option = None; + let mut auth_tokens: Vec<(String, String)> = Vec::new(); + + for line in npmrc_content.lines() { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') || line.starts_with(';') { + continue; + } + + if let Some(url) = line.strip_prefix("registry=") { + registry_url = Some(url.trim().to_string()); + } + + if line.starts_with("//") { + if let Some((prefix, token)) = line.split_once(":_authToken=") { + auth_tokens.push((prefix.to_string(), token.to_string())); + } + } + } + + let url = registry_url?; + let url_without_protocol = url.trim_start_matches("https:").trim_start_matches("http:"); + let url_prefix = url_without_protocol.trim_end_matches('/'); + + let token = auth_tokens + .iter() + .find(|(prefix, _)| { + let p = prefix.trim_end_matches('/'); + p == url_prefix + }) + .map(|(_, token)| token.clone()); + + Some((url, token)) +} + +#[cfg(test)] +mod npmrc_tests { + use super::parse_npmrc_registry; + + #[test] + fn test_parse_simple_registry() { + let npmrc = "registry=https://registry.mycompany.com/\n//registry.mycompany.com/:_authToken=secret123\n"; + let result = parse_npmrc_registry(npmrc); + assert_eq!( + result, + Some(( + "https://registry.mycompany.com/".to_string(), + Some("secret123".to_string()) + )) + ); + } + + #[test] + fn test_parse_registry_without_auth() { + let npmrc = "registry=https://registry.npmjs.org/\n"; + let result = parse_npmrc_registry(npmrc); + assert_eq!( + result, + Some(("https://registry.npmjs.org/".to_string(), None)) + ); + } + + #[test] + fn test_parse_scoped_only_no_default() { + let npmrc = + "@myorg:registry=https://registry.myorg.com/\n//registry.myorg.com/:_authToken=tok\n"; + let result = parse_npmrc_registry(npmrc); + assert_eq!(result, None); + } + + #[test] + fn test_parse_with_comments() { + let npmrc = "# My registry\nregistry=https://r.example.com/\n; auth\n//r.example.com/:_authToken=tok\n"; + let result = parse_npmrc_registry(npmrc); + assert_eq!( + result, + Some(( + "https://r.example.com/".to_string(), + Some("tok".to_string()) + )) + ); + } + + #[test] + fn test_parse_empty_npmrc() { + assert_eq!(parse_npmrc_registry(""), None); + assert_eq!(parse_npmrc_registry("# just a comment"), None); + } +} diff --git a/backend/windmill-worker/src/bun_executor.rs b/backend/windmill-worker/src/bun_executor.rs index 64c434db04..9c46db572d 100644 --- a/backend/windmill-worker/src/bun_executor.rs +++ b/backend/windmill-worker/src/bun_executor.rs @@ -22,8 +22,8 @@ use crate::{ handle_child::handle_child, is_sandboxing_enabled, read_ee_registry, BUNFIG_INSTALL_SCOPES, BUN_BUNDLE_CACHE_DIR, BUN_CACHE_DIR, BUN_NO_CACHE, BUN_PATH, DISABLE_NUSER, HOME_ENV, NODE_BIN_PATH, NODE_PATH, - NPM_CONFIG_REGISTRY, NPM_PATH, NSJAIL_PATH, PATH_ENV, PROXY_ENVS, TRACING_PROXY_CA_CERT_PATH, - TZ_ENV, + NPMRC, NPM_CONFIG_REGISTRY, NPM_PATH, NSJAIL_PATH, PATH_ENV, PROXY_ENVS, + TRACING_PROXY_CA_CERT_PATH, TZ_ENV, }; use windmill_common::{ client::AuthedClient, @@ -299,6 +299,20 @@ async fn gen_bunfig( w_id: &str, db: Option<&Connection>, ) -> Result<()> { + let npmrc = if let Some(conn) = db { + read_ee_registry(NPMRC.read().await.clone(), "npmrc", job_id, w_id, conn).await + } else { + NPMRC.read().await.clone() + }; + + if let Some(ref npmrc_content) = npmrc { + if !npmrc_content.trim().is_empty() { + tracing::debug!("Writing .npmrc for bun from npmrc setting"); + write_file(job_dir, ".npmrc", npmrc_content)?; + return Ok(()); + } + } + let (registry, bunfig_install_scopes) = if let Some(conn) = db { ( read_ee_registry( @@ -402,39 +416,55 @@ pub async fn install_bun_lockfile( }; let has_file = if npm_mode { - let registry = if let Some(conn) = db { - read_ee_registry( - NPM_CONFIG_REGISTRY.read().await.clone(), - "npm registry", - job_id, - w_id, - conn, - ) - .await + let npmrc = if let Some(conn) = db { + read_ee_registry(NPMRC.read().await.clone(), "npmrc", job_id, w_id, conn).await } else { - NPM_CONFIG_REGISTRY.read().await.clone() + NPMRC.read().await.clone() }; - if let Some(registry) = registry { - let content = registry - .trim_start_matches("https:") - .trim_start_matches("http:"); - let mut splitted = registry.split(":_authToken="); - let custom_registry = splitted.next().unwrap_or_default(); - npm_logs.push_str(&format!( - "Using custom npm registry: {custom_registry} {}\n", - if splitted.next().is_some() { - "with authToken" - } else { - "without authToken" - } - )); - - child_cmd.env("NPM_CONFIG_REGISTRY", custom_registry); - write_file(job_dir, ".npmrc", content)?; - true + if let Some(ref npmrc_content) = npmrc { + if !npmrc_content.trim().is_empty() { + npm_logs.push_str("Using .npmrc from instance settings\n"); + write_file(job_dir, ".npmrc", npmrc_content)?; + true + } else { + false + } } else { - false + let registry = if let Some(conn) = db { + read_ee_registry( + NPM_CONFIG_REGISTRY.read().await.clone(), + "npm registry", + job_id, + w_id, + conn, + ) + .await + } else { + NPM_CONFIG_REGISTRY.read().await.clone() + }; + if let Some(registry) = registry { + let content = registry + .trim_start_matches("https:") + .trim_start_matches("http:"); + + let mut splitted = registry.split(":_authToken="); + let custom_registry = splitted.next().unwrap_or_default(); + npm_logs.push_str(&format!( + "Using custom npm registry: {custom_registry} {}\n", + if splitted.next().is_some() { + "with authToken" + } else { + "without authToken" + } + )); + + child_cmd.env("NPM_CONFIG_REGISTRY", custom_registry); + write_file(job_dir, ".npmrc", content)?; + true + } else { + false + } } } else { false @@ -446,9 +476,11 @@ pub async fn install_bun_lockfile( } } - let mut child_process = start_child_process(child_cmd, &*BUN_PATH, false).await?; + if !has_file { + gen_bunfig(job_dir, job_id, w_id, db).await?; + } - gen_bunfig(job_dir, job_id, w_id, db).await?; + let mut child_process = start_child_process(child_cmd, &*BUN_PATH, false).await?; if let Some(db) = db { handle_child( job_id, diff --git a/backend/windmill-worker/src/deno_executor.rs b/backend/windmill-worker/src/deno_executor.rs index 39b619cae4..66d8ae8673 100644 --- a/backend/windmill-worker/src/deno_executor.rs +++ b/backend/windmill-worker/src/deno_executor.rs @@ -13,7 +13,7 @@ use crate::{ }, get_proxy_envs_for_lang, handle_child::handle_child, - is_sandboxing_enabled, read_ee_registry, DENO_CACHE_DIR, DENO_PATH, HOME_ENV, + is_sandboxing_enabled, read_ee_registry, DENO_CACHE_DIR, DENO_PATH, HOME_ENV, NPMRC, NPM_CONFIG_REGISTRY, PATH_ENV, TZ_ENV, }; use windmill_common::client::AuthedClient; @@ -79,21 +79,29 @@ async fn get_common_deno_proc_envs( ), ]); - let registry = if let Some(conn) = conn { - read_ee_registry( - NPM_CONFIG_REGISTRY.read().await.clone(), - "npm registry", - job_id, - w_id, - conn, - ) - .await + let npmrc = if let Some(conn) = conn { + read_ee_registry(NPMRC.read().await.clone(), "npmrc", job_id, w_id, conn).await } else { - NPM_CONFIG_REGISTRY.read().await.clone() + NPMRC.read().await.clone() }; - if let Some(ref s) = registry { - let (url, _token_opt) = parse_npm_config(s); - deno_envs.insert(String::from("NPM_CONFIG_REGISTRY"), url); + + if npmrc.as_ref().map_or(true, |s| s.trim().is_empty()) { + let registry = if let Some(conn) = conn { + read_ee_registry( + NPM_CONFIG_REGISTRY.read().await.clone(), + "npm registry", + job_id, + w_id, + conn, + ) + .await + } else { + NPM_CONFIG_REGISTRY.read().await.clone() + }; + if let Some(ref s) = registry { + let (url, _token_opt) = parse_npm_config(s); + deno_envs.insert(String::from("NPM_CONFIG_REGISTRY"), url); + } } if DENO_CERT.len() > 0 { deno_envs.insert(String::from("DENO_CERT"), DENO_CERT.clone()); @@ -390,6 +398,21 @@ try {{ common_deno_proc_envs.insert("HOME".to_string(), job_dir.to_string()); } + let npmrc = read_ee_registry( + NPMRC.read().await.clone(), + "npmrc", + &job.id, + &job.workspace_id, + conn, + ) + .await; + if let Some(ref npmrc_content) = npmrc { + if !npmrc_content.trim().is_empty() { + write_file(job_dir, ".npmrc", npmrc_content)?; + write_file(job_dir, "deno.json", "{}")?; + } + } + //do not cache local dependencies let child = { let reload = format!("--reload={base_internal_url}"); diff --git a/backend/windmill-worker/src/worker.rs b/backend/windmill-worker/src/worker.rs index 547075933a..71d9f12a90 100644 --- a/backend/windmill-worker/src/worker.rs +++ b/backend/windmill-worker/src/worker.rs @@ -571,6 +571,7 @@ lazy_static::lazy_static! { pub static ref NPM_CONFIG_REGISTRY: Arc>> = Arc::new(RwLock::new(None)); pub static ref BUNFIG_INSTALL_SCOPES: Arc>> = Arc::new(RwLock::new(None)); + pub static ref NPMRC: Arc>> = Arc::new(RwLock::new(None)); pub static ref BUN_NO_CACHE: bool = std::env::var("BUN_NO_CACHE") .ok() .and_then(|x| x.parse::().ok()) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 84a47623bc..55136ec49a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -835,7 +835,6 @@ "version": "1.7.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -847,7 +846,6 @@ "version": "1.7.1", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -858,7 +856,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1348,7 +1345,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.0.tgz", "integrity": "sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1503,7 +1499,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1520,7 +1515,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1537,7 +1531,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1554,7 +1547,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1571,7 +1563,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1588,7 +1579,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1605,7 +1595,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1622,7 +1611,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1639,7 +1627,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1656,7 +1643,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1673,7 +1659,6 @@ "cpu": [ "wasm32" ], - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1690,7 +1675,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1707,7 +1691,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2313,7 +2296,6 @@ "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -7193,7 +7175,7 @@ "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "jiti": "bin/jiti.js" @@ -7692,7 +7674,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7713,7 +7694,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7734,7 +7714,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7755,7 +7734,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7776,7 +7754,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7797,7 +7774,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7818,7 +7794,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7839,7 +7814,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7860,7 +7834,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7881,7 +7854,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7902,7 +7874,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -12529,21 +12500,6 @@ } } }, - "node_modules/svelte-check/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/svelte-eslint-parser": { "version": "0.43.0", "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz", diff --git a/frontend/src/lib/components/instanceSettings.ts b/frontend/src/lib/components/instanceSettings.ts index b336b11fca..9e67ff585c 100644 --- a/frontend/src/lib/components/instanceSettings.ts +++ b/frontend/src/lib/components/instanceSettings.ts @@ -406,23 +406,37 @@ export const settings: Record = { ee_only: '' }, { - label: 'Npm config registry', - description: 'Add private npm registry', - key: 'npm_config_registry', - fieldType: 'password', - placeholder: 'https://registry.npmjs.org/:_authToken=npm_FOOBAR', + label: 'NPM Registry Configuration (.npmrc)', + description: + 'Full .npmrc file content for private npm registries. Used by Bun, Deno, and the npm proxy. Takes precedence over the legacy fields below.', + key: 'npmrc', + fieldType: 'codearea', + codeAreaLang: 'ini', + placeholder: + 'registry=https://registry.mycompany.com/\n//registry.mycompany.com/:_authToken=YOUR_TOKEN\n\n@myorg:registry=https://registry.myorg.com/\n//registry.myorg.com/:_authToken=SCOPED_TOKEN', storage: 'setting', ee_only: '' }, { - label: 'Bunfig install scopes', + label: 'Npm config registry (legacy)', + description: 'Add private npm registry. Prefer using the .npmrc field above.', + key: 'npm_config_registry', + fieldType: 'password', + placeholder: 'https://registry.npmjs.org/:_authToken=npm_FOOBAR', + storage: 'setting', + ee_only: '', + hiddenIfEmpty: true + }, + { + label: 'Bunfig install scopes (legacy)', description: - 'Add private scoped registries for Bun, See: https://bun.sh/docs/install/registries', + 'Add private scoped registries for Bun. Prefer using the .npmrc field above. See: https://bun.sh/docs/install/registries', key: 'bunfig_install_scopes', fieldType: 'password', placeholder: '"@myorg3" = { token = "mytoken", url = "https://registry.myorg.com/" }', storage: 'setting', - ee_only: '' + ee_only: '', + hiddenIfEmpty: true }, { label: 'Nuget Config',