From b0b97f302be987e3fc687ae9076de8a44ac6ffcc Mon Sep 17 00:00:00 2001 From: Sokratis Vidros Date: Fri, 1 Nov 2024 10:58:02 +0200 Subject: [PATCH] fix(framework): Polish secretKey and apiUrl resolution (#6819) Co-authored-by: Richard Fontein <32132657+rifont@users.noreply.github.com> --- packages/framework/src/client.test.ts | 6 +++++ packages/framework/src/client.ts | 22 +++++++++++++------ packages/framework/src/handler.ts | 2 +- .../resources/workflow/workflow.resource.ts | 10 +++------ packages/framework/src/types/config.types.ts | 5 +++++ packages/framework/src/types/event.types.ts | 8 +++++++ packages/framework/src/utils/http.utils.ts | 12 +++++----- packages/framework/src/utils/index.ts | 5 +++-- packages/framework/src/utils/options.utils.ts | 7 ++++++ 9 files changed, 55 insertions(+), 22 deletions(-) create mode 100644 packages/framework/src/utils/options.utils.ts diff --git a/packages/framework/src/client.test.ts b/packages/framework/src/client.test.ts index 2dd70587c12..cb7a7fa78e4 100644 --- a/packages/framework/src/client.test.ts +++ b/packages/framework/src/client.test.ts @@ -44,6 +44,12 @@ describe('Novu Client', () => { expect(newClient.secretKey).toBe(testSecretKey); }); + it('should set apiUrl to provided apiUrl', () => { + const testApiUrl = 'https://test.host'; + const newClient = new Client({ apiUrl: testApiUrl }); + expect(newClient.apiUrl).toBe(testApiUrl); + }); + it('should set strictAuthentication to false when NODE_ENV is development', () => { const originalEnv = process.env.NODE_ENV; process.env = { ...process.env, NODE_ENV: 'development' }; diff --git a/packages/framework/src/client.ts b/packages/framework/src/client.ts index 4a28f534d66..2643ab14903 100644 --- a/packages/framework/src/client.ts +++ b/packages/framework/src/client.ts @@ -35,7 +35,14 @@ import type { Workflow, } from './types'; import { WithPassthrough } from './types/provider.types'; -import { EMOJI, log, sanitizeHtmlInObject, stringifyDataStructureWithSingleQuotes } from './utils'; +import { + EMOJI, + log, + resolveApiUrl, + resolveSecretKey, + sanitizeHtmlInObject, + stringifyDataStructureWithSingleQuotes, +} from './utils'; import { validateData } from './validators'; import { mockSchema } from './jsonSchemaFaker'; @@ -53,7 +60,9 @@ export class Client { }, }); - public secretKey?: string; + public secretKey: string; + + public apiUrl: string; public version: string = SDK_VERSION; @@ -61,6 +70,7 @@ export class Client { constructor(options?: ClientOptions) { const builtOpts = this.buildOptions(options); + this.apiUrl = builtOpts.apiUrl; this.secretKey = builtOpts.secretKey; this.strictAuthentication = builtOpts.strictAuthentication; this.templateEngine.registerFilter('json', (value, spaces) => @@ -69,14 +79,12 @@ export class Client { } private buildOptions(providedOptions?: ClientOptions) { - const builtConfiguration: { secretKey?: string; strictAuthentication: boolean } = { - secretKey: undefined, + const builtConfiguration: Required = { + apiUrl: resolveApiUrl(providedOptions?.apiUrl), + secretKey: resolveSecretKey(providedOptions?.secretKey), strictAuthentication: !isRuntimeInDevelopment(), }; - builtConfiguration.secretKey = - providedOptions?.secretKey || process.env.NOVU_SECRET_KEY || process.env.NOVU_API_KEY; - if (providedOptions?.strictAuthentication !== undefined) { builtConfiguration.strictAuthentication = providedOptions.strictAuthentication; } else if (process.env.NOVU_STRICT_AUTHENTICATION_ENABLED !== undefined) { diff --git a/packages/framework/src/handler.ts b/packages/framework/src/handler.ts index 9ad8953cc7b..63ee99e3afa 100644 --- a/packages/framework/src/handler.ts +++ b/packages/framework/src/handler.ts @@ -73,7 +73,7 @@ export class NovuRequestHandler { this.handler = options.handler; this.client = options.client ? options.client : new Client(); this.client.addWorkflows(options.workflows); - this.http = initApiClient(this.client.secretKey as string); + this.http = initApiClient(this.client.secretKey, this.client.apiUrl); this.frameworkName = options.frameworkName; this.hmacEnabled = this.client.strictAuthentication; } diff --git a/packages/framework/src/resources/workflow/workflow.resource.ts b/packages/framework/src/resources/workflow/workflow.resource.ts index 54c2d1bb298..51dce4d6171 100644 --- a/packages/framework/src/resources/workflow/workflow.resource.ts +++ b/packages/framework/src/resources/workflow/workflow.resource.ts @@ -1,5 +1,5 @@ import { ActionStepEnum, ChannelStepEnum } from '../../constants'; -import { MissingSecretKeyError, WorkflowPayloadInvalidError } from '../../errors'; +import { WorkflowPayloadInvalidError } from '../../errors'; import { channelStepSchemas, delayActionSchemas, digestActionSchemas, emptySchema } from '../../schemas'; import type { CancelEventTriggerResponse, @@ -12,7 +12,7 @@ import type { WorkflowOptions, FromSchemaUnvalidated, } from '../../types'; -import { getBridgeUrl, initApiClient } from '../../utils'; +import { getBridgeUrl, initApiClient, resolveApiUrl, resolveSecretKey } from '../../utils'; import { transformSchema, validateData } from '../../validators'; import { discoverActionStepFactory } from './discover-action-step-factory'; import { discoverChannelStepFactory } from './discover-channel-step-factory'; @@ -36,12 +36,8 @@ export function workflow< ): Workflow { const options = workflowOptions || {}; - const apiClient = initApiClient(process.env.NOVU_SECRET_KEY as string); - const trigger: Workflow['trigger'] = async (event) => { - if (!process.env.NOVU_SECRET_KEY) { - throw new MissingSecretKeyError(); - } + const apiClient = initApiClient(resolveSecretKey(event.secretKey), resolveApiUrl(event.apiUrl)); const unvalidatedData = (event.payload || {}) as T_PayloadUnvalidated; let validatedData: T_PayloadValidated; diff --git a/packages/framework/src/types/config.types.ts b/packages/framework/src/types/config.types.ts index 786ab94f36f..65f9c9e4cfe 100644 --- a/packages/framework/src/types/config.types.ts +++ b/packages/framework/src/types/config.types.ts @@ -1,4 +1,9 @@ export type ClientOptions = { + /** + * Use Novu Cloud US (https://api.novu.co) or EU deployment (https://eu.api.novu.co). Defaults to US. + */ + apiUrl?: string; + /** * Specify your Novu secret key, to secure the Bridge Endpoint, and Novu API communication. * Novu communicates securely with your endpoint using a signed HMAC header, diff --git a/packages/framework/src/types/event.types.ts b/packages/framework/src/types/event.types.ts index c7861d14211..23772a9f8cf 100644 --- a/packages/framework/src/types/event.types.ts +++ b/packages/framework/src/types/event.types.ts @@ -56,6 +56,14 @@ export type EventTriggerParams = { [stepId: string]: Record; }; }; + /** + * Use Novu Cloud US (https://api.novu.co) or EU deployment (https://eu.api.novu.co). Defaults to US. + */ + apiUrl?: string; + /** + * Override secret key for the trigger + */ + secretKey?: string; } & ConditionalPartial< { /** diff --git a/packages/framework/src/utils/http.utils.ts b/packages/framework/src/utils/http.utils.ts index 0c4f7178cf4..361e57c5cfa 100644 --- a/packages/framework/src/utils/http.utils.ts +++ b/packages/framework/src/utils/http.utils.ts @@ -1,8 +1,10 @@ import { checkIsResponseError } from '@novu/shared'; -import { BridgeError, PlatformError } from '../errors'; +import { BridgeError, MissingSecretKeyError, PlatformError } from '../errors'; -export const initApiClient = (apiKey: string, baseURL = 'https://api.novu.co') => { - const apiUrl = process.env.NOVU_API_URL || baseURL; +export const initApiClient = (secretKey: string, apiUrl: string) => { + if (!secretKey) { + throw new MissingSecretKeyError(); + } return { post: async (route: string, data: Record): Promise => { @@ -10,7 +12,7 @@ export const initApiClient = (apiKey: string, baseURL = 'https://api.novu.co') = method: 'POST', headers: { 'Content-Type': 'application/json', - Authorization: `ApiKey ${apiKey}`, + Authorization: `ApiKey ${secretKey}`, }, body: JSON.stringify(data), }); @@ -31,7 +33,7 @@ export const initApiClient = (apiKey: string, baseURL = 'https://api.novu.co') = method: 'DELETE', headers: { 'Content-Type': 'application/json', - Authorization: `ApiKey ${apiKey}`, + Authorization: `ApiKey ${secretKey}`, }, }) ).json() as T; diff --git a/packages/framework/src/utils/index.ts b/packages/framework/src/utils/index.ts index 52018d00613..266a16d37fe 100644 --- a/packages/framework/src/utils/index.ts +++ b/packages/framework/src/utils/index.ts @@ -1,5 +1,6 @@ export * from './env.utils'; -export * from './log.utils'; -export * from './string.utils'; export * from './http.utils'; +export * from './log.utils'; +export * from './options.utils'; export * from './sanitize.utils'; +export * from './string.utils'; diff --git a/packages/framework/src/utils/options.utils.ts b/packages/framework/src/utils/options.utils.ts new file mode 100644 index 00000000000..2fcab606931 --- /dev/null +++ b/packages/framework/src/utils/options.utils.ts @@ -0,0 +1,7 @@ +export function resolveApiUrl(providedApiUrl?: string): string { + return providedApiUrl || process.env.NOVU_API_URL || 'https://api.novu.co'; +} + +export function resolveSecretKey(providedSecretKey?: string): string { + return providedSecretKey || process.env.NOVU_SECRET_KEY || process.env.NOVU_API_KEY || ''; +}