@mr.xcray/thycotic-secretserver-client
Version:
Thycotic Secret Server NodeJS client for WebAPI
824 lines (783 loc) • 28 kB
JavaScript
require('./types');
const array = require('ensure-array');
const soap = require('soap');
const {URL} = require('url');
const ping = require('ping');
const isDomain = require('is-valid-domain');
const isIP = require('is-ip');
/**
* Starts connection negotiation. May result in async exception in case of connection error.
* Connection procedure is async, all methods will wait for it to end before sending requests.
*
* @param {string} url
* @param {string} login - The username for authentication
* @param {string} password - The password for authentication
* @param {string} [organization=null] - The organization code if using Secret Server Online. For installed Secret Server, you must specify null or an
empty string or authentication will fail
* @param {string} [domain=null] - The domain if attempting to authenticate using a domain account. For non-domain accounts, passing null,
empty string, or “(local)” indicates it is not a domain account.
* @constructor
*/
function ThycoticSecretServerClient (url, login, password, organization, domain){
"use strict";
//TODO: add organization and domain params to comply with Authenticate method
this.ERRORS={
GOT_EMPTY_TOKEN: "Authentication resulted in empty token. That is unexpected.",
GOT_EMPTY_SECRET: "Got empty secret with no error. That is unexpected.",
BAD_TSS_URL: "It seems that TSS URL provided is incorrect",
ARRAY_OR_ONE_EXPECTED: "Argument expected to be an array or object."
};
this.connection = this._connect(url, login, password, organization, domain);
}
/**
* Starts connection sequence with error handling.
*
* @param {string} url
* @param {string} login - The username for authentication
* @param {string} password - The password for authentication
* @param {string} [organization=null] - The organization code if using Secret Server Online. For installed Secret Server, you must specify null or an
empty string or authentication will fail
* @param {string} [domain=null] - The domain if attempting to authenticate using a domain account. For non-domain accounts, passing null,
empty string, or “(local)” indicates it is not a domain account.
* @returns {Promise.<[soap, ThycoticSecretServerClient, string]>}
* @private
*/
ThycoticSecretServerClient.prototype._connect = async function (url, login, password, organization, domain){
"use strict";
url = await this.fixURL(url);
let client=null;
try {
client = await soap.createClientAsync(url);
}catch(e){
this._exception('BAD_TSS_URL');
}
let oThis = this;
if (domain===undefined){
domain=null;
}
if (organization===undefined){
organization=null;
}
return await client.AuthenticateAsync({
username: login,
password: password,
organization: organization,
domain: domain
}).then(async answer=>{
if (!oThis.isError(answer)){
return {client:client, context:oThis, token:answer.AuthenticateResult.Token};
}
}).catch(error=>{
oThis._exception(error);
});
};
/**
* method is used to get all of the favorites Secrets for the current user
*
* @param {boolean} [includeRestricted=false]
* @return {Promise.<SecretSummary[]>}
*/
ThycoticSecretServerClient.prototype.GetFavorites = async function(includeRestricted){
"use strict";
return this.connection.then((connection)=>{
let {client, context, token}=connection;
return client.GetFavoritesAsync({
token:token,
includeRestricted: includeRestricted===true
}).then(async answer=>{
if (!context.isError(answer)){
if (
answer.GetFavoritesResult.SecretSummaries!==null
&& answer.GetFavoritesResult.SecretSummaries.hasOwnProperty('SecretSummary')
) {
return answer.GetFavoritesResult.SecretSummaries.SecretSummary;
}else{
return [];
}
}
})
})
};
/**
* Use this method to retrieve the history (past values) that were audited for a specific field of a Secret
*
* @param {number} secretId
* @param {string} fieldDisplayName
* @return {Promise.<SecretItemHistoryWebServiceResult[]>}
* @constructor
*/
ThycoticSecretServerClient.prototype.GetSecretItemHistoryByFieldName = async function(secretId, fieldDisplayName){
"use strict";
return this.connection.then((connection)=>{
let {client, context, token}=connection;
return client.GetSecretItemHistoryByFieldNameAsync({
token:token,
secretId:secretId,
fieldDisplayName: fieldDisplayName
}).then(async answer=>{
if (
!context.isError(answer)
&& answer.GetSecretItemHistoryByFieldNameResult.Success===true
){
if (
answer.GetSecretItemHistoryByFieldNameResult.SecretItemHistories!==null
&& answer.GetSecretItemHistoryByFieldNameResult.SecretItemHistories.hasOwnProperty('SecretItemHistoryWebServiceResult')
&& answer.GetSecretItemHistoryByFieldNameResult.SecretItemHistories.SecretItemHistoryWebServiceResult!==null
){
return answer.GetSecretItemHistoryByFieldNameResult.SecretItemHistories.SecretItemHistoryWebServiceResult
}else{
return [];
}
}
})
})
};
/**
* A web method that downloads a file attachment stored on a Secret
*
* @param {number} secretId
* @returns {Promise.<DownloadFileAttachmentByItemIdResult>} file object
*/
ThycoticSecretServerClient.prototype.DownloadFileAttachment = async function(secretId){
"use strict";
return this.connection.then((connection)=>{
let {client, context, token}=connection;
return client.DownloadFileAttachmentAsync({
token:token,
secretId:secretId,
}).then(async answer=>{
if (!context.isError(answer)){
return answer.DownloadFileAttachmentResult;
}
})
})
};
/**
* This is similar to the DownloadFileAttachment web service, but is meant to be used when a Secret has multiple file
* attachment fields. By setting the third parameter value, the user can choose which file to download.
*
* @param {number} secretId
* @param {number} secretItemId
* @returns {Promise.<DownloadFileAttachmentByItemIdResult>} file object
*/
ThycoticSecretServerClient.prototype.DownloadFileAttachmentByItemId = async function(secretId, secretItemId){
"use strict";
return this.connection.then((connection)=>{
let {client, context, token}=connection;
return client.DownloadFileAttachmentByItemIdAsync({
token:token,
secretId:secretId,
secretItemId:secretItemId
}).then(async answer=>{
if (!context.isError(answer)){
return answer.DownloadFileAttachmentByItemIdResult;
}
})
})
};
/**
* Gets secret with all fields and files attached.
*
* @param {number} secretId - secret ID
* @returns {Promise.<Secret>}
*/
ThycoticSecretServerClient.prototype.GetSecret = async function(secretId){
"use strict";
return this.connection.then((connection)=>{
let {client, context, token}=connection;
return client.GetSecretAsync({
token:token,
secretId:secretId,
loadSettingsAndPermissions:true,
//TODO: add possibility to get a secret that requires comment
//https://thycotic.force.com/support/s/article/Using-GetSecret-to-Get-a-Secret-that-Requires-Comment-API-Example-Ruby
//codeResponses: ...
}).then(async answer=>{
if (!context.isError(answer)){
let secret = answer.GetSecretResult.Secret;
await context.fixItems(secret);
return secret;
}
});
})
};
/**
* Searches for Secrets that match a field name / search term. This will return all Secrets that have a field that is an
* exact match of the fieldName value and that field has a value that is an exact match of the fieldSearchTerm
* parameter.
*
* @param {string} fieldName
* @param {string} searchTerm
* @param {boolean} showDeleted
* @param {boolean} showRestricted
* @return {Promise.<SecretSummary[]>}
*/
ThycoticSecretServerClient.prototype.SearchSecretsByFieldValue = async function(fieldName, searchTerm, showDeleted, showRestricted){
"use strict";
searchTerm = (""+searchTerm).trim();
return this.connection.then(connection=>{
let {client, context, token}=connection;
return client.SearchSecretsByFieldValueAsync({
token:token,
searchTerm:searchTerm,
fieldName: fieldName,
showDeleted: showDeleted===true,
showRestricted: showRestricted===true
}).then(async answer=>{
if (!context.isError(answer)){
if (
answer.SearchSecretsByFieldValueResult.SecretSummaries!==null
&& answer.SearchSecretsByFieldValueResult.SecretSummaries.hasOwnProperty('SecretSummary')
) {
return answer.SearchSecretsByFieldValueResult.SecretSummaries.SecretSummary;
}else{
return [];
}
}
});
})
};
/**
* Searches for Secrets that match a field name / search term. This will return all Secrets that contain a field with
* the specified name and have a value in that field that contains the search term.
*
* @param {string} fieldName
* @param {string} searchTerm
* @param {boolean} showDeleted
* @return {Promise.<Secret[]>}
*/
ThycoticSecretServerClient.prototype.GetSecretsByFieldValue = async function(fieldName, searchTerm, showDeleted){
"use strict";
searchTerm = (""+searchTerm).trim();
return this.connection.then(connection=>{
let {client, context, token}=connection;
return client.GetSecretsByFieldValueAsync({
token:token,
searchTerm:searchTerm,
fieldName: fieldName,
showDeleted: showDeleted===true,
}).then(async answer=>{
if (!context.isError(answer)){
if (
answer.GetSecretsByFieldValueResult.Secrets!==null
&& answer.GetSecretsByFieldValueResult.Secrets.hasOwnProperty('Secret')
) {
let secrets=[];
for(let secret of answer.GetSecretsByFieldValueResult.Secrets.Secret){
await context.fixItems(secret);
secrets.push(secret);
}
return secrets;
}else{
return [];
}
}
});
})
};
/**
* Searches for Secrets that match a field name / search term but only on Secret Fields marked Exposed for Display
* on the Secret Template. This will return all Secrets that contain a field with the specified name and have a value
* in that field that contains the search term.
*
* @param {string} fieldName
* @param {string} searchTerm
* @param {boolean} showPartialMatches
* @param {boolean} showDeleted
* @param {boolean} showRestricted
* @return {Promise.<SecretSummary[]>}
*/
ThycoticSecretServerClient.prototype.SearchSecretsByExposedFieldValue = async function(fieldName, searchTerm, showPartialMatches, showDeleted, showRestricted){
"use strict";
searchTerm = (""+searchTerm).trim();
return this.connection.then(connection=>{
let {client, context, token}=connection;
return client.SearchSecretsByExposedFieldValueAsync({
token:token,
searchTerm:searchTerm,
fieldName: fieldName,
showPartialMatches: showPartialMatches===true,
showDeleted: showDeleted===true,
showRestricted: showRestricted===true
}).then(async answer=>{
if (!context.isError(answer)){
if (
answer.SearchSecretsByExposedFieldValueResult.SecretSummaries!==null
&& answer.SearchSecretsByExposedFieldValueResult.SecretSummaries.hasOwnProperty('SecretSummary')
) {
return answer.SearchSecretsByExposedFieldValueResult.SecretSummaries.SecretSummary;
}else{
return [];
}
}
});
})
};
/**
* Searches for Secrets that match a field name / search term but only on Secret Fields marked Exposed for Display
* on the Secret Template. This will return all Secrets that contain a field with the specified name and have a value
* in that field that contains the search term.
*
* @param {string} fieldName
* @param {string} searchTerm
* @param {boolean} showPartialMatches
* @param {boolean} showDeleted
* @return {Promise.<Secret[]>}
*/
ThycoticSecretServerClient.prototype.GetSecretsByExposedFieldValue = async function(fieldName, searchTerm, showPartialMatches, showDeleted){
"use strict";
searchTerm = (""+searchTerm).trim();
return this.connection.then(connection=>{
let {client, context, token}=connection;
return client.GetSecretsByExposedFieldValueAsync({
token:token,
searchTerm:searchTerm,
fieldName: fieldName,
showPartialMatches: showPartialMatches===true,
showDeleted: showDeleted===true,
}).then(async answer=>{
if (!context.isError(answer)) {
if (
answer.GetSecretsByExposedFieldValueResult.Secrets !== null
&& answer.GetSecretsByExposedFieldValueResult.Secrets.hasOwnProperty('Secret')
) {
let secrets = [];
for (let secret of answer.GetSecretsByExposedFieldValueResult.Secrets.Secret) {
await context.fixItems(secret);
secrets.push(secret);
}
return secrets;
} else {
return [];
}
}
});
})
};
/**
* Searches for Secrets across fields with a search term but only on Secret Fields marked Exposed for Display on
* the Secret Template. This will return all Secrets that have a value in that field that contains the search term.
*
* @param {string} searchTerm
* @param {boolean} showPartialMatches
* @param {boolean} showDeleted
* @param {boolean} showRestricted
* @return {Promise.<SecretSummary[]>}
*/
ThycoticSecretServerClient.prototype.SearchSecretsByExposedValues = async function(searchTerm, showPartialMatches, showDeleted, showRestricted){
"use strict";
searchTerm = (""+searchTerm).trim();
return this.connection.then(connection=>{
let {client, context, token}=connection;
return client.SearchSecretsByExposedValuesAsync({
token:token,
searchTerm:searchTerm,
showPartialMatches: showPartialMatches===true,
showDeleted: showDeleted===true,
showRestricted: showRestricted===true
}).then(async answer=>{
if (!context.isError(answer)){
if (
answer.SearchSecretsByExposedValuesResult.SecretSummaries!==null
&& answer.SearchSecretsByExposedValuesResult.SecretSummaries.hasOwnProperty('SecretSummary')
) {
return answer.SearchSecretsByExposedValuesResult.SecretSummaries.SecretSummary;
}else{
return [];
}
}
});
})
};
/**
* Searches for Secrets within a folder.
*
* @param {string} searchTerm
* @param {number} folderId
* @param {boolean} includeSubFolders
* @param {boolean} showDeleted
* @param {boolean} showRestricted
* @return {Promise.<SecretSummary[]>}
*/
ThycoticSecretServerClient.prototype.SearchSecretsByFolder = async function(searchTerm, folderId, includeSubFolders, showDeleted, showRestricted){
"use strict";
searchTerm = (""+searchTerm).trim();
return this.connection.then(connection=>{
let {client, context, token}=connection;
return client.SearchSecretsByFolderAsync({
token:token,
searchTerm:searchTerm,
folderId: folderId,
includeSubFolders: includeSubFolders===true,
showDeleted: showDeleted===true,
showRestricted: showRestricted===true
}).then(async answer=>{
if (!context.isError(answer)){
if (
answer.SearchSecretsByFolderResult.SecretSummaries!==null
&& answer.SearchSecretsByFolderResult.SecretSummaries.hasOwnProperty('SecretSummary')
) {
return answer.SearchSecretsByFolderResult.SecretSummaries.SecretSummary;
}else{
return [];
}
}
});
})
};
/**
* Searches secrets containing searchTerm in the name
*
* @param {string} searchTerm
* @param {boolean} includeDeleted
* @param {boolean} includeRestricted
* @return {Promise.<SecretSummary[]>}
*/
ThycoticSecretServerClient.prototype.SearchSecrets = async function(searchTerm, includeDeleted, includeRestricted){
"use strict";
searchTerm = (""+searchTerm).trim();
return this.connection.then(connection=>{
let {client, context, token}=connection;
return client.SearchSecretsAsync({
token:token,
searchTerm:searchTerm,
includeDeleted: includeDeleted===true,
includeRestricted: includeRestricted===true
}).then(async answer=>{
if (!context.isError(answer)){
if (
answer.SearchSecretsResult.SecretSummaries!==null
&& answer.SearchSecretsResult.SecretSummaries.hasOwnProperty('SecretSummary')
) {
return answer.SearchSecretsResult.SecretSummaries.SecretSummary;
}else{
return [];
}
}
});
})
};
/**
* Searches secrets with any of provided words in the name
* @param searchTerms
* @param includeDeleted
* @param includeRestricted
* @return {Promise.<SecretSummary[]>}
* @constructor
*/
ThycoticSecretServerClient.prototype.SearchSecrets_union = async function(searchTerms, includeDeleted, includeRestricted){
"use strict";
let promises = [];
if (typeof searchTerms==="string"){
searchTerms = searchTerms.trim().split(" ");
}
let ids=[];
let answer=[];
searchTerms = array(searchTerms);
for (let searchTerm of searchTerms){
promises.push(
this.SearchSecrets(searchTerm, includeDeleted, includeRestricted)
.then(results=>{
for(let result of results){
if (!ids.hasOwnProperty(result.SecretId)){
ids[result.SecretId]=true;
answer.push(result);
}
}
})
);
}
await Promise.all(promises);
return answer;
};
/**
* Searches secrets with all of provided words in the name
* @param searchTerms
* @param includeDeleted
* @param includeRestricted
* @return {Promise.<SecretSummary[]>}
* @constructor
*/
ThycoticSecretServerClient.prototype.SearchSecrets_intersect = async function(searchTerms, includeDeleted, includeRestricted){
"use strict";
let promises = [];
if (typeof searchTerms==="string"){
searchTerms = searchTerms.trim().split(" ");
}
let ids=[];
let answer=[];
searchTerms = array(searchTerms);
for (let searchTerm of searchTerms){
promises.push(
this.SearchSecrets(searchTerm, includeDeleted, includeRestricted)
.then(results=>{
for(let result of results) {
ids[result.SecretId] = ids[result.SecretId] ? ids[result.SecretId]+1 : 1;
if (ids[result.SecretId]===searchTerms.length) {
answer.push(result);
}
}
})
);
}
await Promise.all(promises);
return answer;
};
/**
* Gets folder by Id
*
* @param {number} folderId
* @return {Promise.<Folder|null>}
*/
ThycoticSecretServerClient.prototype.FolderGet = async function(folderId){
"use strict";
return this.connection.then(connection=>{
let {client, context, token}=connection;
return client.FolderGetAsync({
token:token,
folderId:folderId,
}).then(async answer=>{
if (!context.isError(answer)){
if (
answer.FolderGetResult.Success===true &&
answer.FolderGetResult.hasOwnProperty('Folder')
) {
return answer.FolderGetResult.Folder;
}else{
return null;
}
}
});
})
};
/**
* Searches folders by name
*
* @param {string} folderName
* @return {Promise.<Folder[]>}
*/
ThycoticSecretServerClient.prototype.SearchFolders = async function(folderName){
"use strict";
return this.connection.then(connection=>{
let {client, context, token}=connection;
return client.SearchFoldersAsync({
token:token,
folderName:folderName,
}).then(async answer=>{
if (!context.isError(answer)){
if (
answer.SearchFoldersResult.Folders!==null
&& answer.SearchFoldersResult.Folders.hasOwnProperty('Folder')
) {
return answer.SearchFoldersResult.Folders.Folder;
}else{
return [];
}
}
});
})
};
/**
* Gets sub-folders under folder with specified parentFolderId
*
* @param {number} parentFolderId
* @return {Promise.<Folder[]>}
*/
ThycoticSecretServerClient.prototype.FolderGetAllChildren = async function(parentFolderId){
"use strict";
return this.connection.then(connection=>{
let {client, context, token}=connection;
return client.FolderGetAllChildrenAsync({
token:token,
parentFolderId:parentFolderId,
}).then(async answer=>{
if (!context.isError(answer)){
if (
answer.FolderGetAllChildrenResult.Folders!==null
&& answer.FolderGetAllChildrenResult.Folders.hasOwnProperty('Folder')
) {
return answer.FolderGetAllChildrenResult.Folders.Folder;
}else{
return [];
}
}
});
})
};
/**
* Handy query function to get only one secret.
*
* @todo enrich with query language and logic
* @param query
* @return {Promise.<Secret>}
* @constructor
*/
ThycoticSecretServerClient.prototype.QuerySecret = async function(query){
"use strict";
return (await this.SecretSummaryToSecret(await this.SearchSecrets_intersect(query)))[0];
};
/**
* Expands SecretSummary[] into Secret[]
*
* @param {SecretSummary[]|SecretSummary} summaries
* @return {Promise.<Secret[]>}
*/
ThycoticSecretServerClient.prototype.SecretSummaryToSecret = async function(summaries){
"use strict";
if (!Array.isArray(summaries) && typeof summaries !== 'object'){
this._exception('ARRAY_OR_ONE_EXPECTED');
}
let promises=[];
for (let summary of array(summaries)){
promises.push(this.GetSecret(summary.SecretId));
}
if (Array.isArray(summaries)) {
return Promise.all(promises);
}else{
return promises[0]; //option when there was one SecretSummary instance instead of SecretSummary[]
}
};
/**
* Helper function to centralize exception generation.
*
* @param error
* @private
* @throws {string}
*/
ThycoticSecretServerClient.prototype._exception = function (error){
"use strict";
throw this.ERRORS.hasOwnProperty(error) ? this.ERRORS[error] : error;
};
/**
* Helper function to parse and react on Errors in responses.
*
* @param {Object} answer
* @returns {boolean}
*/
ThycoticSecretServerClient.prototype.isError = function(answer){
"use strict";
// Check for common Errors array
for(
let resultType of
[
'AuthenticateResult', 'GetSecretResult', 'DownloadFileAttachmentByItemIdResult', 'DownloadFileAttachmentResult',
'SearchSecretsResult', 'SearchFoldersResult', 'FolderGetResult', 'FolderGetAllChildrenResult',
'SearchSecretsByExposedFieldValueResult', 'SearchSecretsByExposedValuesResult', 'SearchSecretsByFieldValueResult',
'SearchSecretsByFolderResult', 'GetSecretsByFieldValueResult', 'GetSecretsByExposedFieldValueResult',
'GetSecretItemHistoryByFieldNameResult'
]
){
if (answer.hasOwnProperty(resultType) && answer[resultType].hasOwnProperty('Errors') && answer[resultType].Errors) {
this._exception(answer[resultType].Errors.string.join(",").trim());
}
}
// AuthenticateResult
if (answer.hasOwnProperty('AuthenticateResult')) {
if (answer.AuthenticateResult.Token.length === 0) {
this._exception('GOT_EMPTY_TOKEN');
}
}
// GetSecretResult
else if(answer.hasOwnProperty('GetSecretResult')) {
if(answer.GetSecretResult.SecretError){
this._exception(
"["+answer.GetSecretResult.SecretError.ErrorCode+"] "+
answer.GetSecretResult.SecretError.ErrorMessage + ": "+
answer.GetSecretResult.SecretError.CommentTitle + ". "+
answer.GetSecretResult.SecretError.AdditionalCommentTitle
);
// TODO: check if AllowsResponse should be added to error explanation
// <s:element minOccurs="1" maxOccurs="1" name="AllowsResponse" type="s:boolean"/>
}else if(!answer.GetSecretResult.Secret.hasOwnProperty('Id') || !answer.GetSecretResult.Secret.Id){
this._exception('GOT_EMPTY_SECRET');
}
}
return false;
};
/**
* Helper function that changes SecretItems into handy associative array with all attached files inside
*
* @param {Object} secret
*/
ThycoticSecretServerClient.prototype.fixItems = async function(secret){
"use strict";
let items = {};
//some WebAPI methods return secret with Items:null
if (
(
!secret.hasOwnProperty('Items')
|| secret.Items===null
|| ( // since we are working with soap, we need to check for null really deeply
secret.Items.hasOwnProperty('attributes')
&& secret.Items.attributes.hasOwnProperty('xsi:nil')
)
)
&& secret.hasOwnProperty('Id')
){
let fixed = await this.GetSecret(secret.Id);
secret.Items = Object.assign({}, fixed.Items);
return;
}
for (let item of secret.Items.SecretItem){
if (item.IsFile){
item['Value'] = await this.DownloadFileAttachmentByItemId(secret.Id, item.Id);
}
items[item.FieldName]=item;
}
secret.Items = Object.assign({}, items); //force copy, otherwise items will get nulled after this function life
};
/**
* Helper function that allows to provide only domain or TSS URI without full path to WSDL
*
* @param {string} url - domain, or ip, or domain/SecretServer
* @return {Promise.<string>} correct URL to TSS
*/
ThycoticSecretServerClient.prototype.fixURL = async function(url){
"use strict";
let url_=null;
try {
try {
url_ = new URL(url);
}catch(e){
url_ = new URL("https://"+url);
}
if (url_.host.length) {
if (url_.pathname.search("SSWebService.asmx") === -1) {
if (url_.pathname !== '/') { // let's guess that we have TSS URI already
if (url_.pathname.substr(-1) !== '/') {
url_.pathname=url_.pathname + "/";
}
url_.pathname=url_.pathname + "webservices/SSWebService.asmx";
} else { // by default TSS is under /SecretServer/ URI
url_.pathname="/SecretServer/webservices/SSWebService.asmx";
}
}
if (!url_.searchParams.has("WSDL")) { // this one is required
url_.search="WSDL"; // we have to assign directly, since otherwise we will get "?WSDL=" with 500 error
}
url = url_.href;
} else {
this._exception('BAD_TSS_URL');
}
}catch(e){
try {
if (isDomain(url)) { //All is ok, valid domain was provided
url = "https://" + url + "/SecretServer/webservices/SSWebService.asmx?WSDL";
} else if (isIP(url)) { //IP - strange, but ok
url = "http://" + url + "/SecretServer/webservices/SSWebService.asmx?WSDL"; //there is no sense doing HTTPS on IP
} else { // whatever you are, if you answer to me, I will work with you
let result = await ping.promise.probe(url);
if (result) { //let's assume that it is local host with strange name
url = "https://" + url + "/SecretServer/webservices/SSWebService.asmx?WSDL";
}
}
}catch(e){
this._exception('BAD_TSS_URL');
}
}
return url;
};
/**
* @type {ThycoticSecretServerClient}
*/
module.exports = ThycoticSecretServerClient;