diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index 3b9642f2..fecaada8 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -4,9 +4,33 @@ The B2C CLI supports multiple authentication methods and configuration options. ## Authentication Methods -### OAuth (Recommended) +The CLI supports multiple auth methods that can be specified via the `--auth-methods` flag: -OAuth authentication is the recommended method for production use. It uses the Account Manager OAuth flow. +- `client-credentials` - OAuth 2.0 client credentials flow (requires client ID and secret) +- `implicit` - OAuth 2.0 implicit flow (requires client ID only, opens browser for login) +- `basic` - Basic authentication (for WebDAV operations) +- `api-key` - API key authentication + +### Specifying Auth Methods + +You can specify allowed auth methods in priority order using comma-separated values or multiple flags: + +```bash +# Comma-separated (preferred) +b2c code deploy --auth-methods client-credentials,implicit + +# Multiple flags (also supported) +b2c code deploy --auth-methods client-credentials --auth-methods implicit + +# Via environment variable +SFCC_AUTH_METHODS=client-credentials,implicit b2c code deploy +``` + +The CLI will try each method in order until one succeeds. If no methods are specified, the default is `client-credentials,implicit`. + +### OAuth Client Credentials (Recommended) + +OAuth authentication using client credentials is the recommended method for production and CI/CD use. ```bash b2c code deploy \ @@ -15,9 +39,20 @@ b2c code deploy \ --client-secret your-client-secret ``` +### OAuth Implicit Flow + +For development without a client secret, use implicit flow which opens a browser for authentication: + +```bash +b2c code deploy \ + --server your-instance.demandware.net \ + --client-id your-client-id \ + --auth-methods implicit +``` + ### Basic Authentication -For development and testing, you can use basic authentication with Business Manager credentials. +For development and testing, you can use basic authentication with Business Manager credentials: ```bash b2c code deploy \ @@ -32,15 +67,18 @@ For certain operations, you may use an API key. ## Environment Variables -You can also configure authentication using environment variables: +You can configure authentication using environment variables: | Variable | Description | |----------|-------------| -| `B2C_SERVER` | The B2C instance hostname | -| `B2C_CLIENT_ID` | OAuth client ID | -| `B2C_CLIENT_SECRET` | OAuth client secret | -| `B2C_USERNAME` | Basic auth username | -| `B2C_PASSWORD` | Basic auth password | +| `SFCC_SERVER` | The B2C instance hostname | +| `SFCC_CLIENT_ID` | OAuth client ID | +| `SFCC_CLIENT_SECRET` | OAuth client secret | +| `SFCC_USERNAME` | Basic auth username | +| `SFCC_PASSWORD` | Basic auth password | +| `SFCC_AUTH_METHODS` | Comma-separated list of allowed auth methods | +| `SFCC_OAUTH_SCOPES` | OAuth scopes to request | +| `SFCC_CODE_VERSION` | Code version for deployments | ## Configuration File diff --git a/package.json b/package.json index 7a24a3ae..ba7b2bc1 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,11 @@ "description": "Salesforce Commerce Cloud B2C Command Line Tools", "main": "index.js", "scripts": { - "test": "pnpm -r test", "start": "pnpm --filter @salesforce/b2c-cli run dev", + "test": "pnpm -r test", + "format": "pnpm -r run format", + "lint": "pnpm -r run lint", + "build": "pnpm -r run build", "docs:api": "typedoc", "docs:dev": "pnpm run docs:api && vitepress dev docs", "docs:build": "pnpm run docs:api && vitepress build docs", diff --git a/packages/b2c-cli/eslint.config.mjs b/packages/b2c-cli/eslint.config.mjs index 9f5e0c60..11703c9c 100644 --- a/packages/b2c-cli/eslint.config.mjs +++ b/packages/b2c-cli/eslint.config.mjs @@ -26,6 +26,8 @@ export default [ 'no-warning-comments': 'off', // Don't require destructuring 'prefer-destructuring': 'off', + // Disable new-cap - incompatible with openapi-fetch (uses GET, POST, etc. methods) + 'new-cap': 'off', // Allow underscore-prefixed unused variables '@typescript-eslint/no-unused-vars': [ 'error', diff --git a/packages/b2c-cli/src/commands/ods/create.ts b/packages/b2c-cli/src/commands/ods/create.ts new file mode 100644 index 00000000..76826b92 --- /dev/null +++ b/packages/b2c-cli/src/commands/ods/create.ts @@ -0,0 +1,241 @@ +import {Flags, ux} from '@oclif/core'; +import cliui from 'cliui'; +import {OdsCommand} from '@salesforce/b2c-tooling/cli'; +import type {OdsComponents} from '@salesforce/b2c-tooling'; +import {t} from '../../i18n/index.js'; + +type SandboxModel = OdsComponents['schemas']['SandboxModel']; +type SandboxResourceProfile = OdsComponents['schemas']['SandboxResourceProfile']; +type SandboxState = OdsComponents['schemas']['SandboxState']; + +/** States that indicate sandbox creation has completed (success or failure) */ +const TERMINAL_STATES = new Set(['deleted', 'failed', 'started']); + +/** + * Command to create a new on-demand sandbox. + */ +export default class OdsCreate extends OdsCommand { + static description = t('commands.ods.create.description', 'Create a new on-demand sandbox'); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %> --realm abcd', + '<%= config.bin %> <%= command.id %> --realm abcd --ttl 48', + '<%= config.bin %> <%= command.id %> --realm abcd --profile large', + '<%= config.bin %> <%= command.id %> --realm abcd --auto-scheduled', + '<%= config.bin %> <%= command.id %> --realm abcd --wait', + '<%= config.bin %> <%= command.id %> --realm abcd --wait --poll-interval 15', + '<%= config.bin %> <%= command.id %> --realm abcd --json', + ]; + + static flags = { + realm: Flags.string({ + char: 'r', + description: 'Realm ID (four-letter ID)', + required: true, + }), + ttl: Flags.integer({ + description: 'Time to live in hours (0 for infinite)', + default: 24, + }), + profile: Flags.string({ + description: 'Resource profile (medium, large, xlarge, xxlarge)', + default: 'medium', + options: ['medium', 'large', 'xlarge', 'xxlarge'], + }), + 'auto-scheduled': Flags.boolean({ + description: 'Enable automatic start/stop scheduling', + default: false, + }), + wait: Flags.boolean({ + char: 'w', + description: 'Wait for the sandbox to reach started or failed state before returning', + default: false, + }), + 'poll-interval': Flags.integer({ + description: 'Polling interval in seconds when using --wait', + default: 10, + dependsOn: ['wait'], + }), + timeout: Flags.integer({ + description: 'Maximum time to wait in seconds when using --wait (0 for no timeout)', + default: 600, + dependsOn: ['wait'], + }), + }; + + async run(): Promise { + const realm = this.flags.realm; + const profile = this.flags.profile as SandboxResourceProfile; + const ttl = this.flags.ttl; + const autoScheduled = this.flags['auto-scheduled']; + const wait = this.flags.wait; + const pollInterval = this.flags['poll-interval']; + const timeout = this.flags.timeout; + + this.log(t('commands.ods.create.creating', 'Creating sandbox in realm {{realm}}...', {realm})); + this.log(t('commands.ods.create.profile', 'Profile: {{profile}}', {profile})); + this.log(t('commands.ods.create.ttl', 'TTL: {{ttl}} hours', {ttl: ttl === 0 ? 'infinite' : String(ttl)})); + + const result = await this.odsClient.POST('/sandboxes', { + body: { + realm, + ttl, + resourceProfile: profile, + autoScheduled, + analyticsEnabled: false, + }, + }); + + if (!result.data?.data) { + const errorResponse = result.error as OdsComponents['schemas']['ErrorResponse'] | undefined; + const errorMessage = errorResponse?.error?.message || result.response?.statusText || 'Unknown error'; + this.error( + t('commands.ods.create.error', 'Failed to create sandbox: {{message}}', { + message: errorMessage, + }), + ); + } + + let sandbox = result.data.data; + + this.log(''); + this.log(t('commands.ods.create.success', 'Sandbox created successfully!')); + + if (wait && sandbox.id) { + this.log(''); + sandbox = await this.waitForSandbox(sandbox.id, pollInterval, timeout); + } + + if (this.jsonEnabled()) { + return sandbox; + } + + this.printSandboxSummary(sandbox); + + return sandbox; + } + + private printSandboxSummary(sandbox: SandboxModel): void { + const ui = cliui({width: process.stdout.columns || 80}); + + ui.div({text: '', padding: [0, 0, 0, 0]}); + + const fields: [string, string | undefined][] = [ + ['ID', sandbox.id], + ['Realm', sandbox.realm], + ['Instance', sandbox.instance], + ['State', sandbox.state], + ['Profile', sandbox.resourceProfile], + ['Hostname', sandbox.hostName], + ]; + + for (const [label, value] of fields) { + if (value !== undefined) { + ui.div({text: `${label}:`, width: 15, padding: [0, 2, 0, 0]}, {text: value, padding: [0, 0, 0, 0]}); + } + } + + if (sandbox.links?.bm) { + ui.div({text: '', padding: [0, 0, 0, 0]}); + ui.div({text: 'BM URL:', width: 15, padding: [0, 2, 0, 0]}, {text: sandbox.links.bm, padding: [0, 0, 0, 0]}); + } + + ux.stdout(ui.toString()); + } + + /** + * Sleep for a given number of milliseconds. + */ + private async sleep(ms: number): Promise { + await new Promise((resolve) => { + setTimeout(resolve, ms); + }); + } + + /** + * Polls for sandbox status until it reaches a terminal state. + * @param sandboxId - The sandbox ID to poll + * @param pollIntervalSeconds - Interval between polls in seconds + * @param timeoutSeconds - Maximum time to wait (0 for no timeout) + * @returns The final sandbox state + */ + private async waitForSandbox( + sandboxId: string, + pollIntervalSeconds: number, + timeoutSeconds: number, + ): Promise { + const startTime = Date.now(); + const pollIntervalMs = pollIntervalSeconds * 1000; + const timeoutMs = timeoutSeconds * 1000; + let lastState: SandboxState | undefined; + + this.log(t('commands.ods.create.waiting', 'Waiting for sandbox to be ready...')); + + while (true) { + // Check for timeout + if (timeoutSeconds > 0 && Date.now() - startTime > timeoutMs) { + this.error( + t('commands.ods.create.timeout', 'Timeout waiting for sandbox after {{seconds}} seconds', { + seconds: String(timeoutSeconds), + }), + ); + } + + // eslint-disable-next-line no-await-in-loop + const result = await this.odsClient.GET('/sandboxes/{sandboxId}', { + params: { + path: {sandboxId}, + }, + }); + + if (!result.data?.data) { + this.error( + t('commands.ods.create.pollError', 'Failed to fetch sandbox status: {{message}}', { + message: result.response?.statusText || 'Unknown error', + }), + ); + } + + const sandbox = result.data.data; + const currentState = sandbox.state as SandboxState; + + // Log state changes + if (currentState !== lastState) { + const elapsed = Math.round((Date.now() - startTime) / 1000); + this.log( + t('commands.ods.create.stateChange', '[{{elapsed}}s] State: {{state}}', { + elapsed: String(elapsed), + state: currentState || 'unknown', + }), + ); + lastState = currentState; + } + + // Check for terminal states + if (currentState && TERMINAL_STATES.has(currentState)) { + switch (currentState) { + case 'deleted': { + this.error(t('commands.ods.create.deleted', 'Sandbox was deleted')); + break; + } + case 'failed': { + this.error(t('commands.ods.create.failed', 'Sandbox creation failed')); + break; + } + case 'started': { + this.log(''); + this.log(t('commands.ods.create.ready', 'Sandbox is now ready!')); + break; + } + } + return sandbox; + } + + // Wait before next poll + // eslint-disable-next-line no-await-in-loop + await this.sleep(pollIntervalMs); + } + } +} diff --git a/packages/b2c-cli/src/commands/ods/delete.ts b/packages/b2c-cli/src/commands/ods/delete.ts new file mode 100644 index 00000000..3b3e438e --- /dev/null +++ b/packages/b2c-cli/src/commands/ods/delete.ts @@ -0,0 +1,101 @@ +import * as readline from 'node:readline'; +import {Args, Flags} from '@oclif/core'; +import {OdsCommand} from '@salesforce/b2c-tooling/cli'; +import type {OdsComponents} from '@salesforce/b2c-tooling'; +import {t} from '../../i18n/index.js'; + +/** + * Simple confirmation prompt. + */ +async function confirm(message: string): Promise { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stderr, + }); + + return new Promise((resolve) => { + rl.question(`${message} `, (answer) => { + rl.close(); + resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes'); + }); + }); +} + +/** + * Command to delete an on-demand sandbox. + */ +export default class OdsDelete extends OdsCommand { + static args = { + sandboxId: Args.string({ + description: 'Sandbox ID (UUID)', + required: true, + }), + }; + + static description = t('commands.ods.delete.description', 'Delete an on-demand sandbox'); + + static examples = [ + '<%= config.bin %> <%= command.id %> abc12345-1234-1234-1234-abc123456789', + '<%= config.bin %> <%= command.id %> abc12345-1234-1234-1234-abc123456789 --force', + ]; + + static flags = { + force: Flags.boolean({ + char: 'f', + description: 'Skip confirmation prompt', + default: false, + }), + }; + + async run(): Promise { + const sandboxId = this.args.sandboxId; + + // Get sandbox details first to show in confirmation + const getResult = await this.odsClient.GET('/sandboxes/{sandboxId}', { + params: { + path: {sandboxId}, + }, + }); + + if (!getResult.data?.data) { + this.error(t('commands.ods.delete.notFound', 'Sandbox not found: {{sandboxId}}', {sandboxId})); + } + + const sandbox = getResult.data.data; + const sandboxInfo = `${sandbox.realm}/${sandbox.instance || sandboxId}`; + + // Confirm deletion unless --force is used + if (!this.flags.force) { + const confirmed = await confirm( + t('commands.ods.delete.confirm', 'Are you sure you want to delete sandbox "{{sandboxInfo}}"? (y/n)', { + sandboxInfo, + }), + ); + + if (!confirmed) { + this.log(t('commands.ods.delete.cancelled', 'Deletion cancelled')); + return; + } + } + + this.log(t('commands.ods.delete.deleting', 'Deleting sandbox {{sandboxInfo}}...', {sandboxInfo})); + + const result = await this.odsClient.DELETE('/sandboxes/{sandboxId}', { + params: { + path: {sandboxId}, + }, + }); + + if (result.response.status !== 202) { + const errorResponse = result.error as OdsComponents['schemas']['ErrorResponse'] | undefined; + const errorMessage = errorResponse?.error?.message || result.response?.statusText || 'Unknown error'; + this.error( + t('commands.ods.delete.error', 'Failed to delete sandbox: {{message}}', { + message: errorMessage, + }), + ); + } + + this.log(t('commands.ods.delete.success', 'Sandbox deletion initiated. The sandbox will be removed shortly.')); + } +} diff --git a/packages/b2c-cli/src/commands/ods/get.ts b/packages/b2c-cli/src/commands/ods/get.ts new file mode 100644 index 00000000..979a5a40 --- /dev/null +++ b/packages/b2c-cli/src/commands/ods/get.ts @@ -0,0 +1,123 @@ +import {Args, ux} from '@oclif/core'; +import cliui from 'cliui'; +import {OdsCommand} from '@salesforce/b2c-tooling/cli'; +import type {OdsComponents} from '@salesforce/b2c-tooling'; +import {t} from '../../i18n/index.js'; + +type SandboxModel = OdsComponents['schemas']['SandboxModel']; + +/** + * Command to get details of a specific sandbox. + */ +export default class OdsGet extends OdsCommand { + static args = { + sandboxId: Args.string({ + description: 'Sandbox ID (UUID)', + required: true, + }), + }; + + static description = t('commands.ods.get.description', 'Get details of a specific sandbox'); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %> abc12345-1234-1234-1234-abc123456789', + '<%= config.bin %> <%= command.id %> abc12345-1234-1234-1234-abc123456789 --json', + ]; + + async run(): Promise { + const sandboxId = this.args.sandboxId; + + this.log(t('commands.ods.get.fetching', 'Fetching sandbox {{sandboxId}}...', {sandboxId})); + + const result = await this.odsClient.GET('/sandboxes/{sandboxId}', { + params: { + path: {sandboxId}, + }, + }); + + if (!result.data?.data) { + this.error( + t('commands.ods.get.error', 'Failed to fetch sandbox: {{message}}', { + message: result.response?.statusText || 'Sandbox not found', + }), + ); + } + + const sandbox = result.data.data; + + if (this.jsonEnabled()) { + return sandbox; + } + + this.printSandboxDetails(sandbox); + + return sandbox; + } + + private printSandboxDetails(sandbox: SandboxModel): void { + const ui = cliui({width: process.stdout.columns || 80}); + + ui.div({text: 'Sandbox Details', padding: [1, 0, 0, 0]}); + ui.div({text: '─'.repeat(50), padding: [0, 0, 0, 0]}); + + const fields: [string, string | undefined][] = [ + ['ID', sandbox.id], + ['Realm', sandbox.realm], + ['Instance', sandbox.instance], + ['State', sandbox.state], + ['Resource Profile', sandbox.resourceProfile], + ['Enabled', sandbox.enabled?.toString()], + ['Auto Scheduled', sandbox.autoScheduled?.toString()], + ['Hostname', sandbox.hostName], + ['Created At', sandbox.createdAt ? new Date(sandbox.createdAt).toLocaleString() : undefined], + ['Created By', sandbox.createdBy], + ['EOL', sandbox.eol ? new Date(sandbox.eol).toLocaleString() : undefined], + ['App Version', sandbox.versions?.app], + ['Web Version', sandbox.versions?.web], + ]; + + for (const [label, value] of fields) { + if (value !== undefined) { + ui.div({text: `${label}:`, width: 20, padding: [0, 2, 0, 0]}, {text: value, padding: [0, 0, 0, 0]}); + } + } + + // Tags + if (sandbox.tags && sandbox.tags.length > 0) { + ui.div({text: 'Tags:', width: 20, padding: [0, 2, 0, 0]}, {text: sandbox.tags.join(', '), padding: [0, 0, 0, 0]}); + } + + // Emails + if (sandbox.emails && sandbox.emails.length > 0) { + ui.div( + {text: 'Emails:', width: 20, padding: [0, 2, 0, 0]}, + {text: sandbox.emails.join(', '), padding: [0, 0, 0, 0]}, + ); + } + + // Links + if (sandbox.links) { + ui.div({text: '', padding: [0, 0, 0, 0]}); + ui.div({text: 'Links', padding: [1, 0, 0, 0]}); + ui.div({text: '─'.repeat(50), padding: [0, 0, 0, 0]}); + + const links: [string, string | undefined][] = [ + ['Business Manager', sandbox.links.bm], + ['OCAPI', sandbox.links.ocapi], + ['Impex', sandbox.links.impex], + ['Code', sandbox.links.code], + ['Logs', sandbox.links.logs], + ]; + + for (const [label, value] of links) { + if (value) { + ui.div({text: `${label}:`, width: 20, padding: [0, 2, 0, 0]}, {text: value, padding: [0, 0, 0, 0]}); + } + } + } + + ux.stdout(ui.toString()); + } +} diff --git a/packages/b2c-cli/src/commands/ods/info.ts b/packages/b2c-cli/src/commands/ods/info.ts new file mode 100644 index 00000000..665b918e --- /dev/null +++ b/packages/b2c-cli/src/commands/ods/info.ts @@ -0,0 +1,161 @@ +import {ux} from '@oclif/core'; +import cliui from 'cliui'; +import {OdsCommand} from '@salesforce/b2c-tooling/cli'; +import type {OdsComponents} from '@salesforce/b2c-tooling'; +import {t} from '../../i18n/index.js'; + +type UserInfoSpec = OdsComponents['schemas']['UserInfoSpec']; +type SystemInfoSpec = OdsComponents['schemas']['SystemInfoSpec']; + +/** + * Combined response type for the info command. + */ +interface OdsInfoResponse { + user: undefined | UserInfoSpec; + system: SystemInfoSpec | undefined; +} + +/** + * Command to display ODS user and system information. + * Combines data from getUserInfo and getSystemInfo API endpoints. + */ +export default class OdsInfo extends OdsCommand { + static description = t('commands.ods.info.description', 'Display ODS user and system information'); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %>', + '<%= config.bin %> <%= command.id %> --json', + '<%= config.bin %> <%= command.id %> --host admin.eu01.dx.commercecloud.salesforce.com', + ]; + + async run(): Promise { + const host = this.odsHost; + + this.log(t('commands.ods.info.fetching', 'Fetching ODS information from {{host}}...', {host})); + + // Fetch user info and system info in parallel + const [userResult, systemResult] = await Promise.all([ + this.odsClient.GET('/me', {}), + this.odsClient.GET('/system', {}), + ]); + + if (!userResult.data) { + this.error( + t('commands.ods.info.userError', 'Failed to fetch user info: {{message}}', { + message: userResult.response?.statusText || 'Unknown error', + }), + ); + } + + if (!systemResult.data) { + this.error( + t('commands.ods.info.systemError', 'Failed to fetch system info: {{message}}', { + message: systemResult.response?.statusText || 'Unknown error', + }), + ); + } + + const response: OdsInfoResponse = { + user: userResult.data.data, + system: systemResult.data.data, + }; + + // In JSON mode, just return the data + if (this.jsonEnabled()) { + return response; + } + + // Human-readable output + this.printInfo(response); + + return response; + } + + private printInfo(info: OdsInfoResponse): void { + const ui = cliui({width: process.stdout.columns || 80}); + + // User Info Section + ui.div({text: 'User Information', padding: [1, 0, 0, 0]}); + ui.div({text: '─'.repeat(40), padding: [0, 0, 0, 0]}); + + if (info.user?.user) { + ui.div( + {text: 'Name:', width: 20, padding: [0, 2, 0, 0]}, + {text: info.user.user.name || '-', padding: [0, 0, 0, 0]}, + ); + ui.div( + {text: 'Email:', width: 20, padding: [0, 2, 0, 0]}, + {text: info.user.user.email || '-', padding: [0, 0, 0, 0]}, + ); + ui.div( + {text: 'User ID:', width: 20, padding: [0, 2, 0, 0]}, + {text: info.user.user.id || '-', padding: [0, 0, 0, 0]}, + ); + } + + if (info.user?.client) { + ui.div( + {text: 'Client ID:', width: 20, padding: [0, 2, 0, 0]}, + {text: info.user.client.id || '-', padding: [0, 0, 0, 0]}, + ); + } + + if (info.user?.roles && info.user.roles.length > 0) { + ui.div( + {text: 'Roles:', width: 20, padding: [0, 2, 0, 0]}, + {text: info.user.roles.join(', '), padding: [0, 0, 0, 0]}, + ); + } + + if (info.user?.realms && info.user.realms.length > 0) { + ui.div( + {text: 'Realms:', width: 20, padding: [0, 2, 0, 0]}, + {text: info.user.realms.join(', '), padding: [0, 0, 0, 0]}, + ); + } + + if (info.user?.sandboxes && info.user.sandboxes.length > 0) { + ui.div( + {text: 'Sandboxes:', width: 20, padding: [0, 2, 0, 0]}, + {text: info.user.sandboxes.length.toString(), padding: [0, 0, 0, 0]}, + ); + } + + // System Info Section + ui.div({text: '', padding: [0, 0, 0, 0]}); + ui.div({text: 'System Information', padding: [1, 0, 0, 0]}); + ui.div({text: '─'.repeat(40), padding: [0, 0, 0, 0]}); + + if (info.system?.region) { + ui.div({text: 'Region:', width: 20, padding: [0, 2, 0, 0]}, {text: info.system.region, padding: [0, 0, 0, 0]}); + } + + if (info.system?.inboundIps && info.system.inboundIps.length > 0) { + ui.div( + {text: 'Inbound IPs:', width: 20, padding: [0, 2, 0, 0]}, + {text: info.system.inboundIps.join(', '), padding: [0, 0, 0, 0]}, + ); + } + + if (info.system?.outboundIps && info.system.outboundIps.length > 0) { + ui.div( + {text: 'Outbound IPs:', width: 20, padding: [0, 2, 0, 0]}, + {text: info.system.outboundIps.join(', '), padding: [0, 0, 0, 0]}, + ); + } + + if (info.system?.sandboxIps && info.system.sandboxIps.length > 0) { + ui.div( + {text: 'Sandbox IPs:', width: 20, padding: [0, 2, 0, 0]}, + { + text: info.system.sandboxIps.slice(0, 5).join(', ') + (info.system.sandboxIps.length > 5 ? '...' : ''), + padding: [0, 0, 0, 0], + }, + ); + } + + ux.stdout(ui.toString()); + } +} diff --git a/packages/b2c-cli/src/commands/ods/list.ts b/packages/b2c-cli/src/commands/ods/list.ts new file mode 100644 index 00000000..2c794fd3 --- /dev/null +++ b/packages/b2c-cli/src/commands/ods/list.ts @@ -0,0 +1,275 @@ +import {Flags, ux} from '@oclif/core'; +import cliui from 'cliui'; +import {OdsCommand} from '@salesforce/b2c-tooling/cli'; +import type {OdsComponents} from '@salesforce/b2c-tooling'; +import {t} from '../../i18n/index.js'; + +type SandboxModel = OdsComponents['schemas']['SandboxModel']; + +/** + * Response type for the list command. + */ +interface OdsListResponse { + count: number; + data: SandboxModel[]; +} + +/** + * Column definition for table output. + */ +interface ColumnDef { + /** Column header label */ + header: string; + /** Minimum width in characters */ + minWidth?: number; + /** Function to extract value from sandbox */ + get: (s: SandboxModel) => string; + /** Whether this column is only shown with --extended */ + extended?: boolean; +} + +/** + * Available columns for sandbox list output. + */ +const COLUMNS: Record = { + realm: { + header: 'Realm', + get: (s) => s.realm || '-', + }, + instance: { + header: 'Num', + get: (s) => s.instance || '-', + }, + state: { + header: 'State', + get: (s) => s.state || '-', + }, + profile: { + header: 'Profile', + get: (s) => s.resourceProfile || '-', + }, + created: { + header: 'Created', + get: (s) => (s.createdAt ? new Date(s.createdAt).toISOString().slice(0, 10) : '-'), + }, + eol: { + header: 'EOL', + get: (s) => (s.eol ? new Date(s.eol).toISOString().slice(0, 10) : '-'), + }, + id: { + header: 'ID', + get: (s) => s.id || '-', + }, + hostname: { + header: 'Hostname', + get: (s) => s.hostName || '-', + extended: true, + }, + createdBy: { + header: 'Created By', + get: (s) => s.createdBy || '-', + extended: true, + }, + autoScheduled: { + header: 'Auto', + get: (s) => (s.autoScheduled ? 'Yes' : 'No'), + extended: true, + }, +}; + +/** Default columns shown without --extended */ +const DEFAULT_COLUMNS = ['realm', 'instance', 'state', 'profile', 'created', 'eol', 'id']; + +/** + * Command to list all on-demand sandboxes. + */ +export default class OdsList extends OdsCommand { + static description = t('commands.ods.list.description', 'List all on-demand sandboxes'); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %>', + '<%= config.bin %> <%= command.id %> --realm abcd', + '<%= config.bin %> <%= command.id %> --filter-params "realm=abcd&state=started"', + '<%= config.bin %> <%= command.id %> --show-deleted', + '<%= config.bin %> <%= command.id %> --extended', + '<%= config.bin %> <%= command.id %> --columns realm,instance,state,hostname', + '<%= config.bin %> <%= command.id %> --json', + ]; + + static flags = { + realm: Flags.string({ + char: 'r', + description: 'Filter by realm ID (four-letter ID)', + }), + 'filter-params': Flags.string({ + description: 'Raw filter parameters (e.g., "realm=abcd&state=started&resourceProfile=medium")', + }), + 'show-deleted': Flags.boolean({ + description: 'Include deleted sandboxes in the list', + default: false, + }), + columns: Flags.string({ + char: 'c', + description: `Columns to display (comma-separated). Available: ${Object.keys(COLUMNS).join(', ')}`, + }), + extended: Flags.boolean({ + char: 'x', + description: 'Show all columns including extended fields', + default: false, + }), + }; + + async run(): Promise { + const host = this.odsHost; + const includeDeleted = this.flags['show-deleted']; + const realm = this.flags.realm; + const rawFilterParams = this.flags['filter-params']; + + // Build filter params string + let filterParams: string | undefined; + if (realm || rawFilterParams) { + const parts: string[] = []; + if (realm) { + parts.push(`realm=${realm}`); + } + if (rawFilterParams) { + parts.push(rawFilterParams); + } + filterParams = parts.join('&'); + } + + this.log(t('commands.ods.list.fetching', 'Fetching sandboxes from {{host}}...', {host})); + + const result = await this.odsClient.GET('/sandboxes', { + params: { + query: { + // eslint-disable-next-line camelcase + include_deleted: includeDeleted, + // eslint-disable-next-line camelcase + filter_params: filterParams, + }, + }, + }); + + if (!result.data?.data) { + this.error( + t('commands.ods.list.error', 'Failed to fetch sandboxes: {{message}}', { + message: result.response?.statusText || 'Unknown error', + }), + ); + } + + const sandboxes = result.data.data; + const response: OdsListResponse = { + count: sandboxes.length, + data: sandboxes, + }; + + if (this.jsonEnabled()) { + return response; + } + + if (sandboxes.length === 0) { + this.log(t('commands.ods.list.noSandboxes', 'No sandboxes found.')); + return response; + } + + this.printSandboxesTable(sandboxes, this.getSelectedColumns()); + + return response; + } + + /** + * Calculate dynamic column widths based on content. + * Each column width = max(header length, max data length) + padding + */ + private calculateColumnWidths(sandboxes: SandboxModel[], columnKeys: string[]): Map { + const widths = new Map(); + const padding = 2; // Space between columns + + for (const key of columnKeys) { + const col = COLUMNS[key]; + // Start with header length + let maxWidth = col.header.length; + + // Check all data values + for (const sandbox of sandboxes) { + const value = col.get(sandbox); + maxWidth = Math.max(maxWidth, value.length); + } + + // Apply minimum width if specified, add padding + const minWidth = col.minWidth || 0; + widths.set(key, Math.max(maxWidth, minWidth) + padding); + } + + return widths; + } + + /** + * Determines which columns to display based on flags. + */ + private getSelectedColumns(): string[] { + const columnsFlag = this.flags.columns; + const extended = this.flags.extended; + + if (columnsFlag) { + // User specified explicit columns + const requested = columnsFlag.split(',').map((c) => c.trim()); + const valid = requested.filter((c) => c in COLUMNS); + if (valid.length === 0) { + this.warn(`No valid columns specified. Available: ${Object.keys(COLUMNS).join(', ')}`); + return DEFAULT_COLUMNS; + } + return valid; + } + + if (extended) { + // Show all columns + return Object.keys(COLUMNS); + } + + // Default columns (non-extended) + return DEFAULT_COLUMNS; + } + + private printSandboxesTable(sandboxes: SandboxModel[], columnKeys: string[]): void { + const termWidth = process.stdout.columns || 120; + const ui = cliui({width: termWidth}); + + // Calculate dynamic widths based on content + const widths = this.calculateColumnWidths(sandboxes, columnKeys); + + // Build header row + const headerCols = columnKeys.map((key) => { + const col = COLUMNS[key]; + return { + text: col.header, + width: widths.get(key), + padding: [0, 1, 0, 0] as [number, number, number, number], + }; + }); + ui.div(...headerCols); + + // Separator + const totalWidth = [...widths.values()].reduce((sum, w) => sum + w, 0); + ui.div({text: '─'.repeat(Math.min(totalWidth, termWidth)), padding: [0, 0, 0, 0]}); + + // Rows + for (const sandbox of sandboxes) { + const rowCols = columnKeys.map((key) => { + const col = COLUMNS[key]; + return { + text: col.get(sandbox), + width: widths.get(key), + padding: [0, 1, 0, 0] as [number, number, number, number], + }; + }); + ui.div(...rowCols); + } + + ux.stdout(ui.toString()); + } +} diff --git a/packages/b2c-cli/src/commands/ods/restart.ts b/packages/b2c-cli/src/commands/ods/restart.ts new file mode 100644 index 00000000..8aa6c8c2 --- /dev/null +++ b/packages/b2c-cli/src/commands/ods/restart.ts @@ -0,0 +1,63 @@ +import {Args} from '@oclif/core'; +import {OdsCommand} from '@salesforce/b2c-tooling/cli'; +import type {OdsComponents} from '@salesforce/b2c-tooling'; +import {t} from '../../i18n/index.js'; + +type SandboxOperationModel = OdsComponents['schemas']['SandboxOperationModel']; + +/** + * Command to restart an on-demand sandbox. + */ +export default class OdsRestart extends OdsCommand { + static args = { + sandboxId: Args.string({ + description: 'Sandbox ID (UUID)', + required: true, + }), + }; + + static description = t('commands.ods.restart.description', 'Restart an on-demand sandbox'); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %> abc12345-1234-1234-1234-abc123456789', + '<%= config.bin %> <%= command.id %> abc12345-1234-1234-1234-abc123456789 --json', + ]; + + async run(): Promise { + const sandboxId = this.args.sandboxId; + + this.log(t('commands.ods.restart.restarting', 'Restarting sandbox {{sandboxId}}...', {sandboxId})); + + const result = await this.odsClient.POST('/sandboxes/{sandboxId}/operations', { + params: { + path: {sandboxId}, + }, + body: { + operation: 'restart', + }, + }); + + if (!result.data?.data) { + const errorResponse = result.error as OdsComponents['schemas']['ErrorResponse'] | undefined; + const errorMessage = errorResponse?.error?.message || result.response?.statusText || 'Unknown error'; + this.error( + t('commands.ods.restart.error', 'Failed to restart sandbox: {{message}}', { + message: errorMessage, + }), + ); + } + + const operation = result.data.data; + + this.log( + t('commands.ods.restart.success', 'Restart operation {{operationState}}. Sandbox state: {{sandboxState}}', { + operationState: operation.operationState, + sandboxState: operation.sandboxState || 'unknown', + }), + ); + + return operation; + } +} diff --git a/packages/b2c-cli/src/commands/ods/start.ts b/packages/b2c-cli/src/commands/ods/start.ts new file mode 100644 index 00000000..59f85d72 --- /dev/null +++ b/packages/b2c-cli/src/commands/ods/start.ts @@ -0,0 +1,63 @@ +import {Args} from '@oclif/core'; +import {OdsCommand} from '@salesforce/b2c-tooling/cli'; +import type {OdsComponents} from '@salesforce/b2c-tooling'; +import {t} from '../../i18n/index.js'; + +type SandboxOperationModel = OdsComponents['schemas']['SandboxOperationModel']; + +/** + * Command to start an on-demand sandbox. + */ +export default class OdsStart extends OdsCommand { + static args = { + sandboxId: Args.string({ + description: 'Sandbox ID (UUID)', + required: true, + }), + }; + + static description = t('commands.ods.start.description', 'Start an on-demand sandbox'); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %> abc12345-1234-1234-1234-abc123456789', + '<%= config.bin %> <%= command.id %> abc12345-1234-1234-1234-abc123456789 --json', + ]; + + async run(): Promise { + const sandboxId = this.args.sandboxId; + + this.log(t('commands.ods.start.starting', 'Starting sandbox {{sandboxId}}...', {sandboxId})); + + const result = await this.odsClient.POST('/sandboxes/{sandboxId}/operations', { + params: { + path: {sandboxId}, + }, + body: { + operation: 'start', + }, + }); + + if (!result.data?.data) { + const errorResponse = result.error as OdsComponents['schemas']['ErrorResponse'] | undefined; + const errorMessage = errorResponse?.error?.message || result.response?.statusText || 'Unknown error'; + this.error( + t('commands.ods.start.error', 'Failed to start sandbox: {{message}}', { + message: errorMessage, + }), + ); + } + + const operation = result.data.data; + + this.log( + t('commands.ods.start.success', 'Start operation {{operationState}}. Sandbox state: {{sandboxState}}', { + operationState: operation.operationState, + sandboxState: operation.sandboxState || 'unknown', + }), + ); + + return operation; + } +} diff --git a/packages/b2c-cli/src/commands/ods/stop.ts b/packages/b2c-cli/src/commands/ods/stop.ts new file mode 100644 index 00000000..35051cd3 --- /dev/null +++ b/packages/b2c-cli/src/commands/ods/stop.ts @@ -0,0 +1,63 @@ +import {Args} from '@oclif/core'; +import {OdsCommand} from '@salesforce/b2c-tooling/cli'; +import type {OdsComponents} from '@salesforce/b2c-tooling'; +import {t} from '../../i18n/index.js'; + +type SandboxOperationModel = OdsComponents['schemas']['SandboxOperationModel']; + +/** + * Command to stop an on-demand sandbox. + */ +export default class OdsStop extends OdsCommand { + static args = { + sandboxId: Args.string({ + description: 'Sandbox ID (UUID)', + required: true, + }), + }; + + static description = t('commands.ods.stop.description', 'Stop an on-demand sandbox'); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %> abc12345-1234-1234-1234-abc123456789', + '<%= config.bin %> <%= command.id %> abc12345-1234-1234-1234-abc123456789 --json', + ]; + + async run(): Promise { + const sandboxId = this.args.sandboxId; + + this.log(t('commands.ods.stop.stopping', 'Stopping sandbox {{sandboxId}}...', {sandboxId})); + + const result = await this.odsClient.POST('/sandboxes/{sandboxId}/operations', { + params: { + path: {sandboxId}, + }, + body: { + operation: 'stop', + }, + }); + + if (!result.data?.data) { + const errorResponse = result.error as OdsComponents['schemas']['ErrorResponse'] | undefined; + const errorMessage = errorResponse?.error?.message || result.response?.statusText || 'Unknown error'; + this.error( + t('commands.ods.stop.error', 'Failed to stop sandbox: {{message}}', { + message: errorMessage, + }), + ); + } + + const operation = result.data.data; + + this.log( + t('commands.ods.stop.success', 'Stop operation {{operationState}}. Sandbox state: {{sandboxState}}', { + operationState: operation.operationState, + sandboxState: operation.sandboxState || 'unknown', + }), + ); + + return operation; + } +} diff --git a/packages/b2c-cli/src/commands/sandbox/create.ts b/packages/b2c-cli/src/commands/sandbox/create.ts deleted file mode 100644 index a9c33420..00000000 --- a/packages/b2c-cli/src/commands/sandbox/create.ts +++ /dev/null @@ -1,55 +0,0 @@ -import {Args, Flags} from '@oclif/core'; -import {OAuthCommand} from '@salesforce/b2c-tooling/cli'; -import {t} from '../../i18n/index.js'; - -/** - * Stub command demonstrating OAuthCommand usage. - * Sandbox API operations require OAuth but not instance credentials. - */ -export default class SandboxCreate extends OAuthCommand { - static args = { - realm: Args.string({ - description: 'Realm ID', - required: true, - }), - }; - - static description = t('commands.sandbox.create.description', 'Create a new on-demand sandbox'); - - static examples = [ - '<%= config.bin %> <%= command.id %> abcd --ttl 24', - '<%= config.bin %> <%= command.id %> abcd --profile medium', - ]; - - static flags = { - ttl: Flags.integer({ - description: 'Time to live in hours', - default: 24, - }), - profile: Flags.string({ - description: 'Sandbox profile (small, medium, large)', - default: 'medium', - }), - }; - - async run(): Promise { - this.requireOAuthCredentials(); - - const realm = this.args.realm; - const profile = this.flags.profile; - const ttl = this.flags.ttl; - const clientId = this.resolvedConfig.clientId; - - this.log(t('commands.sandbox.create.creating', 'Creating sandbox in realm {{realm}}...', {realm})); - this.log(t('commands.sandbox.create.profile', 'Profile: {{profile}}', {profile})); - this.log(t('commands.sandbox.create.ttl', 'TTL: {{ttl}} hours', {ttl})); - - // TODO: Implement actual ODS API call using this.getOAuthStrategy() - - this.log(''); - this.log(t('commands.sandbox.create.stub', '(stub) Sandbox creation not yet implemented')); - this.log( - t('commands.sandbox.create.wouldCreate', 'Would create sandbox with OAuth client: {{clientId}}', {clientId}), - ); - } -} diff --git a/packages/b2c-cli/src/commands/sites/list.ts b/packages/b2c-cli/src/commands/sites/list.ts index 18a7e676..1c22190c 100644 --- a/packages/b2c-cli/src/commands/sites/list.ts +++ b/packages/b2c-cli/src/commands/sites/list.ts @@ -25,7 +25,6 @@ export default class SitesList extends InstanceCommand { this.log(t('commands.sites.list.fetching', 'Fetching sites from {{hostname}}...', {hostname})); - // eslint-disable-next-line new-cap const {data, error} = await this.instance.ocapi.GET('/sites', { params: {query: {select: '(**)'}}, }); diff --git a/packages/b2c-cli/src/commands/slas/client/create.ts b/packages/b2c-cli/src/commands/slas/client/create.ts index 489e2dc8..04ff621b 100644 --- a/packages/b2c-cli/src/commands/slas/client/create.ts +++ b/packages/b2c-cli/src/commands/slas/client/create.ts @@ -93,7 +93,6 @@ export default class SlasClientCreate extends SlasClientCommand dist/cjs/package.json", diff --git a/packages/b2c-tooling/specs/ods-api-v1.json b/packages/b2c-tooling/specs/ods-api-v1.json new file mode 100644 index 00000000..22bfcc0d --- /dev/null +++ b/packages/b2c-tooling/specs/ods-api-v1.json @@ -0,0 +1,3673 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "1.0", + "title": "B2C Commerce Developer Sandbox REST API", + "description": "B2C Commerce provides a REST API to manage developer sandboxes. The API allows you to create, manage, and delete developer sandboxes.", + "contact": { + "name": "Salesforce B2C Commerce Infocenter", + "url": "https://documentation.b2c.commercecloud.salesforce.com/DOC1/topic/com.demandware.dochelp/content/b2c_commerce/topics/sandboxes/b2c_developer_sandboxes.html?cp=0_6_4" + } + }, + "tags": [ + { + "name": "Common", + "x-sfdc-group-id": "Common", + "description": "General purpose API endpoints." + }, + { + "name": "Realms", + "x-sfdc-group-id": "realms", + "description": "Operations on the realm level." + }, + { + "name": "Sandboxes", + "x-sfdc-group-id": "sandboxes", + "description": "Operations on the sandbox level." + } + ], + "paths": { + "/": { + "get": { + "operationId": "getApiInfo", + "summary": "Retrieve API information.", + "description": "Return API version information.", + "tags": [ + "Common" + ], + "responses": { + "200": { + "description": "API version information.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiVersionResponse" + } + } + } + } + } + } + }, + "/me": { + "get": { + "operationId": "getUserInfo", + "summary": "Retrieve user information.", + "description": "Return information about the user interacting with the API.", + "tags": [ + "Common" + ], + "responses": { + "200": { + "description": "Metadata about the authenticated API user.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserInfoResponse" + } + } + } + } + }, + "security": [ + { + "AccountManager": [] + }, + { + "ClientCredentials": [] + } + ] + } + }, + "/system": { + "get": { + "operationId": "getSystemInfo", + "summary": "Retrieve system information", + "description": "Returns information about the system, the user is interacting with.", + "tags": [ + "Common" + ], + "responses": { + "200": { + "description": "Metadata about the system", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SystemInfoResponse" + } + } + } + } + }, + "security": [ + { + "AccountManager": [] + }, + { + "ClientCredentials": [] + } + ] + } + }, + "/realms/{realm}/system": { + "parameters": [ + { + "$ref": "#/components/parameters/realmParam" + } + ], + "get": { + "operationId": "getRealmSystemInfo", + "summary": "Retrieve system information", + "description": "Returns information about the system, the user is interacting with.", + "tags": [ + "Common" + ], + "responses": { + "200": { + "description": "Metadata about the system", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SystemInfoResponse" + } + } + } + } + }, + "security": [ + { + "AccountManager": [] + }, + { + "ClientCredentials": [] + } + ] + } + }, + "/realms/{realm}": { + "parameters": [ + { + "$ref": "#/components/parameters/realmParam" + }, + { + "in": "query", + "name": "expand", + "description": "Additional information, which should be shown in the realm query. Available options are: [configuration,usage, accountdetails].", + "style": "form", + "explode": false, + "schema": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "configuration", + "usage", + "accountdetails" + ] + } + } + } + ], + "get": { + "operationId": "getRealm", + "summary": "Show realm information.", + "description": "Return metadata about a realm.", + "tags": [ + "Realms" + ], + "responses": { + "200": { + "description": "Realm metadata.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RealmResponse" + } + } + } + }, + "400": { + "description": "The ID is not a valid realm ID.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "The user isn't authenticated.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "The user doesn't have access to the realm.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "There isn't any realm with that ID.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "AccountManager": [] + }, + { + "ClientCredentials": [] + } + ] + } + }, + "/realms/{realm}/configuration": { + "parameters": [ + { + "$ref": "#/components/parameters/realmParam" + } + ], + "get": { + "operationId": "getRealmConfiguration", + "summary": "Show realm configuration.", + "description": "Return the current configuration values of the realm.", + "tags": [ + "Realms" + ], + "responses": { + "200": { + "description": "Current configuration values of the realm.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RealmConfigurationResponse" + } + } + } + }, + "400": { + "description": "The ID isn't valid or the configuration isn't valid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "The user doesn't have access to that realm.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "There isn't any realm with that ID.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "AccountManager": [] + }, + { + "ClientCredentials": [] + } + ] + }, + "patch": { + "operationId": "patchRealmConfiguration", + "summary": "Update realm configuration.", + "description": "Update the customizable configuration of a realm. Note that the internal time format in weekday schedules is [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Times).", + "tags": [ + "Realms" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RealmConfigurationUpdateRequestModel" + } + } + }, + "description": "Realm values to update.", + "required": true + }, + "responses": { + "200": { + "description": "Updated realm configuration data.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RealmConfigurationResponse" + } + } + } + }, + "400": { + "description": "The ID isn't a valid realm ID.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "The user isn't authenticated.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "The user doesn't have access to that realm.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "There isn't any realm with that ID.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "AccountManager": [] + }, + { + "ClientCredentials": [] + } + ] + } + }, + "/realms/{realm}/usage": { + "parameters": [ + { + "$ref": "#/components/parameters/realmParam" + }, + { + "$ref": "#/components/parameters/fromParam" + }, + { + "$ref": "#/components/parameters/toParam" + }, + { + "$ref": "#/components/parameters/detailedReportParam" + }, + { + "$ref": "#/components/parameters/granularityParam" + } + ], + "get": { + "operationId": "getRealmUsage", + "summary": "Show usage information for realm.", + "description": "Return information about the realm's usage.", + "tags": [ + "Realms" + ], + "responses": { + "200": { + "description": "Realm's usage information.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RealmUsageResponse" + } + }, + "text/csv": { + "schema": { + "$ref": "#/components/schemas/RealmUsageResponse" + } + } + } + }, + "400": { + "description": "The ID isn't valid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + }, + "text/csv": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "The user doesn't have access to the realm.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + }, + "text/csv": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "There isn't any realm with that ID.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + }, + "text/csv": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "AccountManager": [] + }, + { + "ClientCredentials": [] + } + ] + } + }, + "/realms/usages": { + "post": { + "operationId": "searchRealmUsage", + "summary": "Show usage information for given realms.", + "description": "Update the customizable configuration of a realm. Note that the internal time format in weekday schedules is [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Times).", + "tags": [ + "Realms" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MultiRealmUsageRequest" + } + } + }, + "description": "Return information for given all realm's usage", + "required": true + }, + "responses": { + "200": { + "description": "Aggregates all realm usage data.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MultiRealmUsageResponse" + } + }, + "text/csv": { + "schema": { + "$ref": "#/components/schemas/MultiRealmUsageResponse" + } + } + } + }, + "400": { + "description": "The ID isn't a valid realm ID.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + }, + "text/csv": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "The user isn't authenticated.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + }, + "text/csv": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "The user doesn't have access to that realm.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + }, + "text/csv": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "There isn't any realm with that ID.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + }, + "text/csv": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "AccountManager": [] + }, + { + "ClientCredentials": [] + } + ] + } + }, + "/sandboxes": { + "get": { + "operationId": "getSandboxes", + "summary": "List sandboxes.", + "description": "Return all sandboxes of a realm.", + "tags": [ + "Sandboxes" + ], + "parameters": [ + { + "name": "include_deleted", + "in": "query", + "required": false, + "description": "If set, return deleted sandboxes.", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter_params", + "in": "query", + "required": false, + "description": "If passed in supported format, returns sandboxes that matches the query. Supported format: realm=zzzz&state=active&resourceProfile=medium&createdBy=user1&tags=[tag1,tag2,tag3].", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "List of sandboxes.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SandboxListResponse" + } + } + } + }, + "400": { + "description": "The request parameters are invalid (bad request).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "The user doesn't have access to that realm.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "There isn't any realm with that ID.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "AccountManager": [] + }, + { + "ClientCredentials": [] + } + ] + }, + "post": { + "operationId": "createSandbox", + "summary": "Create sandbox.", + "description": "Create a new sandbox within the realm.", + "tags": [ + "Sandboxes" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SandboxProvisioningRequestModel" + } + } + }, + "description": "Metadata about the new sandbox.", + "required": true + }, + "responses": { + "201": { + "description": "The sandbox creation has started.", + "headers": { + "Location": { + "description": "URI of the created sandbox.", + "schema": { + "type": "string" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SandboxResponse" + } + } + } + }, + "400": { + "description": "The request parameters are invalid (bad request).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "The user doesn't have access to the realm.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "There isn't any realm with that ID.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "There were server errors initiating the sandbox deployment.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "AccountManager": [] + }, + { + "ClientCredentials": [] + } + ] + } + }, + "/sandboxes/{sandboxId}": { + "parameters": [ + { + "$ref": "#/components/parameters/sandboxIdParam" + } + ], + "get": { + "operationId": "getSandbox", + "summary": "Retrieve sandbox information.", + "description": "Return details on a specific sandbox in a realm.", + "tags": [ + "Sandboxes" + ], + "responses": { + "200": { + "description": "Details on the sandbox (including its state).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SandboxResponse" + } + } + } + }, + "400": { + "description": "The request parameters are invalid (bad request).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "The user doesn't have access to the requested realm.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "There isn't any realm with that ID.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "AccountManager": [] + }, + { + "ClientCredentials": [] + } + ] + }, + "patch": { + "operationId": "patchSandbox", + "summary": "Update sandbox.", + "description": "Update a sandbox.", + "tags": [ + "Sandboxes" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SandboxUpdateRequestModel" + } + } + }, + "description": "Sandbox values to update.", + "required": true + }, + "responses": { + "200": { + "description": "Updated details on the sandbox (including its state).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SandboxResponse" + } + } + } + }, + "400": { + "description": "The request parameters are invalid (bad request).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "The user doesn't have access to the realm.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "There isn't any sandbox with that ID.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "AccountManager": [] + }, + { + "ClientCredentials": [] + } + ] + }, + "delete": { + "operationId": "deleteSandbox", + "summary": "Delete sandbox.", + "description": "Delete a specific sandbox in a realm.", + "tags": [ + "Sandboxes" + ], + "responses": { + "202": { + "description": "The request for deleting the sandbox has been accepted by the API server. This doesn't mean that the sandbox has already been deleted, since the actual deletion process does not necessarily start immediately and might take a while. You can track the deletion process using sandbox GET requests.\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StatusResponse" + } + } + } + }, + "400": { + "description": "The request parameters are invalid (bad request).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "The user doesn't have access to that realm.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "ID not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "AccountManager": [] + }, + { + "ClientCredentials": [] + } + ] + } + }, + "/sandboxes/{sandboxId}/aliases": { + "parameters": [ + { + "$ref": "#/components/parameters/sandboxIdParam" + } + ], + "post": { + "operationId": "createAlias", + "summary": "Create sandbox alias.", + "description": "Create a new sandbox alias.", + "tags": [ + "Sandboxes" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SandboxAliasModel" + } + } + }, + "description": "The alias for the sandbox", + "required": true + }, + "responses": { + "200": { + "description": "The sandbox alias already exists.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SandboxAliasResponse" + } + } + } + }, + "201": { + "description": "The alias has been created.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SandboxAliasResponse" + } + } + } + }, + "400": { + "description": "The request parameters are invalid (bad request).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "The user doesn't have access to the sandbox.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "There isn't any sandbox with that ID.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "AccountManager": [] + }, + { + "ClientCredentials": [] + } + ] + }, + "get": { + "operationId": "getAliases", + "summary": "Read all sandbox aliases", + "description": "Retrieve a list of all past and present operations on a sandbox within the realm.", + "tags": [ + "Sandboxes" + ], + "responses": { + "200": { + "description": "List of Alias configurations.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SandboxAliasListResponse" + } + } + } + }, + "403": { + "description": "The user doesn't have access to the realm or sandbox.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "There isn't any sandbox with that ID.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "AccountManager": [] + }, + { + "ClientCredentials": [] + } + ] + } + }, + "/sandboxes/{sandboxId}/aliases/{sandboxAliasId}": { + "parameters": [ + { + "$ref": "#/components/parameters/sandboxIdParam" + }, + { + "$ref": "#/components/parameters/sandboxAliasIdParam" + } + ], + "get": { + "operationId": "getAlias", + "summary": "Read Alias configuration", + "description": "Retrieves a dedicated alias for the sandbox. Can be called without authentication to get cookie values for the alias.", + "tags": [ + "Sandboxes" + ], + "responses": { + "200": { + "description": "The Alias configuration.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SandboxAliasResponse" + } + } + } + }, + "403": { + "description": "The user doesn't have access to the realm or sandbox.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "There isn't any sandbox or any alias with that ID.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "AccountManager": [] + }, + { + "ClientCredentials": [] + } + ] + }, + "delete": { + "operationId": "deleteAlias", + "summary": "Delete Alias configuration", + "description": "Deletes a dedicated alias configuration for a sandbox.", + "tags": [ + "Sandboxes" + ], + "responses": { + "202": { + "description": "Shows, that alias currently gets deleted.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StatusResponse" + } + } + } + }, + "403": { + "description": "The user doesn't have access to the realm or sandbox.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "There isn't any sandbox or any alias with that ID.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "AccountManager": [] + }, + { + "ClientCredentials": [] + } + ] + } + }, + "/sandboxes/{sandboxId}/operations": { + "parameters": [ + { + "$ref": "#/components/parameters/sandboxIdParam" + } + ], + "post": { + "operationId": "createSandboxOperation", + "summary": "Run sandbox operation.", + "description": "Request an operation on a sandbox within the realm.", + "tags": [ + "Sandboxes" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SandboxOperationRequestModel" + } + } + }, + "description": "Operation to be carried out on a sandbox.", + "required": true + }, + "responses": { + "202": { + "description": "The operation has been accepted.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SandboxOperationResponse" + } + } + } + }, + "400": { + "description": "The request parameters are invalid (bad request).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "The user doesn't have access to the sandbox.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "There isn't any sandbox with that ID.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "422": { + "description": "The operation isn't allowed in the current state of the sandbox.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "There were server errors during the operation.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "AccountManager": [] + }, + { + "ClientCredentials": [] + } + ] + }, + "get": { + "operationId": "getSandboxOperations", + "summary": "List sandbox operations.", + "description": "Retrieve a list of all past and present operations on a sandbox within the realm.", + "tags": [ + "Sandboxes" + ], + "parameters": [ + { + "$ref": "#/components/parameters/fromParam" + }, + { + "$ref": "#/components/parameters/toParam" + }, + { + "$ref": "#/components/parameters/operationStateParam" + }, + { + "$ref": "#/components/parameters/operationStatusParam" + }, + { + "$ref": "#/components/parameters/operationTypeParam" + }, + { + "$ref": "#/components/parameters/sortOrderParam" + }, + { + "$ref": "#/components/parameters/sortByOperationParam" + }, + { + "$ref": "#/components/parameters/pageParam" + }, + { + "$ref": "#/components/parameters/perPageParam" + } + ], + "responses": { + "200": { + "description": "List of operations.", + "headers": { + "Link": { + "description": "Paging metadata, as described in RFC-5988", + "schema": { + "type": "string" + } + }, + "X-Pagination-Count": { + "description": "Total count of elements.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + "X-Pagination-Page": { + "description": "Current page index.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + "X-Pagination-Limit": { + "description": "Maximum count of pages.", + "schema": { + "type": "integer", + "format": "int32" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SandboxOperationListResponse" + } + } + } + }, + "400": { + "description": "The request parameters are invalid (bad request).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "The user doesn't have access to the sandbox.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "There isn't any sandbox with that ID.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "There were server errors during the operation.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "AccountManager": [] + }, + { + "ClientCredentials": [] + } + ] + } + }, + "/sandboxes/{sandboxId}/operations/{operationId}": { + "parameters": [ + { + "$ref": "#/components/parameters/sandboxIdParam" + }, + { + "$ref": "#/components/parameters/operationIdParam" + } + ], + "get": { + "operationId": "getSandboxOperation", + "summary": "Retrieve sandbox operation.", + "description": "Return details of a sandbox operation that was recently submitted, is currently in progress, or has already finished.", + "tags": [ + "Sandboxes" + ], + "responses": { + "200": { + "description": "Details of the sandbox operation's state and the state of its target. If the operation has already finished, indicates whether the operation was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SandboxOperationResponse" + } + } + } + }, + "400": { + "description": "The request parameters are invalid (bad request).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "The user doesn't have access to the requested operation or sandbox.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "There isn't any sandbox or realm matching the given parameters.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "AccountManager": [] + }, + { + "ClientCredentials": [] + } + ] + } + }, + "/sandboxes/{sandboxId}/settings": { + "parameters": [ + { + "$ref": "#/components/parameters/sandboxIdParam" + } + ], + "get": { + "operationId": "getSandboxSettings", + "summary": "Show sandbox settings.", + "description": "Return all settings of the sandbox.", + "tags": [ + "Sandboxes" + ], + "responses": { + "200": { + "description": "Details of the sandbox settings.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SandboxSettingsResponse" + } + } + } + }, + "400": { + "description": "The sandbox ID isn't valid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "The user doesn't have access to the requested sandbox.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "There isn't any sandbox matching the ID.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "AccountManager": [] + }, + { + "ClientCredentials": [] + } + ] + } + }, + "/sandboxes/{sandboxId}/usage": { + "parameters": [ + { + "$ref": "#/components/parameters/sandboxIdParam" + }, + { + "$ref": "#/components/parameters/fromParam" + }, + { + "$ref": "#/components/parameters/toParam" + } + ], + "get": { + "operationId": "getSandboxUsage", + "summary": "Show sandbox usage.", + "description": "Return information on sandbox usage.", + "tags": [ + "Sandboxes" + ], + "responses": { + "200": { + "description": "Sandbox usage information.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SandboxUsageResponse" + } + } + } + }, + "400": { + "description": "The sandbox ID isn't valid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "The user doesn't have access to the requested sandbox.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "There isn't any sandbox matching the ID.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "AccountManager": [] + }, + { + "ClientCredentials": [] + } + ] + } + }, + "/sandboxes/{sandboxId}/storage": { + "parameters": [ + { + "$ref": "#/components/parameters/sandboxIdParam" + } + ], + "get": { + "operationId": "getSandboxStorage", + "summary": "Show sandbox storage", + "description": "Return information on sandbox storage capacity for a currently running sandbox.", + "tags": [ + "Sandboxes" + ], + "responses": { + "200": { + "description": "Sandbox storage information.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SandboxStorageResponse" + } + } + } + }, + "400": { + "description": "The sandbox ID isn't valid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "The user doesn't have access to the requested sandbox.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "There isn't any sandbox matching the ID.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "AccountManager": [] + }, + { + "ClientCredentials": [] + } + ] + } + } + }, + "servers": [ + { + "url": "/api/v1" + } + ], + "components": { + "parameters": { + "realmParam": { + "name": "realm", + "in": "path", + "required": true, + "description": "The four-letter ID of the realm.", + "schema": { + "type": "string" + } + }, + "sandboxIdParam": { + "name": "sandboxId", + "in": "path", + "required": true, + "description": "The sandbox UUID.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + "sandboxAliasIdParam": { + "name": "sandboxAliasId", + "in": "path", + "required": true, + "description": "The sandbox alias UUID.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + "operationIdParam": { + "name": "operationId", + "in": "path", + "required": true, + "description": "The operation UUID.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + "pageParam": { + "name": "page", + "in": "query", + "required": false, + "description": "The page to access in a paged response. Page numbers start with '0', which is the default value.", + "schema": { + "type": "integer", + "format": "int32", + "minimum": 0 + } + }, + "perPageParam": { + "name": "per_page", + "in": "query", + "required": false, + "description": "Count of elements on a page. The default value is '20'.", + "schema": { + "type": "integer", + "format": "int32", + "minimum": 1 + } + }, + "fromParam": { + "name": "from", + "in": "query", + "required": false, + "description": "Earliest date for which data is in the response. Thirty days in the past by default. Format is ISO 8601.", + "schema": { + "type": "string", + "format": "date" + } + }, + "toParam": { + "name": "to", + "in": "query", + "required": false, + "description": "Latest date for which data is included in the response. Today's date by default. Format is ISO 8601.", + "schema": { + "type": "string", + "format": "date" + } + }, + "sortOrderParam": { + "name": "sort_order", + "in": "query", + "required": false, + "description": "Order of the list. Default value is ''asc''.", + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ] + } + }, + "sortByOperationParam": { + "name": "sort_by", + "in": "query", + "required": false, + "description": "Field by which to order the list. By default, the list is not ordered.", + "schema": { + "type": "string", + "enum": [ + "created", + "operation_state", + "status", + "operation" + ] + } + }, + "operationStateParam": { + "name": "operation_state", + "in": "query", + "required": false, + "description": "State of operations included in the response. By default, all operations are included.", + "schema": { + "type": "string", + "enum": [ + "pending", + "running", + "finished" + ] + } + }, + "operationStatusParam": { + "name": "status", + "in": "query", + "required": false, + "description": "Status of operations included in the response. By default, all operations are included.", + "schema": { + "type": "string", + "enum": [ + "success", + "failure" + ] + } + }, + "operationTypeParam": { + "name": "operation", + "in": "query", + "required": false, + "description": "Type of operations included in the response. By default, all operations are included.", + "schema": { + "type": "string", + "enum": [ + "start", + "stop", + "restart", + "reset", + "create", + "delete", + "upgrade" + ] + } + }, + "detailedReportParam": { + "name": "detailedReport", + "in": "query", + "required": false, + "description": "Field to check whether detailed report is to be retrieved, by default detailed report will not be pulled", + "schema": { + "type": "boolean", + "enum": [ + false, + true + ], + "default": false + } + }, + "granularityParam": { + "name": "granularity", + "in": "query", + "required": false, + "description": "Granularity of usage to be included in the response. By default, granular usage is not returned.", + "schema": { + "type": "string", + "enum": [ + "daily", + "weekly", + "monthly" + ] + } + } + }, + "securitySchemes": { + "AccountManager": { + "type": "oauth2", + "description": "Authenticate using Commerce Cloud Account Manager with your SSO credentials.", + "flows": { + "implicit": { + "authorizationUrl": "https://account.demandware.com:443/dwsso/oauth2/authorize", + "scopes": {} + } + } + }, + "ClientCredentials": { + "type": "oauth2", + "description": "Authenticate using Commerce Cloud Account Manager with your client credentials.", + "flows": { + "clientCredentials": { + "tokenUrl": "https://account.demandware.com:443/dwsso/oauth2/access_token", + "scopes": {} + } + } + } + }, + "schemas": { + "Response": { + "type": "object", + "required": [ + "kind", + "code" + ], + "properties": { + "kind": { + "type": "string", + "description": "Type of response object.", + "enum": [ + "ApiVersion", + "UserInfo", + "SystemInfo", + "Realm", + "RealmConfiguration", + "RealmUsage", + "MultiRealmUsage", + "Sandbox", + "SandboxList", + "SandboxAlias", + "SandboxAliasList", + "SandboxSettings", + "SandboxUsage", + "SandboxStorage", + "SandboxOperationList", + "Status" + ] + }, + "code": { + "type": "integer", + "format": "int32", + "description": "Response code sent along with the status." + } + } + }, + "StatusResponse": { + "required": [ + "status" + ], + "allOf": [ + { + "$ref": "#/components/schemas/Response" + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "description": "String with value 'Success' or 'Failure' to indicate request outcome.", + "enum": [ + "Success", + "Failure" + ] + } + } + } + ] + }, + "PagedResponse": { + "allOf": [ + { + "$ref": "#/components/schemas/StatusResponse" + }, + { + "type": "object", + "properties": { + "metadata": { + "$ref": "#/components/schemas/PagingMetadata" + } + } + } + ] + }, + "PagingMetadata": { + "properties": { + "page": { + "type": "integer", + "format": "int32", + "description": "Index of the current page." + }, + "perPage": { + "type": "integer", + "format": "int32", + "description": "Maximum count of elements per page." + }, + "pageCount": { + "type": "integer", + "format": "int32", + "description": "Total count of pages." + }, + "totalCount": { + "type": "integer", + "format": "int64", + "description": "Total count of elements." + }, + "links": { + "$ref": "#/components/schemas/PagingLinks" + } + } + }, + "PagingLinks": { + "properties": { + "self": { + "type": "string", + "description": "Relative link to this page." + }, + "first": { + "type": "string", + "description": "Relative link to the first page." + }, + "previous": { + "type": "string", + "description": "Relative link to the previous page. 'null' if the current page is the first page." + }, + "next": { + "type": "string", + "description": "Relative link to the next page. 'null' if the current page is the last page." + }, + "last": { + "type": "string", + "description": "Relative link to the last page." + } + } + }, + "RealmResponse": { + "allOf": [ + { + "$ref": "#/components/schemas/StatusResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/RealmModel" + } + } + } + ] + }, + "RealmModel": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string", + "description": "GUID of the realm in the system." + }, + "name": { + "type": "string", + "description": "Human-readable four-letter ID of the realm." + }, + "enabled": { + "type": "boolean", + "description": "Flag indicating whether the realm is enabled for any operations." + }, + "usage": { + "$ref": "#/components/schemas/RealmUsageSummaryModel" + }, + "configuration": { + "$ref": "#/components/schemas/RealmConfigurationModel" + }, + "accountdetails": { + "$ref": "#/components/schemas/AccountDetailsModel" + } + } + }, + "RealmUsageSummaryModel": { + "type": "object", + "required": [ + "activeSandboxes" + ], + "properties": { + "activeSandboxes": { + "type": "integer", + "format": "int64", + "description": "Number of currently active sandboxes for a realm.", + "example": 42 + } + } + }, + "RealmConfigurationResponse": { + "allOf": [ + { + "$ref": "#/components/schemas/StatusResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/RealmConfigurationModel" + } + } + } + ] + }, + "ConfigurationIntegerValue": { + "type": "object", + "description": "Object that holds an integer-based configuration property. A zero value means \"unlimited\".", + "properties": { + "fixedValue": { + "type": "integer", + "format": "int32", + "description": "Fixed value for this configuration property. You can't use this along with a maximum or default value." + }, + "maximum": { + "type": "integer", + "format": "int32", + "description": "Maximum value for this property." + }, + "defaultValue": { + "type": "integer", + "format": "int32", + "description": "Default value for this property." + } + } + }, + "WeekdaySchedule": { + "type": "object", + "description": "A schedule definition for a dedicated time on specific weekdays.", + "properties": { + "weekdays": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "MONDAY", + "TUESDAY", + "WEDNESDAY", + "THURSDAY", + "FRIDAY", + "SATURDAY", + "SUNDAY" + ] + }, + "description": "List of weekdays, where the action should take place" + }, + "time": { + "type": "string", + "description": "Time (with timezone) where the action should take place on the specified weekdays. Time format is [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Times). If no time zone is given, the timezone defaults to GMT.", + "example": "20:10:00Z" + } + } + }, + "RealmSandboxConfigurationModel": { + "type": "object", + "description": "Configuration object related to sandboxes of a realm.", + "required": [ + "limitsEnabled", + "totalNumberOfSandboxes", + "sandboxTTL", + "localUsersAllowed" + ], + "properties": { + "limitsEnabled": { + "type": "boolean", + "description": "Flag indicating whether sandbox specific limits are enforced for the realm." + }, + "totalNumberOfSandboxes": { + "type": "integer", + "description": "Total number of sandboxes (regardless of state) that the realm can hold." + }, + "sandboxTTL": { + "$ref": "#/components/schemas/ConfigurationIntegerValue" + }, + "localUsersAllowed": { + "type": "boolean", + "description": "Flag indicating whether users outside the Account Manager are allowed." + } + }, + "example": { + "limitsEnabled": true, + "totalNumberOfSandboxes": 50, + "sandboxTTL": { + "maximum": 240, + "defaultValue": 8 + }, + "localUsersAllowed": false + } + }, + "RealmSandboxConfigurationUpdateModel": { + "type": "object", + "description": "Update data for configuration data related to sandboxes of a realm. The time formats within the weekday schedules have to be passed in [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Times) format.", + "properties": { + "sandboxTTL": { + "$ref": "#/components/schemas/ConfigurationIntegerValue" + }, + "startScheduler": { + "x-type-overwrite": "WeekdaySchedule", + "nullable": true + }, + "stopScheduler": { + "x-type-overwrite": "WeekdaySchedule", + "nullable": true + } + }, + "example": { + "sandboxTTL": { + "maximum": 240, + "defaultValue": 24 + }, + "startScheduler": { + "weekdays": [ + "MONDAY", + "TUESDAY", + "WEDNESDAY", + "THURSDAY", + "FRIDAY" + ], + "time": "08:00:00+03:00" + }, + "stopScheduler": { + "weekdays": [ + "MONDAY", + "TUESDAY", + "WEDNESDAY", + "THURSDAY", + "FRIDAY" + ], + "time": "19:00:00Z" + } + }, + "nullable": true + }, + "RealmRequestConfigurationModel": { + "type": "object", + "description": "Configuration object related to requests targeting the sandboxes of a realm.", + "required": [ + "enforced" + ], + "properties": { + "enforced": { + "type": "boolean", + "description": "If enabled, rate limiting is active." + }, + "maxRate": { + "type": "integer", + "description": "Maximum requests allowed per time period." + }, + "timePeriod": { + "type": "integer", + "description": "Number of seconds during which to count requests." + } + }, + "example": { + "enforced": true, + "maxRate": 50000, + "timePeriod": 60 + } + }, + "RealmConfigurationModel": { + "type": "object", + "properties": { + "emails": { + "type": "array", + "items": { + "type": "string", + "pattern": "(.+)@(.+)" + }, + "example": [ + "email1@example.com", + "email2@example.com" + ] + }, + "sandbox": { + "$ref": "#/components/schemas/RealmSandboxConfigurationModel" + }, + "requests": { + "$ref": "#/components/schemas/RealmRequestConfigurationModel" + }, + "startScheduler": { + "$ref": "#/components/schemas/WeekdaySchedule" + }, + "stopScheduler": { + "$ref": "#/components/schemas/WeekdaySchedule" + } + } + }, + "RealmConfigurationUpdateRequestModel": { + "type": "object", + "properties": { + "emails": { + "type": "array", + "items": { + "type": "string", + "pattern": "(.+)@(.+)" + }, + "example": [ + "email1@example.com", + "email2@example.com" + ] + }, + "sandbox": { + "$ref": "#/components/schemas/RealmSandboxConfigurationUpdateModel" + } + } + }, + "RealmUsageResponse": { + "allOf": [ + { + "$ref": "#/components/schemas/StatusResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/RealmUsageModel" + } + } + } + ] + }, + "MultiRealmUsageModel": { + "type": "object", + "required": [ + "realmName" + ], + "properties": { + "realmName": { + "type": "string", + "description": "GUID of the realm in the system." + }, + "realmUsage": { + "$ref": "#/components/schemas/RealmUsageModel" + }, + "error": { + "type": "string", + "description": "Error while getting usage." + } + } + }, + "RealmUsageModel": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string", + "description": "GUID of the realm in the system." + }, + "accountId": { + "type": "string", + "description": "account/SFID of the realm in clusterstate table or org62 Tenant table" + }, + "createdSandboxes": { + "type": "integer", + "format": "int64", + "description": "Total number of sandboxes created during the requested timeframe (by default, the previous 30 days).", + "example": 93 + }, + "activeSandboxes": { + "type": "integer", + "format": "int64", + "description": "Total number of sandboxes active during the requested timeframe (by default, the previous 30 days).", + "example": 128 + }, + "deletedSandboxes": { + "type": "integer", + "format": "int64", + "description": "Total number of sandboxes deleted during the requested timeframe (by default, the previous 30 days).", + "example": 86 + }, + "sandboxSeconds": { + "type": "integer", + "format": "int64", + "description": "Total number of seconds sandboxes ran during the requested timeframe (by default, the previous 30 days).", + "example": 360000 + }, + "minutesUpByProfile": { + "type": "array", + "items": { + "type": "object", + "properties": { + "profile": { + "$ref": "#/components/schemas/SandboxResourceProfile" + }, + "minutes": { + "type": "integer", + "format": "int64", + "description": "How many minutes sandboxes of this profile type were running during the report timeframe." + } + } + } + }, + "minutesUp": { + "type": "integer", + "format": "int64", + "description": "Sum of minutes sandboxes in this realm were running during the requested timeframe (by default, the previous 30 days).", + "example": 360000 + }, + "minutesDown": { + "type": "integer", + "format": "int64", + "description": "Sum of minutes sandboxes in this realm were not running during the requested timeframe (by default, the previous 30 days).", + "example": 180000 + }, + "sandboxDetails": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SandboxInfo" + } + }, + "granularUsage": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GranularUsage" + } + } + } + }, + "AccountDetailsModel": { + "type": "object", + "properties": { + "accountName": { + "type": "string", + "example": "Disney", + "description": "Account name." + }, + "creditBalance": { + "type": "number", + "format": "double", + "description": "Total Credit Balance left.", + "example": 93.234 + } + } + }, + "MultiRealmUsageRequest": { + "type": "object", + "properties": { + "from": { + "type": "string", + "format": "date", + "description": "Time the sandbox was started." + }, + "to": { + "type": "string", + "format": "date", + "description": "Time the sandbox was stopped. If the sandbox is still running, this value will not exist for the last block." + }, + "realms": { + "type": "array", + "items": { + "type": "string" + } + }, + "detailedReport": { + "type": "boolean", + "default": false, + "enum": [ + false, + true + ], + "description": "Field to check whether detailed report is to be retrieved, by default detailed report will not be pulled." + } + } + }, + "MultiRealmUsageResponse": { + "allOf": [ + { + "$ref": "#/components/schemas/StatusResponse" + }, + { + "type": "object" + } + ], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MultiRealmUsageModel" + } + } + } + }, + "SandboxListResponse": { + "allOf": [ + { + "$ref": "#/components/schemas/StatusResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SandboxModel" + } + } + } + } + ] + }, + "SandboxResponse": { + "allOf": [ + { + "$ref": "#/components/schemas/StatusResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/SandboxModel" + } + } + } + ] + }, + "SandboxStorageModel": { + "type": "object", + "description": "Shows all filesystem storages and how much space is left on them.", + "additionalProperties": { + "$ref": "#/components/schemas/StorageUsageModel" + } + }, + "StorageUsageModel": { + "type": "object", + "description": "Represents a single filesystem storage unit with its available space.", + "properties": { + "spaceTotal": { + "type": "integer", + "format": "int64", + "description": "Total available space in MB." + }, + "spaceUsed": { + "type": "integer", + "format": "int64", + "description": "Used space in MB." + }, + "percentageUsed": { + "type": "integer", + "format": "int32", + "description": "Used space in percent, compared to total space." + } + } + }, + "SandboxModel": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "realm": { + "type": "string" + }, + "emails": { + "type": "array", + "items": { + "type": "string", + "pattern": "(.+)@(.+)" + } + }, + "enabled": { + "type": "boolean", + "description": "Flag indicating whether the sandbox is enabled for any operations." + }, + "instance": { + "type": "string" + }, + "versions": { + "type": "object", + "title": "SandboxModelVersions", + "description": "Versions of the components that make up the sandbox.", + "properties": { + "app": { + "type": "string", + "pattern": "\\d(\\.\\d)*", + "description": "Version of the commerce application." + }, + "web": { + "type": "string", + "description": "Version of the web proxy.", + "pattern": "\\d(\\.\\d)*" + } + } + }, + "autoScheduled": { + "type": "boolean", + "description": "Defaults to false. If set to true, the sandbox is covered by automatic start/stop actions, which can be set to a dedicated time via realm- configuration API." + }, + "analyticsEnabled": { + "type": "boolean", + "description": "Defaults to false. If set to true, analytics will be enabled in ODS." + }, + "resourceProfile": { + "$ref": "#/components/schemas/SandboxResourceProfile" + }, + "state": { + "$ref": "#/components/schemas/SandboxState" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "createdBy": { + "type": "string" + }, + "deletedAt": { + "type": "string", + "format": "date-time", + "description": "Time when the delete operation was created." + }, + "deletedBy": { + "type": "string", + "description": "User who requested the sandbox deletion." + }, + "eol": { + "type": "string", + "format": "date-time" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "hostName": { + "type": "string" + }, + "links": { + "type": "object", + "description": "Set of named links for accessing the sandbox.", + "properties": { + "bm": { + "type": "string", + "description": "Fully qualified URL of the sandbox Business Manager web app." + }, + "ocapi": { + "type": "string", + "description": "Fully qualified URL of OCAPI data API (excluding version selector)." + }, + "impex": { + "type": "string", + "description": "Fully qualified WebDAV URL for accessing import and export files." + }, + "code": { + "type": "string", + "description": "Fully qualified WebDAV URL for accessing code." + }, + "logs": { + "type": "string", + "description": "Fully qualified WebDAV URL for accessing log files." + } + } + }, + "startScheduler": { + "$ref": "#/components/schemas/WeekdaySchedule" + }, + "stopScheduler": { + "$ref": "#/components/schemas/WeekdaySchedule" + } + } + }, + "GranularUsage": { + "type": "object", + "properties": { + "usageDate": { + "type": "string", + "description": "start of the usage being returned" + }, + "creditsUp": { + "type": "number", + "format": "double", + "description": "Credits consumed when sandboxes were up during the requested timeframe.", + "example": 3600.001 + }, + "creditsDown": { + "type": "number", + "format": "double", + "description": "Credits consumed when sandboxes were down during the requested timeframe.", + "example": 1440.001 + }, + "minutesUp": { + "type": "integer", + "format": "int64", + "description": "Minutes sandboxes were up during the requested timeframe.", + "example": 360000 + }, + "minutesDown": { + "type": "integer", + "format": "int64", + "description": "Minutes sandboxes were down during the requested timeframe.", + "example": 180000 + } + } + }, + "SandboxInfo": { + "type": "object", + "properties": { + "realm": { + "type": "string" + }, + "resourceProfile": { + "$ref": "#/components/schemas/SandboxResourceProfile" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "deletedAt": { + "type": "string", + "format": "date-time", + "description": "Time when the delete operation was created." + }, + "name": { + "type": "string", + "description": "Name of the sandbox" + }, + "instanceId": { + "type": "string", + "description": "instanceId of the sandbox" + }, + "minutesUpByProfile": { + "type": "array", + "items": { + "type": "object", + "properties": { + "profile": { + "$ref": "#/components/schemas/SandboxResourceProfile" + }, + "minutes": { + "type": "integer", + "format": "int64", + "description": "How many minutes sandboxes of this profile type were running during the report timeframe." + } + } + } + }, + "minutesUp": { + "type": "integer", + "format": "int64", + "description": "Minutes sandbox in this realm was running during the requested timeframe (by default, the previous 30 days).", + "example": 360000 + }, + "minutesDown": { + "type": "integer", + "format": "int64", + "description": "Minutes sandbox in this realm was not running during the requested timeframe (by default, the previous 30 days).", + "example": 180000 + }, + "autoScheduled": { + "type": "boolean", + "description": "Defaults to false. If set to true, the sandbox is covered by automatic start/stop actions, which can be set to a dedicated time via realm- configuration API.:" + }, + "startScheduler": { + "$ref": "#/components/schemas/WeekdaySchedule" + }, + "stopScheduler": { + "$ref": "#/components/schemas/WeekdaySchedule" + }, + "clusterName": { + "type": "string", + "description": "Cluster where sandbox resides." + } + } + }, + "SandboxState": { + "type": "string", + "enum": [ + "new", + "creating", + "starting", + "started", + "stopping", + "stopped", + "deleting", + "deleted", + "resetting", + "failed", + "unknown", + "upgrading" + ] + }, + "SandboxResourceProfile": { + "type": "string", + "enum": [ + "medium", + "large", + "xlarge", + "xxlarge" + ], + "description": "Determines the resource allocation for the sandbox, \"medium\" is the default. Be careful, more powerful profiles consume more credits." + }, + "SandboxOperationRequestModel": { + "type": "object", + "required": [ + "operation" + ], + "properties": { + "operation": { + "type": "string", + "enum": [ + "start", + "stop", + "restart", + "reset" + ] + } + } + }, + "SandboxAliasResponse": { + "allOf": [ + { + "$ref": "#/components/schemas/StatusResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/SandboxAliasModel" + } + } + } + ] + }, + "SandboxAliasListResponse": { + "allOf": [ + { + "$ref": "#/components/schemas/StatusResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SandboxAliasModel" + } + } + } + } + ] + }, + "SandboxAliasModel": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "id": { + "type": "string", + "format": "uuid", + "readOnly": true, + "description": "The sandbox alias UUID." + }, + "name": { + "type": "string", + "description": "The alias name.", + "example": "www.example.com" + }, + "unique": { + "type": "boolean", + "description": "Define if it's a unique configuration", + "example": false + }, + "requestLetsEncryptCertificate": { + "type": "boolean", + "description": "Request a valid certificate to be generated on the fly through Lets Encrypt. This action consumes certificate requests from the domain quota imposed by Let's Encrypt, please read the Alias documentation carefully.", + "example": false + }, + "sandboxId": { + "type": "string", + "format": "uuid", + "readOnly": true, + "description": "The UUID of the sandbox the sandbox alias is pointing to." + }, + "cookie": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + }, + "path": { + "type": "string" + }, + "domain": { + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "readOnly": true, + "description": "The cookie required for each request to this alias." + }, + "registration": { + "type": "string", + "readOnly": true, + "description": "The link that can be used to save the required cookie for this alias in the browser." + }, + "domainVerificationRecord": { + "type": "string", + "readOnly": true, + "description": "The verification code to be added as TXT record in the DNS" + }, + "status": { + "type": "string", + "enum": [ + "pending", + "verified" + ], + "readOnly": true, + "description": "The status of the alias creation process" + } + } + }, + "SandboxOperationResponse": { + "allOf": [ + { + "$ref": "#/components/schemas/StatusResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/SandboxOperationModel" + } + } + } + ] + }, + "SandboxOperationListResponse": { + "allOf": [ + { + "$ref": "#/components/schemas/PagedResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SandboxOperationModel" + } + } + } + } + ] + }, + "SandboxSettingsResponse": { + "allOf": [ + { + "$ref": "#/components/schemas/StatusResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/SandboxSettings" + } + } + } + ] + }, + "SandboxStorageResponse": { + "allOf": [ + { + "$ref": "#/components/schemas/StatusResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/SandboxStorageModel" + } + } + } + ] + }, + "SandboxUsageResponse": { + "allOf": [ + { + "$ref": "#/components/schemas/StatusResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/SandboxUsageModel" + } + } + } + ] + }, + "SandboxUsageModel": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string" + }, + "sandboxSeconds": { + "type": "integer", + "format": "int64", + "description": "Total number of seconds during which the sandbox ran." + }, + "minutesUpByProfile": { + "type": "array", + "items": { + "type": "object", + "properties": { + "profile": { + "$ref": "#/components/schemas/SandboxResourceProfile" + }, + "minutes": { + "type": "integer", + "format": "int64", + "description": "How many minutes sandboxes of this profile type were running during the report timeframe." + } + } + } + }, + "minutesUp": { + "type": "integer", + "format": "int64", + "description": "Sum of minutes sandboxes in this realm were running during the requested timeframe (by default, the previous 30 days).", + "example": 360000 + }, + "minutesDown": { + "type": "integer", + "format": "int64", + "description": "Sum of minutes sandboxes in this realm were not running during the requested timeframe (by default, the previous 30 days).", + "example": 180000 + }, + "granularUsage": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GranularUsage" + } + }, + "history": { + "description": "List of blocks, which describe the separate uptimes of a sandbox", + "type": "array", + "items": { + "type": "object", + "required": [ + "from" + ], + "properties": { + "from": { + "type": "string", + "format": "date-time", + "description": "Time the sandbox was started." + }, + "to": { + "type": "string", + "format": "date-time", + "description": "Time the sandbox was stopped. If the sandbox is still running, this value will not exist for the last block." + }, + "sandboxSeconds": { + "type": "integer", + "format": "int64", + "description": "Number of seconds that the sandbox was running for this block." + }, + "resourceProfile": { + "$ref": "#/components/schemas/SandboxResourceProfile" + }, + "exceedsTimeframe": { + "type": "boolean", + "description": "This property is set to true if the block exceeds the given timeframe and was therefore trimmed." + }, + "clusterName": { + "type": "string", + "description": "Cluster where sandbox resides." + } + } + } + } + } + }, + "SandboxOperationModel": { + "type": "object", + "required": [ + "id", + "operation", + "operationState" + ], + "properties": { + "id": { + "type": "string" + }, + "operation": { + "type": "string", + "enum": [ + "start", + "stop", + "restart", + "reset", + "create", + "delete", + "upgrade" + ] + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "operationBy": { + "type": "string" + }, + "operationState": { + "type": "string", + "enum": [ + "pending", + "running", + "finished" + ] + }, + "sandboxState": { + "$ref": "#/components/schemas/SandboxState" + }, + "status": { + "type": "string", + "description": "Indicates whether the operation finished successfully ('Success') or not ('Failure').", + "enum": [ + "success", + "failure" + ] + } + } + }, + "SandboxProvisioningRequestModel": { + "type": "object", + "properties": { + "realm": { + "type": "string" + }, + "emails": { + "type": "array", + "items": { + "type": "string", + "pattern": "(.+)@(.+)" + } + }, + "ttl": { + "type": "integer", + "format": "int32", + "description": "Number of hours the sandbox will live (must adhere to the maximum TTL quotas). If set to 0 or less, the sandbox will have an infinite lifetime." + }, + "autoScheduled": { + "type": "boolean", + "description": "Defaults to false. If set to true, the sandbox is covered by automatic start/stop actions, which can be set to a dedicated time via realm- configuration API." + }, + "analyticsEnabled": { + "type": "boolean", + "default": false, + "description": "Defaults to false. If set to true, analytics will be enabled in ODS." + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "startScheduler": { + "x-type-overwrite": "WeekdaySchedule", + "nullable": true + }, + "stopScheduler": { + "x-type-overwrite": "WeekdaySchedule", + "nullable": true + }, + "resourceProfile": { + "$ref": "#/components/schemas/SandboxResourceProfile" + }, + "settings": { + "$ref": "#/components/schemas/SandboxSettings" + } + }, + "required": [ + "realm" + ], + "example": { + "realm": "", + "emails": [ + "email1@example.com", + "email2@example.com" + ], + "ttl": 24, + "autoScheduled": false, + "tags": [ + "" + ], + "analyticsEnabled": false, + "startScheduler": { + "weekdays": [ + "MONDAY", + "TUESDAY", + "WEDNESDAY", + "THURSDAY", + "FRIDAY" + ], + "time": "08:00:00+03:00" + }, + "stopScheduler": { + "weekdays": [ + "MONDAY", + "TUESDAY", + "WEDNESDAY", + "THURSDAY", + "FRIDAY" + ], + "time": "19:00:00Z" + }, + "resourceProfile": "medium", + "settings": { + "ocapi": [ + { + "client_id": "", + "resources": [ + { + "resource_id": "/**", + "methods": [ + "get", + "post", + "put", + "patch", + "delete" + ], + "read_attributes": "(**)", + "write_attributes": "" + } + ] + } + ], + "webdav": [ + { + "client_id": "", + "permissions": [ + { + "path": "/cartridges", + "operations": [ + "read_write" + ] + }, + { + "path": "/impex", + "operations": [ + "read_write" + ] + } + ] + } + ] + } + } + }, + "SandboxUpdateRequestModel": { + "type": "object", + "properties": { + "emails": { + "type": "array", + "items": { + "type": "string", + "pattern": "(.+)@(.+)" + } + }, + "ttl": { + "type": "integer", + "format": "int32", + "description": "Number of hours added to the sandbox lifetime (must, together with previous extensions, adhere to the maximum TTL configuration). If set to 0 or less, the sandbox will have an infinite lifetime." + }, + "resourceProfile": { + "$ref": "#/components/schemas/SandboxResourceProfile" + }, + "autoScheduled": { + "type": "boolean", + "description": "If set to true, this sandbox will be captured by automated start-/stop -management." + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "startScheduler": { + "x-type-overwrite": "WeekdaySchedule", + "nullable": true + }, + "stopScheduler": { + "x-type-overwrite": "WeekdaySchedule", + "nullable": true + } + }, + "example": { + "emails": [ + "email1@example.com", + "email2@example.com" + ], + "ttl": null, + "autoScheduled": false, + "resourceProfile": "desiredProfile", + "tags": [ + "" + ], + "startScheduler": { + "weekdays": [ + "MONDAY", + "TUESDAY", + "WEDNESDAY", + "THURSDAY", + "FRIDAY" + ], + "time": "08:00:00+03:00" + }, + "stopScheduler": { + "weekdays": [ + "MONDAY", + "TUESDAY", + "WEDNESDAY", + "THURSDAY", + "FRIDAY" + ], + "time": "19:00:00Z" + } + } + }, + "SandboxSettings": { + "description": "Map of additional settings evaluated when the sandbox is provisioned and initialized.", + "type": "object", + "properties": { + "ocapi": { + "$ref": "#/components/schemas/OcapiSettings" + }, + "webdav": { + "$ref": "#/components/schemas/WebDavSettings" + } + } + }, + "OcapiSettings": { + "description": "Use this document to configure Open Commerce API permissions for multiple client applications in the context of a single site.", + "type": "array", + "minItems": 1, + "items": { + "description": "Describes Open Commerce API permissions for a client application.", + "type": "object", + "required": [ + "client_id" + ], + "properties": { + "client_id": { + "description": "Client application ID.", + "type": "string", + "format": "uuid" + }, + "resources": { + "description": "Array of resource-specific permission documents.", + "type": "array", + "items": { + "description": "Configures resource specific permissions and settings.", + "type": "object", + "required": [ + "methods", + "resource_id" + ], + "properties": { + "methods": { + "description": "Open Commerce API HTTP method filter. For example, the filter [\"get\",\"patch\"] allows access to the GET and PATCH methods for the specified resource path. You can specify methods that are supported for a resource. You can list all available resources and methods for the Shop API, version 18.1, with the following meta data call: http://{your-domain}/dw/meta/rest/shop/v18_1?client_id={your-client-id}\n", + "type": "array", + "items": { + "type": "string", + "enum": [ + "get", + "delete", + "patch", + "post", + "put" + ] + } + }, + "read_attributes": { + "description": "String that controls which properties are included in the response document. The configuration value must be specified using property selection syntax.\n", + "type": "string" + }, + "write_attributes": { + "description": "String that controls which properties can be included in the request document. The configuration value must be specified using property selection syntax.\n", + "type": "string" + }, + "resource_id": { + "description": "OCAPI resource identifier. For example: /products/*/images or /products/specific_id/images. This property supports Ant path style to describe resource IDs. You can specify wildcards or specific product IDs; you can also specify the pattern /products/** to access to all available sub-resources. You can list all resource identifiers for the Shop API, version 18.1, with the following meta data call: http://{your-domain}/dw/meta/rest/shop/v18_1?client_id={your-client-id}\n", + "type": "string" + }, + "version_range": { + "description": "Version range documents granting permissions only to a subset of OCAPI versions.", + "type": "array", + "items": { + "description": "Use this document to grant resource permissions only to a subset of Open Commerce API versions. You can use the properties from and until to define the range. At least one of both must be specified.\n", + "type": "object", + "properties": { + "from": { + "description": "From version (for example, 18.1). If you don't specify the from version, all versions including the oldest are accessible.", + "type": "string" + }, + "until": { + "description": "Until version (for example, 18.1). The until version is exclusive, which means that it is not part of the range. If you don't specify the until version, all versions including the most recent one are accessible.\n", + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "WebDavSettings": { + "description": "WebDAV settings contain WebDAV client permissions for multiple client applications in the context of your organization. WebDAV client permissions enable you to configure which API clients can access your WebDAV files. These permissions also give you fine-grained control over which directories each client can access.\n", + "type": "array", + "minItems": 1, + "items": { + "description": "An array of client-specific permission documents.", + "type": "object", + "required": [ + "client_id", + "permissions" + ], + "properties": { + "client_id": { + "description": "Client ID indicating the API client for which the permissions are configured.", + "type": "string", + "format": "uuid" + }, + "permissions": { + "description": "Array of directory-based permissions documents. Multiple permissions paths cannot intersect each other; for example, the following two paths intersect and are therefore invalid: /impex/src and /impex/src/foo.\n", + "type": "array", + "items": { + "description": "Use this document to configure WebDAV permissions.", + "type": "object", + "required": [ + "path", + "operations" + ], + "properties": { + "path": { + "description": "Directory for which the WebDAV permission is granted, including all subdirectories. File-specific permissions are not permitted.\n", + "type": "string" + }, + "operations": { + "description": "Array of operations granted on this directory. Possible values are read and read_write.\n", + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "enum": [ + "read", + "read_write" + ] + } + } + } + } + } + } + } + }, + "ApiVersionResponse": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/StatusResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/ApiVersion" + } + } + } + ] + }, + "ApiVersion": { + "type": "object", + "properties": { + "version": { + "type": "string", + "enum": [ + "v1" + ] + }, + "git": { + "type": "object", + "properties": { + "commit": { + "type": "string" + }, + "time": { + "type": "string", + "format": "date-time" + } + } + }, + "build": { + "type": "object", + "properties": { + "version": { + "type": "string" + }, + "time": { + "type": "string", + "format": "date-time" + } + } + } + } + }, + "UserInfoResponse": { + "allOf": [ + { + "$ref": "#/components/schemas/StatusResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/UserInfoSpec" + } + } + } + ] + }, + "UserInfoSpec": { + "type": "object", + "properties": { + "user": { + "type": "object", + "properties": { + "id": { + "description": "User's unique ID on Account Manager.", + "type": "string" + }, + "email": { + "description": "User's email address.", + "type": "string" + }, + "name": { + "description": "User's human-readable, full name.", + "type": "string" + } + } + }, + "client": { + "type": "object", + "properties": { + "id": { + "description": "OAuth client ID used to retrieve the access token.", + "type": "string" + } + } + }, + "roles": { + "description": "User's roles as returned by Account Manager.", + "type": "array", + "items": { + "type": "string" + } + }, + "realms": { + "description": "Realms that the user is allowed to access. All sandboxes within these realms are accessible.", + "type": "array", + "items": { + "type": "string" + } + }, + "sandboxes": { + "description": "Sandboxes that the user is allowed to access.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "SystemInfoResponse": { + "allOf": [ + { + "$ref": "#/components/schemas/StatusResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/SystemInfoSpec" + } + } + } + ] + }, + "SystemInfoSpec": { + "type": "object", + "properties": { + "region": { + "type": "string", + "description": "The region, the system is deployed on." + }, + "systemIps": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Public IP addresses of internal services like API server" + }, + "sandboxIps": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Public IP addresses of all sandboxes" + }, + "inboundIps": { + "type": "array", + "items": { + "type": "string" + }, + "description": "IP addresses for incoming traffic." + }, + "outboundIps": { + "type": "array", + "items": { + "type": "string" + }, + "description": "IP addresses for outgoing traffic." + } + } + }, + "ErrorResponse": { + "allOf": [ + { + "$ref": "#/components/schemas/StatusResponse" + }, + { + "type": "object", + "properties": { + "error": { + "$ref": "#/components/schemas/ErrorModel" + } + } + } + ] + }, + "ErrorModel": { + "type": "object", + "required": [ + "status" + ], + "properties": { + "status": { + "type": "string", + "description": "String with value 'Success' or 'Failure' to indicate request outcome.", + "enum": [ + "Success", + "Failure" + ] + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "reason": { + "type": "string", + "description": "Machine-readable, one-word, CamelCase description of why the operation failed. If this value is empty, there is no information available. The reason clarifies an HTTP status code but does not override it." + }, + "details": { + "description": "Extended data associated with the reason. Each reason can define its own extended details. This field is optional, and the data returned is not guaranteed to conform to any schema except that defined by the reason type.", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + } +} diff --git a/packages/b2c-tooling/src/auth/oauth-implicit.ts b/packages/b2c-tooling/src/auth/oauth-implicit.ts index ac496a9f..9b8cd71e 100644 --- a/packages/b2c-tooling/src/auth/oauth-implicit.ts +++ b/packages/b2c-tooling/src/auth/oauth-implicit.ts @@ -4,13 +4,16 @@ import {URL} from 'node:url'; import type {AuthStrategy, AccessTokenResponse, DecodedJWT} from './types.js'; import {getLogger} from '../logging/logger.js'; import {decodeJWT} from './oauth.js'; +import {DEFAULT_ACCOUNT_MANAGER_HOST} from '../defaults.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(); +// Module-level pending auth promises to prevent concurrent auth flows for the same clientId +const PENDING_AUTH: Map> = new Map(); + /** * Configuration for the implicit OAuth flow. */ @@ -196,13 +199,15 @@ export class ImplicitOAuthStrategy implements AuthStrategy { } /** - * Gets an access token, using cache if valid + * Gets an access token, using cache if valid. + * Uses a mutex to prevent concurrent auth flows for the same clientId. */ private async getAccessToken(): Promise { const logger = getLogger(); - const cached = ACCESS_TOKEN_CACHE.get(this.config.clientId); + const clientId = this.config.clientId; + const cached = ACCESS_TOKEN_CACHE.get(clientId); - logger.trace({clientId: this.config.clientId, hasCached: !!cached}, '[Auth] Getting access token'); + logger.trace({clientId, hasCached: !!cached}, '[Auth] Getting access token'); if (cached) { const now = new Date(); @@ -226,13 +231,13 @@ export class ImplicitOAuthStrategy implements AuthStrategy { {cachedScopes: cached.scopes, requiredScopes}, '[Auth] Access token missing scopes; invalidating and re-authenticating', ); - ACCESS_TOKEN_CACHE.delete(this.config.clientId); + ACCESS_TOKEN_CACHE.delete(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); + ACCESS_TOKEN_CACHE.delete(clientId); } else { logger.debug( {timeUntilExpiryMs: timeUntilExpiry}, @@ -242,15 +247,31 @@ export class ImplicitOAuthStrategy implements AuthStrategy { } } - // Get new token via implicit flow + // Check if there's already an auth flow in progress for this clientId + const pendingAuth = PENDING_AUTH.get(clientId); + if (pendingAuth) { + logger.debug('[Auth] Auth flow already in progress, waiting for it to complete'); + const tokenResponse = await pendingAuth; + return tokenResponse.accessToken; + } + + // Start new auth flow and store the promise so concurrent calls can wait 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; + const authPromise = this.implicitFlowLogin(); + PENDING_AUTH.set(clientId, authPromise); + + try { + const tokenResponse = await authPromise; + ACCESS_TOKEN_CACHE.set(clientId, tokenResponse); + logger.debug( + {expiresAt: tokenResponse.expires.toISOString(), scopes: tokenResponse.scopes}, + '[Auth] New token cached', + ); + return tokenResponse.accessToken; + } finally { + // Clean up pending auth promise + PENDING_AUTH.delete(clientId); + } } /** diff --git a/packages/b2c-tooling/src/auth/oauth.ts b/packages/b2c-tooling/src/auth/oauth.ts index 606d796f..00c976fb 100644 --- a/packages/b2c-tooling/src/auth/oauth.ts +++ b/packages/b2c-tooling/src/auth/oauth.ts @@ -1,7 +1,6 @@ import type {AuthStrategy, AccessTokenResponse, DecodedJWT} from './types.js'; import {getLogger} from '../logging/logger.js'; - -const DEFAULT_ACCOUNT_MANAGER_HOST = 'account.demandware.com'; +import {DEFAULT_ACCOUNT_MANAGER_HOST} from '../defaults.js'; // Module-level token cache to support multiple instances with same clientId const ACCESS_TOKEN_CACHE: Map = new Map(); diff --git a/packages/b2c-tooling/src/cli/base-command.ts b/packages/b2c-tooling/src/cli/base-command.ts index 80859297..bd90686f 100644 --- a/packages/b2c-tooling/src/cli/base-command.ts +++ b/packages/b2c-tooling/src/cli/base-command.ts @@ -3,6 +3,7 @@ import {loadConfig} from './config.js'; import type {ResolvedConfig, LoadConfigOptions} from './config.js'; import {setLanguage} from '../i18n/index.js'; import {configureLogger, getLogger, type LogLevel, type Logger} from '../logging/index.js'; +import type {ExtraParamsConfig} from '../clients/middleware.js'; export type Flags = Interfaces.InferredFlags<(typeof BaseCommand)['baseFlags'] & T['flags']>; export type Args = Interfaces.InferredArgs; @@ -54,6 +55,16 @@ export abstract class BaseCommand extends Command { env: 'SFCC_INSTANCE', helpGroup: 'GLOBAL', }), + 'extra-query': Flags.string({ + description: 'Extra query parameters as JSON (e.g., \'{"debug":"true"}\')', + helpGroup: 'GLOBAL', + hidden: true, + }), + 'extra-body': Flags.string({ + description: 'Extra body fields to merge as JSON (e.g., \'{"_internal":true}\')', + helpGroup: 'GLOBAL', + hidden: true, + }), }; protected flags!: Flags; @@ -164,4 +175,39 @@ export abstract class BaseCommand extends Command { // Use oclif's error() for proper exit code and display this.error(err.message, {exit: err.exitCode ?? 1}); } + + /** + * Parse extra params from --extra-query and --extra-body flags. + * Returns undefined if no extra params are specified. + * + * @returns ExtraParamsConfig or undefined + */ + protected getExtraParams(): ExtraParamsConfig | undefined { + const extraQuery = this.flags['extra-query']; + const extraBody = this.flags['extra-body']; + + if (!extraQuery && !extraBody) { + return undefined; + } + + const config: ExtraParamsConfig = {}; + + if (extraQuery) { + try { + config.query = JSON.parse(extraQuery) as Record; + } catch { + this.error(`Invalid JSON for --extra-query: ${extraQuery}`); + } + } + + if (extraBody) { + try { + config.body = JSON.parse(extraBody) as Record; + } catch { + this.error(`Invalid JSON for --extra-body: ${extraBody}`); + } + } + + return config; + } } diff --git a/packages/b2c-tooling/src/cli/index.ts b/packages/b2c-tooling/src/cli/index.ts index d66d992d..add0cb47 100644 --- a/packages/b2c-tooling/src/cli/index.ts +++ b/packages/b2c-tooling/src/cli/index.ts @@ -5,6 +5,7 @@ export {OAuthCommand} from './oauth-command.js'; export {InstanceCommand} from './instance-command.js'; export {CartridgeCommand} from './cartridge-command.js'; export {MrtCommand} from './mrt-command.js'; +export {OdsCommand} from './ods-command.js'; // Config utilities export {loadConfig, findDwJson} from './config.js'; diff --git a/packages/b2c-tooling/src/cli/instance-command.ts b/packages/b2c-tooling/src/cli/instance-command.ts index 30f9fd95..82579a77 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, AuthMethod} from './config.js'; +import type {ResolvedConfig, LoadConfigOptions} from './config.js'; import {B2CInstance} from '../instance/index.js'; import type {AuthConfig} from '../auth/types.js'; import {t} from '../i18n/index.js'; @@ -83,7 +83,7 @@ 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, + authMethods: this.parseAuthMethods(), }; const config = loadConfig(flagConfig, options); diff --git a/packages/b2c-tooling/src/cli/oauth-command.ts b/packages/b2c-tooling/src/cli/oauth-command.ts index 0a53d4d0..27871d6a 100644 --- a/packages/b2c-tooling/src/cli/oauth-command.ts +++ b/packages/b2c-tooling/src/cli/oauth-command.ts @@ -5,6 +5,7 @@ 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'; +import {DEFAULT_ACCOUNT_MANAGER_HOST} from '../defaults.js'; /** * Base command for operations requiring OAuth authentication. @@ -40,15 +41,45 @@ 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)', + 'auth-methods': Flags.string({ + description: 'Allowed auth methods in priority order (comma-separated or multiple flags)', env: 'SFCC_AUTH_METHODS', multiple: true, options: ALL_AUTH_METHODS, helpGroup: 'AUTH', }), + 'account-manager-host': Flags.string({ + description: 'Account Manager hostname for OAuth', + env: 'SFCC_ACCOUNT_MANAGER_HOST', + default: DEFAULT_ACCOUNT_MANAGER_HOST, + helpGroup: 'AUTH', + }), }; + /** + * Parses auth methods from flags, supporting both comma-separated values and multiple flags. + * Returns methods in the order specified (priority order). + */ + protected parseAuthMethods(): AuthMethod[] | undefined { + const flagValues = this.flags['auth-methods'] as string[] | undefined; + if (!flagValues || flagValues.length === 0) { + return undefined; + } + + // Flatten comma-separated values while preserving order + const methods: AuthMethod[] = []; + for (const value of flagValues) { + const parts = value.split(',').map((s) => s.trim()); + for (const part of parts) { + if (part && ALL_AUTH_METHODS.includes(part as AuthMethod)) { + methods.push(part as AuthMethod); + } + } + } + + return methods.length > 0 ? methods : undefined; + } + protected override loadConfiguration(): ResolvedConfig { const options: LoadConfigOptions = { instance: this.flags.instance, @@ -59,7 +90,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, + authMethods: this.parseAuthMethods(), }; const config = loadConfig(flagConfig, options); @@ -72,6 +103,13 @@ export abstract class OAuthCommand extends BaseCommand return config; } + /** + * Gets the configured Account Manager host. + */ + protected get accountManagerHost(): string { + return this.flags['account-manager-host'] ?? DEFAULT_ACCOUNT_MANAGER_HOST; + } + /** * Gets an OAuth auth strategy based on allowed auth methods and available credentials. * @@ -82,6 +120,7 @@ export abstract class OAuthCommand extends BaseCommand */ protected getOAuthStrategy(): OAuthStrategy | ImplicitOAuthStrategy { const config = this.resolvedConfig; + const accountManagerHost = this.accountManagerHost; // Default to client-credentials and implicit if no methods specified const allowedMethods = config.authMethods || (['client-credentials', 'implicit'] as AuthMethod[]); @@ -93,6 +132,7 @@ export abstract class OAuthCommand extends BaseCommand clientId: config.clientId, clientSecret: config.clientSecret, scopes: config.scopes, + accountManagerHost, }); } break; @@ -102,6 +142,7 @@ export abstract class OAuthCommand extends BaseCommand return new ImplicitOAuthStrategy({ clientId: config.clientId, scopes: config.scopes, + accountManagerHost, }); } break; diff --git a/packages/b2c-tooling/src/cli/ods-command.ts b/packages/b2c-tooling/src/cli/ods-command.ts new file mode 100644 index 00000000..3467854d --- /dev/null +++ b/packages/b2c-tooling/src/cli/ods-command.ts @@ -0,0 +1,80 @@ +import {Command, Flags} from '@oclif/core'; +import {OAuthCommand} from './oauth-command.js'; +import {createOdsClient, type OdsClient} from '../clients/ods.js'; +import {DEFAULT_ODS_HOST} from '../defaults.js'; + +/** + * Base command for ODS (On-Demand Sandbox) operations. + * Use this for commands that interact with the Developer Sandbox API + * (sandbox creation, deletion, start/stop, realm info, etc.) + * + * Environment variables: + * - SFCC_SANDBOX_API_HOST: ODS API hostname + * - Plus all from OAuthCommand (SFCC_CLIENT_ID, SFCC_CLIENT_SECRET) + * + * Provides: + * - Host configuration flag with env var support + * - Typed ODS API client via `this.odsClient` + * + * @example + * export default class MySandboxCommand extends OdsCommand { + * async run(): Promise { + * const { data } = await this.odsClient.GET('/me', {}); + * console.log('User:', data?.data?.user?.name); + * } + * } + */ +export abstract class OdsCommand extends OAuthCommand { + static baseFlags = { + ...OAuthCommand.baseFlags, + 'sandbox-api-host': Flags.string({ + description: 'ODS API hostname', + env: 'SFCC_SANDBOX_API_HOST', + default: DEFAULT_ODS_HOST, + // helpGroup: 'ODS', + }), + }; + + private _odsClient?: OdsClient; + + /** + * Gets the ODS API client for this command. + * + * The client is lazily created using the configured host and OAuth credentials. + * It provides typed methods for all ODS API operations. + * + * @example + * // Get user info + * const { data } = await this.odsClient.GET('/me', {}); + * + * // List sandboxes + * const { data } = await this.odsClient.GET('/sandboxes', {}); + * + * // Create a sandbox operation + * const { data } = await this.odsClient.POST('/sandboxes/{sandboxId}/operations', { + * params: { path: { sandboxId: 'uuid' } }, + * body: { operation: 'start' } + * }); + */ + protected get odsClient(): OdsClient { + if (!this._odsClient) { + this.requireOAuthCredentials(); + const authStrategy = this.getOAuthStrategy(); + this._odsClient = createOdsClient( + { + host: this.odsHost, + extraParams: this.getExtraParams(), + }, + authStrategy, + ); + } + return this._odsClient; + } + + /** + * Gets the configured ODS API host. + */ + protected get odsHost(): string { + return this.flags['sandbox-api-host'] ?? DEFAULT_ODS_HOST; + } +} diff --git a/packages/b2c-tooling/src/clients/index.ts b/packages/b2c-tooling/src/clients/index.ts index d30122b7..ec14d9fe 100644 --- a/packages/b2c-tooling/src/clients/index.ts +++ b/packages/b2c-tooling/src/clients/index.ts @@ -2,13 +2,14 @@ * API clients for B2C Commerce operations. * * This module provides typed client classes for interacting with B2C Commerce - * APIs including WebDAV, OCAPI, and SCAPI. + * APIs including WebDAV, OCAPI, SCAPI, and ODS. * * ## Available Clients * * - {@link WebDavClient} - File operations via WebDAV * - {@link OcapiClient} - Data API operations via OCAPI (openapi-fetch Client) * - {@link SlasClient} - SLAS Admin API for managing tenants and clients + * - {@link OdsClient} - On-Demand Sandbox API for managing developer sandboxes * * ## Usage * @@ -86,9 +87,9 @@ * baseUrl: `https://${config.host}/api/v1`, * }); * - * // Add middleware - use a short identifier for log prefixes - * client.use(createLoggingMiddleware('MYAPI')); + * // Add middleware - auth first, logging last (so logging sees complete request) * client.use(createAuthMiddleware(auth)); + * client.use(createLoggingMiddleware('MYAPI')); * * return client; * } @@ -108,7 +109,8 @@ export {WebDavClient} from './webdav.js'; export type {PropfindEntry} from './webdav.js'; -export {createAuthMiddleware, createLoggingMiddleware} from './middleware.js'; +export {createAuthMiddleware, createLoggingMiddleware, createExtraParamsMiddleware} from './middleware.js'; +export type {ExtraParamsConfig} from './middleware.js'; export {createOcapiClient} from './ocapi.js'; export type { @@ -128,3 +130,13 @@ export type { paths as SlasPaths, components as SlasComponents, } from './slas-admin.js'; + +export {createOdsClient} from './ods.js'; +export type { + OdsClient, + OdsClientConfig, + OdsError, + OdsResponse, + paths as OdsPaths, + components as OdsComponents, +} from './ods.js'; diff --git a/packages/b2c-tooling/src/clients/middleware.ts b/packages/b2c-tooling/src/clients/middleware.ts index 447eedbf..718c4ab5 100644 --- a/packages/b2c-tooling/src/clients/middleware.ts +++ b/packages/b2c-tooling/src/clients/middleware.ts @@ -10,6 +10,16 @@ import type {Middleware} from 'openapi-fetch'; import type {AuthStrategy} from '../auth/types.js'; import {getLogger} from '../logging/logger.js'; +/** + * Configuration for extra parameters middleware. + */ +export interface ExtraParamsConfig { + /** Extra query parameters to add to the URL */ + query?: Record; + /** Extra body fields to merge into JSON request bodies */ + body?: Record; +} + /** * Converts Headers to a plain object for logging. */ @@ -107,3 +117,87 @@ export function createLoggingMiddleware(prefix?: string): Middleware { }, }; } + +/** + * Creates middleware that adds extra query parameters and/or body fields to requests. + * + * This is useful for internal/power-user scenarios where you need to pass + * parameters that aren't in the typed OpenAPI schema. + * + * @param config - Configuration with extra query and/or body params + * @returns Middleware that adds extra params to requests + * + * @example + * ```typescript + * const client = createOdsClient(config, auth); + * client.use(createExtraParamsMiddleware({ + * query: { debug: 'true', internal_flag: '1' }, + * body: { _internal: { trace: true } } + * })); + * ``` + */ +export function createExtraParamsMiddleware(config: ExtraParamsConfig): Middleware { + const logger = getLogger(); + + return { + async onRequest({request}) { + let modifiedRequest = request; + + // Add extra query parameters + if (config.query && Object.keys(config.query).length > 0) { + const url = new URL(request.url); + for (const [key, value] of Object.entries(config.query)) { + if (value !== undefined) { + url.searchParams.set(key, String(value)); + } + } + logger.trace( + {extraQuery: config.query, originalUrl: request.url, newUrl: url.toString()}, + '[ExtraParams] Adding extra query params to URL', + ); + modifiedRequest = new Request(url.toString(), { + method: request.method, + headers: request.headers, + body: request.body, + duplex: request.body ? 'half' : undefined, + } as RequestInit); + } + + // Merge extra body fields for JSON requests + if (config.body && Object.keys(config.body).length > 0) { + const contentType = modifiedRequest.headers.get('content-type'); + if (contentType?.includes('application/json') && modifiedRequest.body) { + const clonedRequest = modifiedRequest.clone(); + const originalBody = await clonedRequest.text(); + try { + const parsedBody = JSON.parse(originalBody) as Record; + const mergedBody = {...parsedBody, ...config.body}; + logger.trace( + {originalBody: parsedBody, extraBody: config.body, mergedBody}, + '[ExtraParams] Merging extra body fields into request', + ); + modifiedRequest = new Request(modifiedRequest.url, { + method: modifiedRequest.method, + headers: modifiedRequest.headers, + body: JSON.stringify(mergedBody), + }); + } catch { + logger.warn('[ExtraParams] Could not parse request body as JSON, skipping body merge'); + } + } else if (!modifiedRequest.body) { + // No existing body, create one with extra fields + logger.trace({body: config.body}, '[ExtraParams] Creating new body with extra fields'); + const headers = new Headers(modifiedRequest.headers); + headers.set('content-type', 'application/json'); + modifiedRequest = new Request(modifiedRequest.url, { + method: modifiedRequest.method, + headers, + body: JSON.stringify(config.body), + }); + } + } + + return modifiedRequest; + }, + }; +} diff --git a/packages/b2c-tooling/src/clients/ocapi.ts b/packages/b2c-tooling/src/clients/ocapi.ts index d93b47aa..09f52a81 100644 --- a/packages/b2c-tooling/src/clients/ocapi.ts +++ b/packages/b2c-tooling/src/clients/ocapi.ts @@ -102,8 +102,9 @@ export function createOcapiClient( baseUrl: `https://${hostname}/s/-/dw/data/${apiVersion}`, }); - client.use(createLoggingMiddleware('OCAPI')); + // Middleware order: auth → logging (logging sees fully modified request) client.use(createAuthMiddleware(auth)); + client.use(createLoggingMiddleware('OCAPI')); return client; } diff --git a/packages/b2c-tooling/src/clients/ods.generated.ts b/packages/b2c-tooling/src/clients/ods.generated.ts new file mode 100644 index 00000000..c2ab301d --- /dev/null +++ b/packages/b2c-tooling/src/clients/ods.generated.ts @@ -0,0 +1,2549 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Retrieve API information. + * @description Return API version information. + */ + get: operations["getApiInfo"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/me": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Retrieve user information. + * @description Return information about the user interacting with the API. + */ + get: operations["getUserInfo"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/system": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Retrieve system information + * @description Returns information about the system, the user is interacting with. + */ + get: operations["getSystemInfo"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/realms/{realm}/system": { + parameters: { + query?: never; + header?: never; + path: { + /** @description The four-letter ID of the realm. */ + realm: components["parameters"]["realmParam"]; + }; + cookie?: never; + }; + /** + * Retrieve system information + * @description Returns information about the system, the user is interacting with. + */ + get: operations["getRealmSystemInfo"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/realms/{realm}": { + parameters: { + query?: { + /** @description Additional information, which should be shown in the realm query. Available options are: [configuration,usage, accountdetails]. */ + expand?: ("configuration" | "usage" | "accountdetails")[]; + }; + header?: never; + path: { + /** @description The four-letter ID of the realm. */ + realm: components["parameters"]["realmParam"]; + }; + cookie?: never; + }; + /** + * Show realm information. + * @description Return metadata about a realm. + */ + get: operations["getRealm"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/realms/{realm}/configuration": { + parameters: { + query?: never; + header?: never; + path: { + /** @description The four-letter ID of the realm. */ + realm: components["parameters"]["realmParam"]; + }; + cookie?: never; + }; + /** + * Show realm configuration. + * @description Return the current configuration values of the realm. + */ + get: operations["getRealmConfiguration"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** + * Update realm configuration. + * @description Update the customizable configuration of a realm. Note that the internal time format in weekday schedules is [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Times). + */ + patch: operations["patchRealmConfiguration"]; + trace?: never; + }; + "/realms/{realm}/usage": { + parameters: { + query?: { + /** @description Earliest date for which data is in the response. Thirty days in the past by default. Format is ISO 8601. */ + from?: components["parameters"]["fromParam"]; + /** @description Latest date for which data is included in the response. Today's date by default. Format is ISO 8601. */ + to?: components["parameters"]["toParam"]; + /** @description Field to check whether detailed report is to be retrieved, by default detailed report will not be pulled */ + detailedReport?: components["parameters"]["detailedReportParam"]; + /** @description Granularity of usage to be included in the response. By default, granular usage is not returned. */ + granularity?: components["parameters"]["granularityParam"]; + }; + header?: never; + path: { + /** @description The four-letter ID of the realm. */ + realm: components["parameters"]["realmParam"]; + }; + cookie?: never; + }; + /** + * Show usage information for realm. + * @description Return information about the realm's usage. + */ + get: operations["getRealmUsage"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/realms/usages": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Show usage information for given realms. + * @description Update the customizable configuration of a realm. Note that the internal time format in weekday schedules is [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Times). + */ + post: operations["searchRealmUsage"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/sandboxes": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List sandboxes. + * @description Return all sandboxes of a realm. + */ + get: operations["getSandboxes"]; + put?: never; + /** + * Create sandbox. + * @description Create a new sandbox within the realm. + */ + post: operations["createSandbox"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/sandboxes/{sandboxId}": { + parameters: { + query?: never; + header?: never; + path: { + /** @description The sandbox UUID. */ + sandboxId: components["parameters"]["sandboxIdParam"]; + }; + cookie?: never; + }; + /** + * Retrieve sandbox information. + * @description Return details on a specific sandbox in a realm. + */ + get: operations["getSandbox"]; + put?: never; + post?: never; + /** + * Delete sandbox. + * @description Delete a specific sandbox in a realm. + */ + delete: operations["deleteSandbox"]; + options?: never; + head?: never; + /** + * Update sandbox. + * @description Update a sandbox. + */ + patch: operations["patchSandbox"]; + trace?: never; + }; + "/sandboxes/{sandboxId}/aliases": { + parameters: { + query?: never; + header?: never; + path: { + /** @description The sandbox UUID. */ + sandboxId: components["parameters"]["sandboxIdParam"]; + }; + cookie?: never; + }; + /** + * Read all sandbox aliases + * @description Retrieve a list of all past and present operations on a sandbox within the realm. + */ + get: operations["getAliases"]; + put?: never; + /** + * Create sandbox alias. + * @description Create a new sandbox alias. + */ + post: operations["createAlias"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/sandboxes/{sandboxId}/aliases/{sandboxAliasId}": { + parameters: { + query?: never; + header?: never; + path: { + /** @description The sandbox UUID. */ + sandboxId: components["parameters"]["sandboxIdParam"]; + /** @description The sandbox alias UUID. */ + sandboxAliasId: components["parameters"]["sandboxAliasIdParam"]; + }; + cookie?: never; + }; + /** + * Read Alias configuration + * @description Retrieves a dedicated alias for the sandbox. Can be called without authentication to get cookie values for the alias. + */ + get: operations["getAlias"]; + put?: never; + post?: never; + /** + * Delete Alias configuration + * @description Deletes a dedicated alias configuration for a sandbox. + */ + delete: operations["deleteAlias"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/sandboxes/{sandboxId}/operations": { + parameters: { + query?: never; + header?: never; + path: { + /** @description The sandbox UUID. */ + sandboxId: components["parameters"]["sandboxIdParam"]; + }; + cookie?: never; + }; + /** + * List sandbox operations. + * @description Retrieve a list of all past and present operations on a sandbox within the realm. + */ + get: operations["getSandboxOperations"]; + put?: never; + /** + * Run sandbox operation. + * @description Request an operation on a sandbox within the realm. + */ + post: operations["createSandboxOperation"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/sandboxes/{sandboxId}/operations/{operationId}": { + parameters: { + query?: never; + header?: never; + path: { + /** @description The sandbox UUID. */ + sandboxId: components["parameters"]["sandboxIdParam"]; + /** @description The operation UUID. */ + operationId: components["parameters"]["operationIdParam"]; + }; + cookie?: never; + }; + /** + * Retrieve sandbox operation. + * @description Return details of a sandbox operation that was recently submitted, is currently in progress, or has already finished. + */ + get: operations["getSandboxOperation"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/sandboxes/{sandboxId}/settings": { + parameters: { + query?: never; + header?: never; + path: { + /** @description The sandbox UUID. */ + sandboxId: components["parameters"]["sandboxIdParam"]; + }; + cookie?: never; + }; + /** + * Show sandbox settings. + * @description Return all settings of the sandbox. + */ + get: operations["getSandboxSettings"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/sandboxes/{sandboxId}/usage": { + parameters: { + query?: { + /** @description Earliest date for which data is in the response. Thirty days in the past by default. Format is ISO 8601. */ + from?: components["parameters"]["fromParam"]; + /** @description Latest date for which data is included in the response. Today's date by default. Format is ISO 8601. */ + to?: components["parameters"]["toParam"]; + }; + header?: never; + path: { + /** @description The sandbox UUID. */ + sandboxId: components["parameters"]["sandboxIdParam"]; + }; + cookie?: never; + }; + /** + * Show sandbox usage. + * @description Return information on sandbox usage. + */ + get: operations["getSandboxUsage"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/sandboxes/{sandboxId}/storage": { + parameters: { + query?: never; + header?: never; + path: { + /** @description The sandbox UUID. */ + sandboxId: components["parameters"]["sandboxIdParam"]; + }; + cookie?: never; + }; + /** + * Show sandbox storage + * @description Return information on sandbox storage capacity for a currently running sandbox. + */ + get: operations["getSandboxStorage"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + Response: { + /** + * @description Type of response object. + * @enum {string} + */ + kind: "ApiVersion" | "UserInfo" | "SystemInfo" | "Realm" | "RealmConfiguration" | "RealmUsage" | "MultiRealmUsage" | "Sandbox" | "SandboxList" | "SandboxAlias" | "SandboxAliasList" | "SandboxSettings" | "SandboxUsage" | "SandboxStorage" | "SandboxOperationList" | "Status"; + /** + * Format: int32 + * @description Response code sent along with the status. + */ + code: number; + }; + StatusResponse: components["schemas"]["Response"] & { + /** + * @description String with value 'Success' or 'Failure' to indicate request outcome. + * @enum {string} + */ + status: "Success" | "Failure"; + }; + PagedResponse: components["schemas"]["StatusResponse"] & { + metadata?: components["schemas"]["PagingMetadata"]; + }; + PagingMetadata: { + /** + * Format: int32 + * @description Index of the current page. + */ + page?: number; + /** + * Format: int32 + * @description Maximum count of elements per page. + */ + perPage?: number; + /** + * Format: int32 + * @description Total count of pages. + */ + pageCount?: number; + /** + * Format: int64 + * @description Total count of elements. + */ + totalCount?: number; + links?: components["schemas"]["PagingLinks"]; + }; + PagingLinks: { + /** @description Relative link to this page. */ + self?: string; + /** @description Relative link to the first page. */ + first?: string; + /** @description Relative link to the previous page. 'null' if the current page is the first page. */ + previous?: string; + /** @description Relative link to the next page. 'null' if the current page is the last page. */ + next?: string; + /** @description Relative link to the last page. */ + last?: string; + }; + RealmResponse: components["schemas"]["StatusResponse"] & { + data?: components["schemas"]["RealmModel"]; + }; + RealmModel: { + /** @description GUID of the realm in the system. */ + id: string; + /** @description Human-readable four-letter ID of the realm. */ + name?: string; + /** @description Flag indicating whether the realm is enabled for any operations. */ + enabled?: boolean; + usage?: components["schemas"]["RealmUsageSummaryModel"]; + configuration?: components["schemas"]["RealmConfigurationModel"]; + accountdetails?: components["schemas"]["AccountDetailsModel"]; + }; + RealmUsageSummaryModel: { + /** + * Format: int64 + * @description Number of currently active sandboxes for a realm. + * @example 42 + */ + activeSandboxes: number; + }; + RealmConfigurationResponse: components["schemas"]["StatusResponse"] & { + data?: components["schemas"]["RealmConfigurationModel"]; + }; + /** @description Object that holds an integer-based configuration property. A zero value means "unlimited". */ + ConfigurationIntegerValue: { + /** + * Format: int32 + * @description Fixed value for this configuration property. You can't use this along with a maximum or default value. + */ + fixedValue?: number; + /** + * Format: int32 + * @description Maximum value for this property. + */ + maximum?: number; + /** + * Format: int32 + * @description Default value for this property. + */ + defaultValue?: number; + }; + /** @description A schedule definition for a dedicated time on specific weekdays. */ + WeekdaySchedule: { + /** @description List of weekdays, where the action should take place */ + weekdays?: ("MONDAY" | "TUESDAY" | "WEDNESDAY" | "THURSDAY" | "FRIDAY" | "SATURDAY" | "SUNDAY")[]; + /** + * @description Time (with timezone) where the action should take place on the specified weekdays. Time format is [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Times). If no time zone is given, the timezone defaults to GMT. + * @example 20:10:00Z + */ + time?: string; + }; + /** + * @description Configuration object related to sandboxes of a realm. + * @example { + * "limitsEnabled": true, + * "totalNumberOfSandboxes": 50, + * "sandboxTTL": { + * "maximum": 240, + * "defaultValue": 8 + * }, + * "localUsersAllowed": false + * } + */ + RealmSandboxConfigurationModel: { + /** @description Flag indicating whether sandbox specific limits are enforced for the realm. */ + limitsEnabled: boolean; + /** @description Total number of sandboxes (regardless of state) that the realm can hold. */ + totalNumberOfSandboxes: number; + sandboxTTL: components["schemas"]["ConfigurationIntegerValue"]; + /** @description Flag indicating whether users outside the Account Manager are allowed. */ + localUsersAllowed: boolean; + }; + /** + * @description Update data for configuration data related to sandboxes of a realm. The time formats within the weekday schedules have to be passed in [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Times) format. + * @example { + * "sandboxTTL": { + * "maximum": 240, + * "defaultValue": 24 + * }, + * "startScheduler": { + * "weekdays": [ + * "MONDAY", + * "TUESDAY", + * "WEDNESDAY", + * "THURSDAY", + * "FRIDAY" + * ], + * "time": "08:00:00+03:00" + * }, + * "stopScheduler": { + * "weekdays": [ + * "MONDAY", + * "TUESDAY", + * "WEDNESDAY", + * "THURSDAY", + * "FRIDAY" + * ], + * "time": "19:00:00Z" + * } + * } + */ + RealmSandboxConfigurationUpdateModel: { + sandboxTTL?: components["schemas"]["ConfigurationIntegerValue"]; + startScheduler?: unknown; + stopScheduler?: unknown; + } | null; + /** + * @description Configuration object related to requests targeting the sandboxes of a realm. + * @example { + * "enforced": true, + * "maxRate": 50000, + * "timePeriod": 60 + * } + */ + RealmRequestConfigurationModel: { + /** @description If enabled, rate limiting is active. */ + enforced: boolean; + /** @description Maximum requests allowed per time period. */ + maxRate?: number; + /** @description Number of seconds during which to count requests. */ + timePeriod?: number; + }; + RealmConfigurationModel: { + /** + * @example [ + * "email1@example.com", + * "email2@example.com" + * ] + */ + emails?: string[]; + sandbox?: components["schemas"]["RealmSandboxConfigurationModel"]; + requests?: components["schemas"]["RealmRequestConfigurationModel"]; + startScheduler?: components["schemas"]["WeekdaySchedule"]; + stopScheduler?: components["schemas"]["WeekdaySchedule"]; + }; + RealmConfigurationUpdateRequestModel: { + /** + * @example [ + * "email1@example.com", + * "email2@example.com" + * ] + */ + emails?: string[]; + sandbox?: components["schemas"]["RealmSandboxConfigurationUpdateModel"]; + }; + RealmUsageResponse: components["schemas"]["StatusResponse"] & { + data?: components["schemas"]["RealmUsageModel"]; + }; + MultiRealmUsageModel: { + /** @description GUID of the realm in the system. */ + realmName: string; + realmUsage?: components["schemas"]["RealmUsageModel"]; + /** @description Error while getting usage. */ + error?: string; + }; + RealmUsageModel: { + /** @description GUID of the realm in the system. */ + id: string; + /** @description account/SFID of the realm in clusterstate table or org62 Tenant table */ + accountId?: string; + /** + * Format: int64 + * @description Total number of sandboxes created during the requested timeframe (by default, the previous 30 days). + * @example 93 + */ + createdSandboxes?: number; + /** + * Format: int64 + * @description Total number of sandboxes active during the requested timeframe (by default, the previous 30 days). + * @example 128 + */ + activeSandboxes?: number; + /** + * Format: int64 + * @description Total number of sandboxes deleted during the requested timeframe (by default, the previous 30 days). + * @example 86 + */ + deletedSandboxes?: number; + /** + * Format: int64 + * @description Total number of seconds sandboxes ran during the requested timeframe (by default, the previous 30 days). + * @example 360000 + */ + sandboxSeconds?: number; + minutesUpByProfile?: { + profile?: components["schemas"]["SandboxResourceProfile"]; + /** + * Format: int64 + * @description How many minutes sandboxes of this profile type were running during the report timeframe. + */ + minutes?: number; + }[]; + /** + * Format: int64 + * @description Sum of minutes sandboxes in this realm were running during the requested timeframe (by default, the previous 30 days). + * @example 360000 + */ + minutesUp?: number; + /** + * Format: int64 + * @description Sum of minutes sandboxes in this realm were not running during the requested timeframe (by default, the previous 30 days). + * @example 180000 + */ + minutesDown?: number; + sandboxDetails?: components["schemas"]["SandboxInfo"][]; + granularUsage?: components["schemas"]["GranularUsage"][]; + }; + AccountDetailsModel: { + /** + * @description Account name. + * @example Disney + */ + accountName?: string; + /** + * Format: double + * @description Total Credit Balance left. + * @example 93.234 + */ + creditBalance?: number; + }; + MultiRealmUsageRequest: { + /** + * Format: date + * @description Time the sandbox was started. + */ + from?: string; + /** + * Format: date + * @description Time the sandbox was stopped. If the sandbox is still running, this value will not exist for the last block. + */ + to?: string; + realms?: string[]; + /** + * @description Field to check whether detailed report is to be retrieved, by default detailed report will not be pulled. + * @default false + * @enum {boolean} + */ + detailedReport: false | true; + }; + MultiRealmUsageResponse: { + data?: components["schemas"]["MultiRealmUsageModel"][]; + } & (components["schemas"]["StatusResponse"] & Record); + SandboxListResponse: components["schemas"]["StatusResponse"] & { + data?: components["schemas"]["SandboxModel"][]; + }; + SandboxResponse: components["schemas"]["StatusResponse"] & { + data?: components["schemas"]["SandboxModel"]; + }; + /** @description Shows all filesystem storages and how much space is left on them. */ + SandboxStorageModel: { + [key: string]: components["schemas"]["StorageUsageModel"]; + }; + /** @description Represents a single filesystem storage unit with its available space. */ + StorageUsageModel: { + /** + * Format: int64 + * @description Total available space in MB. + */ + spaceTotal?: number; + /** + * Format: int64 + * @description Used space in MB. + */ + spaceUsed?: number; + /** + * Format: int32 + * @description Used space in percent, compared to total space. + */ + percentageUsed?: number; + }; + SandboxModel: { + id?: string; + realm?: string; + emails?: string[]; + /** @description Flag indicating whether the sandbox is enabled for any operations. */ + enabled?: boolean; + instance?: string; + /** + * SandboxModelVersions + * @description Versions of the components that make up the sandbox. + */ + versions?: { + /** @description Version of the commerce application. */ + app?: string; + /** @description Version of the web proxy. */ + web?: string; + }; + /** @description Defaults to false. If set to true, the sandbox is covered by automatic start/stop actions, which can be set to a dedicated time via realm- configuration API. */ + autoScheduled?: boolean; + /** @description Defaults to false. If set to true, analytics will be enabled in ODS. */ + analyticsEnabled?: boolean; + resourceProfile?: components["schemas"]["SandboxResourceProfile"]; + state?: components["schemas"]["SandboxState"]; + /** Format: date-time */ + createdAt?: string; + createdBy?: string; + /** + * Format: date-time + * @description Time when the delete operation was created. + */ + deletedAt?: string; + /** @description User who requested the sandbox deletion. */ + deletedBy?: string; + /** Format: date-time */ + eol?: string; + tags?: string[]; + hostName?: string; + /** @description Set of named links for accessing the sandbox. */ + links?: { + /** @description Fully qualified URL of the sandbox Business Manager web app. */ + bm?: string; + /** @description Fully qualified URL of OCAPI data API (excluding version selector). */ + ocapi?: string; + /** @description Fully qualified WebDAV URL for accessing import and export files. */ + impex?: string; + /** @description Fully qualified WebDAV URL for accessing code. */ + code?: string; + /** @description Fully qualified WebDAV URL for accessing log files. */ + logs?: string; + }; + startScheduler?: components["schemas"]["WeekdaySchedule"]; + stopScheduler?: components["schemas"]["WeekdaySchedule"]; + }; + GranularUsage: { + /** @description start of the usage being returned */ + usageDate?: string; + /** + * Format: double + * @description Credits consumed when sandboxes were up during the requested timeframe. + * @example 3600.001 + */ + creditsUp?: number; + /** + * Format: double + * @description Credits consumed when sandboxes were down during the requested timeframe. + * @example 1440.001 + */ + creditsDown?: number; + /** + * Format: int64 + * @description Minutes sandboxes were up during the requested timeframe. + * @example 360000 + */ + minutesUp?: number; + /** + * Format: int64 + * @description Minutes sandboxes were down during the requested timeframe. + * @example 180000 + */ + minutesDown?: number; + }; + SandboxInfo: { + realm?: string; + resourceProfile?: components["schemas"]["SandboxResourceProfile"]; + /** Format: date-time */ + createdAt?: string; + /** + * Format: date-time + * @description Time when the delete operation was created. + */ + deletedAt?: string; + /** @description Name of the sandbox */ + name?: string; + /** @description instanceId of the sandbox */ + instanceId?: string; + minutesUpByProfile?: { + profile?: components["schemas"]["SandboxResourceProfile"]; + /** + * Format: int64 + * @description How many minutes sandboxes of this profile type were running during the report timeframe. + */ + minutes?: number; + }[]; + /** + * Format: int64 + * @description Minutes sandbox in this realm was running during the requested timeframe (by default, the previous 30 days). + * @example 360000 + */ + minutesUp?: number; + /** + * Format: int64 + * @description Minutes sandbox in this realm was not running during the requested timeframe (by default, the previous 30 days). + * @example 180000 + */ + minutesDown?: number; + /** @description Defaults to false. If set to true, the sandbox is covered by automatic start/stop actions, which can be set to a dedicated time via realm- configuration API.: */ + autoScheduled?: boolean; + startScheduler?: components["schemas"]["WeekdaySchedule"]; + stopScheduler?: components["schemas"]["WeekdaySchedule"]; + /** @description Cluster where sandbox resides. */ + clusterName?: string; + }; + /** @enum {string} */ + SandboxState: "new" | "creating" | "starting" | "started" | "stopping" | "stopped" | "deleting" | "deleted" | "resetting" | "failed" | "unknown" | "upgrading"; + /** + * @description Determines the resource allocation for the sandbox, "medium" is the default. Be careful, more powerful profiles consume more credits. + * @enum {string} + */ + SandboxResourceProfile: "medium" | "large" | "xlarge" | "xxlarge"; + SandboxOperationRequestModel: { + /** @enum {string} */ + operation: "start" | "stop" | "restart" | "reset"; + }; + SandboxAliasResponse: components["schemas"]["StatusResponse"] & { + data?: components["schemas"]["SandboxAliasModel"]; + }; + SandboxAliasListResponse: components["schemas"]["StatusResponse"] & { + data?: components["schemas"]["SandboxAliasModel"][]; + }; + SandboxAliasModel: { + /** + * Format: uuid + * @description The sandbox alias UUID. + */ + readonly id?: string; + /** + * @description The alias name. + * @example www.example.com + */ + name: string; + /** + * @description Define if it's a unique configuration + * @example false + */ + unique?: boolean; + /** + * @description Request a valid certificate to be generated on the fly through Lets Encrypt. This action consumes certificate requests from the domain quota imposed by Let's Encrypt, please read the Alias documentation carefully. + * @example false + */ + requestLetsEncryptCertificate?: boolean; + /** + * Format: uuid + * @description The UUID of the sandbox the sandbox alias is pointing to. + */ + readonly sandboxId?: string; + /** @description The cookie required for each request to this alias. */ + readonly cookie?: { + name: string; + value: string; + path?: string; + domain?: string; + }; + /** @description The link that can be used to save the required cookie for this alias in the browser. */ + readonly registration?: string; + /** @description The verification code to be added as TXT record in the DNS */ + readonly domainVerificationRecord?: string; + /** + * @description The status of the alias creation process + * @enum {string} + */ + readonly status?: "pending" | "verified"; + }; + SandboxOperationResponse: components["schemas"]["StatusResponse"] & { + data?: components["schemas"]["SandboxOperationModel"]; + }; + SandboxOperationListResponse: components["schemas"]["PagedResponse"] & { + data?: components["schemas"]["SandboxOperationModel"][]; + }; + SandboxSettingsResponse: components["schemas"]["StatusResponse"] & { + data?: components["schemas"]["SandboxSettings"]; + }; + SandboxStorageResponse: components["schemas"]["StatusResponse"] & { + data?: components["schemas"]["SandboxStorageModel"]; + }; + SandboxUsageResponse: components["schemas"]["StatusResponse"] & { + data?: components["schemas"]["SandboxUsageModel"]; + }; + SandboxUsageModel: { + id: string; + /** + * Format: int64 + * @description Total number of seconds during which the sandbox ran. + */ + sandboxSeconds?: number; + minutesUpByProfile?: { + profile?: components["schemas"]["SandboxResourceProfile"]; + /** + * Format: int64 + * @description How many minutes sandboxes of this profile type were running during the report timeframe. + */ + minutes?: number; + }[]; + /** + * Format: int64 + * @description Sum of minutes sandboxes in this realm were running during the requested timeframe (by default, the previous 30 days). + * @example 360000 + */ + minutesUp?: number; + /** + * Format: int64 + * @description Sum of minutes sandboxes in this realm were not running during the requested timeframe (by default, the previous 30 days). + * @example 180000 + */ + minutesDown?: number; + granularUsage?: components["schemas"]["GranularUsage"][]; + /** @description List of blocks, which describe the separate uptimes of a sandbox */ + history?: { + /** + * Format: date-time + * @description Time the sandbox was started. + */ + from: string; + /** + * Format: date-time + * @description Time the sandbox was stopped. If the sandbox is still running, this value will not exist for the last block. + */ + to?: string; + /** + * Format: int64 + * @description Number of seconds that the sandbox was running for this block. + */ + sandboxSeconds?: number; + resourceProfile?: components["schemas"]["SandboxResourceProfile"]; + /** @description This property is set to true if the block exceeds the given timeframe and was therefore trimmed. */ + exceedsTimeframe?: boolean; + /** @description Cluster where sandbox resides. */ + clusterName?: string; + }[]; + }; + SandboxOperationModel: { + id: string; + /** @enum {string} */ + operation: "start" | "stop" | "restart" | "reset" | "create" | "delete" | "upgrade"; + /** Format: date-time */ + createdAt?: string; + operationBy?: string; + /** @enum {string} */ + operationState: "pending" | "running" | "finished"; + sandboxState?: components["schemas"]["SandboxState"]; + /** + * @description Indicates whether the operation finished successfully ('Success') or not ('Failure'). + * @enum {string} + */ + status?: "success" | "failure"; + }; + /** + * @example { + * "realm": "", + * "emails": [ + * "email1@example.com", + * "email2@example.com" + * ], + * "ttl": 24, + * "autoScheduled": false, + * "tags": [ + * "" + * ], + * "analyticsEnabled": false, + * "startScheduler": { + * "weekdays": [ + * "MONDAY", + * "TUESDAY", + * "WEDNESDAY", + * "THURSDAY", + * "FRIDAY" + * ], + * "time": "08:00:00+03:00" + * }, + * "stopScheduler": { + * "weekdays": [ + * "MONDAY", + * "TUESDAY", + * "WEDNESDAY", + * "THURSDAY", + * "FRIDAY" + * ], + * "time": "19:00:00Z" + * }, + * "resourceProfile": "medium", + * "settings": { + * "ocapi": [ + * { + * "client_id": "", + * "resources": [ + * { + * "resource_id": "/**", + * "methods": [ + * "get", + * "post", + * "put", + * "patch", + * "delete" + * ], + * "read_attributes": "(**)", + * "write_attributes": "" + * } + * ] + * } + * ], + * "webdav": [ + * { + * "client_id": "", + * "permissions": [ + * { + * "path": "/cartridges", + * "operations": [ + * "read_write" + * ] + * }, + * { + * "path": "/impex", + * "operations": [ + * "read_write" + * ] + * } + * ] + * } + * ] + * } + * } + */ + SandboxProvisioningRequestModel: { + realm: string; + emails?: string[]; + /** + * Format: int32 + * @description Number of hours the sandbox will live (must adhere to the maximum TTL quotas). If set to 0 or less, the sandbox will have an infinite lifetime. + */ + ttl?: number; + /** @description Defaults to false. If set to true, the sandbox is covered by automatic start/stop actions, which can be set to a dedicated time via realm- configuration API. */ + autoScheduled?: boolean; + /** + * @description Defaults to false. If set to true, analytics will be enabled in ODS. + * @default false + */ + analyticsEnabled: boolean; + tags?: string[]; + startScheduler?: unknown; + stopScheduler?: unknown; + resourceProfile?: components["schemas"]["SandboxResourceProfile"]; + settings?: components["schemas"]["SandboxSettings"]; + }; + /** + * @example { + * "emails": [ + * "email1@example.com", + * "email2@example.com" + * ], + * "ttl": null, + * "autoScheduled": false, + * "resourceProfile": "desiredProfile", + * "tags": [ + * "" + * ], + * "startScheduler": { + * "weekdays": [ + * "MONDAY", + * "TUESDAY", + * "WEDNESDAY", + * "THURSDAY", + * "FRIDAY" + * ], + * "time": "08:00:00+03:00" + * }, + * "stopScheduler": { + * "weekdays": [ + * "MONDAY", + * "TUESDAY", + * "WEDNESDAY", + * "THURSDAY", + * "FRIDAY" + * ], + * "time": "19:00:00Z" + * } + * } + */ + SandboxUpdateRequestModel: { + emails?: string[]; + /** + * Format: int32 + * @description Number of hours added to the sandbox lifetime (must, together with previous extensions, adhere to the maximum TTL configuration). If set to 0 or less, the sandbox will have an infinite lifetime. + */ + ttl?: number; + resourceProfile?: components["schemas"]["SandboxResourceProfile"]; + /** @description If set to true, this sandbox will be captured by automated start-/stop -management. */ + autoScheduled?: boolean; + tags?: string[]; + startScheduler?: unknown; + stopScheduler?: unknown; + }; + /** @description Map of additional settings evaluated when the sandbox is provisioned and initialized. */ + SandboxSettings: { + ocapi?: components["schemas"]["OcapiSettings"]; + webdav?: components["schemas"]["WebDavSettings"]; + }; + /** @description Use this document to configure Open Commerce API permissions for multiple client applications in the context of a single site. */ + OcapiSettings: { + /** + * Format: uuid + * @description Client application ID. + */ + client_id: string; + /** @description Array of resource-specific permission documents. */ + resources?: { + /** @description Open Commerce API HTTP method filter. For example, the filter ["get","patch"] allows access to the GET and PATCH methods for the specified resource path. You can specify methods that are supported for a resource. You can list all available resources and methods for the Shop API, version 18.1, with the following meta data call: http://{your-domain}/dw/meta/rest/shop/v18_1?client_id={your-client-id} */ + methods: ("get" | "delete" | "patch" | "post" | "put")[]; + /** @description String that controls which properties are included in the response document. The configuration value must be specified using property selection syntax. */ + read_attributes?: string; + /** @description String that controls which properties can be included in the request document. The configuration value must be specified using property selection syntax. */ + write_attributes?: string; + /** @description OCAPI resource identifier. For example: /products/*\/images or /products/specific_id/images. This property supports Ant path style to describe resource IDs. You can specify wildcards or specific product IDs; you can also specify the pattern /products/** to access to all available sub-resources. You can list all resource identifiers for the Shop API, version 18.1, with the following meta data call: http://{your-domain}/dw/meta/rest/shop/v18_1?client_id={your-client-id} */ + resource_id: string; + /** @description Version range documents granting permissions only to a subset of OCAPI versions. */ + version_range?: { + /** @description From version (for example, 18.1). If you don't specify the from version, all versions including the oldest are accessible. */ + from?: string; + /** @description Until version (for example, 18.1). The until version is exclusive, which means that it is not part of the range. If you don't specify the until version, all versions including the most recent one are accessible. */ + until?: string; + }[]; + }[]; + }[]; + /** @description WebDAV settings contain WebDAV client permissions for multiple client applications in the context of your organization. WebDAV client permissions enable you to configure which API clients can access your WebDAV files. These permissions also give you fine-grained control over which directories each client can access. */ + WebDavSettings: { + /** + * Format: uuid + * @description Client ID indicating the API client for which the permissions are configured. + */ + client_id: string; + /** @description Array of directory-based permissions documents. Multiple permissions paths cannot intersect each other; for example, the following two paths intersect and are therefore invalid: /impex/src and /impex/src/foo. */ + permissions: { + /** @description Directory for which the WebDAV permission is granted, including all subdirectories. File-specific permissions are not permitted. */ + path: string; + /** @description Array of operations granted on this directory. Possible values are read and read_write. */ + operations: ("read" | "read_write")[]; + }[]; + }[]; + ApiVersionResponse: components["schemas"]["StatusResponse"] & { + data?: components["schemas"]["ApiVersion"]; + }; + ApiVersion: { + /** @enum {string} */ + version?: "v1"; + git?: { + commit?: string; + /** Format: date-time */ + time?: string; + }; + build?: { + version?: string; + /** Format: date-time */ + time?: string; + }; + }; + UserInfoResponse: components["schemas"]["StatusResponse"] & { + data?: components["schemas"]["UserInfoSpec"]; + }; + UserInfoSpec: { + user?: { + /** @description User's unique ID on Account Manager. */ + id?: string; + /** @description User's email address. */ + email?: string; + /** @description User's human-readable, full name. */ + name?: string; + }; + client?: { + /** @description OAuth client ID used to retrieve the access token. */ + id?: string; + }; + /** @description User's roles as returned by Account Manager. */ + roles?: string[]; + /** @description Realms that the user is allowed to access. All sandboxes within these realms are accessible. */ + realms?: string[]; + /** @description Sandboxes that the user is allowed to access. */ + sandboxes?: string[]; + }; + SystemInfoResponse: components["schemas"]["StatusResponse"] & { + data?: components["schemas"]["SystemInfoSpec"]; + }; + SystemInfoSpec: { + /** @description The region, the system is deployed on. */ + region?: string; + /** @description Public IP addresses of internal services like API server */ + systemIps?: string[]; + /** @description Public IP addresses of all sandboxes */ + sandboxIps?: string[]; + /** @description IP addresses for incoming traffic. */ + inboundIps?: string[]; + /** @description IP addresses for outgoing traffic. */ + outboundIps?: string[]; + }; + ErrorResponse: components["schemas"]["StatusResponse"] & { + error?: components["schemas"]["ErrorModel"]; + }; + ErrorModel: { + /** + * @description String with value 'Success' or 'Failure' to indicate request outcome. + * @enum {string} + */ + status: "Success" | "Failure"; + /** @description Human-readable description of the error. */ + message?: string; + /** @description Machine-readable, one-word, CamelCase description of why the operation failed. If this value is empty, there is no information available. The reason clarifies an HTTP status code but does not override it. */ + reason?: string; + /** @description Extended data associated with the reason. Each reason can define its own extended details. This field is optional, and the data returned is not guaranteed to conform to any schema except that defined by the reason type. */ + details?: { + [key: string]: string; + }; + }; + }; + responses: never; + parameters: { + /** @description The four-letter ID of the realm. */ + realmParam: string; + /** @description The sandbox UUID. */ + sandboxIdParam: string; + /** @description The sandbox alias UUID. */ + sandboxAliasIdParam: string; + /** @description The operation UUID. */ + operationIdParam: string; + /** @description The page to access in a paged response. Page numbers start with '0', which is the default value. */ + pageParam: number; + /** @description Count of elements on a page. The default value is '20'. */ + perPageParam: number; + /** @description Earliest date for which data is in the response. Thirty days in the past by default. Format is ISO 8601. */ + fromParam: string; + /** @description Latest date for which data is included in the response. Today's date by default. Format is ISO 8601. */ + toParam: string; + /** @description Order of the list. Default value is ''asc''. */ + sortOrderParam: "asc" | "desc"; + /** @description Field by which to order the list. By default, the list is not ordered. */ + sortByOperationParam: "created" | "operation_state" | "status" | "operation"; + /** @description State of operations included in the response. By default, all operations are included. */ + operationStateParam: "pending" | "running" | "finished"; + /** @description Status of operations included in the response. By default, all operations are included. */ + operationStatusParam: "success" | "failure"; + /** @description Type of operations included in the response. By default, all operations are included. */ + operationTypeParam: "start" | "stop" | "restart" | "reset" | "create" | "delete" | "upgrade"; + /** @description Field to check whether detailed report is to be retrieved, by default detailed report will not be pulled */ + detailedReportParam: false | true; + /** @description Granularity of usage to be included in the response. By default, granular usage is not returned. */ + granularityParam: "daily" | "weekly" | "monthly"; + }; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + getApiInfo: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description API version information. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiVersionResponse"]; + }; + }; + }; + }; + getUserInfo: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Metadata about the authenticated API user. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserInfoResponse"]; + }; + }; + }; + }; + getSystemInfo: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Metadata about the system */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SystemInfoResponse"]; + }; + }; + }; + }; + getRealmSystemInfo: { + parameters: { + query?: never; + header?: never; + path: { + /** @description The four-letter ID of the realm. */ + realm: components["parameters"]["realmParam"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Metadata about the system */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SystemInfoResponse"]; + }; + }; + }; + }; + getRealm: { + parameters: { + query?: { + /** @description Additional information, which should be shown in the realm query. Available options are: [configuration,usage, accountdetails]. */ + expand?: ("configuration" | "usage" | "accountdetails")[]; + }; + header?: never; + path: { + /** @description The four-letter ID of the realm. */ + realm: components["parameters"]["realmParam"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Realm metadata. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["RealmResponse"]; + }; + }; + /** @description The ID is not a valid realm ID. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description The user isn't authenticated. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description The user doesn't have access to the realm. */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description There isn't any realm with that ID. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + getRealmConfiguration: { + parameters: { + query?: never; + header?: never; + path: { + /** @description The four-letter ID of the realm. */ + realm: components["parameters"]["realmParam"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Current configuration values of the realm. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["RealmConfigurationResponse"]; + }; + }; + /** @description The ID isn't valid or the configuration isn't valid. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description The user doesn't have access to that realm. */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description There isn't any realm with that ID. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + patchRealmConfiguration: { + parameters: { + query?: never; + header?: never; + path: { + /** @description The four-letter ID of the realm. */ + realm: components["parameters"]["realmParam"]; + }; + cookie?: never; + }; + /** @description Realm values to update. */ + requestBody: { + content: { + "application/json": components["schemas"]["RealmConfigurationUpdateRequestModel"]; + }; + }; + responses: { + /** @description Updated realm configuration data. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["RealmConfigurationResponse"]; + }; + }; + /** @description The ID isn't a valid realm ID. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description The user isn't authenticated. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description The user doesn't have access to that realm. */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description There isn't any realm with that ID. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + getRealmUsage: { + parameters: { + query?: { + /** @description Earliest date for which data is in the response. Thirty days in the past by default. Format is ISO 8601. */ + from?: components["parameters"]["fromParam"]; + /** @description Latest date for which data is included in the response. Today's date by default. Format is ISO 8601. */ + to?: components["parameters"]["toParam"]; + /** @description Field to check whether detailed report is to be retrieved, by default detailed report will not be pulled */ + detailedReport?: components["parameters"]["detailedReportParam"]; + /** @description Granularity of usage to be included in the response. By default, granular usage is not returned. */ + granularity?: components["parameters"]["granularityParam"]; + }; + header?: never; + path: { + /** @description The four-letter ID of the realm. */ + realm: components["parameters"]["realmParam"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Realm's usage information. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["RealmUsageResponse"]; + "text/csv": components["schemas"]["RealmUsageResponse"]; + }; + }; + /** @description The ID isn't valid. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + "text/csv": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description The user doesn't have access to the realm. */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + "text/csv": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description There isn't any realm with that ID. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + "text/csv": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + searchRealmUsage: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Return information for given all realm's usage */ + requestBody: { + content: { + "application/json": components["schemas"]["MultiRealmUsageRequest"]; + }; + }; + responses: { + /** @description Aggregates all realm usage data. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["MultiRealmUsageResponse"]; + "text/csv": components["schemas"]["MultiRealmUsageResponse"]; + }; + }; + /** @description The ID isn't a valid realm ID. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + "text/csv": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description The user isn't authenticated. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + "text/csv": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description The user doesn't have access to that realm. */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + "text/csv": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description There isn't any realm with that ID. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + "text/csv": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + getSandboxes: { + parameters: { + query?: { + /** @description If set, return deleted sandboxes. */ + include_deleted?: boolean; + /** @description If passed in supported format, returns sandboxes that matches the query. Supported format: realm=zzzz&state=active&resourceProfile=medium&createdBy=user1&tags=[tag1,tag2,tag3]. */ + filter_params?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List of sandboxes. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SandboxListResponse"]; + }; + }; + /** @description The request parameters are invalid (bad request). */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description The user doesn't have access to that realm. */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description There isn't any realm with that ID. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + createSandbox: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Metadata about the new sandbox. */ + requestBody: { + content: { + "application/json": components["schemas"]["SandboxProvisioningRequestModel"]; + }; + }; + responses: { + /** @description The sandbox creation has started. */ + 201: { + headers: { + /** @description URI of the created sandbox. */ + Location?: string; + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SandboxResponse"]; + }; + }; + /** @description The request parameters are invalid (bad request). */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description The user doesn't have access to the realm. */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description There isn't any realm with that ID. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description There were server errors initiating the sandbox deployment. */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + getSandbox: { + parameters: { + query?: never; + header?: never; + path: { + /** @description The sandbox UUID. */ + sandboxId: components["parameters"]["sandboxIdParam"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Details on the sandbox (including its state). */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SandboxResponse"]; + }; + }; + /** @description The request parameters are invalid (bad request). */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description The user doesn't have access to the requested realm. */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description There isn't any realm with that ID. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + deleteSandbox: { + parameters: { + query?: never; + header?: never; + path: { + /** @description The sandbox UUID. */ + sandboxId: components["parameters"]["sandboxIdParam"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The request for deleting the sandbox has been accepted by the API server. This doesn't mean that the sandbox has already been deleted, since the actual deletion process does not necessarily start immediately and might take a while. You can track the deletion process using sandbox GET requests. */ + 202: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["StatusResponse"]; + }; + }; + /** @description The request parameters are invalid (bad request). */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description The user doesn't have access to that realm. */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description ID not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + patchSandbox: { + parameters: { + query?: never; + header?: never; + path: { + /** @description The sandbox UUID. */ + sandboxId: components["parameters"]["sandboxIdParam"]; + }; + cookie?: never; + }; + /** @description Sandbox values to update. */ + requestBody: { + content: { + "application/json": components["schemas"]["SandboxUpdateRequestModel"]; + }; + }; + responses: { + /** @description Updated details on the sandbox (including its state). */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SandboxResponse"]; + }; + }; + /** @description The request parameters are invalid (bad request). */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description The user doesn't have access to the realm. */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description There isn't any sandbox with that ID. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + getAliases: { + parameters: { + query?: never; + header?: never; + path: { + /** @description The sandbox UUID. */ + sandboxId: components["parameters"]["sandboxIdParam"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List of Alias configurations. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SandboxAliasListResponse"]; + }; + }; + /** @description The user doesn't have access to the realm or sandbox. */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description There isn't any sandbox with that ID. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + createAlias: { + parameters: { + query?: never; + header?: never; + path: { + /** @description The sandbox UUID. */ + sandboxId: components["parameters"]["sandboxIdParam"]; + }; + cookie?: never; + }; + /** @description The alias for the sandbox */ + requestBody: { + content: { + "application/json": components["schemas"]["SandboxAliasModel"]; + }; + }; + responses: { + /** @description The sandbox alias already exists. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SandboxAliasResponse"]; + }; + }; + /** @description The alias has been created. */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SandboxAliasResponse"]; + }; + }; + /** @description The request parameters are invalid (bad request). */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description The user doesn't have access to the sandbox. */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description There isn't any sandbox with that ID. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + getAlias: { + parameters: { + query?: never; + header?: never; + path: { + /** @description The sandbox UUID. */ + sandboxId: components["parameters"]["sandboxIdParam"]; + /** @description The sandbox alias UUID. */ + sandboxAliasId: components["parameters"]["sandboxAliasIdParam"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The Alias configuration. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SandboxAliasResponse"]; + }; + }; + /** @description The user doesn't have access to the realm or sandbox. */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description There isn't any sandbox or any alias with that ID. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + deleteAlias: { + parameters: { + query?: never; + header?: never; + path: { + /** @description The sandbox UUID. */ + sandboxId: components["parameters"]["sandboxIdParam"]; + /** @description The sandbox alias UUID. */ + sandboxAliasId: components["parameters"]["sandboxAliasIdParam"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Shows, that alias currently gets deleted. */ + 202: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["StatusResponse"]; + }; + }; + /** @description The user doesn't have access to the realm or sandbox. */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description There isn't any sandbox or any alias with that ID. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + getSandboxOperations: { + parameters: { + query?: { + /** @description Earliest date for which data is in the response. Thirty days in the past by default. Format is ISO 8601. */ + from?: components["parameters"]["fromParam"]; + /** @description Latest date for which data is included in the response. Today's date by default. Format is ISO 8601. */ + to?: components["parameters"]["toParam"]; + /** @description State of operations included in the response. By default, all operations are included. */ + operation_state?: components["parameters"]["operationStateParam"]; + /** @description Status of operations included in the response. By default, all operations are included. */ + status?: components["parameters"]["operationStatusParam"]; + /** @description Type of operations included in the response. By default, all operations are included. */ + operation?: components["parameters"]["operationTypeParam"]; + /** @description Order of the list. Default value is ''asc''. */ + sort_order?: components["parameters"]["sortOrderParam"]; + /** @description Field by which to order the list. By default, the list is not ordered. */ + sort_by?: components["parameters"]["sortByOperationParam"]; + /** @description The page to access in a paged response. Page numbers start with '0', which is the default value. */ + page?: components["parameters"]["pageParam"]; + /** @description Count of elements on a page. The default value is '20'. */ + per_page?: components["parameters"]["perPageParam"]; + }; + header?: never; + path: { + /** @description The sandbox UUID. */ + sandboxId: components["parameters"]["sandboxIdParam"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List of operations. */ + 200: { + headers: { + /** @description Paging metadata, as described in RFC-5988 */ + Link?: string; + /** @description Total count of elements. */ + "X-Pagination-Count"?: number; + /** @description Current page index. */ + "X-Pagination-Page"?: number; + /** @description Maximum count of pages. */ + "X-Pagination-Limit"?: number; + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SandboxOperationListResponse"]; + }; + }; + /** @description The request parameters are invalid (bad request). */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description The user doesn't have access to the sandbox. */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description There isn't any sandbox with that ID. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description There were server errors during the operation. */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + createSandboxOperation: { + parameters: { + query?: never; + header?: never; + path: { + /** @description The sandbox UUID. */ + sandboxId: components["parameters"]["sandboxIdParam"]; + }; + cookie?: never; + }; + /** @description Operation to be carried out on a sandbox. */ + requestBody: { + content: { + "application/json": components["schemas"]["SandboxOperationRequestModel"]; + }; + }; + responses: { + /** @description The operation has been accepted. */ + 202: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SandboxOperationResponse"]; + }; + }; + /** @description The request parameters are invalid (bad request). */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description The user doesn't have access to the sandbox. */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description There isn't any sandbox with that ID. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description The operation isn't allowed in the current state of the sandbox. */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description There were server errors during the operation. */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + getSandboxOperation: { + parameters: { + query?: never; + header?: never; + path: { + /** @description The sandbox UUID. */ + sandboxId: components["parameters"]["sandboxIdParam"]; + /** @description The operation UUID. */ + operationId: components["parameters"]["operationIdParam"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Details of the sandbox operation's state and the state of its target. If the operation has already finished, indicates whether the operation was successful. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SandboxOperationResponse"]; + }; + }; + /** @description The request parameters are invalid (bad request). */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description The user doesn't have access to the requested operation or sandbox. */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description There isn't any sandbox or realm matching the given parameters. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + getSandboxSettings: { + parameters: { + query?: never; + header?: never; + path: { + /** @description The sandbox UUID. */ + sandboxId: components["parameters"]["sandboxIdParam"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Details of the sandbox settings. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SandboxSettingsResponse"]; + }; + }; + /** @description The sandbox ID isn't valid. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description The user doesn't have access to the requested sandbox. */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description There isn't any sandbox matching the ID. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + getSandboxUsage: { + parameters: { + query?: { + /** @description Earliest date for which data is in the response. Thirty days in the past by default. Format is ISO 8601. */ + from?: components["parameters"]["fromParam"]; + /** @description Latest date for which data is included in the response. Today's date by default. Format is ISO 8601. */ + to?: components["parameters"]["toParam"]; + }; + header?: never; + path: { + /** @description The sandbox UUID. */ + sandboxId: components["parameters"]["sandboxIdParam"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Sandbox usage information. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SandboxUsageResponse"]; + }; + }; + /** @description The sandbox ID isn't valid. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description The user doesn't have access to the requested sandbox. */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description There isn't any sandbox matching the ID. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + getSandboxStorage: { + parameters: { + query?: never; + header?: never; + path: { + /** @description The sandbox UUID. */ + sandboxId: components["parameters"]["sandboxIdParam"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Sandbox storage information. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SandboxStorageResponse"]; + }; + }; + /** @description The sandbox ID isn't valid. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description The user doesn't have access to the requested sandbox. */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description There isn't any sandbox matching the ID. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; +} diff --git a/packages/b2c-tooling/src/clients/ods.ts b/packages/b2c-tooling/src/clients/ods.ts new file mode 100644 index 00000000..b18fe76e --- /dev/null +++ b/packages/b2c-tooling/src/clients/ods.ts @@ -0,0 +1,152 @@ +/** + * ODS (On-Demand Sandbox) API client for B2C Commerce. + * + * Provides a fully typed client for the Developer Sandbox REST API using + * openapi-fetch with OAuth authentication middleware. Used for managing + * developer sandboxes including creation, deletion, start/stop operations, + * and retrieving realm and system information. + * + * @module clients/ods + */ +import createClient, {type Client} from 'openapi-fetch'; +import type {AuthStrategy} from '../auth/types.js'; +import type {paths, components} from './ods.generated.js'; +import {createAuthMiddleware, createLoggingMiddleware, createExtraParamsMiddleware} from './middleware.js'; +import type {ExtraParamsConfig} from './middleware.js'; + +/** + * Default ODS API host for US region. + */ +const DEFAULT_ODS_HOST = 'admin.dx.commercecloud.salesforce.com'; + +/** + * Re-export generated types for external use. + */ +export type {paths, components}; + +/** + * The typed ODS client - this is the openapi-fetch Client with full type safety. + * + * @see {@link createOdsClient} for instantiation + */ +export type OdsClient = Client; + +/** + * Helper type to extract response data from an operation. + */ +export type OdsResponse = T extends {content: {'application/json': infer R}} ? R : never; + +/** + * ODS error response type from the generated schema. + * + * @example + * ```typescript + * const { data, error } = await client.GET('/sandboxes/{sandboxId}', { + * params: { path: { sandboxId: 'invalid-id' } } + * }); + * if (error) { + * // Access the structured error message + * console.error(error.error?.message); + * } + * ``` + */ +export type OdsError = components['schemas']['ErrorResponse']; + +/** + * Configuration for creating an ODS client. + */ +export interface OdsClientConfig { + /** + * The ODS API host. + * Defaults to Unified region: admin.dx.commercecloud.salesforce.com + * + * @example "admin.dx.commercecloud.salesforce.com" + */ + host?: string; + + /** + * Extra parameters to add to all requests. + * Useful for internal/power-user scenarios where you need to pass + * parameters that aren't in the typed OpenAPI schema. + */ + extraParams?: ExtraParamsConfig; +} + +/** + * Creates a typed ODS (On-Demand Sandbox) API client. + * + * Returns the openapi-fetch client directly, with authentication + * handled via middleware. This gives full access to all openapi-fetch + * features with type-safe paths, parameters, and responses. + * + * @param config - ODS client configuration + * @param auth - Authentication strategy (typically OAuth) + * @returns Typed openapi-fetch client + * + * @example + * // Create ODS client with OAuth auth + * const oauthStrategy = new OAuthStrategy({ + * clientId: 'your-client-id', + * clientSecret: 'your-client-secret', + * }); + * + * const client = createOdsClient({}, oauthStrategy); + * + * // Get user info + * const { data, error } = await client.GET('/me', {}); + * if (data) { + * console.log('User:', data.data?.user?.name); + * console.log('Realms:', data.data?.realms); + * } + * + * @example + * // Get system info + * const { data, error } = await client.GET('/system', {}); + * if (data) { + * console.log('Region:', data.data?.region); + * console.log('Sandbox IPs:', data.data?.sandboxIps); + * } + * + * @example + * // List all sandboxes + * const { data, error } = await client.GET('/sandboxes', {}); + * if (data) { + * for (const sandbox of data.data ?? []) { + * console.log(`${sandbox.id}: ${sandbox.state}`); + * } + * } + * + * @example + * // Create a new sandbox + * const { data, error } = await client.POST('/sandboxes', { + * body: { + * realm: 'abcd', + * ttl: 24, + * resourceProfile: 'medium', + * } + * }); + * + * @example + * // Start a sandbox + * const { data, error } = await client.POST('/sandboxes/{sandboxId}/operations', { + * params: { path: { sandboxId: 'sandbox-uuid' } }, + * body: { operation: 'start' } + * }); + */ +export function createOdsClient(config: OdsClientConfig, auth: AuthStrategy): OdsClient { + const host = config.host ?? DEFAULT_ODS_HOST; + + const client = createClient({ + baseUrl: `https://${host}/api/v1`, + }); + + // Middleware order: extraParams → auth → logging + // This ensures logging sees the fully modified request (with auth headers and extra params) + if (config.extraParams) { + client.use(createExtraParamsMiddleware(config.extraParams)); + } + client.use(createAuthMiddleware(auth)); + client.use(createLoggingMiddleware('ODS')); + + return client; +} diff --git a/packages/b2c-tooling/src/clients/slas-admin.ts b/packages/b2c-tooling/src/clients/slas-admin.ts index cbb4b792..bad7c0d1 100644 --- a/packages/b2c-tooling/src/clients/slas-admin.ts +++ b/packages/b2c-tooling/src/clients/slas-admin.ts @@ -103,8 +103,9 @@ export function createSlasClient(config: SlasClientConfig, auth: AuthStrategy): baseUrl: `https://${config.shortCode}.api.commercecloud.salesforce.com/shopper/auth-admin/v1`, }); - client.use(createLoggingMiddleware('SLAS')); + // Middleware order: auth → logging (logging sees fully modified request) client.use(createAuthMiddleware(auth)); + client.use(createLoggingMiddleware('SLAS')); return client; } diff --git a/packages/b2c-tooling/src/defaults.ts b/packages/b2c-tooling/src/defaults.ts new file mode 100644 index 00000000..ff3feee4 --- /dev/null +++ b/packages/b2c-tooling/src/defaults.ts @@ -0,0 +1,24 @@ +/** + * Centralized default values for B2C Commerce APIs. + * + * These defaults are used across auth strategies, clients, and CLI commands. + * Override via environment variables or CLI flags. + * + * @module defaults + */ + +/** + * Default Account Manager host for OAuth authentication. + * Used for client credentials and implicit OAuth flows. + * + * Environment variable: SFCC_ACCOUNT_MANAGER_HOST + */ +export const DEFAULT_ACCOUNT_MANAGER_HOST = 'account.demandware.com'; + +/** + * Default ODS (On-Demand Sandbox) API host. + * Used for sandbox management operations. + * + * Environment variable: SFCC_SANDBOX_API_HOST + */ +export const DEFAULT_ODS_HOST = 'admin.dx.commercecloud.salesforce.com'; diff --git a/packages/b2c-tooling/src/index.ts b/packages/b2c-tooling/src/index.ts index 1de95a74..3410ddbc 100644 --- a/packages/b2c-tooling/src/index.ts +++ b/packages/b2c-tooling/src/index.ts @@ -46,9 +46,17 @@ export {B2CInstance} from './instance/index.js'; export type {InstanceConfig, FromDwJsonOptions, B2CInstanceOptions} from './instance/index.js'; // Clients -export {WebDavClient, createOcapiClient, createAuthMiddleware, createSlasClient} from './clients/index.js'; +export { + WebDavClient, + createOcapiClient, + createAuthMiddleware, + createExtraParamsMiddleware, + createSlasClient, + createOdsClient, +} from './clients/index.js'; export type { PropfindEntry, + ExtraParamsConfig, OcapiClient, OcapiError, OcapiResponse, @@ -60,6 +68,12 @@ export type { SlasResponse, SlasPaths, SlasComponents, + OdsClient as OdsApiClient, + OdsClientConfig, + OdsError, + OdsResponse, + OdsPaths, + OdsComponents, } from './clients/index.js'; // Context Layer - Platform @@ -98,3 +112,6 @@ export type {JobExecutionResult} from './operations/jobs/index.js'; // Operations - Sites export {listSites, getSite} from './operations/sites/index.js'; export type {Site} from './operations/sites/index.js'; + +// Defaults +export {DEFAULT_ACCOUNT_MANAGER_HOST, DEFAULT_ODS_HOST} from './defaults.js';