From 72159b6d7c832cd97bbde26416a668ec34a4e587 Mon Sep 17 00:00:00 2001 From: monteirogc Date: Tue, 23 Jun 2026 13:58:21 -0300 Subject: [PATCH 1/3] Backport janusCatalogSystem + licenseManager.listBindings to 6.x (#617) Adds the base IOClients `janusCatalogSystem` (Janus Catalog) getter and `LicenseManager.listBindings`, plus the binding-mapping helpers (getCanonicalAndAlternateAddresses, inferTargetProduct) and the APIBindingRes/SalesChannel types, used by the Tenant API migration. Splits the single-file LicenseManager into a directory (index/types/utils) and adds the Janus Catalog client. Backport of #617 to the 6.x line; import/key ordering adapted to the stricter 6.x tslint/prettier config. Co-authored-by: Cursor --- CHANGELOG.md | 7 ++ package.json | 2 +- src/clients/IOClients.ts | 6 +- src/clients/janus/Catalog/index.ts | 38 ++++++++++ src/clients/janus/Catalog/types.ts | 5 ++ src/clients/janus/LicenseManager.ts | 64 ----------------- src/clients/janus/LicenseManager/index.ts | 85 +++++++++++++++++++++++ src/clients/janus/LicenseManager/types.ts | 17 +++++ src/clients/janus/LicenseManager/utils.ts | 53 ++++++++++++++ src/clients/janus/index.ts | 1 + 10 files changed, 212 insertions(+), 66 deletions(-) create mode 100644 src/clients/janus/Catalog/index.ts create mode 100644 src/clients/janus/Catalog/types.ts delete mode 100644 src/clients/janus/LicenseManager.ts create mode 100644 src/clients/janus/LicenseManager/index.ts create mode 100644 src/clients/janus/LicenseManager/types.ts create mode 100644 src/clients/janus/LicenseManager/utils.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 62c031478..b54afb492 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +## [6.51.0] - 2026-06-23 +### Added +- Base `IOClients` getter `janusCatalogSystem` (Janus Catalog) and + `licenseManager.listBindings`, plus binding-mapping helpers + (getCanonicalAndAlternateAddresses, inferTargetProduct) used by the + Tenant API migration. Backport of #617 to the 6.x line. + ## [6.50.1] - 2025-09-10 ### Fixed - Axios vuln GHSA-jr5f-v2jv-69x6 by using axios@^0.30.1 diff --git a/package.json b/package.json index bed57f614..13629567c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@vtex/api", - "version": "6.50.2-beta.0", + "version": "6.51.0", "description": "VTEX I/O API client", "main": "lib/index.js", "typings": "lib/index.d.ts", diff --git a/src/clients/IOClients.ts b/src/clients/IOClients.ts index a7f5f320d..b1f05de6d 100644 --- a/src/clients/IOClients.ts +++ b/src/clients/IOClients.ts @@ -5,7 +5,7 @@ import { CatalogGraphQL } from './apps/catalogGraphQL/index' import { ID, MasterData, PaymentProvider } from './external' import { Apps, Assets, BillingMetrics, Events, Registry, Router, VBase, Workspaces } from './infra' import { IOClient, IOClientConstructor } from './IOClient' -import { LicenseManager, Segment, Session, TenantClient } from './janus' +import { Catalog, LicenseManager, Segment, Session, TenantClient } from './janus' export type ClientsImplementation = new ( clientOptions: Record, @@ -93,6 +93,10 @@ export class IOClients { return this.getOrSet('catalogGraphQL', CatalogGraphQL) } + public get janusCatalogSystem() { + return this.getOrSet('janusCatalogSystem', Catalog) + } + public get paymentProvider() { return this.getOrSet('paymentProvider', PaymentProvider) } diff --git a/src/clients/janus/Catalog/index.ts b/src/clients/janus/Catalog/index.ts new file mode 100644 index 000000000..340a8e06e --- /dev/null +++ b/src/clients/janus/Catalog/index.ts @@ -0,0 +1,38 @@ +import { inflightUrlWithQuery, RequestConfig } from '../../../HttpClient' +import { JanusClient } from '../JanusClient' +import { SalesChannel } from './types' + +const BASE_URL = '/api/catalog_system' + +const routes = { + defaultSalesChannel: () => `${BASE_URL}/pub/saleschannel/default`, + salesChannel: (salesChannelId: number) => `${BASE_URL}/pub/saleschannel/${salesChannelId}`, +} + +export class Catalog extends JanusClient { + public getSalesChannel(id: number, config?: RequestConfig) { + const metric = 'catalog-saleschannel' + return this.http.get(routes.salesChannel(id), { + inflightKey: inflightUrlWithQuery, + metric, + ...config, + tracing: { + requestSpanNameSuffix: metric, + ...config?.tracing, + }, + }) + } + + public getDefaultSalesChannel(config?: RequestConfig) { + const metric = 'catalog-saleschannel-default' + return this.http.get(routes.defaultSalesChannel(), { + inflightKey: inflightUrlWithQuery, + metric, + ...config, + tracing: { + requestSpanNameSuffix: metric, + ...config?.tracing, + }, + }) + } +} diff --git a/src/clients/janus/Catalog/types.ts b/src/clients/janus/Catalog/types.ts new file mode 100644 index 000000000..22fe9b91f --- /dev/null +++ b/src/clients/janus/Catalog/types.ts @@ -0,0 +1,5 @@ +export interface SalesChannel { + Id: number + CultureInfo: string + CurrencyCode: string +} diff --git a/src/clients/janus/LicenseManager.ts b/src/clients/janus/LicenseManager.ts deleted file mode 100644 index 2c90e9c42..000000000 --- a/src/clients/janus/LicenseManager.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { stringify } from 'qs' - -import { RequestConfig, RequestTracingConfig } from '../../HttpClient' -import { JanusClient } from './JanusClient' - -const TWO_MINUTES_S = 2 * 60 - -const BASE_URL = '/api/license-manager' - -const routes = { - accountData: `${BASE_URL}/account`, - resourceAccess: `${BASE_URL}/resources`, - topbarData: `${BASE_URL}/site/pvt/newtopbar`, -} - -const inflightKey = ({baseURL, url, params}: RequestConfig) => { - return baseURL! + url! + stringify(params, {arrayFormat: 'repeat', addQueryPrefix: true}) -} - -export class LicenseManager extends JanusClient { - public getAccountData (VtexIdclientAutCookie: string, tracingConfig?: RequestTracingConfig) { - const metric = 'lm-account-data' - return this.http.get(routes.accountData, { - forceMaxAge: TWO_MINUTES_S, - headers: { - VtexIdclientAutCookie, - }, - inflightKey, - metric, - tracing: { - requestSpanNameSuffix: metric, - ...tracingConfig?.tracing, - }, - }) - } - - public getTopbarData (VtexIdclientAutCookie: string, tracingConfig?: RequestTracingConfig) { - const metric = 'lm-topbar-data' - return this.http.get(routes.topbarData, { - headers: { - VtexIdclientAutCookie, - }, - metric, - tracing: { - requestSpanNameSuffix: metric, - ...tracingConfig?.tracing, - }, - }) - } - - public canAccessResource (VtexIdclientAutCookie: string, resourceKey: string, tracingConfig?: RequestTracingConfig) { - const metric = 'lm-resource-access' - return this.http.get(`${routes.resourceAccess}/${resourceKey}/access`, { - headers: { - VtexIdclientAutCookie, - }, - metric, - tracing: { - requestSpanNameSuffix: metric, - ...tracingConfig?.tracing, - }, - }).then(() => true, () => false) - } -} diff --git a/src/clients/janus/LicenseManager/index.ts b/src/clients/janus/LicenseManager/index.ts new file mode 100644 index 000000000..4cc9c4247 --- /dev/null +++ b/src/clients/janus/LicenseManager/index.ts @@ -0,0 +1,85 @@ +import { inflightUrlWithQuery, RequestConfig, RequestTracingConfig } from '../../../HttpClient' +import { JanusClient } from '../JanusClient' +import { APIBindingRes } from './types' + +export * from './types' +export * from './utils' + +const TWO_MINUTES_S = 2 * 60 + +const BASE_URL = '/api/license-manager' + +const routes = { + accountData: () => `${BASE_URL}/account`, + listBindings: (tenant: string) => `${BASE_URL}/binding/site/${encodeURIComponent(tenant)}`, + resourceAccess: (resourceKey: string) => `${BASE_URL}/resources/${encodeURIComponent(resourceKey)}/access`, + topbarData: () => `${BASE_URL}/site/pvt/newtopbar`, +} + +export class LicenseManager extends JanusClient { + public getAccountData(VtexIdclientAutCookie: string, tracingConfig?: RequestTracingConfig) { + const metric = 'lm-account-data' + return this.http.get(routes.accountData(), { + forceMaxAge: TWO_MINUTES_S, + headers: { + VtexIdclientAutCookie, + }, + inflightKey: inflightUrlWithQuery, + metric, + tracing: { + requestSpanNameSuffix: metric, + ...tracingConfig?.tracing, + }, + }) + } + + public getTopbarData(VtexIdclientAutCookie: string, tracingConfig?: RequestTracingConfig) { + const metric = 'lm-topbar-data' + return this.http.get(routes.topbarData(), { + headers: { + VtexIdclientAutCookie, + }, + metric, + tracing: { + requestSpanNameSuffix: metric, + ...tracingConfig?.tracing, + }, + }) + } + + public canAccessResource(VtexIdclientAutCookie: string, resourceKey: string, tracingConfig?: RequestTracingConfig) { + const metric = 'lm-resource-access' + return this.http + .get(routes.resourceAccess(resourceKey), { + headers: { + VtexIdclientAutCookie, + }, + metric, + tracing: { + requestSpanNameSuffix: metric, + ...tracingConfig?.tracing, + }, + }) + .then( + () => true, + () => false + ) + } + + public listBindings(tenant: string, config?: RequestConfig) { + const metric = 'lm-list-bindings' + return this.http.get(routes.listBindings(tenant), { + headers: { + VtexIdclientAutCookie: this.context.authToken, + }, + inflightKey: inflightUrlWithQuery, + memoizeable: true, + metric, + ...config, + tracing: { + requestSpanNameSuffix: metric, + ...config?.tracing, + }, + }) + } +} diff --git a/src/clients/janus/LicenseManager/types.ts b/src/clients/janus/LicenseManager/types.ts new file mode 100644 index 000000000..5b542a203 --- /dev/null +++ b/src/clients/janus/LicenseManager/types.ts @@ -0,0 +1,17 @@ +export interface APIAddress { + Host: string + IsCanonical: boolean + BasePath: string + Localization: { + [k: string]: string + } +} + +export interface APIBindingRes { + Id: string + Addresses: APIAddress[] + SiteName: string + DefaultSalesChannelId: number | null + DefaultLocale: string + SupportedLocales: string[] +} diff --git a/src/clients/janus/LicenseManager/utils.ts b/src/clients/janus/LicenseManager/utils.ts new file mode 100644 index 000000000..3a879364e --- /dev/null +++ b/src/clients/janus/LicenseManager/utils.ts @@ -0,0 +1,53 @@ +import { APIBindingRes } from './types' + +const buildAddressPath = (basePath: string, localizationKey: string): string => { + if (basePath === '') { + return `/${localizationKey}` + } + + if (localizationKey === '') { + return `/${basePath}` + } + + return `/${basePath}/${localizationKey}` +} + +export const getCanonicalAndAlternateAddresses = ( + lmBinding: APIBindingRes +): { canonicalBaseAddress: string; alternateBaseAddresses: string[] } => { + let canonicalBaseAddress = '' + const alternateBaseAddresses: string[] = [] + + for (const addressEntry of lmBinding.Addresses) { + const localizationKeys = Object.keys(addressEntry.Localization) + + for (const localizationKey of localizationKeys) { + alternateBaseAddresses.push(addressEntry.Host + buildAddressPath(addressEntry.BasePath, localizationKey)) + } + + if (addressEntry.IsCanonical) { + // The canonical address is built from the shortest localization key. + const [shortestLocalizationKey = ''] = [...localizationKeys].sort((a, b) => a.length - b.length) + canonicalBaseAddress = addressEntry.Host + buildAddressPath(addressEntry.BasePath, shortestLocalizationKey) + } + } + + // The canonical address must not also appear in the alternates list. + const canonicalIndexInAlternates = alternateBaseAddresses.indexOf(canonicalBaseAddress) + if (canonicalIndexInAlternates !== -1) { + alternateBaseAddresses.splice(canonicalIndexInAlternates, 1) + } + + return { canonicalBaseAddress, alternateBaseAddresses } +} + +export const inferTargetProduct = ( + canonicalBaseAddress: string, + alternateBaseAddresses: string[] +): 'vtex-admin' | 'vtex-storefront' => { + const isAdmin = + canonicalBaseAddress.endsWith('myvtex.com/admin') || + alternateBaseAddresses.some((address) => address.endsWith('myvtex.com/admin')) + + return isAdmin ? 'vtex-admin' : 'vtex-storefront' +} diff --git a/src/clients/janus/index.ts b/src/clients/janus/index.ts index 54081815a..ee8018761 100644 --- a/src/clients/janus/index.ts +++ b/src/clients/janus/index.ts @@ -3,3 +3,4 @@ export * from './LicenseManager' export * from './Segment' export * from './Session' export * from './Tenant' +export * from './Catalog' From d945872779b0b7a9ec0f6bacd8ddaf0b3e3b34ba Mon Sep 17 00:00:00 2001 From: monteirogc Date: Fri, 3 Jul 2026 09:33:09 -0300 Subject: [PATCH 2/3] fix(lint): resolve tslint errors in telemetry, auth, tracing and buildFullPath - telemetry/client.ts: drop semicolons and order public methods before private - Auth.ts: use strict equality (===) - tracing/utils.test.ts: sort object keys alphabetically - utils/buildFullPath.ts: prefer const, strict equality, drop semicolons Co-authored-by: Cursor --- src/service/telemetry/client.ts | 77 +++++++++---------- .../graphql/schema/schemaDirectives/Auth.ts | 4 +- src/tracing/utils.test.ts | 20 ++--- src/utils/buildFullPath.ts | 12 +-- 4 files changed, 56 insertions(+), 57 deletions(-) diff --git a/src/service/telemetry/client.ts b/src/service/telemetry/client.ts index efe704d78..2259a6f66 100644 --- a/src/service/telemetry/client.ts +++ b/src/service/telemetry/client.ts @@ -1,19 +1,38 @@ -import { NewTelemetryClient } from '@vtex/diagnostics-nodejs'; -import { TelemetryClient } from '@vtex/diagnostics-nodejs/dist/telemetry'; -import { APP } from '../../constants'; +import { NewTelemetryClient } from '@vtex/diagnostics-nodejs' +import { TelemetryClient } from '@vtex/diagnostics-nodejs/dist/telemetry' +import { APP } from '../../constants' class TelemetryClientSingleton { - private static instance: TelemetryClientSingleton; - private telemetryClient: TelemetryClient | undefined; - private initializationPromise: Promise | undefined = undefined; + public static getInstance(): TelemetryClientSingleton { + if (!TelemetryClientSingleton.instance) { + TelemetryClientSingleton.instance = new TelemetryClientSingleton() + } + return TelemetryClientSingleton.instance + } + + private static instance: TelemetryClientSingleton + private telemetryClient: TelemetryClient | undefined + private initializationPromise: Promise | undefined = undefined private constructor() {} - public static getInstance(): TelemetryClientSingleton { - if (!TelemetryClientSingleton.instance) { - TelemetryClientSingleton.instance = new TelemetryClientSingleton(); + public async getClient(): Promise { + if (this.telemetryClient) { + return this.telemetryClient } - return TelemetryClientSingleton.instance; + + if (this.initializationPromise) { + return this.initializationPromise + } + + this.initializationPromise = this.initTelemetryClient() + + return this.initializationPromise + } + + public reset(): void { + this.telemetryClient = undefined + this.initializationPromise = undefined } private async initTelemetryClient(): Promise { @@ -23,47 +42,27 @@ class TelemetryClientSingleton { APP.ID || 'vtex-app', { additionalAttrs: { - 'version': APP.VERSION || '', 'environment': process.env.VTEX_WORKSPACE || 'development', + 'version': APP.VERSION || '', }, } - ); + ) - this.telemetryClient = telemetryClient; - return telemetryClient; + this.telemetryClient = telemetryClient + return telemetryClient } catch (error) { - console.error('Failed to initialize telemetry client:', error); - throw error; + console.error('Failed to initialize telemetry client:', error) + throw error } finally { - this.initializationPromise = undefined; - } - } - - public async getClient(): Promise { - if (this.telemetryClient) { - return this.telemetryClient; - } - - if (this.initializationPromise) { - return this.initializationPromise; + this.initializationPromise = undefined } - - this.initializationPromise = this.initTelemetryClient(); - - return this.initializationPromise; } - - public reset(): void { - this.telemetryClient = undefined; - this.initializationPromise = undefined; - } - } export async function getTelemetryClient(): Promise { - return TelemetryClientSingleton.getInstance().getClient(); + return TelemetryClientSingleton.getInstance().getClient() } export function resetTelemetryClient(): void { - TelemetryClientSingleton.getInstance().reset(); + TelemetryClientSingleton.getInstance().reset() } diff --git a/src/service/worker/runtime/graphql/schema/schemaDirectives/Auth.ts b/src/service/worker/runtime/graphql/schema/schemaDirectives/Auth.ts index 6dd8dd862..5a187a1b1 100644 --- a/src/service/worker/runtime/graphql/schema/schemaDirectives/Auth.ts +++ b/src/service/worker/runtime/graphql/schema/schemaDirectives/Auth.ts @@ -69,7 +69,7 @@ async function auth (ctx: ServiceContext, authArgs: AuthDirectiveArgs): Promise< } function parseArgs (authArgs: AuthDirectiveArgs): AuthDirectiveArgs { - if (authArgs.scope == 'PUBLIC') { + if (authArgs.scope === 'PUBLIC') { return authArgs } @@ -84,7 +84,7 @@ export class Auth extends SchemaDirectiveVisitor { const {resolve = defaultFieldResolver} = field field.resolve = async (root, args, ctx, info) => { const authArgs = parseArgs(this.args as AuthDirectiveArgs) - if (!authArgs.scope || authArgs.scope == 'PRIVATE') { + if (!authArgs.scope || authArgs.scope === 'PRIVATE') { await auth(ctx, authArgs) } return resolve(root, args, ctx, info) diff --git a/src/tracing/utils.test.ts b/src/tracing/utils.test.ts index 608204b61..4b451ad00 100644 --- a/src/tracing/utils.test.ts +++ b/src/tracing/utils.test.ts @@ -3,12 +3,12 @@ import { cloneAndSanitizeHeaders, INTERESTING_HEADERS } from './utils' describe('cloneAndSanitizeHeaders', () => { const headers = { - 'content-type': 'application/json', 'authorization': 'Bearer secret-token', + 'content-type': 'application/json', + 'cookie': 'sessionId=abc123', + 'random-header': 'should-be-filtered', 'x-request-id': '12345', 'x-vtex-custom': 'vtex-value', - 'random-header': 'should-be-filtered', - 'cookie': 'sessionId=abc123', } test('Original object is not modified', () => { @@ -61,10 +61,10 @@ describe('cloneAndSanitizeHeaders', () => { test('Handles case-insensitive header matching', () => { const mixedCaseHeaders = { + 'AUTHORIZATION': 'Bearer token', 'Content-Type': 'application/json', 'X-REQUEST-ID': '67890', 'X-VTEX-Store': 'mystore', - 'AUTHORIZATION': 'Bearer token', } const result = cloneAndSanitizeHeaders(mixedCaseHeaders) @@ -83,6 +83,7 @@ describe('cloneAndSanitizeHeaders', () => { test('Handles axios header object structure', () => { const axiosHeaders = { + 'authorization': 'Bearer token', common: { Accept: 'application/json, text/plain, */*', }, @@ -92,9 +93,8 @@ describe('cloneAndSanitizeHeaders', () => { post: { 'Content-Type': 'application/x-www-form-urlencoded', }, - 'authorization': 'Bearer token', - 'x-vtex-account': 'testaccount', 'user-agent': 'axios/1.0.0', + 'x-vtex-account': 'testaccount', } const result = cloneAndSanitizeHeaders(axiosHeaders) @@ -117,16 +117,16 @@ describe('cloneAndSanitizeHeaders', () => { test('Filters out non-whitelisted headers completely', () => { const headersWithNoise = { 'content-type': 'application/json', // whitelisted - 'x-vtex-store': 'mystore', // x-vtex-* allowed - 'x-custom-header': 'custom', // not whitelisted, not x-vtex-* + 'host': 'api.vtex.com', // whitelisted 'server': 'nginx', // not whitelisted + 'x-custom-header': 'custom', // not whitelisted, not x-vtex-* 'x-powered-by': 'Express', // not whitelisted - 'host': 'api.vtex.com', // whitelisted + 'x-vtex-store': 'mystore', // x-vtex-* allowed } const result = cloneAndSanitizeHeaders(headersWithNoise) - expect(Object.keys(result)).toEqual(['content-type', 'x-vtex-store', 'host']) + expect(Object.keys(result)).toEqual(['content-type', 'host', 'x-vtex-store']) expect(result).not.toHaveProperty('x-custom-header') expect(result).not.toHaveProperty('server') expect(result).not.toHaveProperty('x-powered-by') diff --git a/src/utils/buildFullPath.ts b/src/utils/buildFullPath.ts index b4efcdff9..383c786a4 100644 --- a/src/utils/buildFullPath.ts +++ b/src/utils/buildFullPath.ts @@ -20,11 +20,11 @@ * @returns {string} The combined full path */ export default function(baseURL?: string, requestedURL?: string, allowAbsoluteUrls?: boolean): string | undefined { - let isRelativeUrl = !isAbsoluteURL(requestedURL); - if (baseURL && (isRelativeUrl || allowAbsoluteUrls == false)) { - return combineURLs(baseURL, requestedURL); + const isRelativeUrl = !isAbsoluteURL(requestedURL) + if (baseURL && (isRelativeUrl || allowAbsoluteUrls === false)) { + return combineURLs(baseURL, requestedURL) } - return requestedURL; + return requestedURL } /** @@ -38,7 +38,7 @@ export default function(baseURL?: string, requestedURL?: string, allowAbsoluteUr // A URL is considered absolute if it begins with "://" or "//" (protocol-relative URL). // RFC 3986 defines scheme name as a sequence of characters beginning with a letter and followed // by any combination of letters, digits, plus, period, or hyphen. - return !!url && /^([a-z][a-z\d+\-.]*:)?\/\//i.test(url); + return !!url && /^([a-z][a-z\d+\-.]*:)?\/\//i.test(url) } /** @@ -52,5 +52,5 @@ export default function(baseURL?: string, requestedURL?: string, allowAbsoluteUr function combineURLs(baseURL?: string, relativeURL?: string): string | undefined { return relativeURL && baseURL ? baseURL.replace(/\/?\/$/, '') + '/' + relativeURL.replace(/^\/+/, '') - : baseURL; + : baseURL } \ No newline at end of file From 5c25c257599356b5b12d7e64577745129da665aa Mon Sep 17 00:00:00 2001 From: monteirogc Date: Fri, 3 Jul 2026 09:40:46 -0300 Subject: [PATCH 3/3] Revert "fix(lint): resolve tslint errors in telemetry, auth, tracing and buildFullPath" This reverts commit d945872779b0b7a9ec0f6bacd8ddaf0b3e3b34ba. --- src/service/telemetry/client.ts | 77 ++++++++++--------- .../graphql/schema/schemaDirectives/Auth.ts | 4 +- src/tracing/utils.test.ts | 20 ++--- src/utils/buildFullPath.ts | 12 +-- 4 files changed, 57 insertions(+), 56 deletions(-) diff --git a/src/service/telemetry/client.ts b/src/service/telemetry/client.ts index 2259a6f66..efe704d78 100644 --- a/src/service/telemetry/client.ts +++ b/src/service/telemetry/client.ts @@ -1,38 +1,19 @@ -import { NewTelemetryClient } from '@vtex/diagnostics-nodejs' -import { TelemetryClient } from '@vtex/diagnostics-nodejs/dist/telemetry' -import { APP } from '../../constants' +import { NewTelemetryClient } from '@vtex/diagnostics-nodejs'; +import { TelemetryClient } from '@vtex/diagnostics-nodejs/dist/telemetry'; +import { APP } from '../../constants'; class TelemetryClientSingleton { - public static getInstance(): TelemetryClientSingleton { - if (!TelemetryClientSingleton.instance) { - TelemetryClientSingleton.instance = new TelemetryClientSingleton() - } - return TelemetryClientSingleton.instance - } - - private static instance: TelemetryClientSingleton - private telemetryClient: TelemetryClient | undefined - private initializationPromise: Promise | undefined = undefined + private static instance: TelemetryClientSingleton; + private telemetryClient: TelemetryClient | undefined; + private initializationPromise: Promise | undefined = undefined; private constructor() {} - public async getClient(): Promise { - if (this.telemetryClient) { - return this.telemetryClient - } - - if (this.initializationPromise) { - return this.initializationPromise + public static getInstance(): TelemetryClientSingleton { + if (!TelemetryClientSingleton.instance) { + TelemetryClientSingleton.instance = new TelemetryClientSingleton(); } - - this.initializationPromise = this.initTelemetryClient() - - return this.initializationPromise - } - - public reset(): void { - this.telemetryClient = undefined - this.initializationPromise = undefined + return TelemetryClientSingleton.instance; } private async initTelemetryClient(): Promise { @@ -42,27 +23,47 @@ class TelemetryClientSingleton { APP.ID || 'vtex-app', { additionalAttrs: { - 'environment': process.env.VTEX_WORKSPACE || 'development', 'version': APP.VERSION || '', + 'environment': process.env.VTEX_WORKSPACE || 'development', }, } - ) + ); - this.telemetryClient = telemetryClient - return telemetryClient + this.telemetryClient = telemetryClient; + return telemetryClient; } catch (error) { - console.error('Failed to initialize telemetry client:', error) - throw error + console.error('Failed to initialize telemetry client:', error); + throw error; } finally { - this.initializationPromise = undefined + this.initializationPromise = undefined; + } + } + + public async getClient(): Promise { + if (this.telemetryClient) { + return this.telemetryClient; + } + + if (this.initializationPromise) { + return this.initializationPromise; } + + this.initializationPromise = this.initTelemetryClient(); + + return this.initializationPromise; } + + public reset(): void { + this.telemetryClient = undefined; + this.initializationPromise = undefined; + } + } export async function getTelemetryClient(): Promise { - return TelemetryClientSingleton.getInstance().getClient() + return TelemetryClientSingleton.getInstance().getClient(); } export function resetTelemetryClient(): void { - TelemetryClientSingleton.getInstance().reset() + TelemetryClientSingleton.getInstance().reset(); } diff --git a/src/service/worker/runtime/graphql/schema/schemaDirectives/Auth.ts b/src/service/worker/runtime/graphql/schema/schemaDirectives/Auth.ts index 5a187a1b1..6dd8dd862 100644 --- a/src/service/worker/runtime/graphql/schema/schemaDirectives/Auth.ts +++ b/src/service/worker/runtime/graphql/schema/schemaDirectives/Auth.ts @@ -69,7 +69,7 @@ async function auth (ctx: ServiceContext, authArgs: AuthDirectiveArgs): Promise< } function parseArgs (authArgs: AuthDirectiveArgs): AuthDirectiveArgs { - if (authArgs.scope === 'PUBLIC') { + if (authArgs.scope == 'PUBLIC') { return authArgs } @@ -84,7 +84,7 @@ export class Auth extends SchemaDirectiveVisitor { const {resolve = defaultFieldResolver} = field field.resolve = async (root, args, ctx, info) => { const authArgs = parseArgs(this.args as AuthDirectiveArgs) - if (!authArgs.scope || authArgs.scope === 'PRIVATE') { + if (!authArgs.scope || authArgs.scope == 'PRIVATE') { await auth(ctx, authArgs) } return resolve(root, args, ctx, info) diff --git a/src/tracing/utils.test.ts b/src/tracing/utils.test.ts index 4b451ad00..608204b61 100644 --- a/src/tracing/utils.test.ts +++ b/src/tracing/utils.test.ts @@ -3,12 +3,12 @@ import { cloneAndSanitizeHeaders, INTERESTING_HEADERS } from './utils' describe('cloneAndSanitizeHeaders', () => { const headers = { - 'authorization': 'Bearer secret-token', 'content-type': 'application/json', - 'cookie': 'sessionId=abc123', - 'random-header': 'should-be-filtered', + 'authorization': 'Bearer secret-token', 'x-request-id': '12345', 'x-vtex-custom': 'vtex-value', + 'random-header': 'should-be-filtered', + 'cookie': 'sessionId=abc123', } test('Original object is not modified', () => { @@ -61,10 +61,10 @@ describe('cloneAndSanitizeHeaders', () => { test('Handles case-insensitive header matching', () => { const mixedCaseHeaders = { - 'AUTHORIZATION': 'Bearer token', 'Content-Type': 'application/json', 'X-REQUEST-ID': '67890', 'X-VTEX-Store': 'mystore', + 'AUTHORIZATION': 'Bearer token', } const result = cloneAndSanitizeHeaders(mixedCaseHeaders) @@ -83,7 +83,6 @@ describe('cloneAndSanitizeHeaders', () => { test('Handles axios header object structure', () => { const axiosHeaders = { - 'authorization': 'Bearer token', common: { Accept: 'application/json, text/plain, */*', }, @@ -93,8 +92,9 @@ describe('cloneAndSanitizeHeaders', () => { post: { 'Content-Type': 'application/x-www-form-urlencoded', }, - 'user-agent': 'axios/1.0.0', + 'authorization': 'Bearer token', 'x-vtex-account': 'testaccount', + 'user-agent': 'axios/1.0.0', } const result = cloneAndSanitizeHeaders(axiosHeaders) @@ -117,16 +117,16 @@ describe('cloneAndSanitizeHeaders', () => { test('Filters out non-whitelisted headers completely', () => { const headersWithNoise = { 'content-type': 'application/json', // whitelisted - 'host': 'api.vtex.com', // whitelisted - 'server': 'nginx', // not whitelisted + 'x-vtex-store': 'mystore', // x-vtex-* allowed 'x-custom-header': 'custom', // not whitelisted, not x-vtex-* + 'server': 'nginx', // not whitelisted 'x-powered-by': 'Express', // not whitelisted - 'x-vtex-store': 'mystore', // x-vtex-* allowed + 'host': 'api.vtex.com', // whitelisted } const result = cloneAndSanitizeHeaders(headersWithNoise) - expect(Object.keys(result)).toEqual(['content-type', 'host', 'x-vtex-store']) + expect(Object.keys(result)).toEqual(['content-type', 'x-vtex-store', 'host']) expect(result).not.toHaveProperty('x-custom-header') expect(result).not.toHaveProperty('server') expect(result).not.toHaveProperty('x-powered-by') diff --git a/src/utils/buildFullPath.ts b/src/utils/buildFullPath.ts index 383c786a4..b4efcdff9 100644 --- a/src/utils/buildFullPath.ts +++ b/src/utils/buildFullPath.ts @@ -20,11 +20,11 @@ * @returns {string} The combined full path */ export default function(baseURL?: string, requestedURL?: string, allowAbsoluteUrls?: boolean): string | undefined { - const isRelativeUrl = !isAbsoluteURL(requestedURL) - if (baseURL && (isRelativeUrl || allowAbsoluteUrls === false)) { - return combineURLs(baseURL, requestedURL) + let isRelativeUrl = !isAbsoluteURL(requestedURL); + if (baseURL && (isRelativeUrl || allowAbsoluteUrls == false)) { + return combineURLs(baseURL, requestedURL); } - return requestedURL + return requestedURL; } /** @@ -38,7 +38,7 @@ export default function(baseURL?: string, requestedURL?: string, allowAbsoluteUr // A URL is considered absolute if it begins with "://" or "//" (protocol-relative URL). // RFC 3986 defines scheme name as a sequence of characters beginning with a letter and followed // by any combination of letters, digits, plus, period, or hyphen. - return !!url && /^([a-z][a-z\d+\-.]*:)?\/\//i.test(url) + return !!url && /^([a-z][a-z\d+\-.]*:)?\/\//i.test(url); } /** @@ -52,5 +52,5 @@ export default function(baseURL?: string, requestedURL?: string, allowAbsoluteUr function combineURLs(baseURL?: string, relativeURL?: string): string | undefined { return relativeURL && baseURL ? baseURL.replace(/\/?\/$/, '') + '/' + relativeURL.replace(/^\/+/, '') - : baseURL + : baseURL; } \ No newline at end of file