diff --git a/packages/b2c-tooling/package.json b/packages/b2c-tooling/package.json index 84f338f3..34715ba8 100644 --- a/packages/b2c-tooling/package.json +++ b/packages/b2c-tooling/package.json @@ -164,6 +164,7 @@ "chokidar": "^5.0.0", "glob": "^13.0.0", "i18next": "^25.6.3", + "open": "^11.0.0", "openapi-fetch": "^0.15.0", "pino": "^10.1.0", "pino-pretty": "^13.1.2" diff --git a/packages/b2c-tooling/src/auth/index.ts b/packages/b2c-tooling/src/auth/index.ts index c617b463..ab275b28 100644 --- a/packages/b2c-tooling/src/auth/index.ts +++ b/packages/b2c-tooling/src/auth/index.ts @@ -8,28 +8,53 @@ * * - {@link BasicAuthStrategy} - Username/password authentication for WebDAV operations * - {@link OAuthStrategy} - OAuth 2.0 client credentials for OCAPI and platform APIs + * - {@link ImplicitOAuthStrategy} - Interactive browser-based OAuth for CLI/desktop apps * - {@link ApiKeyStrategy} - API key authentication for MRT services * - * ## Usage + * ## Strategy Resolution * - * All strategies implement the {@link AuthStrategy} interface, allowing you to - * switch authentication methods without changing your code: + * Use {@link resolveAuthStrategy} to automatically select the best strategy based on + * available credentials and allowed methods: * * ```typescript - * import { BasicAuthStrategy, OAuthStrategy } from '@salesforce/b2c-tooling'; + * import { resolveAuthStrategy } from '@salesforce/b2c-tooling'; * - * // For WebDAV operations (code upload) - * const basicAuth = new BasicAuthStrategy('username', 'access-key'); + * // Automatically picks client-credentials if secret available, otherwise implicit + * const strategy = resolveAuthStrategy({ + * clientId: 'your-client-id', + * clientSecret: process.env.CLIENT_SECRET, // may be undefined + * }); + * + * // Force a specific method + * const implicitOnly = resolveAuthStrategy( + * { clientId: 'your-client-id' }, + * { allowedMethods: ['implicit'] } + * ); + * ``` + * + * ## Direct Usage + * + * All strategies implement the {@link AuthStrategy} interface: * - * // For OCAPI operations (sites, jobs) + * ```typescript + * import { OAuthStrategy, ImplicitOAuthStrategy } from '@salesforce/b2c-tooling'; + * + * // For automated/server usage (client credentials) * const oauthAuth = new OAuthStrategy({ * clientId: 'your-client-id', * clientSecret: 'your-client-secret', * }); + * + * // For interactive/CLI usage (opens browser) + * const implicitAuth = new ImplicitOAuthStrategy({ + * clientId: 'your-client-id', + * }); * ``` * * @module auth */ + +// Types export type { AuthStrategy, AccessTokenResponse, @@ -38,8 +63,19 @@ export type { BasicAuthConfig, OAuthAuthConfig, ApiKeyAuthConfig, + AuthMethod, + AuthCredentials, } from './types.js'; +export {ALL_AUTH_METHODS} from './types.js'; + +// Strategies export {BasicAuthStrategy} from './basic.js'; export {OAuthStrategy, decodeJWT} from './oauth.js'; export type {OAuthConfig} from './oauth.js'; +export {ImplicitOAuthStrategy} from './oauth-implicit.js'; +export type {ImplicitOAuthConfig} from './oauth-implicit.js'; export {ApiKeyStrategy} from './api-key.js'; + +// Resolution helpers +export {resolveAuthStrategy, checkAvailableAuthMethods} from './resolve.js'; +export type {ResolveAuthStrategyOptions, AvailableAuthMethods} from './resolve.js'; diff --git a/packages/b2c-tooling/src/auth/oauth-implicit.ts b/packages/b2c-tooling/src/auth/oauth-implicit.ts new file mode 100644 index 00000000..ac496a9f --- /dev/null +++ b/packages/b2c-tooling/src/auth/oauth-implicit.ts @@ -0,0 +1,403 @@ +import {createServer, type Server, type IncomingMessage, type ServerResponse} from 'node:http'; +import type {Socket} from 'node:net'; +import {URL} from 'node:url'; +import type {AuthStrategy, AccessTokenResponse, DecodedJWT} from './types.js'; +import {getLogger} from '../logging/logger.js'; +import {decodeJWT} from './oauth.js'; + +const DEFAULT_ACCOUNT_MANAGER_HOST = 'account.demandware.com'; +const DEFAULT_LOCAL_PORT = 8080; + +// Module-level token cache to support multiple instances with same clientId +const ACCESS_TOKEN_CACHE: Map = new Map(); + +/** + * Configuration for the implicit OAuth flow. + */ +export interface ImplicitOAuthConfig { + /** OAuth client ID registered with Account Manager */ + clientId: string; + /** OAuth scopes to request (e.g., 'sfcc.products', 'sfcc.orders') */ + scopes?: string[]; + /** Account Manager host (defaults to 'account.demandware.com') */ + accountManagerHost?: string; + /** + * Local port for the OAuth redirect server. + * Defaults to 8080 or SFCC_OAUTH_LOCAL_PORT environment variable. + */ + localPort?: number; +} + +/** + * Returns the HTML page served to the browser to extract the access token + * from the URL fragment and redirect it as query parameters. + */ +function getOauth2RedirectHTML(port: number): string { + return ` + + + + + OAuth Return Flow + + + + + +`; +} + +/** + * Opens the system default browser to the specified URL. + * Dynamically imports 'open' package to handle the browser opening. + */ +async function openBrowser(url: string): Promise { + try { + // Dynamic import of 'open' package + const open = await import('open'); + await open.default(url); + } catch { + // If open fails, the URL will still be printed to console + getLogger().debug('Could not automatically open browser'); + } +} + +/** + * OAuth 2.0 Implicit Grant Flow authentication strategy. + * + * This strategy is used when only a client ID is available (no client secret). + * It opens a browser for the user to authenticate with Account Manager, + * then captures the access token from the OAuth redirect. + * + * Note: The access token from implicit flow is valid for 30 minutes and cannot be renewed. + * This flow requires user interaction and a TTY. + * + * @example + * ```typescript + * import { ImplicitOAuthStrategy } from '@salesforce/b2c-tooling'; + * + * const auth = new ImplicitOAuthStrategy({ + * clientId: 'your-client-id', + * scopes: ['sfcc.products', 'sfcc.orders'], + * }); + * + * // Will open browser for authentication + * const response = await auth.fetch('https://example.com/api/resource'); + * ``` + */ +export class ImplicitOAuthStrategy implements AuthStrategy { + private accountManagerHost: string; + private localPort: number; + + constructor(private config: ImplicitOAuthConfig) { + this.accountManagerHost = config.accountManagerHost || DEFAULT_ACCOUNT_MANAGER_HOST; + this.localPort = config.localPort || parseInt(process.env.SFCC_OAUTH_LOCAL_PORT || '', 10) || DEFAULT_LOCAL_PORT; + + const logger = getLogger(); + logger.debug( + {clientId: this.config.clientId, accountManagerHost: this.accountManagerHost, localPort: this.localPort}, + `[Auth] ImplicitOAuthStrategy initialized for client: ${this.config.clientId}`, + ); + logger.trace( + {scopes: this.config.scopes}, + `[Auth] Configured scopes: ${this.config.scopes?.join(', ') || '(none)'}`, + ); + } + + async fetch(url: string, init: RequestInit = {}): Promise { + const logger = getLogger(); + const method = init.method || 'GET'; + + logger.trace({method, url}, `[Auth] Fetching with implicit OAuth: ${method} ${url}`); + + const token = await this.getAccessToken(); + + const headers = new Headers(init.headers); + headers.set('Authorization', `Bearer ${token}`); + headers.set('x-dw-client-id', this.config.clientId); + + const startTime = Date.now(); + let res = await fetch(url, {...init, headers}); + const duration = Date.now() - startTime; + + logger.debug( + {method, url, status: res.status, duration}, + `[Auth] Response: ${method} ${url} ${res.status} ${duration}ms`, + ); + + // RESILIENCE: If the server says 401, the token might have expired or been revoked. + // We retry exactly once after invalidating the cached token. + if (res.status === 401) { + logger.debug('[Auth] Received 401, invalidating token and retrying'); + this.invalidateToken(); + const newToken = await this.getAccessToken(); + headers.set('Authorization', `Bearer ${newToken}`); + + const retryStart = Date.now(); + res = await fetch(url, {...init, headers}); + const retryDuration = Date.now() - retryStart; + + logger.debug( + {method, url, status: res.status, duration: retryDuration}, + `[Auth] Retry response: ${method} ${url} ${res.status} ${retryDuration}ms`, + ); + } + + return res; + } + + async getAuthorizationHeader(): Promise { + const token = await this.getAccessToken(); + return `Bearer ${token}`; + } + + /** + * Gets the decoded JWT payload + */ + async getJWT(): Promise { + const token = await this.getAccessToken(); + return decodeJWT(token); + } + + /** + * Gets the full token response including expiration and scopes. + * Useful for commands that need to display or return token metadata. + */ + async getTokenResponse(): Promise { + const logger = getLogger(); + const cached = ACCESS_TOKEN_CACHE.get(this.config.clientId); + + if (cached) { + const now = new Date(); + const requiredScopes = this.config.scopes || []; + const hasAllScopes = requiredScopes.every((scope) => cached.scopes.includes(scope)); + + if (hasAllScopes && now.getTime() <= cached.expires.getTime()) { + logger.debug('Reusing cached access token'); + return cached; + } + } + + // Get new token via implicit flow + const tokenResponse = await this.implicitFlowLogin(); + ACCESS_TOKEN_CACHE.set(this.config.clientId, tokenResponse); + return tokenResponse; + } + + /** + * Invalidates the cached token, forcing re-authentication on next request + */ + invalidateToken(): void { + ACCESS_TOKEN_CACHE.delete(this.config.clientId); + } + + /** + * Gets an access token, using cache if valid + */ + private async getAccessToken(): Promise { + const logger = getLogger(); + const cached = ACCESS_TOKEN_CACHE.get(this.config.clientId); + + logger.trace({clientId: this.config.clientId, hasCached: !!cached}, '[Auth] Getting access token'); + + if (cached) { + const now = new Date(); + const requiredScopes = this.config.scopes || []; + const hasAllScopes = requiredScopes.every((scope) => cached.scopes.includes(scope)); + const timeUntilExpiry = cached.expires.getTime() - now.getTime(); + + logger.trace( + { + cachedScopes: cached.scopes, + requiredScopes, + hasAllScopes, + expiresAt: cached.expires.toISOString(), + timeUntilExpiryMs: timeUntilExpiry, + }, + '[Auth] Checking cached token validity', + ); + + if (!hasAllScopes) { + logger.warn( + {cachedScopes: cached.scopes, requiredScopes}, + '[Auth] Access token missing scopes; invalidating and re-authenticating', + ); + ACCESS_TOKEN_CACHE.delete(this.config.clientId); + } else if (now.getTime() > cached.expires.getTime()) { + logger.warn( + {expiresAt: cached.expires.toISOString()}, + '[Auth] Access token expired; invalidating and re-authenticating', + ); + ACCESS_TOKEN_CACHE.delete(this.config.clientId); + } else { + logger.debug( + {timeUntilExpiryMs: timeUntilExpiry}, + `[Auth] Reusing cached access token (expires in ${Math.round(timeUntilExpiry / 1000)}s)`, + ); + return cached.accessToken; + } + } + + // Get new token via implicit flow + logger.debug('[Auth] No valid cached token, starting implicit flow login'); + const tokenResponse = await this.implicitFlowLogin(); + ACCESS_TOKEN_CACHE.set(this.config.clientId, tokenResponse); + logger.debug( + {expiresAt: tokenResponse.expires.toISOString(), scopes: tokenResponse.scopes}, + '[Auth] New token cached', + ); + return tokenResponse.accessToken; + } + + /** + * Performs an implicit OAuth2 login flow. + * Opens the user's browser for authentication with Account Manager. + * + * NOTE: This method requires a TTY and user intervention; it is interactive. + * NOTE: Access token is valid for 30 minutes and cannot be renewed. + */ + private async implicitFlowLogin(): Promise { + const logger = getLogger(); + const redirectUrl = `http://localhost:${this.localPort}`; + + const params = new URLSearchParams({ + client_id: this.config.clientId, + redirect_uri: redirectUrl, + response_type: 'token', + }); + + if (this.config.scopes && this.config.scopes.length > 0) { + params.append('scope', this.config.scopes.join(' ')); + } + + const authorizeUrl = `https://${this.accountManagerHost}/dwsso/oauth2/authorize?${params.toString()}`; + + logger.debug( + { + clientId: this.config.clientId, + redirectUrl, + scopes: this.config.scopes, + accountManagerHost: this.accountManagerHost, + }, + '[Auth] Starting implicit OAuth flow', + ); + logger.trace({authorizeUrl}, '[Auth] Authorization URL'); + + // Print URL to console (in case machine has no default browser) + logger.info(`Login URL: ${authorizeUrl}`); + logger.info('If the URL does not open automatically, copy/paste it into a browser on this machine.'); + + // Attempt to open the browser + logger.debug('[Auth] Attempting to open browser'); + await openBrowser(authorizeUrl); + + return new Promise((resolve, reject) => { + const sockets: Set = new Set(); + const startTime = Date.now(); + + const server: Server = createServer((request: IncomingMessage, response: ServerResponse) => { + const requestUrl = new URL(request.url || '/', `http://localhost:${this.localPort}`); + const accessToken = requestUrl.searchParams.get('access_token'); + const error = requestUrl.searchParams.get('error'); + const errorDescription = requestUrl.searchParams.get('error_description'); + + logger.trace( + { + path: requestUrl.pathname, + hasAccessToken: !!accessToken, + hasError: !!error, + }, + `[Auth] Received redirect request: ${requestUrl.pathname}`, + ); + + if (!accessToken && !error) { + // Serve HTML page to extract token from URL fragment + logger.debug('[Auth] Serving token extraction HTML page'); + response.writeHead(200, {'Content-Type': 'text/html'}); + response.write(getOauth2RedirectHTML(this.localPort)); + response.end(); + } else if (accessToken) { + const authDuration = Date.now() - startTime; + // Successfully received access token + logger.debug({authDurationMs: authDuration}, `[Auth] Got access token response (took ${authDuration}ms)`); + logger.info('Successfully authenticated'); + + try { + const jwt = decodeJWT(accessToken); + logger.trace({jwt: jwt.payload}, '[Auth] Decoded JWT payload'); + } catch { + logger.debug('[Auth] Error decoding JWT (token may not be a JWT)'); + } + + const expiresIn = parseInt(requestUrl.searchParams.get('expires_in') || '0', 10); + const now = new Date(); + const expiration = new Date(now.getTime() + expiresIn * 1000); + const scopes = requestUrl.searchParams.get('scope')?.split(' ') ?? []; + + logger.debug( + {expiresIn, expiresAt: expiration.toISOString(), scopes}, + `[Auth] Token expires in ${expiresIn}s, scopes: ${scopes.join(', ') || '(none)'}`, + ); + + resolve({ + accessToken, + expires: expiration, + scopes, + }); + + response.writeHead(200, {'Content-Type': 'text/plain'}); + response.write('Authentication successful! You may close this browser window and return to your terminal.'); + response.end(); + + // Shutdown server after a short delay + setTimeout(() => { + logger.debug('[Auth] Shutting down OAuth redirect server'); + server.close(() => logger.trace('[Auth] OAuth redirect server closed')); + for (const socket of sockets) { + socket.destroy(); + } + logger.trace({socketCount: sockets.size}, '[Auth] Cleaned up sockets'); + }, 100); + } else if (error) { + // OAuth error response + const errorMessage = errorDescription || error; + logger.error({error, errorDescription}, `[Auth] OAuth error: ${errorMessage}`); + response.writeHead(500, {'Content-Type': 'text/plain'}); + response.write(`Authentication failed: ${errorMessage}`); + response.end(); + reject(new Error(`OAuth error: ${errorMessage}`)); + + setTimeout(() => { + server.close(); + for (const socket of sockets) { + socket.destroy(); + } + }, 100); + } + }); + + server.on('connection', (socket) => { + sockets.add(socket); + logger.trace({socketCount: sockets.size}, '[Auth] New socket connection'); + socket.on('close', () => { + sockets.delete(socket); + logger.trace({socketCount: sockets.size}, '[Auth] Socket closed'); + }); + }); + + server.listen(this.localPort, () => { + logger.debug({port: this.localPort}, `[Auth] OAuth redirect server listening on port ${this.localPort}`); + logger.info('Waiting for user to authenticate...'); + }); + + server.on('error', (err) => { + logger.error({error: err.message, port: this.localPort}, `[Auth] Failed to start OAuth redirect server`); + reject(new Error(`Failed to start OAuth redirect server: ${err.message}`)); + }); + }); + } +} diff --git a/packages/b2c-tooling/src/auth/resolve.ts b/packages/b2c-tooling/src/auth/resolve.ts new file mode 100644 index 00000000..c6906ea1 --- /dev/null +++ b/packages/b2c-tooling/src/auth/resolve.ts @@ -0,0 +1,206 @@ +/** + * Auth strategy resolution utilities. + * + * This module provides functions to automatically select and create the appropriate + * authentication strategy based on available credentials and allowed methods. + * + * ## Usage + * + * ```typescript + * import { resolveAuthStrategy, checkAvailableAuthMethods } from '@salesforce/b2c-tooling'; + * + * // Auto-select best strategy based on credentials + * const strategy = resolveAuthStrategy({ + * clientId: 'my-client-id', + * clientSecret: process.env.CLIENT_SECRET, + * }); + * + * // Check which methods are available + * const { available, unavailable } = checkAvailableAuthMethods(credentials); + * ``` + * + * @module auth/resolve + */ + +import type {AuthStrategy, AuthMethod, AuthCredentials} from './types.js'; +import {ALL_AUTH_METHODS} from './types.js'; +import {OAuthStrategy} from './oauth.js'; +import {ImplicitOAuthStrategy} from './oauth-implicit.js'; +import {BasicAuthStrategy} from './basic.js'; +import {ApiKeyStrategy} from './api-key.js'; + +/** + * Options for resolving an auth strategy. + */ +export interface ResolveAuthStrategyOptions { + /** + * Allowed authentication methods in priority order. + * The first method with available credentials will be used. + * Defaults to all methods: ['client-credentials', 'implicit', 'basic', 'api-key'] + */ + allowedMethods?: AuthMethod[]; +} + +/** + * Result of checking which auth methods are available. + */ +export interface AvailableAuthMethods { + /** Methods that have all required credentials configured */ + available: AuthMethod[]; + /** Methods that are missing required credentials */ + unavailable: {method: AuthMethod; reason: string}[]; +} + +/** + * Checks which auth methods have the required credentials available. + * + * @param credentials - The available credentials + * @param allowedMethods - Methods to check (defaults to all) + * @returns Object with available and unavailable methods + * + * @example + * ```typescript + * import { checkAvailableAuthMethods } from '@salesforce/b2c-tooling'; + * + * const result = checkAvailableAuthMethods({ + * clientId: 'my-client', + * clientSecret: 'my-secret', + * }); + * + * console.log(result.available); // ['client-credentials', 'implicit'] + * ``` + */ +export function checkAvailableAuthMethods( + credentials: AuthCredentials, + allowedMethods: AuthMethod[] = ALL_AUTH_METHODS, +): AvailableAuthMethods { + const available: AuthMethod[] = []; + const unavailable: {method: AuthMethod; reason: string}[] = []; + + for (const method of allowedMethods) { + switch (method) { + case 'client-credentials': + if (credentials.clientId && credentials.clientSecret) { + available.push(method); + } else if (!credentials.clientId) { + unavailable.push({method, reason: 'clientId is required'}); + } else { + unavailable.push({method, reason: 'clientSecret is required'}); + } + break; + + case 'implicit': + if (credentials.clientId) { + available.push(method); + } else { + unavailable.push({method, reason: 'clientId is required'}); + } + break; + + case 'basic': + if (credentials.username && credentials.password) { + available.push(method); + } else if (!credentials.username) { + unavailable.push({method, reason: 'username is required'}); + } else { + unavailable.push({method, reason: 'password is required'}); + } + break; + + case 'api-key': + if (credentials.apiKey) { + available.push(method); + } else { + unavailable.push({method, reason: 'apiKey is required'}); + } + break; + } + } + + return {available, unavailable}; +} + +/** + * Resolves and creates the appropriate auth strategy based on credentials and allowed methods. + * + * Iterates through allowed methods in priority order and returns the first strategy + * for which the required credentials are available. + * + * @param credentials - The available credentials + * @param options - Resolution options (allowed methods, etc.) + * @returns The resolved auth strategy + * @throws Error if no allowed method has the required credentials + * + * @example + * ```typescript + * import { resolveAuthStrategy } from '@salesforce/b2c-tooling'; + * + * // Will use client-credentials if secret is available, otherwise implicit + * const strategy = resolveAuthStrategy({ + * clientId: 'my-client-id', + * clientSecret: process.env.CLIENT_SECRET, // may be undefined + * scopes: ['sfcc.products'], + * }); + * + * // Force implicit auth only + * const implicitStrategy = resolveAuthStrategy( + * { clientId: 'my-client-id' }, + * { allowedMethods: ['implicit'] } + * ); + * + * // Use the strategy + * const response = await strategy.fetch('https://example.com/api'); + * ``` + */ +export function resolveAuthStrategy( + credentials: AuthCredentials, + options: ResolveAuthStrategyOptions = {}, +): AuthStrategy { + const allowedMethods = options.allowedMethods || ALL_AUTH_METHODS; + + for (const method of allowedMethods) { + switch (method) { + case 'client-credentials': + if (credentials.clientId && credentials.clientSecret) { + return new OAuthStrategy({ + clientId: credentials.clientId, + clientSecret: credentials.clientSecret, + scopes: credentials.scopes, + accountManagerHost: credentials.accountManagerHost, + }); + } + break; + + case 'implicit': + if (credentials.clientId) { + return new ImplicitOAuthStrategy({ + clientId: credentials.clientId, + scopes: credentials.scopes, + accountManagerHost: credentials.accountManagerHost, + }); + } + break; + + case 'basic': + if (credentials.username && credentials.password) { + return new BasicAuthStrategy(credentials.username, credentials.password); + } + break; + + case 'api-key': + if (credentials.apiKey) { + return new ApiKeyStrategy(credentials.apiKey, credentials.apiKeyHeaderName); + } + break; + } + } + + // Build helpful error message + const {unavailable} = checkAvailableAuthMethods(credentials, allowedMethods); + const details = unavailable.map((u) => `${u.method}: ${u.reason}`).join('; '); + + throw new Error( + `No valid auth method available. Allowed methods: [${allowedMethods.join(', ')}]. ` + + `Missing credentials: ${details}`, + ); +} diff --git a/packages/b2c-tooling/src/auth/types.ts b/packages/b2c-tooling/src/auth/types.ts index 480d32a4..94d47e92 100644 --- a/packages/b2c-tooling/src/auth/types.ts +++ b/packages/b2c-tooling/src/auth/types.ts @@ -57,6 +57,12 @@ export interface AuthConfig { /** API key for MRT/external services */ apiKey?: ApiKeyAuthConfig; + + /** + * Allowed authentication methods in priority order. + * If not set, defaults to all methods: ['client-credentials', 'implicit', 'basic', 'api-key'] + */ + authMethods?: AuthMethod[]; } /** @@ -75,3 +81,38 @@ export interface DecodedJWT { header: Record; payload: Record; } + +/** + * Available authentication methods. + * - 'client-credentials': OAuth client credentials flow (requires clientId + clientSecret) + * - 'implicit': Interactive browser-based OAuth (requires clientId only) + * - 'basic': Username/password (access key) authentication + * - 'api-key': API key authentication (for MRT, etc.) + */ +export type AuthMethod = 'client-credentials' | 'implicit' | 'basic' | 'api-key'; + +/** All available auth methods in default priority order */ +export const ALL_AUTH_METHODS: AuthMethod[] = ['client-credentials', 'implicit', 'basic', 'api-key']; + +/** + * Configuration for resolving an auth strategy. + * Combines all possible credential types. + */ +export interface AuthCredentials { + /** OAuth client ID */ + clientId?: string; + /** OAuth client secret (for client-credentials flow) */ + clientSecret?: string; + /** OAuth scopes to request */ + scopes?: string[]; + /** Account Manager host (defaults to account.demandware.com) */ + accountManagerHost?: string; + /** Username for basic auth */ + username?: string; + /** Password/access key for basic auth */ + password?: string; + /** API key for api-key auth */ + apiKey?: string; + /** Header name for API key (defaults to Authorization with Bearer prefix) */ + apiKeyHeaderName?: string; +} diff --git a/packages/b2c-tooling/src/cli/config.ts b/packages/b2c-tooling/src/cli/config.ts index ab5ec58f..88b1befd 100644 --- a/packages/b2c-tooling/src/cli/config.ts +++ b/packages/b2c-tooling/src/cli/config.ts @@ -1,5 +1,11 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; +import type {AuthMethod} from '../auth/types.js'; +import {ALL_AUTH_METHODS} from '../auth/types.js'; + +// Re-export for convenience +export type {AuthMethod}; +export {ALL_AUTH_METHODS}; export interface ResolvedConfig { hostname?: string; @@ -13,6 +19,8 @@ export interface ResolvedConfig { shortCode?: string; mrtApiKey?: string; instanceName?: string; + /** Allowed authentication methods (in priority order). If not set, all methods are allowed. */ + authMethods?: AuthMethod[]; } /** @@ -34,6 +42,8 @@ interface DwJsonConfig { 'scapi-shortcode'?: string; secureHostname?: string; 'secure-server'?: string; + /** Allowed authentication methods (in priority order) */ + 'auth-methods'?: AuthMethod[]; } /** @@ -81,6 +91,7 @@ function mapDwJsonToConfig(json: DwJsonConfig): ResolvedConfig { scopes: json['oauth-scopes'], shortCode: json.shortCode || json['short-code'] || json['scapi-shortcode'], instanceName: json.name, + authMethods: json['auth-methods'], }; } @@ -149,6 +160,7 @@ function mergeConfigs( shortCode: flags.shortCode || dwJson.shortCode, mrtApiKey: flags.mrtApiKey, instanceName: dwJson.instanceName || options.instance, + authMethods: flags.authMethods || dwJson.authMethods, }; } diff --git a/packages/b2c-tooling/src/cli/instance-command.ts b/packages/b2c-tooling/src/cli/instance-command.ts index 5f8f07a0..30f9fd95 100644 --- a/packages/b2c-tooling/src/cli/instance-command.ts +++ b/packages/b2c-tooling/src/cli/instance-command.ts @@ -1,7 +1,7 @@ import {Command, Flags} from '@oclif/core'; import {OAuthCommand} from './oauth-command.js'; import {loadConfig} from './config.js'; -import type {ResolvedConfig, LoadConfigOptions} from './config.js'; +import type {ResolvedConfig, LoadConfigOptions, AuthMethod} from './config.js'; import {B2CInstance} from '../instance/index.js'; import type {AuthConfig} from '../auth/types.js'; import {t} from '../i18n/index.js'; @@ -83,9 +83,17 @@ export abstract class InstanceCommand extends OAuthCom password: this.flags.password, clientId: this.flags['client-id'], clientSecret: this.flags['client-secret'], + authMethods: this.flags['auth-method'] as AuthMethod[] | undefined, }; - return loadConfig(flagConfig, options); + const config = loadConfig(flagConfig, options); + + // Merge scopes from flags with config file scopes (flags take precedence if provided) + if (this.flags.scope && this.flags.scope.length > 0) { + config.scopes = this.flags.scope; + } + + return config; } /** @@ -107,7 +115,9 @@ export abstract class InstanceCommand extends OAuthCom const config = this.resolvedConfig; - const authConfig: AuthConfig = {}; + const authConfig: AuthConfig = { + authMethods: config.authMethods, + }; if (config.username && config.password) { authConfig.basic = { @@ -116,7 +126,8 @@ export abstract class InstanceCommand extends OAuthCom }; } - if (config.clientId && config.clientSecret) { + // Only require clientId for OAuth - clientSecret is optional for implicit flow + if (config.clientId) { authConfig.oauth = { clientId: config.clientId, clientSecret: config.clientSecret, @@ -137,11 +148,12 @@ export abstract class InstanceCommand extends OAuthCom } /** - * Check if WebDAV credentials are available (Basic or OAuth). + * Check if WebDAV credentials are available (Basic or OAuth including implicit). */ protected hasWebDavCredentials(): boolean { const config = this.resolvedConfig; - return Boolean((config.username && config.password) || (config.clientId && config.clientSecret)); + // Basic auth, or OAuth (client-credentials needs secret, implicit only needs clientId) + return Boolean((config.username && config.password) || config.clientId); } /** diff --git a/packages/b2c-tooling/src/cli/oauth-command.ts b/packages/b2c-tooling/src/cli/oauth-command.ts index 42a21e1b..0a53d4d0 100644 --- a/packages/b2c-tooling/src/cli/oauth-command.ts +++ b/packages/b2c-tooling/src/cli/oauth-command.ts @@ -1,8 +1,9 @@ import {Command, Flags} from '@oclif/core'; import {BaseCommand} from './base-command.js'; -import {loadConfig} from './config.js'; -import type {ResolvedConfig, LoadConfigOptions} from './config.js'; +import {loadConfig, ALL_AUTH_METHODS} from './config.js'; +import type {ResolvedConfig, LoadConfigOptions, AuthMethod} from './config.js'; import {OAuthStrategy} from '../auth/oauth.js'; +import {ImplicitOAuthStrategy} from '../auth/oauth-implicit.js'; import {t} from '../i18n/index.js'; /** @@ -39,6 +40,13 @@ export abstract class OAuthCommand extends BaseCommand env: 'SFCC_SHORTCODE', helpGroup: 'AUTH', }), + 'auth-method': Flags.string({ + description: 'Allowed auth methods in priority order (can be specified multiple times)', + env: 'SFCC_AUTH_METHODS', + multiple: true, + options: ALL_AUTH_METHODS, + helpGroup: 'AUTH', + }), }; protected override loadConfiguration(): ResolvedConfig { @@ -51,6 +59,7 @@ export abstract class OAuthCommand extends BaseCommand clientId: this.flags['client-id'], clientSecret: this.flags['client-secret'], shortCode: this.flags['short-code'], + authMethods: this.flags['auth-method'] as AuthMethod[] | undefined, }; const config = loadConfig(flagConfig, options); @@ -64,45 +73,81 @@ export abstract class OAuthCommand extends BaseCommand } /** - * Gets an OAuth auth strategy. + * Gets an OAuth auth strategy based on allowed auth methods and available credentials. + * + * Iterates through allowed methods (in priority order) and returns the first + * strategy for which the required credentials are available. + * + * @throws Error if no allowed method has the required credentials configured */ - protected getOAuthStrategy(): OAuthStrategy { + protected getOAuthStrategy(): OAuthStrategy | ImplicitOAuthStrategy { const config = this.resolvedConfig; + // Default to client-credentials and implicit if no methods specified + const allowedMethods = config.authMethods || (['client-credentials', 'implicit'] as AuthMethod[]); + + for (const method of allowedMethods) { + switch (method) { + case 'client-credentials': + if (config.clientId && config.clientSecret) { + return new OAuthStrategy({ + clientId: config.clientId, + clientSecret: config.clientSecret, + scopes: config.scopes, + }); + } + break; + + case 'implicit': + if (config.clientId) { + return new ImplicitOAuthStrategy({ + clientId: config.clientId, + scopes: config.scopes, + }); + } + break; - if (config.clientId && config.clientSecret) { - return new OAuthStrategy({ - clientId: config.clientId, - clientSecret: config.clientSecret, - scopes: config.scopes, - }); + // 'basic' and 'api-key' are not applicable for OAuth strategies + // They would be handled by different command bases (e.g., InstanceCommand, MRTCommand) + } } + // Build helpful error message based on what methods were allowed + const methodsStr = allowedMethods.join(', '); throw new Error( t( - 'error.oauthCredentialsRequired', - 'OAuth credentials required. Provide --client-id/--client-secret or set SFCC_CLIENT_ID/SFCC_CLIENT_SECRET.', + 'error.noValidAuthMethod', + `No valid auth method available. Allowed methods: [${methodsStr}]. ` + + `Ensure required credentials are configured for at least one method.`, ), ); } /** * Check if OAuth credentials are available. + * Returns true if clientId is configured (with or without clientSecret). */ protected hasOAuthCredentials(): boolean { + const config = this.resolvedConfig; + return Boolean(config.clientId); + } + + /** + * Check if full OAuth credentials (client credentials flow) are available. + * Returns true only if both clientId and clientSecret are configured. + */ + protected hasFullOAuthCredentials(): boolean { const config = this.resolvedConfig; return Boolean(config.clientId && config.clientSecret); } /** * Validates that OAuth credentials are configured, errors if not. + * Only clientId is required (implicit flow can be used without clientSecret). */ protected requireOAuthCredentials(): void { if (!this.hasOAuthCredentials()) { this.error( - t( - 'error.oauthCredentialsRequired', - 'OAuth credentials required. Provide --client-id/--client-secret or set SFCC_CLIENT_ID/SFCC_CLIENT_SECRET.', - ), + t('error.oauthClientIdRequired', 'OAuth client ID required. Provide --client-id or set SFCC_CLIENT_ID.'), ); } } diff --git a/packages/b2c-tooling/src/config/dw-json.ts b/packages/b2c-tooling/src/config/dw-json.ts index ab9410a4..f18d77d9 100644 --- a/packages/b2c-tooling/src/config/dw-json.ts +++ b/packages/b2c-tooling/src/config/dw-json.ts @@ -8,6 +8,7 @@ */ import * as fs from 'node:fs'; import * as path from 'node:path'; +import type {AuthMethod} from '../auth/types.js'; /** * Configuration structure matching dw.json file format. @@ -40,6 +41,8 @@ export interface DwJsonConfig { 'scapi-shortcode'?: string; /** Alternate hostname for WebDAV (if different from main hostname) */ 'webdav-hostname'?: string; + /** Allowed authentication methods in priority order */ + 'auth-methods'?: AuthMethod[]; } /** diff --git a/packages/b2c-tooling/src/index.ts b/packages/b2c-tooling/src/index.ts index d9f26f48..1de95a74 100644 --- a/packages/b2c-tooling/src/index.ts +++ b/packages/b2c-tooling/src/index.ts @@ -14,17 +14,31 @@ export type {TOptions} from './i18n/index.js'; export {loadDwJson, findDwJson} from './config/index.js'; export type {DwJsonConfig, DwJsonMultiConfig, LoadDwJsonOptions} from './config/index.js'; -// Auth Layer - Strategies -export {BasicAuthStrategy, OAuthStrategy, ApiKeyStrategy, decodeJWT} from './auth/index.js'; +// Auth Layer - Strategies and Resolution +export { + BasicAuthStrategy, + OAuthStrategy, + ImplicitOAuthStrategy, + ApiKeyStrategy, + decodeJWT, + resolveAuthStrategy, + checkAvailableAuthMethods, + ALL_AUTH_METHODS, +} from './auth/index.js'; export type { AuthStrategy, AccessTokenResponse, DecodedJWT, OAuthConfig, + ImplicitOAuthConfig, AuthConfig, BasicAuthConfig, OAuthAuthConfig, ApiKeyAuthConfig, + AuthMethod, + AuthCredentials, + ResolveAuthStrategyOptions, + AvailableAuthMethods, } from './auth/index.js'; // Context Layer - Instance diff --git a/packages/b2c-tooling/src/instance/index.ts b/packages/b2c-tooling/src/instance/index.ts index d4606279..07f616fd 100644 --- a/packages/b2c-tooling/src/instance/index.ts +++ b/packages/b2c-tooling/src/instance/index.ts @@ -34,9 +34,9 @@ * * @module instance */ -import type {AuthConfig, AuthStrategy} from '../auth/types.js'; +import type {AuthConfig, AuthStrategy, AuthMethod, AuthCredentials} from '../auth/types.js'; import {BasicAuthStrategy} from '../auth/basic.js'; -import {OAuthStrategy} from '../auth/oauth.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'; @@ -79,6 +79,8 @@ export interface FromDwJsonOptions { clientSecret?: string; /** OAuth scopes */ scopes?: string[]; + /** Allowed auth methods in priority order */ + authMethods?: AuthMethod[]; } /** @@ -140,6 +142,7 @@ export class B2CInstance { const clientId = options.clientId ?? dwConfig?.['client-id']; const clientSecret = options.clientSecret ?? dwConfig?.['client-secret']; const scopes = options.scopes ?? dwConfig?.['oauth-scopes']; + const authMethods = options.authMethods ?? (dwConfig?.['auth-methods'] as AuthMethod[] | undefined); if (!hostname) { throw new Error( @@ -153,7 +156,9 @@ export class B2CInstance { webdavHostname, }; - const auth: AuthConfig = {}; + const auth: AuthConfig = { + authMethods, + }; if (username && password) { auth.basic = {username, password}; @@ -229,35 +234,50 @@ export class B2CInstance { /** * Gets the auth strategy for WebDAV operations. - * Prefers Basic auth, falls back to OAuth. + * Uses authMethods to determine priority, defaulting to basic then OAuth methods. */ private getWebDavAuthStrategy(): AuthStrategy { - if (this.auth.basic) { + // For WebDAV, default priority is basic first, then OAuth methods + const webdavMethods = this.auth.authMethods || (['basic', 'client-credentials', 'implicit'] as AuthMethod[]); + + // If basic auth is allowed and configured, use it directly + if (webdavMethods.includes('basic') && this.auth.basic) { return new BasicAuthStrategy(this.auth.basic.username, this.auth.basic.password); } + // Otherwise try OAuth methods return this.getOAuthStrategy(); } /** - * Gets the OAuth auth strategy. - * @throws Error if OAuth credentials not configured + * Gets the OAuth auth strategy based on allowed methods and available credentials. + * Uses resolveAuthStrategy to automatically select the best OAuth method. + * + * @throws Error if no valid OAuth method is available */ private getOAuthStrategy(): AuthStrategy { if (!this.auth.oauth) { - throw new Error('OAuth credentials required. Provide clientId and clientSecret.'); + throw new Error('OAuth credentials required. Provide at least clientId.'); } - if (!this.auth.oauth.clientSecret) { - throw new Error('OAuth client secret required for non-interactive use.'); - } - - return new OAuthStrategy({ + // Build credentials for resolution + const credentials: AuthCredentials = { clientId: this.auth.oauth.clientId, clientSecret: this.auth.oauth.clientSecret, scopes: this.auth.oauth.scopes, accountManagerHost: this.auth.oauth.accountManagerHost, - }); + }; + + // Filter to only OAuth methods (client-credentials, implicit) + const oauthMethods = (this.auth.authMethods || (['client-credentials', 'implicit'] as AuthMethod[])).filter( + (m): m is 'client-credentials' | 'implicit' => m === 'client-credentials' || m === 'implicit', + ); + + if (oauthMethods.length === 0) { + throw new Error('No OAuth methods allowed. Check authMethods configuration.'); + } + + return resolveAuthStrategy(credentials, {allowedMethods: oauthMethods}); } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b88a85f6..04fb19cb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -117,6 +117,9 @@ importers: i18next: specifier: ^25.6.3 version: 25.6.3(typescript@5.9.3) + open: + specifier: ^11.0.0 + version: 11.0.0 openapi-fetch: specifier: ^0.15.0 version: 0.15.0 @@ -2031,6 +2034,10 @@ packages: builtins@5.1.0: resolution: {integrity: sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==} + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + cacheable-lookup@7.0.0: resolution: {integrity: sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==} engines: {node: '>=14.16'} @@ -2249,6 +2256,14 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.4.0: + resolution: {integrity: sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg==} + engines: {node: '>=18'} + defer-to-connect@2.0.1: resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} engines: {node: '>=10'} @@ -2257,6 +2272,10 @@ packages: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + define-properties@1.2.1: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} @@ -3018,6 +3037,11 @@ packages: engines: {node: '>=8'} hasBin: true + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -3038,6 +3062,15 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-in-ssh@1.0.0: + resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==} + engines: {node: '>=20'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + is-map@2.0.3: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} @@ -3118,6 +3151,10 @@ packages: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} + is-wsl@3.1.0: + resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} + engines: {node: '>=16'} + isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} @@ -3516,6 +3553,10 @@ packages: oniguruma-to-es@3.1.1: resolution: {integrity: sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==} + open@11.0.0: + resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} + engines: {node: '>=20'} + openapi-fetch@0.15.0: resolution: {integrity: sha512-OjQUdi61WO4HYhr9+byCPMj0+bgste/LtSBEcV6FzDdONTs7x0fWn8/ndoYwzqCsKWIxEZwo4FN/TG1c1rI8IQ==} @@ -3664,6 +3705,10 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + powershell-utils@0.1.0: + resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} + engines: {node: '>=20'} + preact@10.27.2: resolution: {integrity: sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==} @@ -3837,6 +3882,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -4385,6 +4434,10 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + wsl-utils@0.3.0: + resolution: {integrity: sha512-3sFIGLiaDP7rTO4xh3g+b3AzhYDIUGGywE/WsmqzJWDxus5aJXVnPTNC/6L+r2WzrwXqVOdD262OaO+cEyPMSQ==} + engines: {node: '>=20'} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -6737,6 +6790,10 @@ snapshots: dependencies: semver: 7.7.3 + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + cacheable-lookup@7.0.0: {} cacheable-request@10.2.14: @@ -6983,6 +7040,13 @@ snapshots: deep-is@0.1.4: {} + default-browser-id@5.0.1: {} + + default-browser@5.4.0: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + defer-to-connect@2.0.1: {} define-data-property@1.1.4: @@ -6991,6 +7055,8 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + define-lazy-prop@3.0.0: {} + define-properties@1.2.1: dependencies: define-data-property: 1.1.4 @@ -7961,6 +8027,8 @@ snapshots: is-docker@2.2.1: {} + is-docker@3.0.0: {} + is-extglob@2.1.1: {} is-finalizationregistry@1.1.1: @@ -7981,6 +8049,12 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-in-ssh@1.0.0: {} + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + is-map@2.0.3: {} is-negative-zero@2.0.3: {} @@ -8047,6 +8121,10 @@ snapshots: dependencies: is-docker: 2.2.1 + is-wsl@3.1.0: + dependencies: + is-inside-container: 1.0.0 + isarray@1.0.0: {} isarray@2.0.5: {} @@ -8396,6 +8474,15 @@ snapshots: regex: 6.0.1 regex-recursion: 6.0.2 + open@11.0.0: + dependencies: + default-browser: 5.4.0 + define-lazy-prop: 3.0.0 + is-in-ssh: 1.0.0 + is-inside-container: 1.0.0 + powershell-utils: 0.1.0 + wsl-utils: 0.3.0 + openapi-fetch@0.15.0: dependencies: openapi-typescript-helpers: 0.0.15 @@ -8568,6 +8655,8 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + powershell-utils@0.1.0: {} + preact@10.27.2: {} prelude-ls@1.2.1: {} @@ -8754,6 +8843,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.53.3 fsevents: 2.3.3 + run-applescript@7.1.0: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -9440,6 +9531,11 @@ snapshots: wrappy@1.0.2: {} + wsl-utils@0.3.0: + dependencies: + is-wsl: 3.1.0 + powershell-utils: 0.1.0 + y18n@5.0.8: {} yaml-ast-parser@0.0.43: {}