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',