diff --git a/docs/utilities/parameters.md b/docs/utilities/parameters.md index d35188c9ea..dbbe813d5c 100644 --- a/docs/utilities/parameters.md +++ b/docs/utilities/parameters.md @@ -298,7 +298,7 @@ All caching logic is handled by the `BaseProvider`, and provided that the return Here's an example of implementing a custom parameter store using an external service like HashiCorp Vault, a widely popular key-value secret storage. === "Provider usage" - ```typescript hl_lines="5-8 12-16" + ```typescript hl_lines="12" --8<-- "examples/snippets/parameters/customProviderVaultUsage.ts" ``` diff --git a/examples/snippets/package.json b/examples/snippets/package.json index ecf240b427..1390f8ef70 100644 --- a/examples/snippets/package.json +++ b/examples/snippets/package.json @@ -40,7 +40,6 @@ "@middy/core": "^4.7.0", "aws-sdk": "^2.1692.0", "aws-sdk-client-mock": "^4.1.0", - "hashi-vault-js": "^0.4.16", "zod": "^3.24.2" } } diff --git a/examples/snippets/parameters/customProviderVault.ts b/examples/snippets/parameters/customProviderVault.ts index 838f1adc9d..0bbae8252b 100644 --- a/examples/snippets/parameters/customProviderVault.ts +++ b/examples/snippets/parameters/customProviderVault.ts @@ -1,41 +1,31 @@ import { BaseProvider } from '@aws-lambda-powertools/parameters/base'; import { GetParameterError } from '@aws-lambda-powertools/parameters/errors'; -import Vault from 'hashi-vault-js'; import type { HashiCorpVaultGetOptions, HashiCorpVaultProviderOptions, } from './customProviderVaultTypes.js'; class HashiCorpVaultProvider extends BaseProvider { - public client: Vault; + readonly #baseUrl: string; readonly #token: string; + readonly #rootPath?: string; + readonly #timeout: number; + readonly #abortController: AbortController; /** * It initializes the HashiCorpVaultProvider class. * - * @param {HashiCorpVaultProviderOptions} config - The configuration object. + * @param config - The configuration object. */ public constructor(config: HashiCorpVaultProviderOptions) { super({}); - const { url, token, clientConfig, vaultClient } = config; - if (vaultClient) { - if (vaultClient instanceof Vault) { - this.client = vaultClient; - } else { - throw Error('Not a valid Vault client provided'); - } - } else { - const config = { - baseUrl: url, - ...(clientConfig ?? { - timeout: 10000, - rootPath: '', - }), - }; - this.client = new Vault(config); - } + const { url, token, rootPath, timeout } = config; + this.#baseUrl = url; + this.#rootPath = rootPath ?? 'secret'; + this.#timeout = timeout ?? 5000; this.#token = token; + this.#abortController = new AbortController(); } /** @@ -46,8 +36,8 @@ class HashiCorpVaultProvider extends BaseProvider { * * `forceFetch` - Whether to always fetch a new value from the store regardless if already available in cache * * `sdkOptions` - Extra options to pass to the HashiCorp Vault SDK, e.g. `mount` or `version` * - * @param {string} name - The name of the secret - * @param {HashiCorpVaultGetOptions} options - Options to customize the retrieval of the secret + * @param name - The name of the secret + * @param options - Options to customize the retrieval of the secret */ public async get>( name: string, @@ -68,27 +58,36 @@ class HashiCorpVaultProvider extends BaseProvider { /** * Retrieve a secret from HashiCorp Vault. * - * @param {string} name - The name of the secret - * @param {HashiCorpVaultGetOptions} options - Options to customize the retrieval of the secret + * @param name - The name of the secret + * @param options - Options to customize the retrieval of the secret */ protected async _get( name: string, options?: HashiCorpVaultGetOptions ): Promise> { - const mount = options?.sdkOptions?.mount ?? 'secret'; - const version = options?.sdkOptions?.version; + const { sdkOptions } = options ?? {}; + const mount = sdkOptions?.mount ?? this.#rootPath; + const version = sdkOptions?.version + ? `?version=${sdkOptions?.version}` + : ''; - const response = await this.client.readKVSecret( - this.#token, - name, - version, - mount - ); + setTimeout(() => { + this.#abortController.abort(); + }, this.#timeout); - if (response.isVaultError) { - throw response; + const res = await fetch( + `${this.#baseUrl}/${mount}/data/${name}${version}`, + { + headers: { 'X-Vault-Token': this.#token }, + method: 'GET', + signal: this.#abortController.signal, + } + ); + if (!res.ok) { + throw new GetParameterError(`Failed to fetch secret ${res.statusText}`); } - return response.data; + const response = await res.json(); + return response.data.data; } /** diff --git a/examples/snippets/parameters/customProviderVaultTypes.ts b/examples/snippets/parameters/customProviderVaultTypes.ts index 4542865e49..d9436134b5 100644 --- a/examples/snippets/parameters/customProviderVaultTypes.ts +++ b/examples/snippets/parameters/customProviderVaultTypes.ts @@ -1,11 +1,13 @@ import type { GetOptionsInterface } from '@aws-lambda-powertools/parameters/base/types'; -import type Vault from 'hashi-vault-js'; /** - * Base interface for HashiCorpVaultProviderOptions. - * @interface + * Options for the HashiCorpVaultProvider class constructor. + * + * @param {string} url - Indicate the server name/IP, port and API version for the Vault instance, all paths are relative to this one. + * @param {string} token - The Vault token to use for authentication. + * */ -interface HashiCorpVaultProviderOptionsBase { +interface HashiCorpVaultProviderOptions { /** * Indicate the server name/IP, port and API version for the Vault instance, all paths are relative to this one. * @example 'https://vault.example.com:8200/v1' @@ -15,53 +17,18 @@ interface HashiCorpVaultProviderOptionsBase { * The Vault token to use for authentication. */ token: string; -} - -/** - * Interface for HashiCorpVaultProviderOptions with clientConfig property. - * @interface - */ -interface HashiCorpVaultProviderOptionsWithClientConfig - extends HashiCorpVaultProviderOptionsBase { /** - * Optional configuration to pass during client initialization to customize the `hashi-vault-js` client. + * The root path to use for the secret engine. Defaults to `secret`. */ - clientConfig?: unknown; + rootPath?: string; /** - * This property should never be passed. + * The timeout in milliseconds for the HTTP requests. Defaults to `5000`. + * @example 10000 + * @default 5000 */ - vaultClient?: never; + timeout?: number; } -/** - * Interface for HashiCorpVaultProviderOptions with vaultClient property. - * - * @interface - */ -interface HashiCorpVaultProviderOptionsWithClientInstance - extends HashiCorpVaultProviderOptionsBase { - /** - * Optional `hashi-vault-js` client to pass during HashiCorpVaultProvider class instantiation. If not provided, a new client will be created. - */ - vaultClient?: Vault; - /** - * This property should never be passed. - */ - clientConfig: never; -} - -/** - * Options for the HashiCorpVaultProvider class constructor. - * - * @param {string} url - Indicate the server name/IP, port and API version for the Vault instance, all paths are relative to this one. - * @param {string} token - The Vault token to use for authentication. - * @param {Vault.VaultConfig} [clientConfig] - Optional configuration to pass during client initialization, e.g. timeout. Mutually exclusive with vaultClient. - * @param {Vault} [vaultClient] - Optional `hashi-vault-js` client to pass during HashiCorpVaultProvider class instantiation. Mutually exclusive with clientConfig. - */ -type HashiCorpVaultProviderOptions = - | HashiCorpVaultProviderOptionsWithClientConfig - | HashiCorpVaultProviderOptionsWithClientInstance; - type HashiCorpVaultReadKVSecretOptions = { /** * The mount point of the secret engine to use. Defaults to `secret`. diff --git a/examples/snippets/parameters/customProviderVaultUsage.ts b/examples/snippets/parameters/customProviderVaultUsage.ts index 5ead5b0a46..014f3b3a2f 100644 --- a/examples/snippets/parameters/customProviderVaultUsage.ts +++ b/examples/snippets/parameters/customProviderVaultUsage.ts @@ -1,25 +1,22 @@ import { Logger } from '@aws-lambda-powertools/logger'; import { HashiCorpVaultProvider } from './customProviderVault.js'; -const logger = new Logger({ logLevel: 'DEBUG' }); +const logger = new Logger({ serviceName: 'serverless-airline' }); const secretsProvider = new HashiCorpVaultProvider({ url: 'https://vault.example.com:8200/v1', token: process.env.ROOT_TOKEN ?? '', + rootPath: 'kv', }); -try { - // Retrieve a secret from HashiCorp Vault - const secret = await secretsProvider.get<{ foo: 'string' }>('my-secret', { - sdkOptions: { - mount: 'secrets', - }, - }); - if (!secret) { - throw new Error('Secret not found'); - } - logger.debug('Secret retrieved!'); -} catch (error) { - if (error instanceof Error) { - logger.error(error.message, error); - } -} +// Retrieve a secret from HashiCorp Vault +const secret = await secretsProvider.get<{ foo: 'string' }>('my-secret'); + +const res = await fetch('https://example.com/api', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${secret?.foo}`, + }, + body: JSON.stringify({ data: 'example' }), +}); +logger.debug('res status', { status: res.status }); diff --git a/package-lock.json b/package-lock.json index 801a374712..a3c79f34c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -100,7 +100,6 @@ "@middy/core": "^4.7.0", "aws-sdk": "^2.1692.0", "aws-sdk-client-mock": "^4.1.0", - "hashi-vault-js": "^0.4.16", "zod": "^3.24.2" } }, @@ -15388,18 +15387,6 @@ "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", "dev": true }, - "node_modules/hashi-vault-js": { - "version": "0.4.16", - "resolved": "https://registry.npmjs.org/hashi-vault-js/-/hashi-vault-js-0.4.16.tgz", - "integrity": "sha512-5pEQEYGOUP7USJc9m1O0HRG4tX/Pdvx8V4U7FozpceiU0/ECSUhtR8NVTirnSYixLusiQ5HSuvYc+8u4IOFF9w==", - "dev": true, - "dependencies": { - "axios": "^1.7.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", diff --git a/packages/parameters/src/types/BaseProvider.ts b/packages/parameters/src/types/BaseProvider.ts index 44c3ee417e..3154bb0893 100644 --- a/packages/parameters/src/types/BaseProvider.ts +++ b/packages/parameters/src/types/BaseProvider.ts @@ -17,7 +17,7 @@ type BaseProviderConstructorOptions = { * * If the `awsSdkV3Client` is not provided, this will be used to create a new client. */ - awsSdkV3ClientPrototype: new ( + awsSdkV3ClientPrototype?: new ( config?: unknown ) => unknown; };