diff --git a/docs/api-readme.md b/docs/api-readme.md index 0e973ed..31544fc 100644 --- a/docs/api-readme.md +++ b/docs/api-readme.md @@ -10,19 +10,27 @@ npm install @salesforce/b2c-tooling-sdk ## Quick Start -### From Environment Configuration (Recommended) +### From Configuration (Recommended) -The easiest way to create an instance is from environment configuration files: +Use `resolveConfig()` to load configuration from project files (dw.json) and create a B2C instance: ```typescript -import { B2CInstance } from '@salesforce/b2c-tooling-sdk'; +import { resolveConfig } from '@salesforce/b2c-tooling-sdk/config'; -// Load configuration from environment files (dw.json, etc.), override secrets from environment -const instance = B2CInstance.fromEnvironment({ +// Load configuration, override secrets from environment +const config = resolveConfig({ clientId: process.env.SFCC_CLIENT_ID, clientSecret: process.env.SFCC_CLIENT_SECRET, }); +// Validate configuration before use +if (!config.hasB2CInstanceConfig()) { + throw new Error('Missing B2C instance configuration'); +} + +// Create instance from validated config +const instance = config.createB2CInstance(); + // Use typed WebDAV client await instance.webdav.mkcol('Cartridges/v1'); await instance.webdav.put('Cartridges/v1/app.zip', zipBuffer); @@ -31,11 +39,16 @@ await instance.webdav.put('Cartridges/v1/app.zip', zipBuffer); const { data, error } = await instance.ocapi.GET('/sites', { params: { query: { select: '(**)' } }, }); + +// Check for configuration warnings +for (const warning of config.warnings) { + console.warn(warning.message); +} ``` ### Direct Construction -You can also construct an instance directly with configuration: +For advanced use cases, you can construct a B2CInstance directly: ```typescript import { B2CInstance } from '@salesforce/b2c-tooling-sdk'; @@ -51,6 +64,70 @@ const instance = new B2CInstance( ); ``` +## Configuration Resolution + +The `resolveConfig()` function provides a robust configuration system with multi-source loading and validation. + +### Multi-Source Loading + +Configuration is loaded from multiple sources with the following priority (highest to lowest): + +1. **Explicit overrides** - Values passed to `resolveConfig()` +2. **dw.json** - Project configuration file (searched upward from cwd) +3. **~/.mobify** - Home directory file for MRT API key + +```typescript +import { resolveConfig } from '@salesforce/b2c-tooling-sdk/config'; + +// Override specific values, rest loaded from dw.json +const config = resolveConfig({ + hostname: process.env.SFCC_SERVER, // Override hostname + clientId: process.env.SFCC_CLIENT_ID, // Override from env + clientSecret: process.env.SFCC_CLIENT_SECRET, +}); +``` + +### Validation Helpers + +The resolved config provides methods to check what configuration is available: + +```typescript +const config = resolveConfig(); + +// Check for B2C instance configuration +if (config.hasB2CInstanceConfig()) { + const instance = config.createB2CInstance(); +} + +// Check for MRT configuration +if (config.hasMrtConfig()) { + const mrtClient = config.createMrtClient({ project: 'my-project' }); +} + +// Other validation methods +config.hasOAuthConfig(); // OAuth credentials available? +config.hasBasicAuthConfig(); // Basic auth credentials available? +``` + +### Configuration Warnings + +The config system detects potential issues and provides warnings: + +```typescript +const config = resolveConfig({ + hostname: 'staging.demandware.net', // Different from dw.json +}); + +// Check warnings +for (const warning of config.warnings) { + console.warn(`[${warning.code}] ${warning.message}`); +} +``` + +### Hostname Protection + +When you explicitly override the hostname with a value that differs from dw.json, the system protects against credential leakage by ignoring the dw.json credentials. This prevents accidentally using production credentials against a staging server. + ## Authentication B2CInstance supports multiple authentication methods: @@ -60,16 +137,13 @@ B2CInstance supports multiple authentication methods: Used for OCAPI and can be used for WebDAV: ```typescript -const instance = new B2CInstance( - { hostname: 'sandbox.demandware.net' }, - { - oauth: { - clientId: 'your-client-id', - clientSecret: 'your-client-secret', - scopes: ['SALESFORCE_COMMERCE_API:...:dwsid'], - } - } -); +const config = resolveConfig({ + clientId: 'your-client-id', + clientSecret: 'your-client-secret', + scopes: ['SALESFORCE_COMMERCE_API:...:dwsid'], +}); + +const instance = config.createB2CInstance(); ``` ### Basic Auth @@ -77,19 +151,14 @@ const instance = new B2CInstance( Used for WebDAV operations (Business Manager credentials): ```typescript -const instance = new B2CInstance( - { hostname: 'sandbox.demandware.net' }, - { - basic: { - username: 'admin', - password: 'your-access-key' - }, - oauth: { - clientId: 'your-client-id', - clientSecret: 'your-client-secret', - } - } -); +const config = resolveConfig({ + username: 'admin', + password: 'your-access-key', + clientId: 'your-client-id', // Still needed for OCAPI + clientSecret: 'your-client-secret', +}); + +const instance = config.createB2CInstance(); ``` When both are configured, WebDAV uses Basic auth and OCAPI uses OAuth. diff --git a/packages/b2c-tooling-sdk/README.md b/packages/b2c-tooling-sdk/README.md index 3058142..a3ba670 100644 --- a/packages/b2c-tooling-sdk/README.md +++ b/packages/b2c-tooling-sdk/README.md @@ -15,19 +15,22 @@ npm install @salesforce/b2c-tooling-sdk ## Quick Start -### From Environment Configuration (Recommended) +### From Configuration (Recommended) -The easiest way to create an instance is from environment configuration files: +Use `resolveConfig()` to load configuration from project files (dw.json) and create a B2C instance: ```typescript -import { B2CInstance } from '@salesforce/b2c-tooling-sdk'; +import { resolveConfig } from '@salesforce/b2c-tooling-sdk/config'; -// Load configuration from environment files (dw.json, etc.), override secrets from environment -const instance = B2CInstance.fromEnvironment({ +// Load configuration, override secrets from environment +const config = resolveConfig({ clientId: process.env.SFCC_CLIENT_ID, clientSecret: process.env.SFCC_CLIENT_SECRET, }); +// Create instance from validated config +const instance = config.createB2CInstance(); + // Use typed WebDAV client await instance.webdav.mkcol('Cartridges/v1'); await instance.webdav.put('Cartridges/v1/app.zip', zipBuffer); @@ -40,7 +43,7 @@ const { data, error } = await instance.ocapi.GET('/sites', { ### Direct Construction -You can also construct an instance directly with configuration: +For advanced use cases, you can construct a B2CInstance directly: ```typescript import { B2CInstance } from '@salesforce/b2c-tooling-sdk'; @@ -129,6 +132,7 @@ The SDK provides subpath exports for tree-shaking and organization: | Export | Description | |--------|-------------| | `@salesforce/b2c-tooling-sdk` | Main entry point with all exports | +| `@salesforce/b2c-tooling-sdk/config` | Configuration resolution (resolveConfig) | | `@salesforce/b2c-tooling-sdk/auth` | Authentication strategies (OAuth, Basic, API Key) | | `@salesforce/b2c-tooling-sdk/instance` | B2CInstance class | | `@salesforce/b2c-tooling-sdk/clients` | Low-level API clients (WebDAV, OCAPI, SLAS, ODS, MRT) | diff --git a/packages/b2c-tooling-sdk/src/clients/index.ts b/packages/b2c-tooling-sdk/src/clients/index.ts index b795f49..0591330 100644 --- a/packages/b2c-tooling-sdk/src/clients/index.ts +++ b/packages/b2c-tooling-sdk/src/clients/index.ts @@ -24,11 +24,13 @@ * and provides convenient `webdav` and `ocapi` getters. * * ```typescript - * import { B2CInstance } from '@salesforce/b2c-tooling-sdk'; + * import { resolveConfig } from '@salesforce/b2c-tooling-sdk/config'; * - * const instance = B2CInstance.fromEnvironment({ + * const config = resolveConfig({ + * clientId: process.env.SFCC_CLIENT_ID, * clientSecret: process.env.SFCC_CLIENT_SECRET, * }); + * const instance = config.createB2CInstance(); * * // WebDAV operations via instance.webdav * await instance.webdav.put('Cartridges/v1/app.zip', content); diff --git a/packages/b2c-tooling-sdk/src/clients/ocapi.ts b/packages/b2c-tooling-sdk/src/clients/ocapi.ts index 7e3986d..8d0a572 100644 --- a/packages/b2c-tooling-sdk/src/clients/ocapi.ts +++ b/packages/b2c-tooling-sdk/src/clients/ocapi.ts @@ -91,7 +91,9 @@ export interface OcapiClientOptions { * * @example * // Via B2CInstance (recommended) - * const instance = B2CInstance.fromEnvironment(); + * import { resolveConfig } from '@salesforce/b2c-tooling-sdk/config'; + * const config = resolveConfig(); + * const instance = config.createB2CInstance(); * const { data, error } = await instance.ocapi.GET('/sites', {}); * * @example diff --git a/packages/b2c-tooling-sdk/src/clients/webdav.ts b/packages/b2c-tooling-sdk/src/clients/webdav.ts index f61b14d..dac2d4e 100644 --- a/packages/b2c-tooling-sdk/src/clients/webdav.ts +++ b/packages/b2c-tooling-sdk/src/clients/webdav.ts @@ -40,7 +40,9 @@ export interface PropfindEntry { * * @example * // Via B2CInstance (recommended) - * const instance = B2CInstance.fromEnvironment(); + * import { resolveConfig } from '@salesforce/b2c-tooling-sdk/config'; + * const config = resolveConfig(); + * const instance = config.createB2CInstance(); * await instance.webdav.mkcol('Cartridges/v1'); * await instance.webdav.put('Cartridges/v1/app.zip', zipBuffer); * diff --git a/packages/b2c-tooling-sdk/src/index.ts b/packages/b2c-tooling-sdk/src/index.ts index b130114..68d8e52 100644 --- a/packages/b2c-tooling-sdk/src/index.ts +++ b/packages/b2c-tooling-sdk/src/index.ts @@ -60,7 +60,7 @@ export type { // Context Layer - Instance export {B2CInstance} from './instance/index.js'; -export type {InstanceConfig, FromEnvironmentOptions, B2CInstanceOptions} from './instance/index.js'; +export type {InstanceConfig} from './instance/index.js'; // Clients export { diff --git a/packages/b2c-tooling-sdk/src/instance/index.ts b/packages/b2c-tooling-sdk/src/instance/index.ts index ca84edd..b4ba40e 100644 --- a/packages/b2c-tooling-sdk/src/instance/index.ts +++ b/packages/b2c-tooling-sdk/src/instance/index.ts @@ -12,17 +12,20 @@ * * ## Usage * - * ### From environment configuration (recommended) + * ### From configuration (recommended) + * + * Use {@link resolveConfig} to load configuration from dw.json and create an instance: * * ```typescript - * import { B2CInstance } from '@salesforce/b2c-tooling-sdk'; + * import { resolveConfig } from '@salesforce/b2c-tooling-sdk/config'; * - * // Load from environment files (dw.json, etc.), override secrets from environment - * const instance = B2CInstance.fromEnvironment({ + * const config = resolveConfig({ * clientId: process.env.SFCC_CLIENT_ID, * clientSecret: process.env.SFCC_CLIENT_SECRET, * }); * + * const instance = config.createB2CInstance(); + * * // Use typed clients * await instance.webdav.put('Cartridges/v1/app.zip', content); * const { data } = await instance.ocapi.GET('/sites', {}); @@ -44,9 +47,6 @@ import {BasicAuthStrategy} from '../auth/basic.js'; import {resolveAuthStrategy} from '../auth/resolve.js'; import {WebDavClient} from '../clients/webdav.js'; import {createOcapiClient, type OcapiClient} from '../clients/ocapi.js'; -import {loadDwJson} from '../config/dw-json.js'; -import {mapDwJsonToNormalizedConfig, mergeConfigsWithProtection} from '../config/mapping.js'; -import type {NormalizedConfig} from '../config/types.js'; /** * Instance configuration (hostname, code version, etc.) @@ -60,36 +60,6 @@ export interface InstanceConfig { webdavHostname?: string; } -/** - * Options for creating a B2CInstance from environment configuration. - */ -export interface FromEnvironmentOptions { - /** Named instance from dw.json "configs" array */ - instance?: string; - /** Path to dw.json (defaults to searching up from cwd) */ - configPath?: string; - - // Overrides (take precedence over dw.json values) - /** B2C instance hostname */ - hostname?: string; - /** Code version */ - codeVersion?: string; - /** WebDAV hostname (if different) */ - webdavHostname?: string; - /** Username for Basic auth */ - username?: string; - /** Password for Basic auth */ - password?: string; - /** OAuth client ID */ - clientId?: string; - /** OAuth client secret */ - clientSecret?: string; - /** OAuth scopes */ - scopes?: string[]; - /** Allowed auth methods in priority order */ - authMethods?: AuthMethod[]; -} - /** * Represents a connection to a B2C Commerce instance. * @@ -97,10 +67,14 @@ export interface FromEnvironmentOptions { * Authentication is handled automatically based on the configured credentials. * * @example - * // From environment configuration - * const instance = B2CInstance.fromEnvironment({ + * // From configuration (recommended) + * import { resolveConfig } from '@salesforce/b2c-tooling-sdk/config'; + * + * const config = resolveConfig({ + * clientId: process.env.SFCC_CLIENT_ID, * clientSecret: process.env.SFCC_CLIENT_SECRET, * }); + * const instance = config.createB2CInstance(); * * // WebDAV uses Basic auth if available, falls back to OAuth * await instance.webdav.mkcol('Cartridges/v1'); @@ -112,90 +86,6 @@ export class B2CInstance { private _webdav?: WebDavClient; private _ocapi?: OcapiClient; - /** - * Creates a B2CInstance from environment configuration files with optional overrides. - * - * Searches upward from the current directory for configuration files (dw.json, etc.), - * then applies any provided overrides. - * - * @param options - Loading options and overrides - * @returns Configured B2CInstance - * @throws Error if no configuration found or required configuration missing - * - * @example - * // Auto-find configuration, override secrets - * const instance = B2CInstance.fromEnvironment({ - * clientId: process.env.SFCC_CLIENT_ID, - * clientSecret: process.env.SFCC_CLIENT_SECRET, - * }); - * - * // Use named instance - * const instance = B2CInstance.fromEnvironment({ - * instance: 'staging', - * }); - */ - static fromEnvironment(options: FromEnvironmentOptions = {}): B2CInstance { - // Load dw.json and map to normalized config - const dwJsonRaw = loadDwJson({ - instance: options.instance, - path: options.configPath, - }); - const dwConfig = dwJsonRaw ? mapDwJsonToNormalizedConfig(dwJsonRaw) : {}; - - // Build overrides from options - const overrides: Partial = { - hostname: options.hostname, - codeVersion: options.codeVersion, - webdavHostname: options.webdavHostname, - username: options.username, - password: options.password, - clientId: options.clientId, - clientSecret: options.clientSecret, - scopes: options.scopes, - authMethods: options.authMethods, - }; - - // Merge with hostname mismatch protection (consistent with CLI behavior) - const {config: resolved, warnings} = mergeConfigsWithProtection(overrides, dwConfig, { - hostnameProtection: true, - }); - - // Log warnings (optional - could integrate with SDK logger) - for (const warning of warnings) { - console.warn(`[B2CInstance] ${warning.message}`); - } - - if (!resolved.hostname) { - throw new Error( - 'Hostname is required. Set in dw.json or provide via options. ' + (dwJsonRaw ? '' : 'No dw.json file found.'), - ); - } - - const config: InstanceConfig = { - hostname: resolved.hostname, - codeVersion: resolved.codeVersion, - webdavHostname: resolved.webdavHostname, - }; - - const auth: AuthConfig = { - authMethods: resolved.authMethods, - }; - - if (resolved.username && resolved.password) { - auth.basic = {username: resolved.username, password: resolved.password}; - } - - if (resolved.clientId) { - auth.oauth = { - clientId: resolved.clientId, - clientSecret: resolved.clientSecret, - scopes: resolved.scopes, - }; - } - - return new B2CInstance(config, auth); - } - /** * Creates a new B2CInstance. * @@ -303,4 +193,4 @@ export class B2CInstance { } // Re-export types for convenience -export type {AuthConfig, FromEnvironmentOptions as B2CInstanceOptions}; +export type {AuthConfig}; diff --git a/packages/b2c-tooling-sdk/src/operations/code/index.ts b/packages/b2c-tooling-sdk/src/operations/code/index.ts index dd9eb1c..40a30ba 100644 --- a/packages/b2c-tooling-sdk/src/operations/code/index.ts +++ b/packages/b2c-tooling-sdk/src/operations/code/index.ts @@ -39,9 +39,10 @@ * activateCodeVersion, * watchCartridges, * } from '@salesforce/b2c-tooling-sdk/operations/code'; - * import { B2CInstance } from '@salesforce/b2c-tooling-sdk'; + * import { resolveConfig } from '@salesforce/b2c-tooling-sdk/config'; * - * const instance = B2CInstance.fromEnvironment(); + * const config = resolveConfig(); + * const instance = config.createB2CInstance(); * * // Deploy cartridges (requires instance.config.codeVersion to be set) * await findAndDeployCartridges(instance, './cartridges', { reload: true }); diff --git a/packages/b2c-tooling-sdk/src/operations/jobs/index.ts b/packages/b2c-tooling-sdk/src/operations/jobs/index.ts index 689b9cb..0cf4d4d 100644 --- a/packages/b2c-tooling-sdk/src/operations/jobs/index.ts +++ b/packages/b2c-tooling-sdk/src/operations/jobs/index.ts @@ -34,9 +34,10 @@ * siteArchiveImport, * siteArchiveExport, * } from '@salesforce/b2c-tooling-sdk/operations/jobs'; - * import { B2CInstance } from '@salesforce/b2c-tooling-sdk'; + * import { resolveConfig } from '@salesforce/b2c-tooling-sdk/config'; * - * const instance = B2CInstance.fromEnvironment(); + * const config = resolveConfig(); + * const instance = config.createB2CInstance(); * * // Run a custom job and wait for completion * const execution = await executeJob(instance, 'my-job-id');