@sap-cloud-sdk/connectivity
Version:
SAP Cloud SDK for JavaScript connectivity
264 lines • 13.1 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.fetchDestinations = fetchDestinations;
exports.fetchDestinationWithoutTokenRetrieval = fetchDestinationWithoutTokenRetrieval;
exports.fetchCertificate = fetchCertificate;
exports.fetchDestinationWithTokenRetrieval = fetchDestinationWithTokenRetrieval;
const util_1 = require("@sap-cloud-sdk/util");
const axios_1 = __importStar(require("axios"));
const internal_1 = require("@sap-cloud-sdk/resilience/internal");
const resilience_1 = require("@sap-cloud-sdk/resilience");
const async_retry_1 = __importDefault(require("async-retry"));
const jwt_1 = require("../jwt");
const http_agent_1 = require("../../http-agent");
const authorization_header_1 = require("../authorization-header");
const destination_1 = require("./destination");
const destination_service_cache_1 = require("./destination-service-cache");
const logger = (0, util_1.createLogger)({
package: 'connectivity',
messageContext: 'destination-service'
});
/**
* @internal
* Fetch either subaccount or instance destinations (no token retrieval).
* @param destinationServiceUri - The URI of the destination service
* @param serviceToken - The service token for the destination service.
* @param type - Either 'instance' or 'subaccount', depending on what destinations should be fetched.
* @param options - Options to use for retrieving destinations.
* @returns A promise resolving to a list of destinations of the requested type.
*/
async function fetchDestinations(destinationServiceUri, serviceToken, type, options) {
const targetUri = `${(0, util_1.removeTrailingSlashes)(destinationServiceUri)}/destination-configuration/v1/${type}Destinations`;
if (options?.useCache) {
const destinationsFromCache = destination_service_cache_1.destinationServiceCache.retrieveDestinationsFromCache(targetUri, (0, jwt_1.decodeJwt)(serviceToken));
if (destinationsFromCache) {
logger.debug(`Destinations retrieved from cache. There were ${destinationsFromCache.length} destinations returned from the cache.`);
return destinationsFromCache;
}
}
const headers = (0, jwt_1.wrapJwtInHeader)(serviceToken).headers;
return callDestinationEndpoint({ uri: targetUri, tenantId: getTenantIdFromTokens(serviceToken) }, headers)
.then(response => {
const destinations = response.data.map((destination) => (0, destination_1.parseDestination)(destination));
if (options?.useCache) {
destination_service_cache_1.destinationServiceCache.cacheRetrievedDestinations(targetUri, (0, jwt_1.decodeJwt)(serviceToken), destinations);
}
return destinations;
})
.catch(error => {
throw new util_1.ErrorWithCause(`Failed to fetch ${type} destinations.${errorMessageFromResponse(error)}`, error);
});
}
/**
* @internal
* Fetch a destination from the destination find API (`/destinations`) and skip the automatic token retrieval.
* @param destinationName - Name of the destination.
* @param destinationServiceUri - The URI of the destination service.
* @param serviceToken - The service token for the destination service.
* @returns A promise resolving to the requested destination.
*/
async function fetchDestinationWithoutTokenRetrieval(destinationName, destinationServiceUri, serviceToken) {
const targetUri = `${(0, util_1.removeTrailingSlashes)(destinationServiceUri)}/destination-configuration/v1/destinations/${destinationName}?$skipTokenRetrieval=true`;
try {
const response = await callDestinationEndpoint({ uri: targetUri, tenantId: getTenantIdFromTokens(serviceToken) }, { Authorization: `Bearer ${serviceToken}` });
const destination = (0, destination_1.parseDestination)(response.data.destinationConfiguration);
return {
instance: response.data.owner?.InstanceId ? [destination] : [],
subaccount: !response.data.owner?.InstanceId && response.data.owner?.SubaccountId
? [destination]
: []
};
}
catch (err) {
if ((0, axios_1.isAxiosError)(err) &&
err.response?.status === 404 &&
err.response?.data?.ErrorMessage ===
'Configuration with the specified name was not found') {
return {
instance: [],
subaccount: []
};
}
throw new util_1.ErrorWithCause(`Failed to fetch destination.${errorMessageFromResponse(err)}`, err);
}
}
/**
* Fetches a certificate from the subaccount and destination instance for a given a name.
* Subaccount is tried first.
* @param destinationServiceUri - The URI of the destination service
* @param token - The access token for destination service.
* @param certificateName - Name of the Certificate to be fetched
* @returns A Promise resolving to the destination
* @internal
*/
async function fetchCertificate(destinationServiceUri, token, certificateName) {
const filetype = certificateName.split('.')[1];
if (filetype.toLowerCase() !== 'pem') {
logger.warn(`The provided truststore ${certificateName} is not in 'pem' format which is currently the only supported format. Truststore is ignored.`);
return;
}
const accountUri = `${(0, util_1.removeTrailingSlashes)(destinationServiceUri)}/destination-configuration/v1/subaccountCertificates/${certificateName}`;
const instanceUri = `${(0, util_1.removeTrailingSlashes)(destinationServiceUri)}/destination-configuration/v1/instanceCertificates/${certificateName}`;
const header = (0, jwt_1.wrapJwtInHeader)(token).headers;
try {
const response = await callCertificateEndpoint({ uri: accountUri, tenantId: getTenantIdFromTokens(token) }, header).catch(() => callCertificateEndpoint({
uri: instanceUri,
tenantId: getTenantIdFromTokens(token)
}, header));
return (0, destination_1.parseCertificate)(response.data);
}
catch (err) {
logger.warn(`Failed to fetch truststore certificate ${certificateName} - Continuing without certificate. This may cause failing requests`, err);
}
}
function getTenantIdFromTokens(token) {
let tenant;
if (typeof token === 'string') {
tenant = (0, jwt_1.getTenantId)(token);
}
else {
tenant =
// represents the tenant as string already see https://api.sap.com/api/SAP_CP_CF_Connectivity_Destination/resource
token.exchangeTenant ||
(0, jwt_1.getTenantId)(token.exchangeHeaderJwt) ||
(0, jwt_1.getTenantId)(token.authHeaderJwt);
}
if (!tenant) {
throw new Error('Could not obtain tenant identifier from JWT.');
}
return tenant;
}
/**
* @internal
* Fetches a specific destination including authorization tokens from the given URI.
* For destinations with authenticationType `OAuth2SAMLBearerAssertion`, this call will trigger the `OAuth2SAMLBearer` flow against the target destination.
* @param destinationServiceUri - The URI of the destination service
* @param token - The access token or `AuthAndExchangeTokens` if you want to include other token headers for e.g. `OAuth2UserTokenExchange`.
* @param options - Options to use for retrieving destinations.
* @returns A promise resolving to the destination.
*/
async function fetchDestinationWithTokenRetrieval(destinationServiceUri, token, options) {
const targetUri = `${(0, util_1.removeTrailingSlashes)(destinationServiceUri)}/destination-configuration/v1/destinations/${options.destinationName}`;
token = typeof token === 'string' ? { authHeaderJwt: token } : token;
let authHeader = (0, jwt_1.wrapJwtInHeader)(token.authHeaderJwt).headers;
authHeader = token.exchangeHeaderJwt
? { ...authHeader, 'X-user-token': token.exchangeHeaderJwt }
: authHeader;
authHeader = token.exchangeTenant
? { ...authHeader, 'X-tenant': token.exchangeTenant }
: authHeader;
authHeader = token.refreshToken
? { ...authHeader, 'X-refresh-token': token.refreshToken }
: authHeader;
return callDestinationEndpoint({ uri: targetUri, tenantId: getTenantIdFromTokens(token) }, authHeader, options)
.then(response => {
const destination = (0, destination_1.parseDestination)(response.data);
return destination;
})
.catch(error => {
{
throw new util_1.ErrorWithCause(`Failed to fetch destination ${options.destinationName}.${errorMessageFromResponse(error)}`, error);
}
});
}
function errorMessageFromResponse(error) {
return (0, util_1.propertyExists)(error, 'response', 'data', 'ErrorMessage')
? ` ${error.response.data.ErrorMessage}`
: '';
}
function retryDestination(destinationName) {
return options => arg => {
let retryCount = 1;
return (0, async_retry_1.default)(async (bail) => {
try {
const destination = await options.fn(arg);
if (retryCount < 3) {
retryCount++;
// this will throw if the destination does not contain valid auth headers and a second try is done to get a destination with valid tokens.
await (0, authorization_header_1.buildAuthorizationHeaders)((0, destination_1.parseDestination)(destination.data));
}
return destination;
}
catch (error) {
const status = (0, axios_1.isAxiosError)(error)
? error.response?.status
: undefined;
if (status?.toString().startsWith('4')) {
bail(new util_1.ErrorWithCause(`Request failed with status code ${status}`, error));
// We need to return something here but the actual value does not matter
return undefined;
}
throw error;
}
}, {
retries: 3,
onRetry: (err) => logger.warn(`Failed to retrieve destination ${destinationName} - doing a retry. Original Error ${err.message}`)
});
};
}
async function callCertificateEndpoint(context, headers) {
if (!context.uri.includes('Certificates')) {
throw new Error(`callCertificateEndpoint was called with illegal argument: ${context.uri}. URL must be certificate endpoint of destination service.`);
}
return callDestinationService(context, headers);
}
async function callDestinationEndpoint(context, headers, options) {
if (!context.uri.match(/[instance|subaccount]Destinations|v1\/destinations/)) {
throw new Error(`callDestinationEndpoint was called with illegal argument: ${context.uri}. URL must be destination(s) endpoint of destination service.`);
}
return callDestinationService(context, headers, options);
}
async function callDestinationService(context, headers, options) {
const { destinationName, retry } = options || {};
const requestConfig = {
...(await (0, http_agent_1.urlAndAgent)(context.uri)),
method: 'get',
headers
};
const resilienceMiddleware = (0, resilience_1.resilience)();
if (destinationName && retry) {
resilienceMiddleware.unshift(retryDestination(destinationName));
}
return (0, internal_1.executeWithMiddleware)(resilienceMiddleware, {
context,
fnArgument: requestConfig,
fn: config => axios_1.default.request(config)
});
}
//# sourceMappingURL=destination-service.js.map