diff --git a/README.md b/README.md index 8d05fbe..5b7b52f 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,6 @@ The extension exposes several settings under `cloudsmith-vsc.*`: | `cloudsmith-vsc.showLicenseIndicators` | Show license risk classification on packages. Default: `true`. | | `cloudsmith-vsc.showDockerDigestCommand` | Show an additional "Pull by digest" option for Docker install commands. Default: `false`. | | `cloudsmith-vsc.experimentalSSOBrowser` | Enable experimental browser-based SSO authentication. Default: `false`. | -| `cloudsmith-vsc.useLegacyWebApp` | Use the legacy `cloudsmith.io` webapp for platform links. Default: `false`. | | `cloudsmith-vsc.autoScanOnOpen` | Automatically scan project dependencies against Cloudsmith when a workspace is opened. Default: `false`. | | `cloudsmith-vsc.dependencyScanWorkspace` | Cloudsmith workspace slug to use for dependency health scanning. | | `cloudsmith-vsc.dependencyScanRepo` | Cloudsmith repository slug to use for dependency health scanning. | diff --git a/extension.js b/extension.js index 3d7cb23..1fa9f5a 100644 --- a/extension.js +++ b/extension.js @@ -755,21 +755,9 @@ async function activate(context) { const version = unwrapValue(item.version); const identifier = unwrapValue(item.slug_perm); - //need to replace '/' in name as UI URL replaces these with _ - const pkg = name.replaceAll("/", "_"); - - const config = vscode.workspace.getConfiguration("cloudsmith-vsc"); - const useLegacyApp = await config.get("useLegacyWebApp"); - - - if (identifier) { - if (useLegacyApp) { - const url = `https://cloudsmith.io/~${workspace}/repos/${repo}/packages/detail/${format}/${pkg}/${version}`; - vscode.env.openExternal(vscode.Uri.parse(url)); - } else { - const url = `https://app.cloudsmith.com/${workspace}/${repo}/${format}/${pkg}/${version}/${identifier}`; - vscode.env.openExternal(vscode.Uri.parse(url)); - } + const url = buildPackageUrl(workspace, repo, format, name, version, identifier); + if (url) { + vscode.env.openExternal(vscode.Uri.parse(url)); } else { vscode.window.showWarningMessage("Run this command from a package context menu."); } @@ -786,10 +774,12 @@ async function activate(context) { const name = typeof item === "string" ? item : item.name; if (name) { - // Encode special characters for URL - const encodedName = name.replaceAll("/", "%2F").replaceAll(":", "%3A"); - const url = `https://app.cloudsmith.com/${workspace}/${repo}?page=1&query=name:${encodedName}&sort=name`; - vscode.env.openExternal(vscode.Uri.parse(url)); + const url = buildPackageGroupUrl(workspace, repo, name); + if (url) { + vscode.env.openExternal(vscode.Uri.parse(url)); + return; + } + vscode.window.showWarningMessage("Please use this command from the package context menu."); } else { vscode.window.showWarningMessage("Run this command from a package context menu."); } @@ -1288,6 +1278,7 @@ async function activate(context) { } else { vscode.window.showInformationMessage("Could not open this package in Cloudsmith."); } + vscode.env.openExternal(vscode.Uri.parse(packageUrl)); } else if (action.id === "inspect") { const inspectItem = { name: name, diff --git a/models/dependencyHealthNode.js b/models/dependencyHealthNode.js index 27b99ad..45bc680 100644 --- a/models/dependencyHealthNode.js +++ b/models/dependencyHealthNode.js @@ -32,7 +32,6 @@ class DependencyHealthNode { this.slug_perm_raw = cloudsmithMatch.slug_perm; this.version = { id: "Version", value: cloudsmithMatch.version }; this.status_str = { id: "Status", value: cloudsmithMatch.status_str }; - this.self_webapp_url = cloudsmithMatch.self_webapp_url || null; this.checksum_sha256 = cloudsmithMatch.checksum_sha256 || null; this.version_digest = cloudsmithMatch.version_digest || null; this.tags_raw = cloudsmithMatch.tags || {}; diff --git a/test/webAppUrls.test.js b/test/webAppUrls.test.js new file mode 100644 index 0000000..a41ecbd --- /dev/null +++ b/test/webAppUrls.test.js @@ -0,0 +1,30 @@ +const assert = require("assert"); +const { + WEB_APP_BASE_URL, + buildPackageGroupUrl, + buildPackageUrl, + buildRepositoryUrl, +} = require("../util/webAppUrls"); + +suite("Web app URL helpers", () => { + test("buildRepositoryUrl always uses the app domain", () => { + assert.strictEqual( + buildRepositoryUrl("my-org", "my-repo"), + `${WEB_APP_BASE_URL}/my-org/my-repo` + ); + }); + + test("buildPackageUrl always uses the app domain and package slug path", () => { + assert.strictEqual( + buildPackageUrl("my-org", "my-repo", "npm", "@scope/pkg", "1.0.0", "pkg-id"), + `${WEB_APP_BASE_URL}/my-org/my-repo/npm/@scope_pkg/1.0.0/pkg-id` + ); + }); + + test("buildPackageGroupUrl always uses the app domain and repo search path", () => { + assert.strictEqual( + buildPackageGroupUrl("my-org", "my-repo", "group/name:latest"), + `${WEB_APP_BASE_URL}/my-org/my-repo?page=1&query=name:group%2Fname%3Alatest&sort=name` + ); + }); +}); diff --git a/util/diagnosticsPublisher.js b/util/diagnosticsPublisher.js index 1416af6..9a9d3ea 100644 --- a/util/diagnosticsPublisher.js +++ b/util/diagnosticsPublisher.js @@ -3,6 +3,7 @@ const vscode = require("vscode"); const { ManifestParser } = require("./manifestParser"); +const { buildRepositoryUrl } = require("./webAppUrls"); class DiagnosticsPublisher { constructor() { @@ -61,12 +62,17 @@ class DiagnosticsPublisher { // Add related info if there's a fix version available if (dep.cloudsmithMatch && dep.cloudsmithMatch.num_vulnerabilities > 0) { - diagnostic.code = { - value: `${dep.cloudsmithMatch.num_vulnerabilities} vulnerabilities`, - target: vscode.Uri.parse( - `https://app.cloudsmith.com/${dep.cloudsmithMatch.namespace}/${dep.cloudsmithMatch.repository}` - ), - }; + const repositoryUrl = buildRepositoryUrl( + dep.cloudsmithMatch.namespace, + dep.cloudsmithMatch.repository + ); + const vulnerabilityCode = `${dep.cloudsmithMatch.num_vulnerabilities} vulnerabilities`; + diagnostic.code = repositoryUrl + ? { + value: vulnerabilityCode, + target: vscode.Uri.parse(repositoryUrl), + } + : vulnerabilityCode; } diagnostics.push(diagnostic); diff --git a/util/installCommandBuilder.js b/util/installCommandBuilder.js index 0c96506..e1a46ce 100644 --- a/util/installCommandBuilder.js +++ b/util/installCommandBuilder.js @@ -1,6 +1,8 @@ // Install command builder - generates format-native install commands // with Cloudsmith registry URLs pre-filled. +const { WEB_APP_BASE_URL, buildRepositoryUrl } = require("./webAppUrls"); + const VERIFICATION_BANNER = "# Verify package details before running"; class InstallCommandBuilder { @@ -194,9 +196,10 @@ class InstallCommandBuilder { const entry = commands[format]; if (!entry) { + const repositoryUrl = buildRepositoryUrl(workspace, repo) || WEB_APP_BASE_URL; return { command: `# Verify package details before running\n# No install command template for format: ${format}`, - note: `Visit https://app.cloudsmith.com/${workspace}/${repo} for setup instructions.`, + note: `Visit ${repositoryUrl} for setup instructions.`, }; } return entry; diff --git a/util/webAppUrls.js b/util/webAppUrls.js new file mode 100644 index 0000000..6b59f2d --- /dev/null +++ b/util/webAppUrls.js @@ -0,0 +1,41 @@ +// Copyright 2026 Cloudsmith Ltd. All rights reserved. +const WEB_APP_BASE_URL = "https://app.cloudsmith.com"; + +function encodePathSegment(value) { + return encodeURIComponent(String(value)); +} + +function buildRepositoryUrl(workspace, repo) { + if (!workspace || !repo) { + return null; + } + + return `${WEB_APP_BASE_URL}/${encodePathSegment(workspace)}/${encodePathSegment(repo)}`; +} + +function buildPackageUrl(workspace, repo, format, name, version, identifier) { + if (!workspace || !repo || !format || !name || !version || !identifier) { + return null; + } + + const packageName = String(name).replaceAll("/", "_"); + const encodedPackageName = encodePathSegment(packageName).replaceAll("%40", "@"); + return `${WEB_APP_BASE_URL}/${encodePathSegment(workspace)}/${encodePathSegment(repo)}/${encodePathSegment(format)}/${encodedPackageName}/${encodePathSegment(version)}/${encodePathSegment(identifier)}`; +} + +function buildPackageGroupUrl(workspace, repo, name) { + const repositoryUrl = buildRepositoryUrl(workspace, repo); + if (!repositoryUrl || !name) { + return null; + } + + const query = encodePathSegment(name); + return `${repositoryUrl}?page=1&query=name:${query}&sort=name`; +} + +module.exports = { + WEB_APP_BASE_URL, + buildRepositoryUrl, + buildPackageUrl, + buildPackageGroupUrl, +}; diff --git a/views/quarantineExplainProvider.js b/views/quarantineExplainProvider.js index d81042f..7d5b73a 100644 --- a/views/quarantineExplainProvider.js +++ b/views/quarantineExplainProvider.js @@ -5,6 +5,7 @@ const crypto = require("crypto"); const vscode = require("vscode"); const { CloudsmithAPI } = require("../util/cloudsmithAPI"); +const { buildPackageUrl } = require("../util/webAppUrls"); class QuarantineExplainProvider { constructor(context) { @@ -43,7 +44,7 @@ class QuarantineExplainProvider { } const statusReason = item.status_reason || null; - const selfUrl = item.self_webapp_url || null; + const packageUrl = buildPackageUrl(workspace, repo, format, name, version, slugPerm); if (!workspace || !slugPerm) { vscode.window.showWarningMessage("Could not determine package details for quarantine explanation."); @@ -82,7 +83,7 @@ class QuarantineExplainProvider { // Render the full panel panel.webview.html = this._getHtmlContent( nonce, name, version, format, workspace, repo, slugPerm, - statusReason, selfUrl, policyTrace + statusReason, packageUrl, policyTrace ); // Handle messages from the WebView @@ -91,8 +92,8 @@ class QuarantineExplainProvider { vscode.commands.executeCommand("cloudsmith-vsc.findSafeVersion", item); } else if (message.command === "showVulnerabilities") { vscode.commands.executeCommand("cloudsmith-vsc.showVulnerabilities", item); - } else if (message.command === "openInCloudsmith" && selfUrl) { - await vscode.env.openExternal(vscode.Uri.parse(selfUrl)); + } else if (message.command === "openInCloudsmith" && packageUrl) { + await vscode.env.openExternal(vscode.Uri.parse(packageUrl)); } else if (message.command === "copyReport") { const report = this._buildPlainTextReport(name, version, statusReason, policyTrace); await vscode.env.clipboard.writeText(report); @@ -175,7 +176,7 @@ class QuarantineExplainProvider { `; } - _getHtmlContent(nonce, name, version, format, workspace, repo, slugPerm, statusReason, selfUrl, policyTrace) { + _getHtmlContent(nonce, name, version, format, workspace, repo, slugPerm, statusReason, packageUrl, policyTrace) { // Determine if this is vulnerability-related by checking for CVE references in decision logs const hasCVEs = policyTrace.decisionLogs.some(entry => (entry.reason && /CVE-/i.test(entry.reason)) ||