diff --git a/.changeset/mrt-schema-refresh-skills.md b/.changeset/mrt-schema-refresh-skills.md new file mode 100644 index 00000000..fe5e95b8 --- /dev/null +++ b/.changeset/mrt-schema-refresh-skills.md @@ -0,0 +1,5 @@ +--- +'@salesforce/b2c-agent-plugins': patch +--- + +Document new MRT environment clone, bundle delete, organization member, and organization certificate commands in the `b2c-mrt` skill. diff --git a/.changeset/mrt-schema-refresh.md b/.changeset/mrt-schema-refresh.md new file mode 100644 index 00000000..f73dac6a --- /dev/null +++ b/.changeset/mrt-schema-refresh.md @@ -0,0 +1,11 @@ +--- +'@salesforce/b2c-cli': minor +'@salesforce/b2c-tooling-sdk': minor +--- + +Refresh the MRT admin API schema and add new commands: + +- `b2c mrt env clone` — clone an environment from an existing source, optionally copying redirects, environment variables, and B2C target info +- `b2c mrt bundle delete` — delete one or more bundles (uses bulk-delete when more than one ID is supplied) +- `b2c mrt org member list|add|get|update|remove` — manage organization-level members +- `b2c mrt org cert list|get|create|delete|restart-validation` — manage custom domain certificates referenced by environments diff --git a/docs/cli/mrt.md b/docs/cli/mrt.md index d3953373..9a7a4bab 100644 --- a/docs/cli/mrt.md +++ b/docs/cli/mrt.md @@ -314,6 +314,35 @@ b2c mrt env delete staging --project my-storefront b2c mrt env delete old-env -p my-storefront --force ``` +### b2c mrt env clone + +Clone an environment from an existing source environment. The new target receives the source's configuration (excluding proxies and the production flag) and is automatically deployed with the source target's current bundle (if any). Optionally clones redirects, environment variables, and B2C target info. + +The source environment is the one selected by `--environment` / `-e` (or `MRT_ENVIRONMENT` / `mrtEnvironment` in `dw.json`). The positional argument is the **new** environment's slug. + +```bash +# Clone the configured environment into a new slug +b2c mrt env clone staging-copy -p my-storefront -e staging + +# Clone with redirects and environment variables +b2c mrt env clone qa -p my-storefront -e staging --clone-redirects --clone-env-vars + +# Clone using a custom domain certificate +b2c mrt env clone qa -p my-storefront -e staging \ + --external-hostname qa.example.com --certificate-id 123 --wait +``` + +| Flag | Description | +|------|-------------| +| `--environment`, `-e` | Source environment slug (defaults to `mrtEnvironment` / `MRT_ENVIRONMENT`) | +| `--external-hostname` | Full external hostname (required for non-MRT-managed certificates) | +| `--external-domain` | External domain for Universal PWA SSR | +| `--certificate-id` | Certificate ID for custom domain (use `b2c mrt org cert list` to find) | +| `--clone-redirects` | Clone redirects from the source environment | +| `--clone-env-vars` | Clone environment variables from the source environment | +| `--clone-b2c-info` | Clone B2C target info from the source environment | +| `--wait`, `-w` | Wait for the new environment to reach a terminal state | + ### b2c mrt env invalidate Invalidate CDN cache for an environment. @@ -508,6 +537,105 @@ b2c mrt bundle download 12345 -p my-storefront -o ./artifacts/bundle.tgz b2c mrt bundle download 12345 -p my-storefront --url-only ``` +### b2c mrt bundle delete + +Delete one or more bundles. Bundles are deleted asynchronously by the server and only project admins can run this command. With more than one bundle ID the CLI uses the bulk-delete endpoint and reports any rejected bundles (e.g. bundles in use by an active deployment). + +```bash +# Delete a single bundle +b2c mrt bundle delete 12345 -p my-storefront + +# Delete several at once +b2c mrt bundle delete 12345 12346 12347 -p my-storefront + +# Skip the confirmation prompt +b2c mrt bundle delete 12345 -p my-storefront --force +``` + +--- + +## Organization Member Commands + +Organization members are distinct from project members: they hold a role at the organization level and can optionally be granted permission to view all projects and manage custom domain certificates. + +### b2c mrt org member list + +```bash +b2c mrt org member list --org my-org +b2c mrt org member list --org my-org --search alice +``` + +### b2c mrt org member add + +Roles: `owner` or `member`. + +```bash +b2c mrt org member add alice@example.com --org my-org --role member +b2c mrt org member add bob@example.com --org my-org --role owner --view-all-projects +``` + +### b2c mrt org member get + +```bash +b2c mrt org member get alice@example.com --org my-org +``` + +### b2c mrt org member update + +```bash +b2c mrt org member update alice@example.com --org my-org --view-all-projects +b2c mrt org member update alice@example.com --org my-org --no-cert-permission +``` + +### b2c mrt org member remove + +```bash +b2c mrt org member remove alice@example.com --org my-org +``` + +--- + +## Organization Certificate Commands + +Manage custom domain certificates for environments that use a non-MRT-managed hostname. Certificates are organization-scoped; reference a certificate from `env create`, `env update`, or `env clone` via `--certificate-id`. + +### b2c mrt org cert list + +```bash +b2c mrt org cert list --org my-org +b2c mrt org cert list --org my-org --custom-only +``` + +### b2c mrt org cert get + +Returns the validation record (the DNS entry the customer must add to validate the certificate). + +```bash +b2c mrt org cert get 123 --org my-org +``` + +### b2c mrt org cert create + +```bash +b2c mrt org cert create shop.example.com --org my-org +``` + +The output includes the validation record. Add it to your DNS to complete validation. + +### b2c mrt org cert delete + +```bash +b2c mrt org cert delete 123 --org my-org +``` + +### b2c mrt org cert restart-validation + +Restart validation for a certificate that has not yet been validated. The response includes a fresh validation record. + +```bash +b2c mrt org cert restart-validation 123 --org my-org +``` + --- ## Tail Logs diff --git a/packages/b2c-cli/package.json b/packages/b2c-cli/package.json index 78fccbee..4e49be18 100644 --- a/packages/b2c-cli/package.json +++ b/packages/b2c-cli/package.json @@ -147,6 +147,12 @@ "mrt:org": { "description": "List organizations and view B2C Commerce instance connections\n\nDocs: https://salesforcecommercecloud.github.io/b2c-developer-tooling/cli/mrt.html#organization-commands" }, + "mrt:org:member": { + "description": "Manage organization-level members and their permissions\n\nDocs: https://salesforcecommercecloud.github.io/b2c-developer-tooling/cli/mrt.html#organization-member-commands" + }, + "mrt:org:cert": { + "description": "Manage custom domain certificates for organization environments\n\nDocs: https://salesforcecommercecloud.github.io/b2c-developer-tooling/cli/mrt.html#organization-certificate-commands" + }, "mrt:project": { "description": "Create, update, delete, and configure MRT projects\n\nDocs: https://salesforcecommercecloud.github.io/b2c-developer-tooling/cli/mrt.html#project-commands" }, diff --git a/packages/b2c-cli/src/commands/mrt/bundle/delete.ts b/packages/b2c-cli/src/commands/mrt/bundle/delete.ts new file mode 100644 index 00000000..dc6e65c8 --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/bundle/delete.ts @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {Args, Flags} from '@oclif/core'; +import {MrtCommand} from '@salesforce/b2c-tooling-sdk/cli'; +import { + bulkDeleteBundles, + deleteBundle, + type BulkDeleteBundlesResult, +} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t, withDocs} from '../../../i18n/index.js'; +import {confirm} from '../../../prompts.js'; + +interface DeleteBundleResult { + queued: number[]; + rejected: BulkDeleteBundlesResult['rejected']; +} + +export default class MrtBundleDelete extends MrtCommand { + static args = { + bundleId: Args.integer({ + description: 'Bundle ID to delete (additional IDs may follow)', + required: true, + }), + }; + + static description = withDocs( + t('commands.mrt.bundle.delete.description', 'Delete one or more bundles from a Managed Runtime project'), + '/cli/mrt.html#b2c-mrt-bundle-delete', + ); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %> 12345 --project my-storefront', + '<%= config.bin %> <%= command.id %> 12345 12346 12347 -p my-storefront', + '<%= config.bin %> <%= command.id %> 12345 -p my-storefront --force', + ]; + + static flags = { + ...MrtCommand.baseFlags, + force: Flags.boolean({ + char: 'f', + description: 'Skip confirmation prompt', + default: false, + }), + }; + + // Allow multiple positional bundle IDs + static strict = false; + + protected operations = { + deleteBundle, + bulkDeleteBundles, + }; + + async run(): Promise { + this.requireMrtCredentials(); + + const {argv, flags} = await this.parse(MrtBundleDelete); + const {mrtProject: project} = this.resolvedConfig.values; + + if (!project) { + this.error('MRT project is required. Provide --project flag, set MRT_PROJECT, or set mrtProject in dw.json.'); + } + + const bundleIds: number[] = []; + for (const arg of argv as Array) { + const n = typeof arg === 'number' ? arg : Number.parseInt(String(arg), 10); + if (!Number.isInteger(n) || n <= 0) { + this.error(t('commands.mrt.bundle.delete.invalidId', 'Invalid bundle ID: {{arg}}', {arg: String(arg)})); + } + bundleIds.push(n); + } + + if (!flags.force && !this.jsonEnabled()) { + const message = + bundleIds.length === 1 + ? t('commands.mrt.bundle.delete.confirmOne', 'Delete bundle {{id}} from {{project}}?', { + id: String(bundleIds[0]), + project, + }) + : t('commands.mrt.bundle.delete.confirmMany', 'Delete {{n}} bundles from {{project}}?', { + n: String(bundleIds.length), + project, + }); + const confirmed = await confirm(message); + if (!confirmed) { + this.log(t('commands.mrt.bundle.delete.cancelled', 'Delete cancelled.')); + return {queued: [], rejected: []}; + } + } + + try { + if (bundleIds.length === 1) { + const [bundleId] = bundleIds; + await this.operations.deleteBundle( + { + projectSlug: project, + bundleId, + origin: this.resolvedConfig.values.mrtOrigin, + }, + this.getMrtAuth(), + ); + + if (!this.jsonEnabled()) { + this.log( + t('commands.mrt.bundle.delete.queuedOne', 'Bundle {{id}} queued for deletion.', {id: String(bundleId)}), + ); + } + + return {queued: [bundleId], rejected: []}; + } + + const result = await this.operations.bulkDeleteBundles( + { + projectSlug: project, + bundleIds, + origin: this.resolvedConfig.values.mrtOrigin, + }, + this.getMrtAuth(), + ); + + if (!this.jsonEnabled()) { + this.log( + t('commands.mrt.bundle.delete.queuedMany', '{{n}} bundle(s) queued for deletion.', { + n: String(result.queued.length), + }), + ); + if (result.rejected.length > 0) { + this.log(t('commands.mrt.bundle.delete.rejectedHeader', 'Rejected bundles:')); + for (const r of result.rejected) { + this.log(` - ${r.bundleId ?? '?'}: ${r.reason}`); + } + } + } + + return result; + } catch (error) { + if (error instanceof Error) { + this.error( + t('commands.mrt.bundle.delete.failed', 'Failed to delete bundle(s): {{message}}', {message: error.message}), + ); + } + throw error; + } + } +} diff --git a/packages/b2c-cli/src/commands/mrt/env/clone.ts b/packages/b2c-cli/src/commands/mrt/env/clone.ts new file mode 100644 index 00000000..27ef8a8c --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/env/clone.ts @@ -0,0 +1,204 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {Args, Flags, ux} from '@oclif/core'; +import cliui from 'cliui'; +import {MrtCommand} from '@salesforce/b2c-tooling-sdk/cli'; +import {cloneEnv, waitForEnv, type MrtEnvironment} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t, withDocs} from '../../../i18n/index.js'; + +function printEnvDetails(env: MrtEnvironment, project: string): void { + const ui = cliui({width: process.stdout.columns || 80}); + const labelWidth = 18; + + ui.div(''); + ui.div({text: 'Slug:', width: labelWidth}, {text: env.slug ?? ''}); + ui.div({text: 'Name:', width: labelWidth}, {text: env.name ?? ''}); + ui.div({text: 'Project:', width: labelWidth}, {text: project}); + ui.div({text: 'State:', width: labelWidth}, {text: env.state ?? 'unknown'}); + + if (env.ssr_region) { + ui.div({text: 'Region:', width: labelWidth}, {text: env.ssr_region}); + } + + if (env.hostname) { + ui.div({text: 'Hostname:', width: labelWidth}, {text: env.hostname}); + } + + if (env.ssr_external_hostname) { + ui.div({text: 'External Host:', width: labelWidth}, {text: env.ssr_external_hostname}); + } + + if (env.ssr_external_domain) { + ui.div({text: 'External Domain:', width: labelWidth}, {text: env.ssr_external_domain}); + } + + ux.stdout(ui.toString()); +} + +export default class MrtEnvClone extends MrtCommand { + static args = { + slug: Args.string({ + description: 'Slug for the new environment created by the clone', + required: true, + }), + }; + + static description = withDocs( + t('commands.mrt.env.clone.description', 'Clone a Managed Runtime environment from an existing source environment'), + '/cli/mrt.html#b2c-mrt-env-clone', + ); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %> staging-copy -p my-storefront -e staging', + '<%= config.bin %> <%= command.id %> qa -p my-storefront -e staging --clone-redirects --clone-env-vars', + '<%= config.bin %> <%= command.id %> qa -p my-storefront -e staging --external-hostname qa.example.com --certificate-id 123 --wait', + ]; + + static flags = { + ...MrtCommand.baseFlags, + 'external-hostname': Flags.string({ + description: 'Full external hostname for the new environment (required for non-MRT-managed certs)', + }), + 'external-domain': Flags.string({ + description: 'External domain for Universal PWA SSR (e.g., example.com)', + }), + 'certificate-id': Flags.integer({ + description: 'ID of the certificate to associate with the new environment (required for custom domains)', + }), + 'clone-redirects': Flags.boolean({ + description: 'Clone redirects from the source environment', + default: false, + }), + 'clone-env-vars': Flags.boolean({ + description: 'Clone environment variables from the source environment', + default: false, + }), + 'clone-b2c-info': Flags.boolean({ + description: 'Clone B2C target info from the source environment', + default: false, + }), + wait: Flags.boolean({ + char: 'w', + description: 'Wait for the new environment to be ready 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'], + }), + }; + + protected operations = { + cloneEnv, + waitForEnv, + }; + + async run(): Promise { + this.requireMrtCredentials(); + + const {slug} = this.args; + const {mrtProject: project, mrtEnvironment: fromSlug} = this.resolvedConfig.values; + + if (!project) { + this.error('MRT project is required. Provide --project flag, set MRT_PROJECT, or set mrtProject in dw.json.'); + } + if (!fromSlug) { + this.error( + 'Source environment is required. Provide --environment / -e, set MRT_ENVIRONMENT, or set mrtEnvironment in dw.json.', + ); + } + if (fromSlug === slug) { + this.error(`Source and destination environment slugs must differ (both are "${slug}").`); + } + + const { + 'external-hostname': externalHostname, + 'external-domain': externalDomain, + 'certificate-id': certificateId, + 'clone-redirects': cloneRedirectsFlag, + 'clone-env-vars': cloneEnvVarsFlag, + 'clone-b2c-info': cloneB2cInfoFlag, + wait, + 'poll-interval': pollInterval, + timeout, + } = this.flags; + + this.log( + t('commands.mrt.env.clone.cloning', 'Cloning environment "{{fromSlug}}" → "{{slug}}" in {{project}}...', { + fromSlug, + slug, + project, + }), + ); + + try { + let result = await this.operations.cloneEnv( + { + projectSlug: project, + slug, + fromSlug, + externalHostname, + externalDomain, + certificateId, + cloneRedirects: cloneRedirectsFlag, + cloneEnvironmentVariables: cloneEnvVarsFlag, + cloneB2cTargetInfo: cloneB2cInfoFlag, + origin: this.resolvedConfig.values.mrtOrigin, + }, + this.getMrtAuth(), + ); + + if (wait) { + this.log(t('commands.mrt.env.clone.waiting', 'Waiting for environment "{{slug}}" to be ready...', {slug})); + + result = await this.operations.waitForEnv( + { + projectSlug: project, + slug, + origin: this.resolvedConfig.values.mrtOrigin, + pollIntervalSeconds: pollInterval, + timeoutSeconds: timeout, + onPoll: (info) => { + if (!this.jsonEnabled()) { + this.log( + t('commands.mrt.env.clone.state', '[{{elapsed}}s] State: {{state}}', { + elapsed: String(info.elapsedSeconds), + state: info.state, + }), + ); + } + }, + }, + this.getMrtAuth(), + ); + } + + if (this.jsonEnabled()) { + return result; + } + + this.log(t('commands.mrt.env.clone.success', 'Environment cloned successfully.')); + printEnvDetails(result, project); + + return result; + } catch (error) { + if (error instanceof Error) { + this.error( + t('commands.mrt.env.clone.failed', 'Failed to clone environment: {{message}}', {message: error.message}), + ); + } + throw error; + } + } +} diff --git a/packages/b2c-cli/src/commands/mrt/org/cert/create.ts b/packages/b2c-cli/src/commands/mrt/org/cert/create.ts new file mode 100644 index 00000000..6cd63190 --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/org/cert/create.ts @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {Args, Flags} from '@oclif/core'; +import {MrtCommand} from '@salesforce/b2c-tooling-sdk/cli'; +import {createCertificate, type MrtCertificateListCreate} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t, withDocs} from '../../../../i18n/index.js'; + +export default class MrtOrgCertCreate extends MrtCommand { + static args = { + domain: Args.string({ + description: 'Domain for the certificate (e.g. shop.example.com)', + required: true, + }), + }; + + static description = withDocs( + t('commands.mrt.org.cert.create.description', 'Create a custom domain certificate for an organization'), + '/cli/mrt.html#b2c-mrt-org-cert-create', + ); + + static enableJsonFlag = true; + + static examples = ['<%= config.bin %> <%= command.id %> shop.example.com --org my-org']; + + static flags = { + ...MrtCommand.baseFlags, + org: Flags.string({description: 'Organization slug', required: true}), + }; + + async run(): Promise { + this.requireMrtCredentials(); + + const {domain} = this.args; + const {org} = this.flags; + + const cert = await createCertificate( + {organizationSlug: org, domainName: domain, origin: this.resolvedConfig.values.mrtOrigin}, + this.getMrtAuth(), + ); + + if (!this.jsonEnabled()) { + this.log( + t('commands.mrt.org.cert.create.success', 'Certificate created (id={{id}}) for {{domain}}.', { + id: String(cert.id ?? '?'), + domain, + }), + ); + if (cert.validation_record) { + this.log( + t( + 'commands.mrt.org.cert.create.validationRecord', + 'Add the following DNS record to validate this certificate:\n {{record}}', + {record: cert.validation_record}, + ), + ); + } + } + + return cert; + } +} diff --git a/packages/b2c-cli/src/commands/mrt/org/cert/delete.ts b/packages/b2c-cli/src/commands/mrt/org/cert/delete.ts new file mode 100644 index 00000000..996fdd6e --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/org/cert/delete.ts @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {Args, Flags} from '@oclif/core'; +import {MrtCommand} from '@salesforce/b2c-tooling-sdk/cli'; +import {deleteCertificate} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t, withDocs} from '../../../../i18n/index.js'; +import {confirm} from '../../../../prompts.js'; + +export default class MrtOrgCertDelete extends MrtCommand { + static args = { + certId: Args.integer({description: 'Certificate ID to delete', required: true}), + }; + + static description = withDocs( + t('commands.mrt.org.cert.delete.description', 'Delete a custom domain certificate'), + '/cli/mrt.html#b2c-mrt-org-cert-delete', + ); + + static enableJsonFlag = true; + + static examples = ['<%= config.bin %> <%= command.id %> 123 --org my-org']; + + static flags = { + ...MrtCommand.baseFlags, + org: Flags.string({description: 'Organization slug', required: true}), + force: Flags.boolean({char: 'f', description: 'Skip confirmation prompt', default: false}), + }; + + async run(): Promise<{certId: number; deleted: boolean}> { + this.requireMrtCredentials(); + + const {certId} = this.args; + const {org, force} = this.flags; + + if (!force && !this.jsonEnabled()) { + const confirmed = await confirm( + t('commands.mrt.org.cert.delete.confirm', 'Delete certificate {{id}} from {{org}}?', { + id: String(certId), + org, + }), + ); + if (!confirmed) { + this.log(t('commands.mrt.org.cert.delete.cancelled', 'Cancelled.')); + return {certId, deleted: false}; + } + } + + await deleteCertificate( + {organizationSlug: org, certId, origin: this.resolvedConfig.values.mrtOrigin}, + this.getMrtAuth(), + ); + + if (!this.jsonEnabled()) { + this.log(t('commands.mrt.org.cert.delete.success', 'Certificate {{id}} deleted.', {id: String(certId)})); + } + + return {certId, deleted: true}; + } +} diff --git a/packages/b2c-cli/src/commands/mrt/org/cert/get.ts b/packages/b2c-cli/src/commands/mrt/org/cert/get.ts new file mode 100644 index 00000000..84a1d0da --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/org/cert/get.ts @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {Args, Flags, ux} from '@oclif/core'; +import cliui from 'cliui'; +import {MrtCommand} from '@salesforce/b2c-tooling-sdk/cli'; +import {getCertificate, type MrtCertificate} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t, withDocs} from '../../../../i18n/index.js'; + +export default class MrtOrgCertGet extends MrtCommand { + static args = { + certId: Args.integer({description: 'Certificate ID', required: true}), + }; + + static description = withDocs( + t('commands.mrt.org.cert.get.description', 'Get details for a custom domain certificate'), + '/cli/mrt.html#b2c-mrt-org-cert-get', + ); + + static enableJsonFlag = true; + + static examples = ['<%= config.bin %> <%= command.id %> 123 --org my-org']; + + static flags = { + ...MrtCommand.baseFlags, + org: Flags.string({description: 'Organization slug', required: true}), + }; + + async run(): Promise { + this.requireMrtCredentials(); + + const {certId} = this.args; + const {org} = this.flags; + + const cert = await getCertificate( + {organizationSlug: org, certId, origin: this.resolvedConfig.values.mrtOrigin}, + this.getMrtAuth(), + ); + + if (!this.jsonEnabled()) { + const ui = cliui({width: process.stdout.columns || 80}); + const w = 24; + ui.div(''); + ui.div({text: 'ID:', width: w}, {text: cert.id?.toString() ?? '-'}); + ui.div({text: 'Domain:', width: w}, {text: cert.domain_name ?? '-'}); + ui.div({text: 'Validation Status:', width: w}, {text: cert.validation_status ?? '-'}); + ui.div({text: 'Validation Record:', width: w}, {text: cert.validation_record ?? '-'}); + ui.div({text: 'Validation Requested:', width: w}, {text: cert.validation_requested_at ?? '-'}); + ui.div({text: 'Expires:', width: w}, {text: cert.expires_at ?? '-'}); + ui.div({text: 'Renewal Status:', width: w}, {text: (cert.renewal_status as null | string) ?? '-'}); + ui.div({text: 'Renewal Eligibility:', width: w}, {text: (cert.renewal_eligibility as null | string) ?? '-'}); + ux.stdout(ui.toString()); + } + + return cert; + } +} diff --git a/packages/b2c-cli/src/commands/mrt/org/cert/list.ts b/packages/b2c-cli/src/commands/mrt/org/cert/list.ts new file mode 100644 index 00000000..d280182d --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/org/cert/list.ts @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {Flags} from '@oclif/core'; +import {MrtCommand, createTable, type ColumnDef} from '@salesforce/b2c-tooling-sdk/cli'; +import { + listCertificates, + type ListCertificatesResult, + type MrtCertificateListCreate, +} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t, withDocs} from '../../../../i18n/index.js'; + +const COLUMNS: Record> = { + id: {header: 'ID', get: (c) => c.id?.toString() ?? '-'}, + domain: {header: 'Domain', get: (c) => c.domain_name ?? '-'}, + validation: {header: 'Validation', get: (c) => c.validation_status ?? '-'}, + expires: { + header: 'Expires', + get: (c) => (c.expires_at ? new Date(c.expires_at).toLocaleDateString() : '-'), + }, + renewal: {header: 'Renewal', get: (c) => (c.renewal_status as null | string) ?? '-'}, +}; + +const DEFAULT_COLUMNS = ['id', 'domain', 'validation', 'expires', 'renewal']; + +export default class MrtOrgCertList extends MrtCommand { + static description = withDocs( + t('commands.mrt.org.cert.list.description', 'List custom domain certificates for an organization'), + '/cli/mrt.html#b2c-mrt-org-cert-list', + ); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %> --org my-org', + '<%= config.bin %> <%= command.id %> --org my-org --custom-only', + ]; + + static flags = { + ...MrtCommand.baseFlags, + org: Flags.string({description: 'Organization slug', required: true}), + limit: Flags.integer({description: 'Maximum number of results to return'}), + offset: Flags.integer({description: 'Offset for pagination'}), + search: Flags.string({description: 'Search term for filtering'}), + 'custom-only': Flags.boolean({description: 'Show only customer-managed certificates'}), + }; + + async run(): Promise { + this.requireMrtCredentials(); + + const {org, limit, offset, search, 'custom-only': customOnly} = this.flags; + + const result = await listCertificates( + { + organizationSlug: org, + limit, + offset, + search, + customOnly, + origin: this.resolvedConfig.values.mrtOrigin, + }, + this.getMrtAuth(), + ); + + if (!this.jsonEnabled()) { + if (result.certificates.length === 0) { + this.log(t('commands.mrt.org.cert.list.empty', 'No certificates found.')); + } else { + this.log(t('commands.mrt.org.cert.list.count', 'Found {{count}} certificate(s):', {count: result.count})); + createTable(COLUMNS).render(result.certificates, DEFAULT_COLUMNS); + } + } + + return result; + } +} diff --git a/packages/b2c-cli/src/commands/mrt/org/cert/restart-validation.ts b/packages/b2c-cli/src/commands/mrt/org/cert/restart-validation.ts new file mode 100644 index 00000000..96b571cf --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/org/cert/restart-validation.ts @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {Args, Flags} from '@oclif/core'; +import {MrtCommand} from '@salesforce/b2c-tooling-sdk/cli'; +import {restartCertificateValidation, type MrtCertificate} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t, withDocs} from '../../../../i18n/index.js'; + +export default class MrtOrgCertRestartValidation extends MrtCommand { + static args = { + certId: Args.integer({description: 'Certificate ID', required: true}), + }; + + static description = withDocs( + t( + 'commands.mrt.org.cert.restartValidation.description', + 'Restart validation for a certificate that has not yet been validated', + ), + '/cli/mrt.html#b2c-mrt-org-cert-restart-validation', + ); + + static enableJsonFlag = true; + + static examples = ['<%= config.bin %> <%= command.id %> 123 --org my-org']; + + static flags = { + ...MrtCommand.baseFlags, + org: Flags.string({description: 'Organization slug', required: true}), + }; + + async run(): Promise { + this.requireMrtCredentials(); + + const {certId} = this.args; + const {org} = this.flags; + + const cert = await restartCertificateValidation( + {organizationSlug: org, certId, origin: this.resolvedConfig.values.mrtOrigin}, + this.getMrtAuth(), + ); + + if (!this.jsonEnabled()) { + this.log( + t('commands.mrt.org.cert.restartValidation.success', 'Validation restarted for certificate {{id}}.', { + id: String(certId), + }), + ); + if (cert.validation_record) { + this.log( + t('commands.mrt.org.cert.restartValidation.record', 'New validation record:\n {{record}}', { + record: cert.validation_record, + }), + ); + } + } + + return cert; + } +} diff --git a/packages/b2c-cli/src/commands/mrt/org/member/add.ts b/packages/b2c-cli/src/commands/mrt/org/member/add.ts new file mode 100644 index 00000000..74b9b447 --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/org/member/add.ts @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {Args, Flags} from '@oclif/core'; +import {MrtCommand} from '@salesforce/b2c-tooling-sdk/cli'; +import { + addOrgMember, + ORG_ROLES, + type MrtOrgMember, + type OrgRoleValue, +} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t, withDocs} from '../../../../i18n/index.js'; + +const ROLE_NAMES_LOWER = Object.values(ORG_ROLES).map((n) => n.toLowerCase()); + +function roleNameToValue(name: string): OrgRoleValue { + const lower = name.toLowerCase(); + for (const [value, label] of Object.entries(ORG_ROLES)) { + if (label.toLowerCase() === lower) { + return Number(value) as OrgRoleValue; + } + } + // Unreachable because the flag is constrained by `options`. + throw new Error(`Unknown role: ${name}`); +} + +export default class MrtOrgMemberAdd extends MrtCommand { + static args = { + email: Args.string({description: 'Email address of the member to add', required: true}), + }; + + static description = withDocs( + t('commands.mrt.org.member.add.description', 'Add a member to a Managed Runtime organization'), + '/cli/mrt.html#b2c-mrt-org-member-add', + ); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %> alice@example.com --org my-org --role member', + '<%= config.bin %> <%= command.id %> bob@example.com --org my-org --role owner --view-all-projects', + ]; + + static flags = { + ...MrtCommand.baseFlags, + org: Flags.string({description: 'Organization slug', required: true}), + role: Flags.string({ + description: 'Role for the member', + required: true, + options: ROLE_NAMES_LOWER, + }), + 'view-all-projects': Flags.boolean({ + description: 'Allow the member to view all projects in the organization', + allowNo: true, + }), + 'cert-permission': Flags.boolean({ + description: 'Allow the member to manage custom domain certificates', + allowNo: true, + }), + }; + + protected operations = {addOrgMember}; + + async run(): Promise { + this.requireMrtCredentials(); + + const {email} = this.args; + const {org, role: roleFlag, 'view-all-projects': viewAll, 'cert-permission': certPerm} = this.flags; + + const role = roleNameToValue(roleFlag); + + const result = await this.operations.addOrgMember( + { + organizationSlug: org, + email, + role, + canViewAllProjects: viewAll, + customDomainCertPermission: certPerm === undefined ? undefined : certPerm ? 2 : 1, + origin: this.resolvedConfig.values.mrtOrigin, + }, + this.getMrtAuth(), + ); + + if (!this.jsonEnabled()) { + this.log( + t('commands.mrt.org.member.add.success', 'Added {{email}} to {{org}} as {{role}}.', { + email, + org, + role: ORG_ROLES[role], + }), + ); + } + + return result; + } +} diff --git a/packages/b2c-cli/src/commands/mrt/org/member/get.ts b/packages/b2c-cli/src/commands/mrt/org/member/get.ts new file mode 100644 index 00000000..b2eaf585 --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/org/member/get.ts @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {Args, Flags, ux} from '@oclif/core'; +import cliui from 'cliui'; +import {MrtCommand} from '@salesforce/b2c-tooling-sdk/cli'; +import { + getOrgMember, + ORG_ROLES, + type MrtOrgMember, + type OrgRoleValue, +} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t, withDocs} from '../../../../i18n/index.js'; + +export default class MrtOrgMemberGet extends MrtCommand { + static args = { + email: Args.string({description: 'Email address of the member', required: true}), + }; + + static description = withDocs( + t('commands.mrt.org.member.get.description', 'Get details for a Managed Runtime organization member'), + '/cli/mrt.html#b2c-mrt-org-member-get', + ); + + static enableJsonFlag = true; + + static examples = ['<%= config.bin %> <%= command.id %> alice@example.com --org my-org']; + + static flags = { + ...MrtCommand.baseFlags, + org: Flags.string({description: 'Organization slug', required: true}), + }; + + async run(): Promise { + this.requireMrtCredentials(); + + const {email} = this.args; + const {org} = this.flags; + + const member = await getOrgMember( + {organizationSlug: org, email, origin: this.resolvedConfig.values.mrtOrigin}, + this.getMrtAuth(), + ); + + if (!this.jsonEnabled()) { + const ui = cliui({width: process.stdout.columns || 80}); + const w = 22; + ui.div(''); + ui.div({text: 'Email:', width: w}, {text: member.email ?? member.user ?? '-'}); + ui.div({text: 'Name:', width: w}, {text: [member.first_name, member.last_name].filter(Boolean).join(' ') || '-'}); + ui.div({text: 'Role:', width: w}, {text: ORG_ROLES[member.role as OrgRoleValue] ?? String(member.role)}); + ui.div({text: 'View All Projects:', width: w}, {text: member.can_view_all_projects ? 'Yes' : 'No'}); + ui.div( + {text: 'Cert Permission:', width: w}, + {text: member.custom_domain_cert_permission === 2 ? 'Enabled' : 'Disabled'}, + ); + ux.stdout(ui.toString()); + } + + return member; + } +} diff --git a/packages/b2c-cli/src/commands/mrt/org/member/list.ts b/packages/b2c-cli/src/commands/mrt/org/member/list.ts new file mode 100644 index 00000000..1e207b75 --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/org/member/list.ts @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {Flags} from '@oclif/core'; +import {MrtCommand, createTable, type ColumnDef} from '@salesforce/b2c-tooling-sdk/cli'; +import { + listOrgMembers, + ORG_ROLES, + type ListOrgMembersResult, + type MrtOrgMember, + type OrgRoleValue, +} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t, withDocs} from '../../../../i18n/index.js'; + +const COLUMNS: Record> = { + email: { + header: 'Email', + get: (m) => m.email ?? m.user ?? '-', + }, + name: { + header: 'Name', + get: (m) => [m.first_name, m.last_name].filter(Boolean).join(' ') || '-', + }, + role: { + header: 'Role', + get: (m) => ORG_ROLES[m.role as OrgRoleValue] ?? String(m.role), + }, + allProjects: { + header: 'View All Projects', + get: (m) => (m.can_view_all_projects ? 'Yes' : 'No'), + }, + certPerm: { + header: 'Cert Perm', + get: (m) => (m.custom_domain_cert_permission === 2 ? 'Enabled' : 'Disabled'), + }, +}; + +const DEFAULT_COLUMNS = ['email', 'name', 'role', 'allProjects', 'certPerm']; + +export default class MrtOrgMemberList extends MrtCommand { + static description = withDocs( + t('commands.mrt.org.member.list.description', 'List members of a Managed Runtime organization'), + '/cli/mrt.html#b2c-mrt-org-member-list', + ); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %> --org my-org', + '<%= config.bin %> <%= command.id %> --org my-org --search alice', + ]; + + static flags = { + ...MrtCommand.baseFlags, + org: Flags.string({ + description: 'Organization slug', + required: true, + }), + limit: Flags.integer({description: 'Maximum number of results to return'}), + offset: Flags.integer({description: 'Offset for pagination'}), + search: Flags.string({description: 'Search term for filtering'}), + }; + + async run(): Promise { + this.requireMrtCredentials(); + + const {org, limit, offset, search} = this.flags; + + const result = await listOrgMembers( + { + organizationSlug: org, + limit, + offset, + search, + origin: this.resolvedConfig.values.mrtOrigin, + }, + this.getMrtAuth(), + ); + + if (!this.jsonEnabled()) { + if (result.members.length === 0) { + this.log(t('commands.mrt.org.member.list.empty', 'No members found.')); + } else { + this.log(t('commands.mrt.org.member.list.count', 'Found {{count}} member(s):', {count: result.count})); + createTable(COLUMNS).render(result.members, DEFAULT_COLUMNS); + } + } + + return result; + } +} diff --git a/packages/b2c-cli/src/commands/mrt/org/member/remove.ts b/packages/b2c-cli/src/commands/mrt/org/member/remove.ts new file mode 100644 index 00000000..da0c15f8 --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/org/member/remove.ts @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {Args, Flags} from '@oclif/core'; +import {MrtCommand} from '@salesforce/b2c-tooling-sdk/cli'; +import {removeOrgMember} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t, withDocs} from '../../../../i18n/index.js'; +import {confirm} from '../../../../prompts.js'; + +export default class MrtOrgMemberRemove extends MrtCommand { + static args = { + email: Args.string({description: 'Email address of the member to remove', required: true}), + }; + + static description = withDocs( + t('commands.mrt.org.member.remove.description', 'Remove a member from a Managed Runtime organization'), + '/cli/mrt.html#b2c-mrt-org-member-remove', + ); + + static enableJsonFlag = true; + + static examples = ['<%= config.bin %> <%= command.id %> alice@example.com --org my-org']; + + static flags = { + ...MrtCommand.baseFlags, + org: Flags.string({description: 'Organization slug', required: true}), + force: Flags.boolean({char: 'f', description: 'Skip confirmation prompt', default: false}), + }; + + async run(): Promise<{email: string; removed: boolean}> { + this.requireMrtCredentials(); + + const {email} = this.args; + const {org, force} = this.flags; + + if (!force && !this.jsonEnabled()) { + const confirmed = await confirm( + t('commands.mrt.org.member.remove.confirm', 'Remove {{email}} from {{org}}?', {email, org}), + ); + if (!confirmed) { + this.log(t('commands.mrt.org.member.remove.cancelled', 'Cancelled.')); + return {email, removed: false}; + } + } + + await removeOrgMember( + {organizationSlug: org, email, origin: this.resolvedConfig.values.mrtOrigin}, + this.getMrtAuth(), + ); + + if (!this.jsonEnabled()) { + this.log(t('commands.mrt.org.member.remove.success', 'Removed {{email}} from {{org}}.', {email, org})); + } + + return {email, removed: true}; + } +} diff --git a/packages/b2c-cli/src/commands/mrt/org/member/update.ts b/packages/b2c-cli/src/commands/mrt/org/member/update.ts new file mode 100644 index 00000000..2d03ff31 --- /dev/null +++ b/packages/b2c-cli/src/commands/mrt/org/member/update.ts @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {Args, Flags} from '@oclif/core'; +import {MrtCommand} from '@salesforce/b2c-tooling-sdk/cli'; +import {updateOrgMember, type MrtOrgMember} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {t, withDocs} from '../../../../i18n/index.js'; + +export default class MrtOrgMemberUpdate extends MrtCommand { + static args = { + email: Args.string({description: 'Email address of the member to update', required: true}), + }; + + static description = withDocs( + t('commands.mrt.org.member.update.description', "Update a Managed Runtime organization member's permissions"), + '/cli/mrt.html#b2c-mrt-org-member-update', + ); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %> alice@example.com --org my-org --view-all-projects', + '<%= config.bin %> <%= command.id %> alice@example.com --org my-org --no-cert-permission', + ]; + + static flags = { + ...MrtCommand.baseFlags, + org: Flags.string({description: 'Organization slug', required: true}), + 'view-all-projects': Flags.boolean({ + description: 'Whether the member can view all projects', + allowNo: true, + }), + 'cert-permission': Flags.boolean({ + description: 'Whether the member can manage custom domain certificates', + allowNo: true, + }), + }; + + async run(): Promise { + this.requireMrtCredentials(); + + const {email} = this.args; + const {org, 'view-all-projects': viewAll, 'cert-permission': certPerm} = this.flags; + + if (viewAll === undefined && certPerm === undefined) { + this.error( + t( + 'commands.mrt.org.member.update.noChanges', + 'No changes specified. Provide --view-all-projects and/or --cert-permission.', + ), + ); + } + + const result = await updateOrgMember( + { + organizationSlug: org, + email, + canViewAllProjects: viewAll, + customDomainCertPermission: certPerm === undefined ? undefined : certPerm ? 2 : 1, + origin: this.resolvedConfig.values.mrtOrigin, + }, + this.getMrtAuth(), + ); + + if (!this.jsonEnabled()) { + this.log(t('commands.mrt.org.member.update.success', 'Updated permissions for {{email}}.', {email})); + } + + return result; + } +} diff --git a/packages/b2c-cli/test/commands/mrt/bundle/delete.test.ts b/packages/b2c-cli/test/commands/mrt/bundle/delete.test.ts new file mode 100644 index 00000000..0ab51f78 --- /dev/null +++ b/packages/b2c-cli/test/commands/mrt/bundle/delete.test.ts @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import {expect} from 'chai'; +import sinon from 'sinon'; +import {Config} from '@oclif/core'; +import MrtBundleDelete from '../../../../src/commands/mrt/bundle/delete.js'; +import {isolateConfig, restoreConfig} from '@salesforce/b2c-tooling-sdk/test-utils'; + +describe('mrt bundle delete', () => { + let config: Config; + + beforeEach(async () => { + isolateConfig(); + config = await Config.load(); + }); + + afterEach(() => { + sinon.restore(); + restoreConfig(); + }); + + function createCommand(argv: string[]): any { + return new MrtBundleDelete(argv, config); + } + + function stubAuthAndConfig(command: any, project = 'p'): void { + sinon.stub(command, 'requireMrtCredentials').returns(void 0); + sinon.stub(command, 'getMrtAuth').returns({} as any); + sinon.stub(command, 'jsonEnabled').returns(true); + sinon.stub(command, 'log').returns(void 0); + sinon.stub(command, 'resolvedConfig').get(() => ({values: {mrtProject: project}})); + } + + it('uses single-bundle DELETE when one ID is provided', async () => { + const command = createCommand(['42', '--project', 'p', '--force']); + stubAuthAndConfig(command); + + const deleteStub = sinon.stub().resolves(); + const bulkStub = sinon.stub(); + command.operations = {deleteBundle: deleteStub, bulkDeleteBundles: bulkStub}; + + const result = await command.run(); + + expect(deleteStub.calledOnce).to.equal(true); + expect(bulkStub.notCalled).to.equal(true); + expect(deleteStub.firstCall.args[0].bundleId).to.equal(42); + expect(result.queued).to.deep.equal([42]); + }); + + it('uses bulk-delete when multiple IDs are provided', async () => { + const command = createCommand(['1', '2', '3', '--project', 'p', '--force']); + stubAuthAndConfig(command); + + const deleteStub = sinon.stub(); + const bulkStub = sinon.stub().resolves({queued: [1, 3], rejected: [{bundleId: 2, reason: 'in use'}]}); + command.operations = {deleteBundle: deleteStub, bulkDeleteBundles: bulkStub}; + + const result = await command.run(); + + expect(bulkStub.calledOnce).to.equal(true); + expect(deleteStub.notCalled).to.equal(true); + expect(bulkStub.firstCall.args[0].bundleIds).to.deep.equal([1, 2, 3]); + expect(result.queued).to.deep.equal([1, 3]); + expect(result.rejected).to.have.lengthOf(1); + }); +}); diff --git a/packages/b2c-cli/test/commands/mrt/env/clone.test.ts b/packages/b2c-cli/test/commands/mrt/env/clone.test.ts new file mode 100644 index 00000000..e7f99167 --- /dev/null +++ b/packages/b2c-cli/test/commands/mrt/env/clone.test.ts @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import {expect} from 'chai'; +import sinon from 'sinon'; +import {Config} from '@oclif/core'; +import MrtEnvClone from '../../../../src/commands/mrt/env/clone.js'; +import {isolateConfig, restoreConfig} from '@salesforce/b2c-tooling-sdk/test-utils'; +import {stubParse} from '../../../helpers/stub-parse.js'; + +describe('mrt env clone', () => { + let config: Config; + + beforeEach(async () => { + isolateConfig(); + config = await Config.load(); + }); + + afterEach(() => { + sinon.restore(); + restoreConfig(); + }); + + function createCommand(): any { + return new MrtEnvClone([], config); + } + + function stubCommonAuth(command: any): void { + sinon.stub(command, 'requireMrtCredentials').returns(void 0); + sinon.stub(command, 'getMrtAuth').returns({} as any); + } + + it('errors when project is missing', async () => { + const command = createCommand(); + stubParse(command, {}, {slug: 'staging-copy'}); + await command.init(); + stubCommonAuth(command); + sinon.stub(command, 'resolvedConfig').get(() => ({values: {mrtProject: undefined, mrtEnvironment: 'staging'}})); + const errorStub = sinon.stub(command, 'error').throws(new Error('expected')); + + try { + await command.run(); + expect.fail('expected error'); + } catch { + expect(errorStub.calledOnce).to.equal(true); + } + }); + + it('uses mrtEnvironment as the source and passes flags through to cloneEnv', async () => { + const command = createCommand(); + stubParse( + command, + { + 'external-hostname': 'qa.example.com', + 'certificate-id': 123, + 'clone-redirects': true, + 'clone-env-vars': true, + 'clone-b2c-info': false, + wait: false, + }, + {slug: 'qa'}, + ); + await command.init(); + stubCommonAuth(command); + sinon.stub(command, 'jsonEnabled').returns(true); + sinon.stub(command, 'log').returns(void 0); + sinon + .stub(command, 'resolvedConfig') + .get(() => ({values: {mrtProject: 'p', mrtEnvironment: 'staging', mrtOrigin: 'https://example.com'}})); + + const cloneStub = sinon.stub().resolves({slug: 'qa', name: 'qa', state: 'CREATE_IN_PROGRESS'} as any); + const waitStub = sinon.stub(); + command.operations = {cloneEnv: cloneStub, waitForEnv: waitStub}; + + const result = await command.run(); + + expect(cloneStub.calledOnce).to.equal(true); + const [input] = cloneStub.firstCall.args; + expect(input.projectSlug).to.equal('p'); + expect(input.slug).to.equal('qa'); + expect(input.fromSlug).to.equal('staging'); + expect(input.externalHostname).to.equal('qa.example.com'); + expect(input.certificateId).to.equal(123); + expect(input.cloneRedirects).to.equal(true); + expect(input.cloneEnvironmentVariables).to.equal(true); + expect(input.cloneB2cTargetInfo).to.equal(false); + expect(waitStub.notCalled).to.equal(true); + expect(result.slug).to.equal('qa'); + }); + + it('errors when mrtEnvironment is not set', async () => { + const command = createCommand(); + stubParse(command, {'clone-redirects': false, 'clone-env-vars': false, 'clone-b2c-info': false}, {slug: 'qa'}); + await command.init(); + stubCommonAuth(command); + sinon.stub(command, 'resolvedConfig').get(() => ({values: {mrtProject: 'p'}})); + const errorStub = sinon.stub(command, 'error').throws(new Error('expected')); + + try { + await command.run(); + expect.fail('expected error'); + } catch { + expect(errorStub.firstCall.args[0]).to.include('Source environment is required'); + } + }); + + it('errors when source and destination slugs are equal', async () => { + const command = createCommand(); + stubParse(command, {'clone-redirects': false, 'clone-env-vars': false, 'clone-b2c-info': false}, {slug: 'staging'}); + await command.init(); + stubCommonAuth(command); + sinon.stub(command, 'resolvedConfig').get(() => ({values: {mrtProject: 'p', mrtEnvironment: 'staging'}})); + const errorStub = sinon.stub(command, 'error').throws(new Error('expected')); + + try { + await command.run(); + expect.fail('expected error'); + } catch { + expect(errorStub.firstCall.args[0]).to.include('must differ'); + } + }); + + it('waits for env when --wait is set', async () => { + const command = createCommand(); + stubParse( + command, + { + wait: true, + 'poll-interval': 1, + timeout: 30, + 'clone-redirects': false, + 'clone-env-vars': false, + 'clone-b2c-info': false, + }, + {slug: 'qa'}, + ); + await command.init(); + stubCommonAuth(command); + sinon.stub(command, 'jsonEnabled').returns(true); + sinon.stub(command, 'log').returns(void 0); + sinon.stub(command, 'resolvedConfig').get(() => ({values: {mrtProject: 'p', mrtEnvironment: 'staging'}})); + + const cloneStub = sinon.stub().resolves({slug: 'qa', state: 'CREATE_IN_PROGRESS'} as any); + const waitStub = sinon.stub().resolves({slug: 'qa', state: 'ACTIVE'} as any); + command.operations = {cloneEnv: cloneStub, waitForEnv: waitStub}; + + const result = await command.run(); + + expect(waitStub.calledOnce).to.equal(true); + expect(result.state).to.equal('ACTIVE'); + }); +}); diff --git a/packages/b2c-cli/test/commands/mrt/org/member/add.test.ts b/packages/b2c-cli/test/commands/mrt/org/member/add.test.ts new file mode 100644 index 00000000..5446ccc5 --- /dev/null +++ b/packages/b2c-cli/test/commands/mrt/org/member/add.test.ts @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import {expect} from 'chai'; +import sinon from 'sinon'; +import {Config} from '@oclif/core'; +import MrtOrgMemberAdd from '../../../../../src/commands/mrt/org/member/add.js'; +import {isolateConfig, restoreConfig} from '@salesforce/b2c-tooling-sdk/test-utils'; +import {stubParse} from '../../../../helpers/stub-parse.js'; + +describe('mrt org member add', () => { + let config: Config; + + beforeEach(async () => { + isolateConfig(); + config = await Config.load(); + }); + + afterEach(() => { + sinon.restore(); + restoreConfig(); + }); + + function createCommand(): any { + return new MrtOrgMemberAdd([], config); + } + + function stubAuth(command: any): void { + sinon.stub(command, 'requireMrtCredentials').returns(void 0); + sinon.stub(command, 'getMrtAuth').returns({} as any); + sinon.stub(command, 'jsonEnabled').returns(true); + sinon.stub(command, 'log').returns(void 0); + sinon.stub(command, 'resolvedConfig').get(() => ({values: {}})); + } + + it('maps the role name to its numeric value when calling addOrgMember', async () => { + const command = createCommand(); + stubParse(command, {org: 'my-org', role: 'owner'}, {email: 'alice@example.com'}); + await command.init(); + stubAuth(command); + + const addStub = sinon.stub().resolves({user: 'alice@example.com', email: 'alice@example.com', role: 0} as any); + command.operations = {addOrgMember: addStub}; + + await command.run(); + + expect(addStub.calledOnce).to.equal(true); + expect(addStub.firstCall.args[0].role).to.equal(0); + }); + + it('passes through view-all-projects and cert-permission flags', async () => { + const command = createCommand(); + stubParse( + command, + {org: 'my-org', role: 'member', 'view-all-projects': true, 'cert-permission': false}, + {email: 'bob@example.com'}, + ); + await command.init(); + stubAuth(command); + + const addStub = sinon.stub().resolves({} as any); + command.operations = {addOrgMember: addStub}; + + await command.run(); + + const [input] = addStub.firstCall.args; + expect(input.canViewAllProjects).to.equal(true); + expect(input.customDomainCertPermission).to.equal(1); + }); +}); diff --git a/packages/b2c-tooling-sdk/specs/mrt-api-v1.json b/packages/b2c-tooling-sdk/specs/mrt-api-v1.json index 0cfb856b..1f279cca 100644 --- a/packages/b2c-tooling-sdk/specs/mrt-api-v1.json +++ b/packages/b2c-tooling-sdk/specs/mrt-api-v1.json @@ -60,6 +60,7 @@ "name": "my org", "slug": "my-org", "has_mobify_tag_project": false, + "can_configure_ssr_architecture": false, "created_at": "2020-07-14T19:33:21.197800Z", "updated_at": "2020-07-21T17:02:38.745978Z", "permissions": { @@ -80,6 +81,893 @@ } } }, + "/api/organizations/{organization_slug}/certificates/": { + "get": { + "operationId": "organizations_certificates_list", + "description": "List certificates for an organization with filtering and search", + "parameters": [ + { + "in": "query", + "name": "custom_only", + "schema": { + "type": "boolean" + }, + "description": "If true, returns only custom domain certificates created by the customer." + }, + { + "name": "limit", + "required": false, + "in": "query", + "description": "Number of results to return per page.", + "schema": { + "type": "integer" + } + }, + { + "name": "offset", + "required": false, + "in": "query", + "description": "The initial index from which to return the results.", + "schema": { + "type": "integer" + } + }, + { + "name": "ordering", + "required": false, + "in": "query", + "description": "Which field to use when ordering the results.", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "organization_slug", + "schema": { + "type": "string" + }, + "description": "The organization identifier.", + "required": true + }, + { + "name": "search", + "required": false, + "in": "query", + "description": "A search term.", + "schema": { + "type": "string" + } + } + ], + "tags": [ + "organizations" + ], + "security": [ + { + "Basic": [] + }, + { + "BearerToken": [] + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaginatedCertificateListCreateList" + }, + "examples": { + "ListCertificates": { + "value": { + "count": 123, + "next": "http://api.example.org/accounts/?offset=400&limit=100", + "previous": "http://api.example.org/accounts/?offset=200&limit=100", + "results": [ + { + "count": 2, + "next": null, + "previous": null, + "results": [ + { + "id": "123e4567-e89b-12d3-a456-426614174000", + "domain_name": "*.example.com", + "validation_status": "PENDING_VALIDATION", + "validation_record": { + "name": "_acme-challenge.example.com", + "value": "validation123.example.com" + }, + "aws_arns": [ + "arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012" + ], + "created_at": "2023-10-28T10:00:00Z", + "updated_at": "2023-10-28T10:00:00Z", + "validation_requested_at": "2023-10-28T10:00:00Z", + "validation_requested_by": "user@example.com", + "expires_at": "2024-10-28T10:00:00Z", + "targets": [], + "created_by": "user@example.com" + } + ], + "limits": { + "max_certificates": { + "limit": 10, + "used": 2 + } + }, + "customer_action_needed": true + } + ] + }, + "summary": "List certificates" + } + } + } + }, + "description": "" + } + } + }, + "post": { + "operationId": "organizations_certificates_create", + "description": "Create a new certificate for an organization", + "parameters": [ + { + "in": "path", + "name": "organization_slug", + "schema": { + "type": "string" + }, + "description": "The organization identifier.", + "required": true + } + ], + "tags": [ + "organizations" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CertificateListCreate" + }, + "examples": { + "CreateCertificateRequest": { + "value": { + "domain_name": "*.example.com" + }, + "summary": "Create certificate request" + } + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CertificateListCreate" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/CertificateListCreate" + } + } + }, + "required": true + }, + "security": [ + { + "Basic": [] + }, + { + "BearerToken": [] + } + ], + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CertificateListCreate" + }, + "examples": { + "CreateCertificateResponse": { + "value": { + "id": "123e4567-e89b-12d3-a456-426614174000", + "domain_name": "*.example.com", + "validation_status": "PENDING_VALIDATION", + "validation_record": { + "name": "_acme-challenge.example.com", + "value": "validation123.example.com" + }, + "aws_arns": [ + "arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012" + ], + "created_at": "2023-10-28T10:00:00Z", + "updated_at": "2023-10-28T10:00:00Z", + "validation_requested_at": "2023-10-28T10:00:00Z", + "validation_requested_by": "user@example.com", + "targets": [] + }, + "summary": "Create certificate response" + } + } + } + }, + "description": "" + } + } + } + }, + "/api/organizations/{organization_slug}/certificates/{cert_id}/": { + "get": { + "operationId": "organizations_certificates_retrieve", + "description": "Retrieve a specific certificate for an organization", + "parameters": [ + { + "in": "path", + "name": "cert_id", + "schema": { + "type": "string" + }, + "description": "The certificate's id", + "required": true + }, + { + "in": "path", + "name": "organization_slug", + "schema": { + "type": "string" + }, + "description": "The organization identifier.", + "required": true + } + ], + "tags": [ + "organizations" + ], + "security": [ + { + "Basic": [] + }, + { + "BearerToken": [] + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CertificateBase" + }, + "examples": { + "GetCertificateResponse": { + "value": { + "id": "123e4567-e89b-12d3-a456-426614174000", + "domain_name": "*.example.com", + "validation_status": "PENDING_VALIDATION", + "validation_record": { + "name": "_acme-challenge.example.com", + "value": "validation123.example.com" + }, + "aws_arns": [ + "arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012" + ], + "created_at": "2023-10-28T10:00:00Z", + "updated_at": "2023-10-28T10:00:00Z", + "validation_requested_at": "2023-10-28T10:00:00Z", + "validation_requested_by": "user@example.com", + "targets": [] + }, + "summary": "Get certificate response" + } + } + } + }, + "description": "" + } + } + }, + "delete": { + "operationId": "organizations_certificates_destroy", + "description": "Delete a specific certificate for an organization", + "parameters": [ + { + "in": "path", + "name": "cert_id", + "schema": { + "type": "string" + }, + "description": "The certificate's id", + "required": true + }, + { + "in": "path", + "name": "organization_slug", + "schema": { + "type": "string" + }, + "description": "The organization identifier.", + "required": true + } + ], + "tags": [ + "organizations" + ], + "security": [ + { + "Basic": [] + }, + { + "BearerToken": [] + } + ], + "responses": { + "204": { + "description": "No response body" + } + } + } + }, + "/api/organizations/{organization_slug}/certificates/{cert_id}/restart-validation/": { + "put": { + "operationId": "organizations_certificates_restart_validation_update", + "description": "Restart validation for a certificate. Creates a new certificate in AWS and updates the existing certificate record. Only works for certificates that are not yet validated.", + "parameters": [ + { + "in": "path", + "name": "cert_id", + "schema": { + "type": "string" + }, + "description": "Certificate ID", + "required": true + }, + { + "in": "path", + "name": "organization_slug", + "schema": { + "type": "string" + }, + "description": "The organization identifier.", + "required": true + } + ], + "tags": [ + "organizations" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CertificateBase" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CertificateBase" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/CertificateBase" + } + } + }, + "required": true + }, + "security": [ + { + "Basic": [] + }, + { + "BearerToken": [] + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CertificateBase" + }, + "examples": { + "ResponseExample1": { + "value": { + "id": "uuid", + "domain_name": "shop.example.com", + "validation_status": "pending_validation", + "validation_record": { + "name": "_abc.shop.example.com", + "value": "_xyz.acm-validations.aws" + }, + "validation_requested_at": "2025-10-16T12:00:00Z", + "deletion_status": "ACTIVE" + } + } + } + } + }, + "description": "Certificate validation restarted successfully" + }, + "400": { + "description": "Certificate is already validated or cannot be restarted" + } + } + }, + "patch": { + "operationId": "organizations_certificates_restart_validation_partial_update", + "description": "Restart validation for a specific certificate for an organization.", + "parameters": [ + { + "in": "path", + "name": "cert_id", + "schema": { + "type": "string", + "pattern": "^[\\w-]+$" + }, + "required": true + }, + { + "in": "path", + "name": "organization_slug", + "schema": { + "type": "string", + "pattern": "^[\\w-]+$" + }, + "required": true + } + ], + "tags": [ + "organizations" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PatchedCertificateBase" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/PatchedCertificateBase" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/PatchedCertificateBase" + } + } + } + }, + "security": [ + { + "Basic": [] + }, + { + "BearerToken": [] + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CertificateBase" + } + } + }, + "description": "" + } + } + } + }, + "/api/organizations/{organization_slug}/members/": { + "get": { + "operationId": "list_organization_members", + "description": "List all the members of an organization.", + "parameters": [ + { + "name": "limit", + "required": false, + "in": "query", + "description": "Number of results to return per page.", + "schema": { + "type": "integer" + } + }, + { + "name": "offset", + "required": false, + "in": "query", + "description": "The initial index from which to return the results.", + "schema": { + "type": "integer" + } + }, + { + "name": "ordering", + "required": false, + "in": "query", + "description": "Which field to use when ordering the results.", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "organization_slug", + "schema": { + "type": "string" + }, + "description": "The organization identifier.", + "required": true + }, + { + "name": "search", + "required": false, + "in": "query", + "description": "A search term.", + "schema": { + "type": "string" + } + } + ], + "tags": [ + "organizations" + ], + "security": [ + { + "Basic": [] + }, + { + "BearerToken": [] + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaginatedAPIOrganizationMemberList" + }, + "examples": { + "ResponseExample1": { + "value": { + "count": 123, + "next": "http://api.example.org/accounts/?offset=400&limit=100", + "previous": "http://api.example.org/accounts/?offset=200&limit=100", + "results": [ + [ + { + "user": "user1@example.com", + "role": { + "name": "Owner", + "value": 0 + }, + "first_name": "John", + "last_name": "Doe", + "can_view_all_projects": true, + "custom_domain_cert_permission": { + "name": "Manage", + "value": 0 + } + }, + { + "user": "user2@example.com", + "role": { + "name": "Member", + "value": 1 + }, + "first_name": "Jane", + "last_name": "Smith", + "can_view_all_projects": false, + "custom_domain_cert_permission": { + "name": "View Only", + "value": 1 + } + } + ] + ] + } + } + } + } + }, + "description": "" + } + } + }, + "post": { + "operationId": "organizations_members_create", + "description": "Add a new member to an organization.", + "parameters": [ + { + "in": "path", + "name": "organization_slug", + "schema": { + "type": "string" + }, + "description": "The organization identifier.", + "required": true + } + ], + "tags": [ + "organizations" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIOrganizationMemberCreate" + }, + "examples": { + "RequestExample1": { + "value": { + "user": "newuser@example.com", + "role": 1, + "can_view_all_projects": false, + "custom_domain_cert_permission": 2 + } + } + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/APIOrganizationMemberCreate" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/APIOrganizationMemberCreate" + } + } + }, + "required": true + }, + "security": [ + { + "Basic": [] + }, + { + "BearerToken": [] + } + ], + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIOrganizationMemberCreate" + }, + "examples": { + "ResponseExample1": { + "value": { + "user": "newuser@example.com", + "role": { + "name": "Member", + "value": 1 + }, + "first_name": "New", + "last_name": "User", + "can_view_all_projects": false, + "custom_domain_cert_permission": { + "name": "No Access", + "value": 2 + } + } + } + } + } + }, + "description": "" + } + } + } + }, + "/api/organizations/{organization_slug}/members/{email}/": { + "get": { + "operationId": "organizations_members_retrieve", + "description": "Read data about an organization member's permissions.", + "parameters": [ + { + "in": "path", + "name": "email", + "schema": { + "type": "string" + }, + "description": "The email address of the organization member.", + "required": true + }, + { + "in": "path", + "name": "organization_slug", + "schema": { + "type": "string" + }, + "description": "The organization identifier.", + "required": true + } + ], + "tags": [ + "organizations" + ], + "security": [ + { + "Basic": [] + }, + { + "BearerToken": [] + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIOrganizationMember" + }, + "examples": { + "ResponseExample1": { + "value": { + "user": "user@example.com", + "role": { + "name": "Owner", + "value": 0 + }, + "first_name": "John", + "last_name": "Doe", + "can_view_all_projects": true, + "custom_domain_cert_permission": { + "name": "Manage", + "value": 0 + } + } + } + } + } + }, + "description": "" + } + } + }, + "patch": { + "operationId": "organizations_members_partial_update", + "description": "Update an organization member's permissions.", + "parameters": [ + { + "in": "path", + "name": "email", + "schema": { + "type": "string" + }, + "description": "The email address of the organization member.", + "required": true + }, + { + "in": "path", + "name": "organization_slug", + "schema": { + "type": "string" + }, + "description": "The organization identifier.", + "required": true + } + ], + "tags": [ + "organizations" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PatchedAPIOrganizationMemberUpdate" + }, + "examples": { + "RequestExample1": { + "value": { + "role": { + "name": "Member", + "value": 1 + }, + "can_view_all_projects": false, + "custom_domain_cert_permission": 1 + } + } + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/PatchedAPIOrganizationMemberUpdate" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/PatchedAPIOrganizationMemberUpdate" + } + } + } + }, + "security": [ + { + "Basic": [] + }, + { + "BearerToken": [] + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIOrganizationMemberUpdate" + }, + "examples": { + "ResponseExample1": { + "value": { + "user": "user@example.com", + "role": { + "name": "Member", + "value": 1 + }, + "first_name": "John", + "last_name": "Doe", + "can_view_all_projects": false, + "custom_domain_cert_permission": { + "name": "View Only", + "value": 1 + } + } + } + } + } + }, + "description": "" + } + } + }, + "delete": { + "operationId": "organizations_members_destroy", + "description": "Remove a member from an organization.", + "parameters": [ + { + "in": "path", + "name": "email", + "schema": { + "type": "string" + }, + "description": "The email address of the organization member.", + "required": true + }, + { + "in": "path", + "name": "organization_slug", + "schema": { + "type": "string" + }, + "description": "The organization identifier.", + "required": true + } + ], + "tags": [ + "organizations" + ], + "security": [ + { + "Basic": [] + }, + { + "BearerToken": [] + } + ], + "responses": { + "204": { + "description": "No response body" + } + } + } + }, "/api/projects/": { "get": { "operationId": "projects_list", @@ -163,7 +1051,8 @@ "create_jwt": true, "upload_bundle": true, "edit_environment_variable": true, - "edit_access_control_header": true + "edit_access_control_header": true, + "edit_data_access_layer_entry": true }, "deletion_status": "ACTIVE", "created_at": "2025-07-24T07:30:22.288165Z", @@ -256,7 +1145,8 @@ "create_jwt": true, "upload_bundle": true, "edit_environment_variable": true, - "edit_access_control_header": true + "edit_access_control_header": true, + "edit_data_access_layer_entry": true }, "deletion_status": "ACTIVE", "created_at": "2025-07-24T07:30:22.288165Z", @@ -331,7 +1221,8 @@ "create_jwt": true, "upload_bundle": true, "edit_environment_variable": true, - "edit_access_control_header": true + "edit_access_control_header": true, + "edit_data_access_layer_entry": true }, "deletion_status": "ACTIVE", "created_at": "2025-07-24T07:30:22.288165Z", @@ -432,7 +1323,8 @@ "create_jwt": true, "upload_bundle": true, "edit_environment_variable": true, - "edit_access_control_header": true + "edit_access_control_header": true, + "edit_data_access_layer_entry": true }, "deletion_status": "ACTIVE", "created_at": "2025-07-24T07:30:22.288165Z", @@ -648,6 +1540,15 @@ "type": "integer" } }, + { + "name": "ordering", + "required": false, + "in": "query", + "description": "Which field to use when ordering the results.", + "schema": { + "type": "string" + } + }, { "in": "path", "name": "project_slug", @@ -703,7 +1604,12 @@ "is_uploaded": true, "is_ssr_bundle": true, "ssr_runtime": "12.x", - "ssr_runtime_description": "NodeJS 12.x" + "ssr_runtime_description": "NodeJS 12.x", + "in_use": true, + "in_use_by": [ + "production", + "staging" + ] } ] } @@ -725,7 +1631,9 @@ "is_uploaded": true, "is_ssr_bundle": true, "ssr_runtime": "10.x", - "ssr_runtime_description": "NodeJS 10.x" + "ssr_runtime_description": "NodeJS 10.x", + "in_use": false, + "in_use_by": [] } ] } @@ -747,7 +1655,11 @@ "is_uploaded": true, "is_ssr_bundle": true, "ssr_runtime": "8.10", - "ssr_runtime_description": "NodeJS 8.10" + "ssr_runtime_description": "NodeJS 8.10", + "in_use": true, + "in_use_by": [ + "development" + ] } ] } @@ -760,6 +1672,57 @@ } } }, + "/api/projects/{project_slug}/bundles/{bundle_id}/": { + "delete": { + "operationId": "projects_bundles_destroy", + "description": "Request deletion of a bundle. The bundle will be asynchronously deleted. This operation can only be performed by project admins.", + "parameters": [ + { + "in": "path", + "name": "bundle_id", + "schema": { + "type": "string" + }, + "description": "The bundle's id", + "required": true + }, + { + "in": "path", + "name": "project_slug", + "schema": { + "type": "string" + }, + "description": "The project identifier.", + "required": true + } + ], + "tags": [ + "projects" + ], + "security": [ + { + "Basic": [] + }, + { + "BearerToken": [] + } + ], + "responses": { + "202": { + "description": "Bundle deletion request accepted. The bundle will be asynchronously deleted." + }, + "404": { + "description": "Bundle not found." + }, + "409": { + "description": "Bundle cannot be deleted (in progress, in failed state)." + }, + "403": { + "description": "Bundle deployed to one or more target, cannot delete." + } + } + } + }, "/api/projects/{project_slug}/bundles/{bundle_id}/download/": { "get": { "operationId": "projects_bundles_download_retrieve", @@ -816,6 +1779,158 @@ } } }, + "/api/projects/{project_slug}/bundles/bulk-delete/": { + "post": { + "operationId": "projects_bundles_bulk_delete_create", + "description": "Request deletion for multiple bundles. Bundles are deleted asynchronously. Only project admins can perform this call. The response indicates which bundles were successfully queued for deletion and which failed validation.", + "parameters": [ + { + "in": "path", + "name": "project_slug", + "schema": { + "type": "string" + }, + "description": "The project identifier.", + "required": true + } + ], + "tags": [ + "projects" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BundleBulkDelete" + }, + "examples": { + "RequestExample1": { + "value": { + "bundle_ids": [ + 1, + 2, + 3 + ] + } + } + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/BundleBulkDelete" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/BundleBulkDelete" + } + } + }, + "required": true + }, + "security": [ + { + "Basic": [] + }, + { + "BearerToken": [] + } + ], + "responses": { + "202": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BundleBulkDeleteResponse" + }, + "examples": { + "ResponseExample1-AllSuccessful": { + "value": { + "bundles_queued_for_cleanup": [ + 1, + 2, + 3 + ] + }, + "summary": "ResponseExample1 - All Successful" + }, + "ResponseExample2-PartialSuccess": { + "value": { + "bundles_queued_for_cleanup": [ + 1, + 3 + ], + "rejected_bundles": [ + { + "bundle_id": 2, + "detail": "We can't delete this bundle because it's in use by 1 target(s)." + } + ] + }, + "summary": "ResponseExample2 - Partial Success" + }, + "ResponseExample2-PartialSuccess2": { + "value": { + "bundles_queued_for_cleanup": [ + 1 + ], + "rejected_bundles": [ + { + "bundle_ids": [ + 9999 + ], + "detail": "Bundles not found in project testproject." + }, + { + "bundle_id": 2, + "detail": "We can't delete this bundle because it's in use by 3 target(s)." + }, + { + "bundle_id": 3, + "detail": "We can't delete this bundle because bundle deletion is in progress." + }, + { + "bundle_id": 4, + "detail": "We can't delete this bundle because bundle deletion is in progress." + }, + { + "bundle_id": 5, + "detail": "We can't delete this bundle because bundle deletion is in progress." + }, + { + "bundle_id": 6, + "detail": "Bundle deletion failed. We've been notified of this error and are working to resolve it." + } + ] + }, + "summary": "ResponseExample2 - Partial Success 2" + }, + "ResponseExample3-AllFailed": { + "value": { + "rejected_bundles": [ + { + "bundle_id": 1, + "detail": "We can't delete this bundle because bundle deletion is in progress." + }, + { + "bundle_id": 2, + "detail": "We can't delete this bundle because it's in use by 1 target(s)." + } + ] + }, + "summary": "ResponseExample3 - All Failed" + } + } + } + }, + "description": "Bundle deletion was requested. Check the response body for details about the bundles that were queued and that failed deletion." + }, + "500": { + "description": "An unexpected error occurred during the bulk delete operation. The operation has been rolled back and no bundles were deleted." + } + } + } + }, "/api/projects/{project_slug}/members/": { "get": { "operationId": "projects_members_list", @@ -1517,6 +2632,7 @@ "ssr_external_hostname": "www-testing.example.com", "ssr_external_domain": "example.com", "ssr_region": "eu-central-1", + "ssr_architecture": "x86", "ssr_whitelisted_ips": "103.12.25.0/24", "ssr_proxy_configs": [ { @@ -1529,7 +2645,9 @@ ], "allow_cookies": false, "enable_source_maps": false, - "log_level": "INFO" + "log_level": "INFO", + "certificate_id": 0, + "certificate_domain": "*.example.com" } ] } @@ -1572,6 +2690,7 @@ "ssr_external_hostname": "www-testing.example.com", "ssr_external_domain": "example.com", "ssr_region": "eu-central-1", + "ssr_architecture": "x86", "ssr_whitelisted_ips": "103.12.25.0/24", "ssr_proxy_configs": [ { @@ -1584,7 +2703,8 @@ ], "allow_cookies": false, "enable_source_maps": false, - "log_level": "INFO" + "log_level": "INFO", + "certificate_id": 0 } } } @@ -1625,6 +2745,7 @@ "ssr_external_hostname": "www-testing.example.com", "ssr_external_domain": "example.com", "ssr_region": "eu-central-1", + "ssr_architecture": "x86", "ssr_whitelisted_ips": "103.12.25.0/24", "ssr_proxy_configs": [ { @@ -1637,7 +2758,9 @@ ], "allow_cookies": true, "enable_source_maps": false, - "log_level": "INFO" + "log_level": "INFO", + "certificate_id": 0, + "certificate_domain": "*.example.com" } } } @@ -1737,6 +2860,7 @@ "ssr_external_hostname": "www-testing.example.com", "ssr_external_domain": "example.com", "ssr_region": "eu-central-1", + "ssr_architecture": "x86", "ssr_whitelisted_ips": "103.12.25.0/24", "ssr_proxy_configs": [ { @@ -1750,7 +2874,9 @@ ], "allow_cookies": false, "enable_source_maps": false, - "log_level": "INFO" + "log_level": "INFO", + "certificate_id": 0, + "certificate_domain": "*.example.com" } } } @@ -1799,6 +2925,7 @@ "ssr_external_hostname": "www-testing.example.com", "ssr_external_domain": "example.com", "ssr_region": "eu-central-1", + "ssr_architecture": "arm64", "ssr_whitelisted_ips": "103.12.25.0/24", "ssr_proxy_configs": [ { @@ -1812,7 +2939,8 @@ ], "allow_cookies": true, "enable_source_maps": true, - "log_level": "INFO" + "log_level": "INFO", + "certificate_id": 123 } } } @@ -1889,6 +3017,7 @@ "ssr_external_hostname": "www-testing.example.com", "ssr_external_domain": "example.com", "ssr_region": "eu-central-1", + "ssr_architecture": "arm64", "ssr_whitelisted_ips": "103.12.25.0/24", "ssr_proxy_configs": [ { @@ -1902,7 +3031,9 @@ ], "allow_cookies": true, "enable_source_maps": true, - "log_level": "INFO" + "log_level": "INFO", + "certificate_id": 0, + "certificate_domain": "*.example.com" } } } @@ -2256,8 +3387,110 @@ } ], "responses": { - "204": { - "description": "No response body" + "204": { + "description": "No response body" + } + } + } + }, + "/api/projects/{project_slug}/target/{target_slug}/clone/": { + "post": { + "operationId": "projects_target_clone_create", + "description": "Clone a target from another target in the same project. This creates a new target with the same configuration as the source target, excluding proxies and production flag. Optionally clones redirects and environment variables. The new target will be automatically deployed with the same bundle as the source target's current deployment (if source has a deployed bundle) or with a reset bundle.", + "parameters": [ + { + "in": "path", + "name": "project_slug", + "schema": { + "type": "string" + }, + "description": "The project identifier.", + "required": true + }, + { + "in": "path", + "name": "target_slug", + "schema": { + "type": "string" + }, + "description": "The target identifier.", + "required": true + } + ], + "tags": [ + "projects" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APITargetV2Clone" + }, + "examples": { + "RequestExample1": { + "value": { + "from_target_slug": "staging", + "ssr_external_hostname": "www-copy.example.com", + "certificate_id": 123, + "clone_redirects": true, + "clone_environment_variables": true, + "clone_b2c_target_info": true + } + } + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/APITargetV2Clone" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/APITargetV2Clone" + } + } + }, + "required": true + }, + "security": [ + { + "Basic": [] + }, + { + "BearerToken": [] + } + ], + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APITargetV2Clone" + }, + "examples": { + "ResponseExample1": { + "value": { + "slug": "staging-copy", + "name": "Staging Copy", + "state": "CREATE_IN_PROGRESS", + "deletion_status": "ACTIVE", + "hostname": null, + "ssr_external_hostname": "www-copy.example.com", + "ssr_external_domain": "example.com", + "ssr_region": "eu-central-1", + "ssr_architecture": "x86", + "ssr_whitelisted_ips": "103.12.25.0/24", + "allow_cookies": false, + "enable_source_maps": false, + "log_level": "INFO", + "certificate_id": 123, + "certificate_domain": "*.example.com" + } + } + } + } + }, + "description": "" } } } @@ -3313,7 +4546,7 @@ "examples": { "ResponseExample1": { "value": { - "api_key": "qLh_IRpIh594lPeURV4giczpW0q-Ki0YIcOdDz0aw0g" + "api_key": "qLh_IRpIh59..." } } } @@ -3350,6 +4583,7 @@ "ResponseExample1": { "value": { "node_deprecation_notifications": true, + "custom_domain_certificate_notifications": true, "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T10:30:00Z" } @@ -3376,7 +4610,8 @@ "examples": { "RequestExample1": { "value": { - "node_deprecation_notifications": false + "node_deprecation_notifications": false, + "custom_domain_certificate_notifications": false } } } @@ -3412,6 +4647,7 @@ "ResponseExample1": { "value": { "node_deprecation_notifications": false, + "custom_domain_certificate_notifications": false, "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T10:35:00Z" } @@ -3493,7 +4729,7 @@ "publishing_status": { "allOf": [ { - "$ref": "#/components/schemas/PublishingStatusEnum" + "$ref": "#/components/schemas/Status1d2Enum" } ], "readOnly": true @@ -3572,6 +4808,11 @@ } ], "readOnly": true + }, + "can_configure_ssr_architecture": { + "type": "boolean", + "readOnly": true, + "description": "Enable SSR architecture selection (x86 or arm64) for this organization" } }, "required": [ @@ -3579,6 +4820,105 @@ "slug" ] }, + "APIOrganizationMember": { + "type": "object", + "properties": { + "user": { + "type": "string", + "format": "email", + "title": "Email address", + "readOnly": true + }, + "email": { + "type": "string", + "readOnly": true + }, + "role": { + "$ref": "#/components/schemas/RoleEnum" + }, + "first_name": { + "type": "string", + "readOnly": true + }, + "last_name": { + "type": "string", + "readOnly": true + }, + "can_view_all_projects": { + "type": "boolean", + "readOnly": true + }, + "custom_domain_cert_permission": { + "allOf": [ + { + "$ref": "#/components/schemas/Status1d2Enum" + } + ], + "readOnly": true + } + }, + "required": [ + "role" + ] + }, + "APIOrganizationMemberCreate": { + "type": "object", + "description": "Serializer for creating organization members.", + "properties": { + "user": { + "type": "string", + "format": "email", + "title": "Email address" + }, + "role": { + "$ref": "#/components/schemas/RoleEnum" + }, + "can_view_all_projects": { + "type": "boolean" + }, + "custom_domain_cert_permission": { + "$ref": "#/components/schemas/Status1d2Enum" + } + }, + "required": [ + "role", + "user" + ] + }, + "APIOrganizationMemberUpdate": { + "type": "object", + "description": "Serializer for updating organization role permissions.\nAllows updating can_view_all_projects and custom_domain_cert_permission.", + "properties": { + "user": { + "type": "string", + "format": "email", + "title": "Email address", + "readOnly": true + }, + "role": { + "allOf": [ + { + "$ref": "#/components/schemas/RoleEnum" + } + ], + "readOnly": true + }, + "first_name": { + "type": "string", + "readOnly": true + }, + "last_name": { + "type": "string", + "readOnly": true + }, + "can_view_all_projects": { + "type": "boolean" + }, + "custom_domain_cert_permission": { + "$ref": "#/components/schemas/Status1d2Enum" + } + } + }, "APIProjectMember": { "type": "object", "properties": { @@ -3629,8 +4969,7 @@ "pattern": "^[a-z0-9]+(?:-+[a-z0-9]+)*$" }, "organization": { - "type": "string", - "description": "User-friendly identifier for this instance." + "type": "string" }, "deletion_status": { "allOf": [ @@ -3710,6 +5049,9 @@ }, "edit_access_control_header": { "type": "boolean" + }, + "edit_data_access_layer_entry": { + "type": "boolean" } }, "readOnly": true @@ -3723,6 +5065,18 @@ "title": "SSR AWS Region", "description": "The default AWS region for newly created targets\n\n* `us-east-1` - US East (N. Virginia)\n* `us-east-2` - US East (Ohio)\n* `us-west-1` - US West (N. California)\n* `us-west-2` - US West (Oregon)\n* `ap-south-1` - Asia Pacific (Mumbai)\n* `ap-south-2` - Asia Pacific (Hyderabad)\n* `ap-northeast-2` - Asia Pacific (Seoul)\n* `ap-southeast-1` - Asia Pacific (Singapore)\n* `ap-southeast-2` - Asia Pacific (Sydney)\n* `ap-southeast-3` - Asia Pacific (Jakarta)\n* `ap-northeast-1` - Asia Pacific (Tokyo)\n* `ap-northeast-3` - Asia Pacific (Osaka)\n* `ca-central-1` - Canada (Central)\n* `eu-central-1` - EU (Frankfurt)\n* `eu-central-2` - EU (Zurich)\n* `eu-west-1` - EU (Ireland)\n* `eu-west-2` - EU (London)\n* `eu-west-3` - EU (Paris)\n* `eu-north-1` - EU (Stockholm)\n* `eu-south-1` - EU (Milan)\n* `il-central-1` - Israel (Tel Aviv)\n* `me-central-1` - Middle East (UAE)\n* `sa-east-1` - South America (Sao Paulo)" }, + "ssr_architecture": { + "nullable": true, + "description": "Default Server-Side Rendering architecture (x86 or arm64) for targets under this project.\n\n* `x86` - x86\n* `arm64` - ARM64", + "oneOf": [ + { + "$ref": "#/components/schemas/SsrArchitectureEnum" + }, + { + "$ref": "#/components/schemas/NullEnum" + } + ] + }, "created_at": { "type": "string", "format": "date-time", @@ -3734,6 +5088,19 @@ "format": "date-time", "readOnly": true, "description": "Timestamp in the extended ISO 8601 format for when the object was last updated." + }, + "source": { + "readOnly": true, + "nullable": true, + "description": "Source of the project. One of: ecom, core, direct.\n\n* `ecom` - ecom\n* `core` - core\n* `direct` - direct", + "oneOf": [ + { + "$ref": "#/components/schemas/SourceEnum" + }, + { + "$ref": "#/components/schemas/NullEnum" + } + ] } }, "required": [ @@ -3841,6 +5208,9 @@ }, "edit_access_control_header": { "type": "boolean" + }, + "edit_data_access_layer_entry": { + "type": "boolean" } }, "readOnly": true @@ -3854,6 +5224,18 @@ "title": "SSR AWS Region", "description": "The default AWS region for newly created targets\n\n* `us-east-1` - US East (N. Virginia)\n* `us-east-2` - US East (Ohio)\n* `us-west-1` - US West (N. California)\n* `us-west-2` - US West (Oregon)\n* `ap-south-1` - Asia Pacific (Mumbai)\n* `ap-south-2` - Asia Pacific (Hyderabad)\n* `ap-northeast-2` - Asia Pacific (Seoul)\n* `ap-southeast-1` - Asia Pacific (Singapore)\n* `ap-southeast-2` - Asia Pacific (Sydney)\n* `ap-southeast-3` - Asia Pacific (Jakarta)\n* `ap-northeast-1` - Asia Pacific (Tokyo)\n* `ap-northeast-3` - Asia Pacific (Osaka)\n* `ca-central-1` - Canada (Central)\n* `eu-central-1` - EU (Frankfurt)\n* `eu-central-2` - EU (Zurich)\n* `eu-west-1` - EU (Ireland)\n* `eu-west-2` - EU (London)\n* `eu-west-3` - EU (Paris)\n* `eu-north-1` - EU (Stockholm)\n* `eu-south-1` - EU (Milan)\n* `il-central-1` - Israel (Tel Aviv)\n* `me-central-1` - Middle East (UAE)\n* `sa-east-1` - South America (Sao Paulo)" }, + "ssr_architecture": { + "nullable": true, + "description": "Default Server-Side Rendering architecture (x86 or arm64) for targets under this project.\n\n* `x86` - x86\n* `arm64` - ARM64", + "oneOf": [ + { + "$ref": "#/components/schemas/SsrArchitectureEnum" + }, + { + "$ref": "#/components/schemas/NullEnum" + } + ] + }, "created_at": { "type": "string", "format": "date-time", @@ -3865,6 +5247,19 @@ "format": "date-time", "readOnly": true, "description": "Timestamp in the extended ISO 8601 format for when the object was last updated." + }, + "source": { + "readOnly": true, + "nullable": true, + "description": "Source of the project. One of: ecom, core, direct.\n\n* `ecom` - ecom\n* `core` - core\n* `direct` - direct", + "oneOf": [ + { + "$ref": "#/components/schemas/SourceEnum" + }, + { + "$ref": "#/components/schemas/NullEnum" + } + ] } }, "required": [ @@ -3951,6 +5346,53 @@ "to_url" ] }, + "APITargetV2Clone": { + "type": "object", + "description": "Serializer for target cloning request.\nInherits from APITargetV2BaseSerializer to reuse validation helper methods.", + "properties": { + "from_target_slug": { + "type": "string", + "description": "The slug of the target to clone from.", + "pattern": "^[-a-zA-Z0-9_]+$" + }, + "ssr_external_hostname": { + "type": "string", + "nullable": true, + "description": "Full hostname to be used by the cloned environment. Required when using non-MRT managed certificate.", + "maxLength": 128 + }, + "ssr_external_domain": { + "type": "string", + "nullable": true, + "description": "The domain to be used for a Universal PWA SSR deployment (e.g. customer.com). If not provided and hostname is provided, will be extracted from hostname.", + "maxLength": 128 + }, + "certificate_id": { + "type": "integer", + "minimum": 0, + "nullable": true, + "description": "The ID of the certificate to associate with the cloned target's custom domain. Required for custom domains." + }, + "clone_redirects": { + "type": "boolean", + "default": false, + "description": "Whether to clone redirects from the source target." + }, + "clone_environment_variables": { + "type": "boolean", + "default": false, + "description": "Whether to clone environment variables from the source target." + }, + "clone_b2c_target_info": { + "type": "boolean", + "default": false, + "description": "Whether to clone B2C target info from the source target." + } + }, + "required": [ + "from_target_slug" + ] + }, "APITargetV2Create": { "type": "object", "description": "This is the serializer for target create/list APIs.", @@ -3996,7 +5438,7 @@ "ssr_external_hostname": { "type": "string", "nullable": true, - "description": "Full hostname to used by the environment eg. www.customer.com.", + "description": "Full hostname to be used by the environment eg. www.customer.com.", "maxLength": 128 }, "ssr_external_domain": { @@ -4017,6 +5459,18 @@ } ] }, + "ssr_architecture": { + "nullable": true, + "description": "The architecture for the Server-Side Rendering function (x86 or ARM64). If not specified, the ssr_architecture that's set in the project is used.\n\n* `x86` - x86\n* `arm64` - ARM64", + "oneOf": [ + { + "$ref": "#/components/schemas/SsrArchitectureEnum" + }, + { + "$ref": "#/components/schemas/NullEnum" + } + ] + }, "ssr_whitelisted_ips": { "type": "string", "nullable": true, @@ -4040,27 +5494,76 @@ ] } }, + "cdn_domain_name": { + "type": "string", + "readOnly": true, + "description": "The Managed Runtime CDN origin domain name." + }, "is_production": { "type": "boolean", "title": "Production", "description": "Treat this target as a production environment." }, - "allow_cookies": { - "type": "boolean", + "allow_cookies": { + "type": "boolean", + "nullable": true, + "description": "Set true to forward the HTTP cookie header sent by clients to your origin and ensure the Set-Cookie header sent by your app is respected and not stripped." + }, + "enable_source_maps": { + "type": "boolean", + "nullable": true, + "description": "Set true to enable source map support. This will set the NODE_OPTIONS environment variable to \"--enable-source-maps\" in your MRT environment." + }, + "log_level": { + "nullable": true, + "description": "The minimum log level that will be emitted for this target\n\n* `TRACE` - TRACE\n* `DEBUG` - DEBUG\n* `INFO` - INFO\n* `WARN` - WARN\n* `ERROR` - ERROR\n* `FATAL` - FATAL", + "oneOf": [ + { + "$ref": "#/components/schemas/LogLevelEnum" + }, + { + "$ref": "#/components/schemas/NullEnum" + } + ] + }, + "certificate_id": { + "type": "integer", + "minimum": 0, + "writeOnly": true, + "nullable": true, + "description": "The ID of the certificate to associate with this target's custom domain. Must be an integer unique within the organization." + }, + "certificate_domain": { + "type": "string", + "readOnly": true, + "description": "The certificate domain used by this target. For MRT default domains, returns wildcard format (e.g., *.mobify-storefront-staging.com). For custom domains, returns the certificate domain name." + }, + "configured_cdn": { + "readOnly": true, "nullable": true, - "description": "Set true to forward the HTTP cookie header sent by clients to your origin and ensure the Set-Cookie header sent by your app is respected and not stripped." + "description": "The content delivery network used for content, traffic, and security. A B2C instance must be connected to this environment to select eCDN.\n\n* `unknown` - unknown\n* `mrt_cdn` - mrt_cdn\n* `ecdn` - ecdn\n* `stacked_cdn` - stacked_cdn", + "oneOf": [ + { + "$ref": "#/components/schemas/ConfiguredCdnEnum" + }, + { + "$ref": "#/components/schemas/NullEnum" + } + ] }, - "enable_source_maps": { - "type": "boolean", + "cdn_public_hostname": { + "type": "string", + "readOnly": true, "nullable": true, - "description": "Set true to enable source map support. This will set the NODE_OPTIONS environment variable to \"--enable-source-maps\" in your MRT environment." + "description": "Add a publicly visible hostname. Enter a subdomain if one is not already provided with your organization’s certified domain." }, - "log_level": { + "source": { + "readOnly": true, "nullable": true, - "description": "The minimum log level emitted for this target.n\n* `TRACE`\n* `DEBUG`\n* `INFO`\n* `WARN`\n* `ERROR`\n* `FATAL`", + "description": "Source of the environment. One of: ecom, core, direct.\n\n* `ecom` - ecom\n* `core` - core\n* `direct` - direct", "oneOf": [ { - "$ref": "#/components/schemas/LogLevelEnum" + "$ref": "#/components/schemas/SourceEnum" }, { "$ref": "#/components/schemas/NullEnum" @@ -4146,7 +5649,7 @@ "ssr_external_hostname": { "type": "string", "nullable": true, - "description": "Full hostname to used by the environment eg. www.customer.com.", + "description": "Full hostname to be used by the environment eg. www.customer.com.", "maxLength": 128 }, "ssr_external_domain": { @@ -4167,6 +5670,18 @@ } ] }, + "ssr_architecture": { + "nullable": true, + "description": "The architecture for the Server-Side Rendering function (x86 or ARM64). If not specified, the ssr_architecture that's set in the project is used.\n\n* `x86` - x86\n* `arm64` - ARM64", + "oneOf": [ + { + "$ref": "#/components/schemas/SsrArchitectureEnum" + }, + { + "$ref": "#/components/schemas/NullEnum" + } + ] + }, "ssr_whitelisted_ips": { "type": "string", "nullable": true, @@ -4190,6 +5705,11 @@ ] } }, + "cdn_domain_name": { + "type": "string", + "readOnly": true, + "description": "The Managed Runtime CDN origin domain name." + }, "is_production": { "type": "boolean", "title": "Production", @@ -4207,7 +5727,7 @@ }, "log_level": { "nullable": true, - "description": "The minimum log level emitted for this target.n\n* `TRACE`\n* `DEBUG`\n* `INFO`\n* `WARN`\n* `ERROR`\n* `FATAL`", + "description": "The minimum log level that will be emitted for this target\n\n* `TRACE` - TRACE\n* `DEBUG` - DEBUG\n* `INFO` - INFO\n* `WARN` - WARN\n* `ERROR` - ERROR\n* `FATAL` - FATAL", "oneOf": [ { "$ref": "#/components/schemas/LogLevelEnum" @@ -4216,6 +5736,50 @@ "$ref": "#/components/schemas/NullEnum" } ] + }, + "certificate_id": { + "type": "integer", + "minimum": 0, + "writeOnly": true, + "nullable": true, + "description": "The ID of the certificate to associate with this target's custom domain. Must be an integer unique within the organization. Set to null to remove the certificate association." + }, + "certificate_domain": { + "type": "string", + "readOnly": true, + "description": "The certificate domain used by this target. For MRT default domains, returns wildcard format (e.g., *.mobify-storefront-staging.com). For custom domains, returns the certificate domain name." + }, + "configured_cdn": { + "readOnly": true, + "nullable": true, + "description": "The content delivery network used for content, traffic, and security. A B2C instance must be connected to this environment to select eCDN.\n\n* `unknown` - unknown\n* `mrt_cdn` - mrt_cdn\n* `ecdn` - ecdn\n* `stacked_cdn` - stacked_cdn", + "oneOf": [ + { + "$ref": "#/components/schemas/ConfiguredCdnEnum" + }, + { + "$ref": "#/components/schemas/NullEnum" + } + ] + }, + "cdn_public_hostname": { + "type": "string", + "readOnly": true, + "nullable": true, + "description": "Add a publicly visible hostname. Enter a subdomain if one is not already provided with your organization’s certified domain." + }, + "source": { + "readOnly": true, + "nullable": true, + "description": "Source of the environment. One of: ecom, core, direct.\n\n* `ecom` - ecom\n* `core` - core\n* `direct` - direct", + "oneOf": [ + { + "$ref": "#/components/schemas/SourceEnum" + }, + { + "$ref": "#/components/schemas/NullEnum" + } + ] } }, "required": [ @@ -4307,6 +5871,56 @@ "message" ] }, + "BundleBulkDelete": { + "type": "object", + "properties": { + "bundle_ids": { + "type": "array", + "items": { + "type": "integer" + } + } + }, + "required": [ + "bundle_ids" + ] + }, + "BundleBulkDeleteFailedRequest": { + "type": "object", + "properties": { + "bundle_id": { + "type": "integer", + "description": "The ID of the bundle that failed validation." + }, + "errors": { + "type": "string", + "description": "Error message that explains why the bundle can't be queued for deletion." + } + }, + "required": [ + "bundle_id", + "errors" + ] + }, + "BundleBulkDeleteResponse": { + "type": "object", + "properties": { + "rejected_bundles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BundleBulkDeleteFailedRequest" + }, + "description": "Bundles that failed validation and couldn't be queued for deletion." + }, + "bundles_queued_for_cleanup": { + "type": "array", + "items": { + "type": "integer" + }, + "description": "Bundle IDs for bundles that were queued for deletion." + } + } + }, "BundleDownload": { "type": "object", "properties": { @@ -4368,6 +5982,209 @@ "message" ] }, + "CertificateBase": { + "type": "object", + "description": "Base serializer for certificate serializers with common fields and methods.", + "properties": { + "id": { + "type": "integer", + "readOnly": true, + "description": "An ID unique within a business." + }, + "domain_name": { + "type": "string", + "title": "Certificate domain", + "description": "The domain for the certificate either wildcard (e.g. *.example.com) or single domain (e.g. sub.example.com)", + "maxLength": 255 + }, + "validation_requested_at": { + "type": "string", + "readOnly": true + }, + "validation_status": { + "allOf": [ + { + "$ref": "#/components/schemas/ValidationStatusEnum" + } + ], + "readOnly": true, + "description": "Current validation status of the certificate\n\n* `pending_validation` - Pending Validation\n* `validation_succeeded` - Validation Succeeded\n* `validation_failed` - Validation Failed" + }, + "validation_record": { + "type": "string", + "readOnly": true + }, + "expires_at": { + "type": "string", + "format": "date-time", + "readOnly": true, + "nullable": true, + "title": "Certificate Expiry Date", + "description": "Expiry date of the certificate from ACM." + }, + "renewal_status": { + "readOnly": true, + "nullable": true, + "description": "Current status of certificate renewal.\n\n* `PENDING_AUTO_RENEWAL` - Pending Auto Renewal\n* `FAILED` - Failed", + "oneOf": [ + { + "$ref": "#/components/schemas/RenewalStatusEnum" + }, + { + "$ref": "#/components/schemas/NullEnum" + } + ] + }, + "renewal_eligibility": { + "readOnly": true, + "nullable": true, + "description": "Whether the certificate is eligible for renewal.\n\n* `ELIGIBLE` - Eligible\n* `INELIGIBLE` - Ineligible", + "oneOf": [ + { + "$ref": "#/components/schemas/RenewalEligibilityEnum" + }, + { + "$ref": "#/components/schemas/NullEnum" + } + ] + }, + "targets": { + "type": "string", + "readOnly": true + }, + "created_by": { + "type": "string", + "readOnly": true + }, + "created_at": { + "type": "string", + "format": "date-time", + "readOnly": true, + "description": "Timestamp in the extended ISO 8601 format for when the object was created." + }, + "is_mrt_managed": { + "type": "string", + "readOnly": true + }, + "deletion_status": { + "allOf": [ + { + "$ref": "#/components/schemas/DeletionStatusEnum" + } + ], + "readOnly": true + } + }, + "required": [ + "domain_name" + ] + }, + "CertificateListCreate": { + "type": "object", + "description": "Base serializer for certificate serializers with common fields and methods.", + "properties": { + "id": { + "type": "integer", + "readOnly": true, + "description": "An ID unique within a business." + }, + "domain_name": { + "type": "string", + "description": "The domain for the certificate (e.g. shop.example.com)", + "maxLength": 255 + }, + "validation_requested_at": { + "type": "string", + "readOnly": true + }, + "validation_status": { + "allOf": [ + { + "$ref": "#/components/schemas/ValidationStatusEnum" + } + ], + "readOnly": true, + "description": "Current validation status of the certificate\n\n* `pending_validation` - Pending Validation\n* `validation_succeeded` - Validation Succeeded\n* `validation_failed` - Validation Failed" + }, + "validation_record": { + "type": "string", + "readOnly": true + }, + "expires_at": { + "type": "string", + "format": "date-time", + "readOnly": true, + "nullable": true, + "title": "Certificate Expiry Date", + "description": "Expiry date of the certificate from ACM." + }, + "renewal_status": { + "readOnly": true, + "nullable": true, + "description": "Current status of certificate renewal.\n\n* `PENDING_AUTO_RENEWAL` - Pending Auto Renewal\n* `FAILED` - Failed", + "oneOf": [ + { + "$ref": "#/components/schemas/RenewalStatusEnum" + }, + { + "$ref": "#/components/schemas/NullEnum" + } + ] + }, + "renewal_eligibility": { + "readOnly": true, + "nullable": true, + "description": "Whether the certificate is eligible for renewal.\n\n* `ELIGIBLE` - Eligible\n* `INELIGIBLE` - Ineligible", + "oneOf": [ + { + "$ref": "#/components/schemas/RenewalEligibilityEnum" + }, + { + "$ref": "#/components/schemas/NullEnum" + } + ] + }, + "targets": { + "type": "string", + "readOnly": true + }, + "created_by": { + "type": "string", + "readOnly": true + }, + "created_at": { + "type": "string", + "format": "date-time", + "readOnly": true, + "description": "Timestamp in the extended ISO 8601 format for when the object was created." + }, + "is_mrt_managed": { + "type": "string", + "readOnly": true + }, + "deletion_status": { + "allOf": [ + { + "$ref": "#/components/schemas/DeletionStatusEnum" + } + ], + "readOnly": true + } + }, + "required": [ + "domain_name" + ] + }, + "ConfiguredCdnEnum": { + "enum": [ + "unknown", + "mrt_cdn", + "ecdn", + "stacked_cdn" + ], + "type": "string", + "description": "* `unknown` - unknown\n* `mrt_cdn` - mrt_cdn\n* `ecdn` - ecdn\n* `stacked_cdn` - stacked_cdn" + }, "DeletionStatusEnum": { "enum": [ "ACTIVE", @@ -4560,7 +6377,6 @@ "properties": { "name": { "type": "string", - "description": "Name of the Environment Variable.", "maxLength": 512 }, "value": { @@ -4594,7 +6410,7 @@ "publishing_status": { "allOf": [ { - "$ref": "#/components/schemas/PublishingStatusEnum" + "$ref": "#/components/schemas/Status1d2Enum" } ], "readOnly": true @@ -4627,7 +6443,7 @@ "FATAL" ], "type": "string", - "description": "The minimum log level emitted for a target." + "description": "* `TRACE` - TRACE\n* `DEBUG` - DEBUG\n* `INFO` - INFO\n* `WARN` - WARN\n* `ERROR` - ERROR\n* `FATAL` - FATAL" }, "NullEnum": { "enum": [ @@ -4726,6 +6542,37 @@ } } }, + "PaginatedAPIOrganizationMemberList": { + "type": "object", + "required": [ + "count", + "results" + ], + "properties": { + "count": { + "type": "integer", + "example": 123 + }, + "next": { + "type": "string", + "nullable": true, + "format": "uri", + "example": "http://api.example.org/accounts/?offset=400&limit=100" + }, + "previous": { + "type": "string", + "nullable": true, + "format": "uri", + "example": "http://api.example.org/accounts/?offset=200&limit=100" + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/APIOrganizationMember" + } + } + } + }, "PaginatedAPIProjectMemberList": { "type": "object", "required": [ @@ -4881,6 +6728,37 @@ } } }, + "PaginatedCertificateListCreateList": { + "type": "object", + "required": [ + "count", + "results" + ], + "properties": { + "count": { + "type": "integer", + "example": 123 + }, + "next": { + "type": "string", + "nullable": true, + "format": "uri", + "example": "http://api.example.org/accounts/?offset=400&limit=100" + }, + "previous": { + "type": "string", + "nullable": true, + "format": "uri", + "example": "http://api.example.org/accounts/?offset=200&limit=100" + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CertificateListCreate" + } + } + } + }, "PaginatedDeployListList": { "type": "object", "required": [ @@ -4974,6 +6852,40 @@ } } }, + "PatchedAPIOrganizationMemberUpdate": { + "type": "object", + "description": "Serializer for updating organization role permissions.\nAllows updating can_view_all_projects and custom_domain_cert_permission.", + "properties": { + "user": { + "type": "string", + "format": "email", + "title": "Email address", + "readOnly": true + }, + "role": { + "allOf": [ + { + "$ref": "#/components/schemas/RoleEnum" + } + ], + "readOnly": true + }, + "first_name": { + "type": "string", + "readOnly": true + }, + "last_name": { + "type": "string", + "readOnly": true + }, + "can_view_all_projects": { + "type": "boolean" + }, + "custom_domain_cert_permission": { + "$ref": "#/components/schemas/Status1d2Enum" + } + } + }, "PatchedAPIProjectMember": { "type": "object", "properties": { @@ -5101,6 +7013,9 @@ }, "edit_access_control_header": { "type": "boolean" + }, + "edit_data_access_layer_entry": { + "type": "boolean" } }, "readOnly": true @@ -5114,6 +7029,18 @@ "title": "SSR AWS Region", "description": "The default AWS region for newly created targets\n\n* `us-east-1` - US East (N. Virginia)\n* `us-east-2` - US East (Ohio)\n* `us-west-1` - US West (N. California)\n* `us-west-2` - US West (Oregon)\n* `ap-south-1` - Asia Pacific (Mumbai)\n* `ap-south-2` - Asia Pacific (Hyderabad)\n* `ap-northeast-2` - Asia Pacific (Seoul)\n* `ap-southeast-1` - Asia Pacific (Singapore)\n* `ap-southeast-2` - Asia Pacific (Sydney)\n* `ap-southeast-3` - Asia Pacific (Jakarta)\n* `ap-northeast-1` - Asia Pacific (Tokyo)\n* `ap-northeast-3` - Asia Pacific (Osaka)\n* `ca-central-1` - Canada (Central)\n* `eu-central-1` - EU (Frankfurt)\n* `eu-central-2` - EU (Zurich)\n* `eu-west-1` - EU (Ireland)\n* `eu-west-2` - EU (London)\n* `eu-west-3` - EU (Paris)\n* `eu-north-1` - EU (Stockholm)\n* `eu-south-1` - EU (Milan)\n* `il-central-1` - Israel (Tel Aviv)\n* `me-central-1` - Middle East (UAE)\n* `sa-east-1` - South America (Sao Paulo)" }, + "ssr_architecture": { + "nullable": true, + "description": "Default Server-Side Rendering architecture (x86 or arm64) for targets under this project.\n\n* `x86` - x86\n* `arm64` - ARM64", + "oneOf": [ + { + "$ref": "#/components/schemas/SsrArchitectureEnum" + }, + { + "$ref": "#/components/schemas/NullEnum" + } + ] + }, "created_at": { "type": "string", "format": "date-time", @@ -5125,6 +7052,19 @@ "format": "date-time", "readOnly": true, "description": "Timestamp in the extended ISO 8601 format for when the object was last updated." + }, + "source": { + "readOnly": true, + "nullable": true, + "description": "Source of the project. One of: ecom, core, direct.\n\n* `ecom` - ecom\n* `core` - core\n* `direct` - direct", + "oneOf": [ + { + "$ref": "#/components/schemas/SourceEnum" + }, + { + "$ref": "#/components/schemas/NullEnum" + } + ] } } }, @@ -5237,7 +7177,7 @@ "ssr_external_hostname": { "type": "string", "nullable": true, - "description": "Full hostname to used by the environment eg. www.customer.com.", + "description": "Full hostname to be used by the environment eg. www.customer.com.", "maxLength": 128 }, "ssr_external_domain": { @@ -5258,6 +7198,18 @@ } ] }, + "ssr_architecture": { + "nullable": true, + "description": "The architecture for the Server-Side Rendering function (x86 or ARM64). If not specified, the ssr_architecture that's set in the project is used.\n\n* `x86` - x86\n* `arm64` - ARM64", + "oneOf": [ + { + "$ref": "#/components/schemas/SsrArchitectureEnum" + }, + { + "$ref": "#/components/schemas/NullEnum" + } + ] + }, "ssr_whitelisted_ips": { "type": "string", "nullable": true, @@ -5281,6 +7233,11 @@ ] } }, + "cdn_domain_name": { + "type": "string", + "readOnly": true, + "description": "The Managed Runtime CDN origin domain name." + }, "is_production": { "type": "boolean", "title": "Production", @@ -5298,7 +7255,7 @@ }, "log_level": { "nullable": true, - "description": "The minimum log level emitted for this target.\n\n* `TRACE`\n* `DEBUG`\n* `INFO`\n* `WARN`\n* `ERROR`\n* `FATAL`", + "description": "The minimum log level that will be emitted for this target\n\n* `TRACE` - TRACE\n* `DEBUG` - DEBUG\n* `INFO` - INFO\n* `WARN` - WARN\n* `ERROR` - ERROR\n* `FATAL` - FATAL", "oneOf": [ { "$ref": "#/components/schemas/LogLevelEnum" @@ -5307,6 +7264,144 @@ "$ref": "#/components/schemas/NullEnum" } ] + }, + "certificate_id": { + "type": "integer", + "minimum": 0, + "writeOnly": true, + "nullable": true, + "description": "The ID of the certificate to associate with this target's custom domain. Must be an integer unique within the organization. Set to null to remove the certificate association." + }, + "certificate_domain": { + "type": "string", + "readOnly": true, + "description": "The certificate domain used by this target. For MRT default domains, returns wildcard format (e.g., *.mobify-storefront-staging.com). For custom domains, returns the certificate domain name." + }, + "configured_cdn": { + "readOnly": true, + "nullable": true, + "description": "The content delivery network used for content, traffic, and security. A B2C instance must be connected to this environment to select eCDN.\n\n* `unknown` - unknown\n* `mrt_cdn` - mrt_cdn\n* `ecdn` - ecdn\n* `stacked_cdn` - stacked_cdn", + "oneOf": [ + { + "$ref": "#/components/schemas/ConfiguredCdnEnum" + }, + { + "$ref": "#/components/schemas/NullEnum" + } + ] + }, + "cdn_public_hostname": { + "type": "string", + "readOnly": true, + "nullable": true, + "description": "Add a publicly visible hostname. Enter a subdomain if one is not already provided with your organization’s certified domain." + }, + "source": { + "readOnly": true, + "nullable": true, + "description": "Source of the environment. One of: ecom, core, direct.\n\n* `ecom` - ecom\n* `core` - core\n* `direct` - direct", + "oneOf": [ + { + "$ref": "#/components/schemas/SourceEnum" + }, + { + "$ref": "#/components/schemas/NullEnum" + } + ] + } + } + }, + "PatchedCertificateBase": { + "type": "object", + "description": "Base serializer for certificate serializers with common fields and methods.", + "properties": { + "id": { + "type": "integer", + "readOnly": true, + "description": "An ID unique within a business." + }, + "domain_name": { + "type": "string", + "title": "Certificate domain", + "description": "The domain for the certificate either wildcard (e.g. *.example.com) or single domain (e.g. sub.example.com)", + "maxLength": 255 + }, + "validation_requested_at": { + "type": "string", + "readOnly": true + }, + "validation_status": { + "allOf": [ + { + "$ref": "#/components/schemas/ValidationStatusEnum" + } + ], + "readOnly": true, + "description": "Current validation status of the certificate\n\n* `pending_validation` - Pending Validation\n* `validation_succeeded` - Validation Succeeded\n* `validation_failed` - Validation Failed" + }, + "validation_record": { + "type": "string", + "readOnly": true + }, + "expires_at": { + "type": "string", + "format": "date-time", + "readOnly": true, + "nullable": true, + "title": "Certificate Expiry Date", + "description": "Expiry date of the certificate from ACM." + }, + "renewal_status": { + "readOnly": true, + "nullable": true, + "description": "Current status of certificate renewal.\n\n* `PENDING_AUTO_RENEWAL` - Pending Auto Renewal\n* `FAILED` - Failed", + "oneOf": [ + { + "$ref": "#/components/schemas/RenewalStatusEnum" + }, + { + "$ref": "#/components/schemas/NullEnum" + } + ] + }, + "renewal_eligibility": { + "readOnly": true, + "nullable": true, + "description": "Whether the certificate is eligible for renewal.\n\n* `ELIGIBLE` - Eligible\n* `INELIGIBLE` - Ineligible", + "oneOf": [ + { + "$ref": "#/components/schemas/RenewalEligibilityEnum" + }, + { + "$ref": "#/components/schemas/NullEnum" + } + ] + }, + "targets": { + "type": "string", + "readOnly": true + }, + "created_by": { + "type": "string", + "readOnly": true + }, + "created_at": { + "type": "string", + "format": "date-time", + "readOnly": true, + "description": "Timestamp in the extended ISO 8601 format for when the object was created." + }, + "is_mrt_managed": { + "type": "string", + "readOnly": true + }, + "deletion_status": { + "allOf": [ + { + "$ref": "#/components/schemas/DeletionStatusEnum" + } + ], + "readOnly": true } } }, @@ -5398,7 +7493,11 @@ "properties": { "node_deprecation_notifications": { "type": "boolean", - "description": "The user's email notification preferences for Node.js runtime's deprecation and retirement." + "description": "Receive email notifications about Node.js runtime deprecations" + }, + "custom_domain_certificate_notifications": { + "type": "boolean", + "description": "Receive email notifications about custom domain certificate changes" }, "created_at": { "type": "string", @@ -5438,14 +7537,21 @@ "type": "string", "description": "* `MOBIFY_STUDIO` - MOBIFY_STUDIO\n* `MOBIFYJS_CLIENT` - MOBIFYJS_CLIENT\n* `MOBIFY_ADAPTIVEJS` - MOBIFY_ADAPTIVEJS\n* `MOBIFY_TAG_BASED_PWA` - MOBIFY_TAG_BASED_PWA\n* `SSR` - SSR" }, - "PublishingStatusEnum": { + "RenewalEligibilityEnum": { "enum": [ - 0, - 1, - 2 + "ELIGIBLE", + "INELIGIBLE" ], - "type": "integer", - "description": "* `0` - Pending\n* `1` - Completed\n* `2` - Failed" + "type": "string", + "description": "* `ELIGIBLE` - Eligible\n* `INELIGIBLE` - Ineligible" + }, + "RenewalStatusEnum": { + "enum": [ + "PENDING_AUTO_RENEWAL", + "FAILED" + ], + "type": "string", + "description": "* `PENDING_AUTO_RENEWAL` - Pending Auto Renewal\n* `FAILED` - Failed" }, "ResourceLimit": { "type": "object", @@ -5462,6 +7568,31 @@ "used" ] }, + "RoleEnum": { + "enum": [ + 0, + 1 + ], + "type": "integer", + "description": "* `0` - Owner\n* `1` - Member" + }, + "SourceEnum": { + "enum": [ + "ecom", + "core", + "direct" + ], + "type": "string", + "description": "* `ecom` - ecom\n* `core` - core\n* `direct` - direct" + }, + "SsrArchitectureEnum": { + "enum": [ + "x86", + "arm64" + ], + "type": "string", + "description": "* `x86` - x86\n* `arm64` - ARM64" + }, "SsrRegionEnum": { "enum": [ "us-east-1", @@ -5509,14 +7640,18 @@ 2 ], "type": "integer", - "description": "* `0` - ok\n* `1` - broken\n* `2` - preparing" + "description": "* `0` - Pending\n* `1` - Completed\n* `2` - Failed" }, "UserEmailPreferences": { "type": "object", "properties": { "node_deprecation_notifications": { "type": "boolean", - "description": "The user's email notification preferences for Node.js runtime's deprecation and retirement." + "description": "Receive email notifications about Node.js runtime deprecations" + }, + "custom_domain_certificate_notifications": { + "type": "boolean", + "description": "Receive email notifications about custom domain certificate changes" }, "created_at": { "type": "string", @@ -5531,6 +7666,15 @@ "description": "Timestamp in the extended ISO 8601 format for when the object was last updated." } } + }, + "ValidationStatusEnum": { + "enum": [ + "pending_validation", + "validation_succeeded", + "validation_failed" + ], + "type": "string", + "description": "* `pending_validation` - Pending Validation\n* `validation_succeeded` - Validation Succeeded\n* `validation_failed` - Validation Failed" } }, "securitySchemes": { @@ -5549,7 +7693,7 @@ }, "servers": [ { - "url": "https://cloud.mobify.com" + "url": "https://falcon-cloud-test1.mrt-soak.com" } ] -} +} \ No newline at end of file diff --git a/packages/b2c-tooling-sdk/src/clients/mrt.generated.ts b/packages/b2c-tooling-sdk/src/clients/mrt.generated.ts index a3f088b8..a5b6740e 100644 --- a/packages/b2c-tooling-sdk/src/clients/mrt.generated.ts +++ b/packages/b2c-tooling-sdk/src/clients/mrt.generated.ts @@ -21,6 +21,97 @@ export interface paths { patch?: never; trace?: never; }; + "/api/organizations/{organization_slug}/certificates/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description List certificates for an organization with filtering and search */ + get: operations["organizations_certificates_list"]; + put?: never; + /** @description Create a new certificate for an organization */ + post: operations["organizations_certificates_create"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/organizations/{organization_slug}/certificates/{cert_id}/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Retrieve a specific certificate for an organization */ + get: operations["organizations_certificates_retrieve"]; + put?: never; + post?: never; + /** @description Delete a specific certificate for an organization */ + delete: operations["organizations_certificates_destroy"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/organizations/{organization_slug}/certificates/{cert_id}/restart-validation/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** @description Restart validation for a certificate. Creates a new certificate in AWS and updates the existing certificate record. Only works for certificates that are not yet validated. */ + put: operations["organizations_certificates_restart_validation_update"]; + post?: never; + delete?: never; + options?: never; + head?: never; + /** @description Restart validation for a specific certificate for an organization. */ + patch: operations["organizations_certificates_restart_validation_partial_update"]; + trace?: never; + }; + "/api/organizations/{organization_slug}/members/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description List all the members of an organization. */ + get: operations["list_organization_members"]; + put?: never; + /** @description Add a new member to an organization. */ + post: operations["organizations_members_create"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/organizations/{organization_slug}/members/{email}/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Read data about an organization member's permissions. */ + get: operations["organizations_members_retrieve"]; + put?: never; + post?: never; + /** @description Remove a member from an organization. */ + delete: operations["organizations_members_destroy"]; + options?: never; + head?: never; + /** @description Update an organization member's permissions. */ + patch: operations["organizations_members_partial_update"]; + trace?: never; + }; "/api/projects/": { parameters: { query?: never; @@ -109,6 +200,23 @@ export interface paths { patch?: never; trace?: never; }; + "/api/projects/{project_slug}/bundles/{bundle_id}/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** @description Request deletion of a bundle. The bundle will be asynchronously deleted. This operation can only be performed by project admins. */ + delete: operations["projects_bundles_destroy"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/projects/{project_slug}/bundles/{bundle_id}/download/": { parameters: { query?: never; @@ -126,6 +234,23 @@ export interface paths { patch?: never; trace?: never; }; + "/api/projects/{project_slug}/bundles/bulk-delete/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Request deletion for multiple bundles. Bundles are deleted asynchronously. Only project admins can perform this call. The response indicates which bundles were successfully queued for deletion and which failed validation. */ + post: operations["projects_bundles_bulk_delete_create"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/projects/{project_slug}/members/": { parameters: { query?: never; @@ -273,6 +398,23 @@ export interface paths { patch?: never; trace?: never; }; + "/api/projects/{project_slug}/target/{target_slug}/clone/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Clone a target from another target in the same project. This creates a new target with the same configuration as the source target, excluding proxies and production flag. Optionally clones redirects and environment variables. The new target will be automatically deployed with the same bundle as the source target's current deployment (if source has a deployed bundle) or with a reset bundle. */ + post: operations["projects_target_clone_create"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/projects/{project_slug}/target/{target_slug}/deploy/": { parameters: { query?: never; @@ -454,7 +596,7 @@ export interface components { * @description Timestamp in the extended ISO 8601 format for when the object was created. */ readonly created_at?: string; - readonly publishing_status?: components["schemas"]["PublishingStatusEnum"]; + readonly publishing_status?: components["schemas"]["Status1d2Enum"]; readonly publishing_status_description?: string; }; APIOrganization: { @@ -479,6 +621,48 @@ export interface components { readonly has_mobify_tag_project?: boolean; readonly limits?: components["schemas"]["OrganizationLimits"]; readonly auto_delete?: components["schemas"]["OrganizationAutoDelete"]; + /** @description Enable SSR architecture selection (x86 or arm64) for this organization */ + readonly can_configure_ssr_architecture?: boolean; + }; + APIOrganizationMember: { + /** + * Email address + * Format: email + */ + readonly user?: string; + readonly email?: string; + role: components["schemas"]["RoleEnum"]; + readonly first_name?: string; + readonly last_name?: string; + readonly can_view_all_projects?: boolean; + readonly custom_domain_cert_permission?: components["schemas"]["Status1d2Enum"]; + }; + /** @description Serializer for creating organization members. */ + APIOrganizationMemberCreate: { + /** + * Email address + * Format: email + */ + user: string; + role: components["schemas"]["RoleEnum"]; + can_view_all_projects?: boolean; + custom_domain_cert_permission?: components["schemas"]["Status1d2Enum"]; + }; + /** + * @description Serializer for updating organization role permissions. + * Allows updating can_view_all_projects and custom_domain_cert_permission. + */ + APIOrganizationMemberUpdate: { + /** + * Email address + * Format: email + */ + readonly user?: string; + readonly role?: components["schemas"]["RoleEnum"]; + readonly first_name?: string; + readonly last_name?: string; + can_view_all_projects?: boolean; + custom_domain_cert_permission?: components["schemas"]["Status1d2Enum"]; }; APIProjectMember: { /** @@ -498,7 +682,6 @@ export interface components { /** Format: uri */ url?: string; slug?: string; - /** @description User-friendly identifier for this instance. */ organization: string; readonly deletion_status?: components["schemas"]["DeletionStatusEnum"]; readonly project_type?: components["schemas"]["ProjectTypeEnum"]; @@ -523,6 +706,7 @@ export interface components { upload_bundle?: boolean; edit_environment_variable?: boolean; edit_access_control_header?: boolean; + edit_data_access_layer_entry?: boolean; }; /** * SSR AWS Region @@ -553,6 +737,13 @@ export interface components { * * `sa-east-1` - South America (Sao Paulo) */ ssr_region?: components["schemas"]["SsrRegionEnum"]; + /** + * @description Default Server-Side Rendering architecture (x86 or arm64) for targets under this project. + * + * * `x86` - x86 + * * `arm64` - ARM64 + */ + ssr_architecture?: (components["schemas"]["SsrArchitectureEnum"] | components["schemas"]["NullEnum"]) | null; /** * Format: date-time * @description Timestamp in the extended ISO 8601 format for when the object was created. @@ -563,6 +754,14 @@ export interface components { * @description Timestamp in the extended ISO 8601 format for when the object was last updated. */ readonly updated_at?: string; + /** + * @description Source of the project. One of: ecom, core, direct. + * + * * `ecom` - ecom + * * `core` - core + * * `direct` - direct + */ + readonly source?: (components["schemas"]["SourceEnum"] | components["schemas"]["NullEnum"]) | null; }; APIProjectV2Update: { /** @description User-friendly name for this project */ @@ -595,6 +794,7 @@ export interface components { upload_bundle?: boolean; edit_environment_variable?: boolean; edit_access_control_header?: boolean; + edit_data_access_layer_entry?: boolean; }; /** * SSR AWS Region @@ -625,6 +825,13 @@ export interface components { * * `sa-east-1` - South America (Sao Paulo) */ ssr_region?: components["schemas"]["SsrRegionEnum"]; + /** + * @description Default Server-Side Rendering architecture (x86 or arm64) for targets under this project. + * + * * `x86` - x86 + * * `arm64` - ARM64 + */ + ssr_architecture?: (components["schemas"]["SsrArchitectureEnum"] | components["schemas"]["NullEnum"]) | null; /** * Format: date-time * @description Timestamp in the extended ISO 8601 format for when the object was created. @@ -635,6 +842,14 @@ export interface components { * @description Timestamp in the extended ISO 8601 format for when the object was last updated. */ readonly updated_at?: string; + /** + * @description Source of the project. One of: ecom, core, direct. + * + * * `ecom` - ecom + * * `core` - core + * * `direct` - direct + */ + readonly source?: (components["schemas"]["SourceEnum"] | components["schemas"]["NullEnum"]) | null; }; APIRedirectV2Clone: { from_target_slug: string; @@ -680,6 +895,35 @@ export interface components { */ readonly updated_by?: string; }; + /** + * @description Serializer for target cloning request. + * Inherits from APITargetV2BaseSerializer to reuse validation helper methods. + */ + APITargetV2Clone: { + /** @description The slug of the target to clone from. */ + from_target_slug: string; + /** @description Full hostname to be used by the cloned environment. Required when using non-MRT managed certificate. */ + ssr_external_hostname?: string | null; + /** @description The domain to be used for a Universal PWA SSR deployment (e.g. customer.com). If not provided and hostname is provided, will be extracted from hostname. */ + ssr_external_domain?: string | null; + /** @description The ID of the certificate to associate with the cloned target's custom domain. Required for custom domains. */ + certificate_id?: number | null; + /** + * @description Whether to clone redirects from the source target. + * @default false + */ + clone_redirects: boolean; + /** + * @description Whether to clone environment variables from the source target. + * @default false + */ + clone_environment_variables: boolean; + /** + * @description Whether to clone B2C target info from the source target. + * @default false + */ + clone_b2c_target_info: boolean; + }; /** @description This is the serializer for target create/list APIs. */ APITargetV2Create: { slug?: string; @@ -701,7 +945,7 @@ export interface components { readonly current_deploy?: { [key: string]: unknown; }; - /** @description Full hostname to used by the environment eg. www.customer.com. */ + /** @description Full hostname to be used by the environment eg. www.customer.com. */ ssr_external_hostname?: string | null; /** @description The domain to be used for a Universal PWA SSR deployment (e.g. customer.com) */ ssr_external_domain?: string | null; @@ -734,12 +978,21 @@ export interface components { * * `sa-east-1` - South America (Sao Paulo) */ ssr_region?: components["schemas"]["SsrRegionEnum"] | components["schemas"]["BlankEnum"]; + /** + * @description The architecture for the Server-Side Rendering function (x86 or ARM64). If not specified, the ssr_architecture that's set in the project is used. + * + * * `x86` - x86 + * * `arm64` - ARM64 + */ + ssr_architecture?: (components["schemas"]["SsrArchitectureEnum"] | components["schemas"]["NullEnum"]) | null; /** @description Optional space-separated list of IP addresses (CIDR blocks) that can access this target. Leave blank to allow all IPs. */ ssr_whitelisted_ips?: string | null; ssr_proxy_configs?: { host: string; protocol?: string; }[] | null; + /** @description The Managed Runtime CDN origin domain name. */ + readonly cdn_domain_name?: string; /** * Production * @description Treat this target as a production environment. @@ -750,15 +1003,39 @@ export interface components { /** @description Set true to enable source map support. This will set the NODE_OPTIONS environment variable to "--enable-source-maps" in your MRT environment. */ enable_source_maps?: boolean | null; /** - * @description The minimum log level emitted for this target.n - * * `TRACE` - * * `DEBUG` - * * `INFO` - * * `WARN` - * * `ERROR` - * * `FATAL` + * @description The minimum log level that will be emitted for this target + * + * * `TRACE` - TRACE + * * `DEBUG` - DEBUG + * * `INFO` - INFO + * * `WARN` - WARN + * * `ERROR` - ERROR + * * `FATAL` - FATAL */ log_level?: (components["schemas"]["LogLevelEnum"] | components["schemas"]["NullEnum"]) | null; + /** @description The ID of the certificate to associate with this target's custom domain. Must be an integer unique within the organization. */ + certificate_id?: number | null; + /** @description The certificate domain used by this target. For MRT default domains, returns wildcard format (e.g., *.mobify-storefront-staging.com). For custom domains, returns the certificate domain name. */ + readonly certificate_domain?: string; + /** + * @description The content delivery network used for content, traffic, and security. A B2C instance must be connected to this environment to select eCDN. + * + * * `unknown` - unknown + * * `mrt_cdn` - mrt_cdn + * * `ecdn` - ecdn + * * `stacked_cdn` - stacked_cdn + */ + readonly configured_cdn?: (components["schemas"]["ConfiguredCdnEnum"] | components["schemas"]["NullEnum"]) | null; + /** @description Add a publicly visible hostname. Enter a subdomain if one is not already provided with your organization’s certified domain. */ + readonly cdn_public_hostname?: string | null; + /** + * @description Source of the environment. One of: ecom, core, direct. + * + * * `ecom` - ecom + * * `core` - core + * * `direct` - direct + */ + readonly source?: (components["schemas"]["SourceEnum"] | components["schemas"]["NullEnum"]) | null; }; /** @description This is the serializer for cache invalidation API endpoint. */ APITargetV2CreateInvalidation: { @@ -797,7 +1074,7 @@ export interface components { readonly current_deploy?: { [key: string]: unknown; }; - /** @description Full hostname to used by the environment eg. www.customer.com. */ + /** @description Full hostname to be used by the environment eg. www.customer.com. */ ssr_external_hostname?: string | null; /** @description The domain to be used for a Universal PWA SSR deployment (e.g. customer.com) */ ssr_external_domain?: string | null; @@ -830,12 +1107,21 @@ export interface components { * * `sa-east-1` - South America (Sao Paulo) */ ssr_region?: components["schemas"]["SsrRegionEnum"] | components["schemas"]["BlankEnum"]; + /** + * @description The architecture for the Server-Side Rendering function (x86 or ARM64). If not specified, the ssr_architecture that's set in the project is used. + * + * * `x86` - x86 + * * `arm64` - ARM64 + */ + ssr_architecture?: (components["schemas"]["SsrArchitectureEnum"] | components["schemas"]["NullEnum"]) | null; /** @description Optional space-separated list of IP addresses (CIDR blocks) that can access this target. Leave blank to allow all IPs. */ ssr_whitelisted_ips?: string | null; ssr_proxy_configs?: { host: string; protocol?: string; }[] | null; + /** @description The Managed Runtime CDN origin domain name. */ + readonly cdn_domain_name?: string; /** * Production * @description Treat this target as a production environment. @@ -846,15 +1132,39 @@ export interface components { /** @description Set true to enable source map support. This will set the NODE_OPTIONS environment variable to "--enable-source-maps" in your MRT environment. */ enable_source_maps?: boolean | null; /** - * @description The minimum log level emitted for this target.n - * * `TRACE` - * * `DEBUG` - * * `INFO` - * * `WARN` - * * `ERROR` - * * `FATAL` + * @description The minimum log level that will be emitted for this target + * + * * `TRACE` - TRACE + * * `DEBUG` - DEBUG + * * `INFO` - INFO + * * `WARN` - WARN + * * `ERROR` - ERROR + * * `FATAL` - FATAL */ log_level?: (components["schemas"]["LogLevelEnum"] | components["schemas"]["NullEnum"]) | null; + /** @description The ID of the certificate to associate with this target's custom domain. Must be an integer unique within the organization. Set to null to remove the certificate association. */ + certificate_id?: number | null; + /** @description The certificate domain used by this target. For MRT default domains, returns wildcard format (e.g., *.mobify-storefront-staging.com). For custom domains, returns the certificate domain name. */ + readonly certificate_domain?: string; + /** + * @description The content delivery network used for content, traffic, and security. A B2C instance must be connected to this environment to select eCDN. + * + * * `unknown` - unknown + * * `mrt_cdn` - mrt_cdn + * * `ecdn` - ecdn + * * `stacked_cdn` - stacked_cdn + */ + readonly configured_cdn?: (components["schemas"]["ConfiguredCdnEnum"] | components["schemas"]["NullEnum"]) | null; + /** @description Add a publicly visible hostname. Enter a subdomain if one is not already provided with your organization’s certified domain. */ + readonly cdn_public_hostname?: string | null; + /** + * @description Source of the environment. One of: ecom, core, direct. + * + * * `ecom` - ecom + * * `core` - core + * * `direct` - direct + */ + readonly source?: (components["schemas"]["SourceEnum"] | components["schemas"]["NullEnum"]) | null; }; APIUserProfile: { first_name?: string; @@ -891,6 +1201,21 @@ export interface components { [key: string]: unknown; }; }; + BundleBulkDelete: { + bundle_ids: number[]; + }; + BundleBulkDeleteFailedRequest: { + /** @description The ID of the bundle that failed validation. */ + bundle_id: number; + /** @description Error message that explains why the bundle can't be queued for deletion. */ + errors: string; + }; + BundleBulkDeleteResponse: { + /** @description Bundles that failed validation and couldn't be queued for deletion. */ + rejected_bundles?: components["schemas"]["BundleBulkDeleteFailedRequest"][]; + /** @description Bundle IDs for bundles that were queued for deletion. */ + bundles_queued_for_cleanup?: number[]; + }; BundleDownload: { /** Format: uri */ download_url: string; @@ -917,6 +1242,109 @@ export interface components { */ readonly updated_at?: string; }; + /** @description Base serializer for certificate serializers with common fields and methods. */ + CertificateBase: { + /** @description An ID unique within a business. */ + readonly id?: number; + /** + * Certificate domain + * @description The domain for the certificate either wildcard (e.g. *.example.com) or single domain (e.g. sub.example.com) + */ + domain_name: string; + readonly validation_requested_at?: string; + /** + * @description Current validation status of the certificate + * + * * `pending_validation` - Pending Validation + * * `validation_succeeded` - Validation Succeeded + * * `validation_failed` - Validation Failed + */ + readonly validation_status?: components["schemas"]["ValidationStatusEnum"]; + readonly validation_record?: string; + /** + * Certificate Expiry Date + * Format: date-time + * @description Expiry date of the certificate from ACM. + */ + readonly expires_at?: string | null; + /** + * @description Current status of certificate renewal. + * + * * `PENDING_AUTO_RENEWAL` - Pending Auto Renewal + * * `FAILED` - Failed + */ + readonly renewal_status?: (components["schemas"]["RenewalStatusEnum"] | components["schemas"]["NullEnum"]) | null; + /** + * @description Whether the certificate is eligible for renewal. + * + * * `ELIGIBLE` - Eligible + * * `INELIGIBLE` - Ineligible + */ + readonly renewal_eligibility?: (components["schemas"]["RenewalEligibilityEnum"] | components["schemas"]["NullEnum"]) | null; + readonly targets?: string; + readonly created_by?: string; + /** + * Format: date-time + * @description Timestamp in the extended ISO 8601 format for when the object was created. + */ + readonly created_at?: string; + readonly is_mrt_managed?: string; + readonly deletion_status?: components["schemas"]["DeletionStatusEnum"]; + }; + /** @description Base serializer for certificate serializers with common fields and methods. */ + CertificateListCreate: { + /** @description An ID unique within a business. */ + readonly id?: number; + /** @description The domain for the certificate (e.g. shop.example.com) */ + domain_name: string; + readonly validation_requested_at?: string; + /** + * @description Current validation status of the certificate + * + * * `pending_validation` - Pending Validation + * * `validation_succeeded` - Validation Succeeded + * * `validation_failed` - Validation Failed + */ + readonly validation_status?: components["schemas"]["ValidationStatusEnum"]; + readonly validation_record?: string; + /** + * Certificate Expiry Date + * Format: date-time + * @description Expiry date of the certificate from ACM. + */ + readonly expires_at?: string | null; + /** + * @description Current status of certificate renewal. + * + * * `PENDING_AUTO_RENEWAL` - Pending Auto Renewal + * * `FAILED` - Failed + */ + readonly renewal_status?: (components["schemas"]["RenewalStatusEnum"] | components["schemas"]["NullEnum"]) | null; + /** + * @description Whether the certificate is eligible for renewal. + * + * * `ELIGIBLE` - Eligible + * * `INELIGIBLE` - Ineligible + */ + readonly renewal_eligibility?: (components["schemas"]["RenewalEligibilityEnum"] | components["schemas"]["NullEnum"]) | null; + readonly targets?: string; + readonly created_by?: string; + /** + * Format: date-time + * @description Timestamp in the extended ISO 8601 format for when the object was created. + */ + readonly created_at?: string; + readonly is_mrt_managed?: string; + readonly deletion_status?: components["schemas"]["DeletionStatusEnum"]; + }; + /** + * @description * `unknown` - unknown + * * `mrt_cdn` - mrt_cdn + * * `ecdn` - ecdn + * * `stacked_cdn` - stacked_cdn + * @enum {string} + */ + ConfiguredCdnEnum: "unknown" | "mrt_cdn" | "ecdn" | "stacked_cdn"; /** * @description * `ACTIVE` - Active * * `CLEANUP_REQUESTED` - Cleanup Requested @@ -1017,7 +1445,6 @@ export interface components { */ EncodingEnum: "base64"; EnvironmentVariableList: { - /** @description Name of the Environment Variable. */ name: string; /** @description Value to be encrypted. */ value: string; @@ -1041,7 +1468,7 @@ export interface components { * Format: email */ readonly updated_by?: string; - readonly publishing_status?: components["schemas"]["PublishingStatusEnum"]; + readonly publishing_status?: components["schemas"]["Status1d2Enum"]; readonly publishing_status_description?: string; }; /** @@ -1051,7 +1478,12 @@ export interface components { */ HttpStatusCodeEnum: 301 | 302; /** - * @description The minimum log level emitted for a target. + * @description * `TRACE` - TRACE + * * `DEBUG` - DEBUG + * * `INFO` - INFO + * * `WARN` - WARN + * * `ERROR` - ERROR + * * `FATAL` - FATAL * @enum {string} */ LogLevelEnum: "TRACE" | "DEBUG" | "INFO" | "WARN" | "ERROR" | "FATAL"; @@ -1095,6 +1527,21 @@ export interface components { previous?: string | null; results: components["schemas"]["APIOrganization"][]; }; + PaginatedAPIOrganizationMemberList: { + /** @example 123 */ + count: number; + /** + * Format: uri + * @example http://api.example.org/accounts/?offset=400&limit=100 + */ + next?: string | null; + /** + * Format: uri + * @example http://api.example.org/accounts/?offset=200&limit=100 + */ + previous?: string | null; + results: components["schemas"]["APIOrganizationMember"][]; + }; PaginatedAPIProjectMemberList: { /** @example 123 */ count: number; @@ -1170,6 +1617,21 @@ export interface components { previous?: string | null; results: components["schemas"]["BundleList"][]; }; + PaginatedCertificateListCreateList: { + /** @example 123 */ + count: number; + /** + * Format: uri + * @example http://api.example.org/accounts/?offset=400&limit=100 + */ + next?: string | null; + /** + * Format: uri + * @example http://api.example.org/accounts/?offset=200&limit=100 + */ + previous?: string | null; + results: components["schemas"]["CertificateListCreate"][]; + }; PaginatedDeployListList: { /** @example 123 */ count: number; @@ -1215,6 +1677,22 @@ export interface components { previous?: string | null; results: components["schemas"]["PolymorphicNotification"][]; }; + /** + * @description Serializer for updating organization role permissions. + * Allows updating can_view_all_projects and custom_domain_cert_permission. + */ + PatchedAPIOrganizationMemberUpdate: { + /** + * Email address + * Format: email + */ + readonly user?: string; + readonly role?: components["schemas"]["RoleEnum"]; + readonly first_name?: string; + readonly last_name?: string; + can_view_all_projects?: boolean; + custom_domain_cert_permission?: components["schemas"]["Status1d2Enum"]; + }; PatchedAPIProjectMember: { /** * Email address @@ -1258,6 +1736,7 @@ export interface components { upload_bundle?: boolean; edit_environment_variable?: boolean; edit_access_control_header?: boolean; + edit_data_access_layer_entry?: boolean; }; /** * SSR AWS Region @@ -1289,8 +1768,15 @@ export interface components { */ ssr_region?: components["schemas"]["SsrRegionEnum"]; /** - * Format: date-time - * @description Timestamp in the extended ISO 8601 format for when the object was created. + * @description Default Server-Side Rendering architecture (x86 or arm64) for targets under this project. + * + * * `x86` - x86 + * * `arm64` - ARM64 + */ + ssr_architecture?: (components["schemas"]["SsrArchitectureEnum"] | components["schemas"]["NullEnum"]) | null; + /** + * Format: date-time + * @description Timestamp in the extended ISO 8601 format for when the object was created. */ readonly created_at?: string; /** @@ -1298,6 +1784,14 @@ export interface components { * @description Timestamp in the extended ISO 8601 format for when the object was last updated. */ readonly updated_at?: string; + /** + * @description Source of the project. One of: ecom, core, direct. + * + * * `ecom` - ecom + * * `core` - core + * * `direct` - direct + */ + readonly source?: (components["schemas"]["SourceEnum"] | components["schemas"]["NullEnum"]) | null; }; PatchedAPIRedirectV2CreateUpdate: { /** @description A relative URL. For example, the `from_path` value `/spring` redirects shoppers from the URL `www.example.com/spring`. An asterisk (`*`) at the end of the `from_path` indicates a wildcard. For example, a redirect from `/a/*` matches `/a/`, `/a/b`, and `/a/b/c`. */ @@ -1361,7 +1855,7 @@ export interface components { readonly current_deploy?: { [key: string]: unknown; }; - /** @description Full hostname to used by the environment eg. www.customer.com. */ + /** @description Full hostname to be used by the environment eg. www.customer.com. */ ssr_external_hostname?: string | null; /** @description The domain to be used for a Universal PWA SSR deployment (e.g. customer.com) */ ssr_external_domain?: string | null; @@ -1394,12 +1888,21 @@ export interface components { * * `sa-east-1` - South America (Sao Paulo) */ ssr_region?: components["schemas"]["SsrRegionEnum"] | components["schemas"]["BlankEnum"]; + /** + * @description The architecture for the Server-Side Rendering function (x86 or ARM64). If not specified, the ssr_architecture that's set in the project is used. + * + * * `x86` - x86 + * * `arm64` - ARM64 + */ + ssr_architecture?: (components["schemas"]["SsrArchitectureEnum"] | components["schemas"]["NullEnum"]) | null; /** @description Optional space-separated list of IP addresses (CIDR blocks) that can access this target. Leave blank to allow all IPs. */ ssr_whitelisted_ips?: string | null; ssr_proxy_configs?: { host: string; protocol?: string; }[] | null; + /** @description The Managed Runtime CDN origin domain name. */ + readonly cdn_domain_name?: string; /** * Production * @description Treat this target as a production environment. @@ -1410,16 +1913,88 @@ export interface components { /** @description Set true to enable source map support. This will set the NODE_OPTIONS environment variable to "--enable-source-maps" in your MRT environment. */ enable_source_maps?: boolean | null; /** - * @description The minimum log level emitted for this target. + * @description The minimum log level that will be emitted for this target * - * * `TRACE` - * * `DEBUG` - * * `INFO` - * * `WARN` - * * `ERROR` - * * `FATAL` + * * `TRACE` - TRACE + * * `DEBUG` - DEBUG + * * `INFO` - INFO + * * `WARN` - WARN + * * `ERROR` - ERROR + * * `FATAL` - FATAL */ log_level?: (components["schemas"]["LogLevelEnum"] | components["schemas"]["NullEnum"]) | null; + /** @description The ID of the certificate to associate with this target's custom domain. Must be an integer unique within the organization. Set to null to remove the certificate association. */ + certificate_id?: number | null; + /** @description The certificate domain used by this target. For MRT default domains, returns wildcard format (e.g., *.mobify-storefront-staging.com). For custom domains, returns the certificate domain name. */ + readonly certificate_domain?: string; + /** + * @description The content delivery network used for content, traffic, and security. A B2C instance must be connected to this environment to select eCDN. + * + * * `unknown` - unknown + * * `mrt_cdn` - mrt_cdn + * * `ecdn` - ecdn + * * `stacked_cdn` - stacked_cdn + */ + readonly configured_cdn?: (components["schemas"]["ConfiguredCdnEnum"] | components["schemas"]["NullEnum"]) | null; + /** @description Add a publicly visible hostname. Enter a subdomain if one is not already provided with your organization’s certified domain. */ + readonly cdn_public_hostname?: string | null; + /** + * @description Source of the environment. One of: ecom, core, direct. + * + * * `ecom` - ecom + * * `core` - core + * * `direct` - direct + */ + readonly source?: (components["schemas"]["SourceEnum"] | components["schemas"]["NullEnum"]) | null; + }; + /** @description Base serializer for certificate serializers with common fields and methods. */ + PatchedCertificateBase: { + /** @description An ID unique within a business. */ + readonly id?: number; + /** + * Certificate domain + * @description The domain for the certificate either wildcard (e.g. *.example.com) or single domain (e.g. sub.example.com) + */ + domain_name?: string; + readonly validation_requested_at?: string; + /** + * @description Current validation status of the certificate + * + * * `pending_validation` - Pending Validation + * * `validation_succeeded` - Validation Succeeded + * * `validation_failed` - Validation Failed + */ + readonly validation_status?: components["schemas"]["ValidationStatusEnum"]; + readonly validation_record?: string; + /** + * Certificate Expiry Date + * Format: date-time + * @description Expiry date of the certificate from ACM. + */ + readonly expires_at?: string | null; + /** + * @description Current status of certificate renewal. + * + * * `PENDING_AUTO_RENEWAL` - Pending Auto Renewal + * * `FAILED` - Failed + */ + readonly renewal_status?: (components["schemas"]["RenewalStatusEnum"] | components["schemas"]["NullEnum"]) | null; + /** + * @description Whether the certificate is eligible for renewal. + * + * * `ELIGIBLE` - Eligible + * * `INELIGIBLE` - Ineligible + */ + readonly renewal_eligibility?: (components["schemas"]["RenewalEligibilityEnum"] | components["schemas"]["NullEnum"]) | null; + readonly targets?: string; + readonly created_by?: string; + /** + * Format: date-time + * @description Timestamp in the extended ISO 8601 format for when the object was created. + */ + readonly created_at?: string; + readonly is_mrt_managed?: string; + readonly deletion_status?: components["schemas"]["DeletionStatusEnum"]; }; PatchedEmailNotification: { /** Format: uuid */ @@ -1459,8 +2034,10 @@ export interface components { }; PatchedPolymorphicNotification: components["schemas"]["PatchedEmailNotificationTyped"]; PatchedUserEmailPreferences: { - /** @description The user's email notification preferences for Node.js runtime's deprecation and retirement. */ + /** @description Receive email notifications about Node.js runtime deprecations */ node_deprecation_notifications?: boolean; + /** @description Receive email notifications about custom domain certificate changes */ + custom_domain_certificate_notifications?: boolean; /** * Format: date-time * @description Timestamp in the extended ISO 8601 format for when the object was created. @@ -1483,16 +2060,40 @@ export interface components { */ ProjectTypeEnum: "MOBIFY_STUDIO" | "MOBIFYJS_CLIENT" | "MOBIFY_ADAPTIVEJS" | "MOBIFY_TAG_BASED_PWA" | "SSR"; /** - * @description * `0` - Pending - * * `1` - Completed - * * `2` - Failed - * @enum {integer} + * @description * `ELIGIBLE` - Eligible + * * `INELIGIBLE` - Ineligible + * @enum {string} + */ + RenewalEligibilityEnum: "ELIGIBLE" | "INELIGIBLE"; + /** + * @description * `PENDING_AUTO_RENEWAL` - Pending Auto Renewal + * * `FAILED` - Failed + * @enum {string} */ - PublishingStatusEnum: 0 | 1 | 2; + RenewalStatusEnum: "PENDING_AUTO_RENEWAL" | "FAILED"; ResourceLimit: { limit: number; used: number; }; + /** + * @description * `0` - Owner + * * `1` - Member + * @enum {integer} + */ + RoleEnum: 0 | 1; + /** + * @description * `ecom` - ecom + * * `core` - core + * * `direct` - direct + * @enum {string} + */ + SourceEnum: "ecom" | "core" | "direct"; + /** + * @description * `x86` - x86 + * * `arm64` - ARM64 + * @enum {string} + */ + SsrArchitectureEnum: "x86" | "arm64"; /** * @description * `us-east-1` - US East (N. Virginia) * * `us-east-2` - US East (Ohio) @@ -1530,15 +2131,17 @@ export interface components { */ StateEnum: "CREATE_IN_PROGRESS" | "PUBLISH_IN_PROGRESS" | "ACTIVE" | "CREATE_FAILED" | "PUBLISH_FAILED"; /** - * @description * `0` - ok - * * `1` - broken - * * `2` - preparing + * @description * `0` - Pending + * * `1` - Completed + * * `2` - Failed * @enum {integer} */ Status1d2Enum: 0 | 1 | 2; UserEmailPreferences: { - /** @description The user's email notification preferences for Node.js runtime's deprecation and retirement. */ + /** @description Receive email notifications about Node.js runtime deprecations */ node_deprecation_notifications?: boolean; + /** @description Receive email notifications about custom domain certificate changes */ + custom_domain_certificate_notifications?: boolean; /** * Format: date-time * @description Timestamp in the extended ISO 8601 format for when the object was created. @@ -1550,6 +2153,13 @@ export interface components { */ readonly updated_at?: string; }; + /** + * @description * `pending_validation` - Pending Validation + * * `validation_succeeded` - Validation Succeeded + * * `validation_failed` - Validation Failed + * @enum {string} + */ + ValidationStatusEnum: "pending_validation" | "validation_succeeded" | "validation_failed"; }; responses: never; parameters: never; @@ -1583,6 +2193,316 @@ export interface operations { }; }; }; + organizations_certificates_list: { + parameters: { + query?: { + /** @description If true, returns only custom domain certificates created by the customer. */ + custom_only?: boolean; + /** @description Number of results to return per page. */ + limit?: number; + /** @description The initial index from which to return the results. */ + offset?: number; + /** @description Which field to use when ordering the results. */ + ordering?: string; + /** @description A search term. */ + search?: string; + }; + header?: never; + path: { + /** @description The organization identifier. */ + organization_slug: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PaginatedCertificateListCreateList"]; + }; + }; + }; + }; + organizations_certificates_create: { + parameters: { + query?: never; + header?: never; + path: { + /** @description The organization identifier. */ + organization_slug: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CertificateListCreate"]; + "application/x-www-form-urlencoded": components["schemas"]["CertificateListCreate"]; + "multipart/form-data": components["schemas"]["CertificateListCreate"]; + }; + }; + responses: { + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CertificateListCreate"]; + }; + }; + }; + }; + organizations_certificates_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + /** @description The certificate's id */ + cert_id: string; + /** @description The organization identifier. */ + organization_slug: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CertificateBase"]; + }; + }; + }; + }; + organizations_certificates_destroy: { + parameters: { + query?: never; + header?: never; + path: { + /** @description The certificate's id */ + cert_id: string; + /** @description The organization identifier. */ + organization_slug: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + organizations_certificates_restart_validation_update: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Certificate ID */ + cert_id: string; + /** @description The organization identifier. */ + organization_slug: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CertificateBase"]; + "application/x-www-form-urlencoded": components["schemas"]["CertificateBase"]; + "multipart/form-data": components["schemas"]["CertificateBase"]; + }; + }; + responses: { + /** @description Certificate validation restarted successfully */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CertificateBase"]; + }; + }; + /** @description Certificate is already validated or cannot be restarted */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + organizations_certificates_restart_validation_partial_update: { + parameters: { + query?: never; + header?: never; + path: { + cert_id: string; + organization_slug: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedCertificateBase"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedCertificateBase"]; + "multipart/form-data": components["schemas"]["PatchedCertificateBase"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CertificateBase"]; + }; + }; + }; + }; + list_organization_members: { + parameters: { + query?: { + /** @description Number of results to return per page. */ + limit?: number; + /** @description The initial index from which to return the results. */ + offset?: number; + /** @description Which field to use when ordering the results. */ + ordering?: string; + /** @description A search term. */ + search?: string; + }; + header?: never; + path: { + /** @description The organization identifier. */ + organization_slug: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PaginatedAPIOrganizationMemberList"]; + }; + }; + }; + }; + organizations_members_create: { + parameters: { + query?: never; + header?: never; + path: { + /** @description The organization identifier. */ + organization_slug: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["APIOrganizationMemberCreate"]; + "application/x-www-form-urlencoded": components["schemas"]["APIOrganizationMemberCreate"]; + "multipart/form-data": components["schemas"]["APIOrganizationMemberCreate"]; + }; + }; + responses: { + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["APIOrganizationMemberCreate"]; + }; + }; + }; + }; + organizations_members_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + /** @description The email address of the organization member. */ + email: string; + /** @description The organization identifier. */ + organization_slug: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["APIOrganizationMember"]; + }; + }; + }; + }; + organizations_members_destroy: { + parameters: { + query?: never; + header?: never; + path: { + /** @description The email address of the organization member. */ + email: string; + /** @description The organization identifier. */ + organization_slug: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + organizations_members_partial_update: { + parameters: { + query?: never; + header?: never; + path: { + /** @description The email address of the organization member. */ + email: string; + /** @description The organization identifier. */ + organization_slug: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedAPIOrganizationMemberUpdate"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedAPIOrganizationMemberUpdate"]; + "multipart/form-data": components["schemas"]["PatchedAPIOrganizationMemberUpdate"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["APIOrganizationMemberUpdate"]; + }; + }; + }; + }; projects_list: { parameters: { query?: { @@ -1774,6 +2694,8 @@ export interface operations { limit?: number; /** @description The initial index from which to return the results. */ offset?: number; + /** @description Which field to use when ordering the results. */ + ordering?: string; /** @description A search term. */ search?: string; }; @@ -1796,6 +2718,50 @@ export interface operations { }; }; }; + projects_bundles_destroy: { + parameters: { + query?: never; + header?: never; + path: { + /** @description The bundle's id */ + bundle_id: string; + /** @description The project identifier. */ + project_slug: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Bundle deletion request accepted. The bundle will be asynchronously deleted. */ + 202: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bundle deployed to one or more target, cannot delete. */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bundle not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bundle cannot be deleted (in progress, in failed state). */ + 409: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; projects_bundles_download_retrieve: { parameters: { query?: never; @@ -1820,6 +2786,42 @@ export interface operations { }; }; }; + projects_bundles_bulk_delete_create: { + parameters: { + query?: never; + header?: never; + path: { + /** @description The project identifier. */ + project_slug: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BundleBulkDelete"]; + "application/x-www-form-urlencoded": components["schemas"]["BundleBulkDelete"]; + "multipart/form-data": components["schemas"]["BundleBulkDelete"]; + }; + }; + responses: { + /** @description Bundle deletion was requested. Check the response body for details about the bundles that were queued and that failed deletion. */ + 202: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BundleBulkDeleteResponse"]; + }; + }; + /** @description An unexpected error occurred during the bulk delete operation. The operation has been rolled back and no bundles were deleted. */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; projects_members_list: { parameters: { query?: { @@ -2353,6 +3355,36 @@ export interface operations { }; }; }; + projects_target_clone_create: { + parameters: { + query?: never; + header?: never; + path: { + /** @description The project identifier. */ + project_slug: string; + /** @description The target identifier. */ + target_slug: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["APITargetV2Clone"]; + "application/x-www-form-urlencoded": components["schemas"]["APITargetV2Clone"]; + "multipart/form-data": components["schemas"]["APITargetV2Clone"]; + }; + }; + responses: { + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["APITargetV2Clone"]; + }; + }; + }; + }; projects_target_deploy_list: { parameters: { query?: { diff --git a/packages/b2c-tooling-sdk/src/operations/mrt/certificate.ts b/packages/b2c-tooling-sdk/src/operations/mrt/certificate.ts new file mode 100644 index 00000000..5c76a8bd --- /dev/null +++ b/packages/b2c-tooling-sdk/src/operations/mrt/certificate.ts @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +/** + * Custom domain certificate operations for Managed Runtime. + * + * Certificates are organization-scoped and can be associated with environments + * via the `certificate_id` field on target create/update/clone operations. + * + * @module operations/mrt/certificate + */ +import type {AuthStrategy} from '../../auth/types.js'; +import {createMrtClient, DEFAULT_MRT_ORIGIN} from '../../clients/mrt.js'; +import type {components} from '../../clients/mrt.js'; +import {getLogger} from '../../logging/logger.js'; + +export type MrtCertificate = components['schemas']['CertificateBase']; +export type MrtCertificateListCreate = components['schemas']['CertificateListCreate']; + +function describeError(error: unknown, action: string): Error { + const message = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + return new Error(`Failed to ${action}: ${message}`); +} + +export interface ListCertificatesOptions { + organizationSlug: string; + limit?: number; + offset?: number; + ordering?: string; + search?: string; + /** When true, return only customer-managed certificates. */ + customOnly?: boolean; + origin?: string; +} + +export interface ListCertificatesResult { + count: number; + next: string | null; + previous: string | null; + certificates: MrtCertificateListCreate[]; +} + +export async function listCertificates( + options: ListCertificatesOptions, + auth: AuthStrategy, +): Promise { + const logger = getLogger(); + const {organizationSlug, limit, offset, ordering, search, customOnly, origin} = options; + + logger.debug({organizationSlug}, '[MRT] Listing certificates'); + + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const {data, error} = await client.GET('/api/organizations/{organization_slug}/certificates/', { + params: { + path: {organization_slug: organizationSlug}, + query: {limit, offset, ordering, search, custom_only: customOnly}, + }, + }); + + if (error) throw describeError(error, 'list certificates'); + + return { + count: data.count ?? 0, + next: data.next ?? null, + previous: data.previous ?? null, + certificates: data.results ?? [], + }; +} + +export interface GetCertificateOptions { + organizationSlug: string; + certId: number; + origin?: string; +} + +export async function getCertificate(options: GetCertificateOptions, auth: AuthStrategy): Promise { + const {organizationSlug, certId, origin} = options; + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const {data, error} = await client.GET('/api/organizations/{organization_slug}/certificates/{cert_id}/', { + params: {path: {organization_slug: organizationSlug, cert_id: String(certId)}}, + }); + + if (error) throw describeError(error, 'get certificate'); + + return data; +} + +export interface CreateCertificateOptions { + organizationSlug: string; + /** + * The domain for the certificate (e.g. shop.example.com). + */ + domainName: string; + origin?: string; +} + +export async function createCertificate( + options: CreateCertificateOptions, + auth: AuthStrategy, +): Promise { + const logger = getLogger(); + const {organizationSlug, domainName, origin} = options; + + logger.debug({organizationSlug, domainName}, '[MRT] Creating certificate'); + + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const {data, error} = await client.POST('/api/organizations/{organization_slug}/certificates/', { + params: {path: {organization_slug: organizationSlug}}, + body: {domain_name: domainName}, + }); + + if (error) throw describeError(error, 'create certificate'); + + return data; +} + +export interface DeleteCertificateOptions { + organizationSlug: string; + certId: number; + origin?: string; +} + +export async function deleteCertificate(options: DeleteCertificateOptions, auth: AuthStrategy): Promise { + const {organizationSlug, certId, origin} = options; + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const {error} = await client.DELETE('/api/organizations/{organization_slug}/certificates/{cert_id}/', { + params: {path: {organization_slug: organizationSlug, cert_id: String(certId)}}, + }); + + if (error) throw describeError(error, 'delete certificate'); +} + +export interface RestartCertificateValidationOptions { + organizationSlug: string; + certId: number; + origin?: string; +} + +/** + * Restarts validation for a certificate. Only works for certificates that have + * not yet been validated. + */ +export async function restartCertificateValidation( + options: RestartCertificateValidationOptions, + auth: AuthStrategy, +): Promise { + const {organizationSlug, certId, origin} = options; + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const {data, error} = await client.PATCH( + '/api/organizations/{organization_slug}/certificates/{cert_id}/restart-validation/', + { + params: {path: {organization_slug: organizationSlug, cert_id: String(certId)}}, + body: {}, + }, + ); + + if (error) throw describeError(error, 'restart certificate validation'); + + return data; +} diff --git a/packages/b2c-tooling-sdk/src/operations/mrt/env.ts b/packages/b2c-tooling-sdk/src/operations/mrt/env.ts index 0c96dfcd..571c3e94 100644 --- a/packages/b2c-tooling-sdk/src/operations/mrt/env.ts +++ b/packages/b2c-tooling-sdk/src/operations/mrt/env.ts @@ -482,6 +482,140 @@ export async function waitForEnv(options: WaitForEnvOptions, auth: AuthStrategy) */ export type MrtEnvironmentUpdate = components['schemas']['APITargetV2Update']; +/** + * Options for cloning an MRT environment. + */ +export interface CloneEnvOptions { + /** + * The project slug containing the source and new environment. + */ + projectSlug: string; + + /** + * Slug for the new environment created by the clone. + */ + slug: string; + + /** + * Slug of the source environment to clone from. + */ + fromSlug: string; + + /** + * Full external hostname (e.g., www.example.com). + * Required when not using an MRT-managed certificate. + */ + externalHostname?: string | null; + + /** + * External domain for Universal PWA SSR (e.g., example.com). + */ + externalDomain?: string | null; + + /** + * ID of the certificate to associate with the new environment. + * Required when using a custom domain. + */ + certificateId?: number | null; + + /** + * Clone redirects from the source environment. + * @default false + */ + cloneRedirects?: boolean; + + /** + * Clone environment variables from the source environment. + * @default false + */ + cloneEnvironmentVariables?: boolean; + + /** + * Clone B2C target info from the source environment. + * @default false + */ + cloneB2cTargetInfo?: boolean; + + /** + * MRT API origin URL. + * @default "https://cloud.mobify.com" + */ + origin?: string; +} + +/** + * Clones an environment (target) from an existing source environment. + * + * The new environment receives the source's configuration (excluding proxies and + * production flag) and is automatically deployed with the same bundle as the + * source target's current deployment (if any). + * + * @param options - Clone options + * @param auth - Authentication strategy (ApiKeyStrategy) + * @returns The newly created environment + * @throws Error if the clone fails + * + * @example + * ```typescript + * const env = await cloneEnv({ + * projectSlug: 'my-storefront', + * slug: 'staging-copy', + * fromSlug: 'staging', + * cloneRedirects: true, + * cloneEnvironmentVariables: true + * }, auth); + * ``` + */ +export async function cloneEnv(options: CloneEnvOptions, auth: AuthStrategy): Promise { + const logger = getLogger(); + const {projectSlug, slug, fromSlug, origin} = options; + + logger.debug({projectSlug, slug, fromSlug}, '[MRT] Cloning environment'); + + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const body: components['schemas']['APITargetV2Clone'] = { + from_target_slug: fromSlug, + clone_redirects: options.cloneRedirects ?? false, + clone_environment_variables: options.cloneEnvironmentVariables ?? false, + clone_b2c_target_info: options.cloneB2cTargetInfo ?? false, + }; + + if (options.externalHostname !== undefined) { + body.ssr_external_hostname = options.externalHostname; + } + + if (options.externalDomain !== undefined) { + body.ssr_external_domain = options.externalDomain; + } + + if (options.certificateId !== undefined) { + body.certificate_id = options.certificateId; + } + + const {data, error} = await client.POST('/api/projects/{project_slug}/target/{target_slug}/clone/', { + params: { + path: {project_slug: projectSlug, target_slug: slug}, + }, + body, + }); + + if (error) { + const errorMessage = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + throw new Error(`Failed to clone environment: ${errorMessage}`); + } + + // The OpenAPI spec types this response as APITargetV2Clone (the request body schema), + // but the API actually returns a target object (slug, name, state, ssr_*, etc.). + // Cast through unknown rather than re-fetching to avoid a second round-trip. + logger.debug({slug, fromSlug}, '[MRT] Environment cloned successfully'); + + return data as unknown as MrtEnvironment; +} + /** * Patched environment for partial updates. */ diff --git a/packages/b2c-tooling-sdk/src/operations/mrt/index.ts b/packages/b2c-tooling-sdk/src/operations/mrt/index.ts index 0d858e66..02b0d2cf 100644 --- a/packages/b2c-tooling-sdk/src/operations/mrt/index.ts +++ b/packages/b2c-tooling-sdk/src/operations/mrt/index.ts @@ -50,7 +50,7 @@ export {createBundle, createGlobFilter, getDefaultMessage, DEFAULT_SSR_PARAMETER export type {CreateBundleOptions, Bundle} from './bundle.js'; // Push and bundle operations -export {pushBundle, uploadBundle, listBundles, downloadBundle} from './push.js'; +export {pushBundle, uploadBundle, listBundles, downloadBundle, deleteBundle, bulkDeleteBundles} from './push.js'; export type { PushOptions, PushResult, @@ -58,6 +58,10 @@ export type { ListBundlesResult, DownloadBundleOptions, DownloadBundleResult, + DeleteBundleOptions, + BulkDeleteBundlesOptions, + BulkDeleteBundlesResult, + BulkDeleteRejectedBundle, MrtBundle, } from './push.js'; @@ -73,8 +77,9 @@ export type { } from './env-var.js'; // Environment (target) operations -export {createEnv, deleteEnv, getEnv, waitForEnv, listEnvs, updateEnv} from './env.js'; +export {cloneEnv, createEnv, deleteEnv, getEnv, waitForEnv, listEnvs, updateEnv} from './env.js'; export type { + CloneEnvOptions, CreateEnvOptions, DeleteEnvOptions, GetEnvOptions, @@ -109,6 +114,48 @@ export type { OrganizationLimits, } from './organization.js'; +// Certificate operations (organization-scoped, used by env clone/create with custom domains) +export { + listCertificates, + getCertificate, + createCertificate, + deleteCertificate, + restartCertificateValidation, +} from './certificate.js'; +export type { + ListCertificatesOptions, + ListCertificatesResult, + GetCertificateOptions, + CreateCertificateOptions, + DeleteCertificateOptions, + RestartCertificateValidationOptions, + MrtCertificate, + MrtCertificateListCreate, +} from './certificate.js'; + +// Organization member operations +export { + listOrgMembers, + addOrgMember, + getOrgMember, + updateOrgMember, + removeOrgMember, + ORG_ROLES, +} from './organization-member.js'; +export type { + ListOrgMembersOptions, + ListOrgMembersResult, + AddOrgMemberOptions, + GetOrgMemberOptions, + UpdateOrgMemberOptions, + RemoveOrgMemberOptions, + MrtOrgMember, + MrtOrgMemberCreate, + MrtOrgMemberUpdate, + OrgRoleValue, + OrgCertPermission, +} from './organization-member.js'; + // Project operations export {listProjects, createProject, getProject, updateProject, deleteProject} from './project.js'; export type { diff --git a/packages/b2c-tooling-sdk/src/operations/mrt/organization-member.ts b/packages/b2c-tooling-sdk/src/operations/mrt/organization-member.ts new file mode 100644 index 00000000..0d155ad1 --- /dev/null +++ b/packages/b2c-tooling-sdk/src/operations/mrt/organization-member.ts @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +/** + * Organization member operations for Managed Runtime. + * + * Distinct from project members: organization members hold a role at the + * organization level and may be granted permission to view all projects + * and manage custom domain certificates. + * + * @module operations/mrt/organization-member + */ +import type {AuthStrategy} from '../../auth/types.js'; +import {createMrtClient, DEFAULT_MRT_ORIGIN} from '../../clients/mrt.js'; +import type {components} from '../../clients/mrt.js'; +import {getLogger} from '../../logging/logger.js'; + +export type MrtOrgMember = components['schemas']['APIOrganizationMember']; +export type MrtOrgMemberCreate = components['schemas']['APIOrganizationMemberCreate']; +export type MrtOrgMemberUpdate = components['schemas']['PatchedAPIOrganizationMemberUpdate']; + +/** + * Organization role values. + * 0 = Owner, 1 = Member + */ +export type OrgRoleValue = 0 | 1; + +export const ORG_ROLES: Record = { + 0: 'Owner', + 1: 'Member', +}; + +/** + * Cert permission status. 1 = Disabled, 2 = Enabled (per Status1d2Enum). + */ +export type OrgCertPermission = 1 | 2; + +function describeError(error: unknown, action: string): Error { + const message = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + return new Error(`Failed to ${action}: ${message}`); +} + +export interface ListOrgMembersOptions { + organizationSlug: string; + limit?: number; + offset?: number; + ordering?: string; + search?: string; + origin?: string; +} + +export interface ListOrgMembersResult { + count: number; + next: string | null; + previous: string | null; + members: MrtOrgMember[]; +} + +export async function listOrgMembers( + options: ListOrgMembersOptions, + auth: AuthStrategy, +): Promise { + const logger = getLogger(); + const {organizationSlug, limit, offset, ordering, search, origin} = options; + + logger.debug({organizationSlug}, '[MRT] Listing organization members'); + + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const {data, error} = await client.GET('/api/organizations/{organization_slug}/members/', { + params: { + path: {organization_slug: organizationSlug}, + query: {limit, offset, ordering, search}, + }, + }); + + if (error) throw describeError(error, 'list organization members'); + + return { + count: data.count ?? 0, + next: data.next ?? null, + previous: data.previous ?? null, + members: data.results ?? [], + }; +} + +export interface AddOrgMemberOptions { + organizationSlug: string; + email: string; + role: OrgRoleValue; + canViewAllProjects?: boolean; + customDomainCertPermission?: OrgCertPermission; + origin?: string; +} + +export async function addOrgMember(options: AddOrgMemberOptions, auth: AuthStrategy): Promise { + const logger = getLogger(); + const {organizationSlug, email, role, canViewAllProjects, customDomainCertPermission, origin} = options; + + logger.debug({organizationSlug, email, role}, '[MRT] Adding organization member'); + + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const body: MrtOrgMemberCreate = {user: email, role}; + if (canViewAllProjects !== undefined) body.can_view_all_projects = canViewAllProjects; + if (customDomainCertPermission !== undefined) body.custom_domain_cert_permission = customDomainCertPermission; + + const {error} = await client.POST('/api/organizations/{organization_slug}/members/', { + params: {path: {organization_slug: organizationSlug}}, + body, + }); + + if (error) throw describeError(error, 'add organization member'); + + // Re-fetch to get the full member shape (POST response omits email/first_name/last_name). + return getOrgMember({organizationSlug, email, origin}, auth); +} + +export interface GetOrgMemberOptions { + organizationSlug: string; + email: string; + origin?: string; +} + +export async function getOrgMember(options: GetOrgMemberOptions, auth: AuthStrategy): Promise { + const {organizationSlug, email, origin} = options; + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const {data, error} = await client.GET('/api/organizations/{organization_slug}/members/{email}/', { + params: {path: {organization_slug: organizationSlug, email}}, + }); + + if (error) throw describeError(error, 'get organization member'); + + return data; +} + +export interface UpdateOrgMemberOptions { + organizationSlug: string; + email: string; + canViewAllProjects?: boolean; + customDomainCertPermission?: OrgCertPermission; + origin?: string; +} + +export async function updateOrgMember(options: UpdateOrgMemberOptions, auth: AuthStrategy): Promise { + const {organizationSlug, email, canViewAllProjects, customDomainCertPermission, origin} = options; + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const body: MrtOrgMemberUpdate = {}; + if (canViewAllProjects !== undefined) body.can_view_all_projects = canViewAllProjects; + if (customDomainCertPermission !== undefined) body.custom_domain_cert_permission = customDomainCertPermission; + + const {error} = await client.PATCH('/api/organizations/{organization_slug}/members/{email}/', { + params: {path: {organization_slug: organizationSlug, email}}, + body, + }); + + if (error) throw describeError(error, 'update organization member'); + + // Re-fetch to get the full member shape (PATCH response omits email/first_name/last_name). + return getOrgMember({organizationSlug, email, origin}, auth); +} + +export interface RemoveOrgMemberOptions { + organizationSlug: string; + email: string; + origin?: string; +} + +export async function removeOrgMember(options: RemoveOrgMemberOptions, auth: AuthStrategy): Promise { + const {organizationSlug, email, origin} = options; + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const {error} = await client.DELETE('/api/organizations/{organization_slug}/members/{email}/', { + params: {path: {organization_slug: organizationSlug, email}}, + }); + + if (error) throw describeError(error, 'remove organization member'); +} diff --git a/packages/b2c-tooling-sdk/src/operations/mrt/push.ts b/packages/b2c-tooling-sdk/src/operations/mrt/push.ts index 2fbd31a1..acdca185 100644 --- a/packages/b2c-tooling-sdk/src/operations/mrt/push.ts +++ b/packages/b2c-tooling-sdk/src/operations/mrt/push.ts @@ -413,3 +413,134 @@ export async function downloadBundle( downloadUrl: data.download_url, }; } + +/** + * Options for deleting a single bundle. + */ +export interface DeleteBundleOptions { + /** The project slug containing the bundle. */ + projectSlug: string; + /** The bundle ID to delete. */ + bundleId: number; + /** + * MRT API origin URL. + * @default "https://cloud.mobify.com" + */ + origin?: string; +} + +/** + * Requests deletion of a single bundle. Bundles are deleted asynchronously. + * Only project admins can perform this operation. + * + * @param options - Delete options + * @param auth - Authentication strategy + * @throws Error if the request fails + */ +export async function deleteBundle(options: DeleteBundleOptions, auth: AuthStrategy): Promise { + const logger = getLogger(); + const {projectSlug, bundleId, origin} = options; + + logger.debug({projectSlug, bundleId}, '[MRT] Deleting bundle'); + + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const {error} = await client.DELETE('/api/projects/{project_slug}/bundles/{bundle_id}/', { + params: { + path: {project_slug: projectSlug, bundle_id: String(bundleId)}, + }, + }); + + if (error) { + const errorMessage = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + throw new Error(`Failed to delete bundle: ${errorMessage}`); + } + + logger.debug({bundleId}, '[MRT] Bundle queued for deletion'); +} + +/** + * Options for bulk-deleting bundles. + */ +export interface BulkDeleteBundlesOptions { + /** The project slug containing the bundles. */ + projectSlug: string; + /** Bundle IDs to delete. */ + bundleIds: number[]; + /** + * MRT API origin URL. + * @default "https://cloud.mobify.com" + */ + origin?: string; +} + +/** + * A bundle that the server rejected during a bulk-delete request. + */ +export interface BulkDeleteRejectedBundle { + /** Bundle ID that was rejected. May be null when the server returned a batch error without a specific id. */ + bundleId?: number; + /** Reason the bundle was rejected. */ + reason: string; +} + +/** + * Result of a bulk-delete request. + */ +export interface BulkDeleteBundlesResult { + /** Bundle IDs that were queued for asynchronous deletion. */ + queued: number[]; + /** Bundles the server rejected, with reasons. */ + rejected: BulkDeleteRejectedBundle[]; +} + +/** + * Requests deletion of multiple bundles in a single call. + * + * The response indicates which bundles were queued and which were rejected. + * Only project admins can perform this operation. + * + * @param options - Bulk delete options + * @param auth - Authentication strategy + * @returns Lists of queued and rejected bundle IDs + * @throws Error if the request itself fails + */ +export async function bulkDeleteBundles( + options: BulkDeleteBundlesOptions, + auth: AuthStrategy, +): Promise { + const logger = getLogger(); + const {projectSlug, bundleIds, origin} = options; + + logger.debug({projectSlug, count: bundleIds.length}, '[MRT] Bulk-deleting bundles'); + + const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth); + + const {data, error} = await client.POST('/api/projects/{project_slug}/bundles/bulk-delete/', { + params: { + path: {project_slug: projectSlug}, + }, + body: {bundle_ids: bundleIds}, + }); + + if (error) { + const errorMessage = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as {message: unknown}).message) + : JSON.stringify(error); + throw new Error(`Failed to bulk-delete bundles: ${errorMessage}`); + } + + const rejected: BulkDeleteRejectedBundle[] = (data?.rejected_bundles ?? []).map((entry) => ({ + bundleId: entry.bundle_id, + reason: entry.errors, + })); + + return { + queued: data?.bundles_queued_for_cleanup ?? [], + rejected, + }; +} diff --git a/packages/b2c-tooling-sdk/test/operations/mrt/certificate.test.ts b/packages/b2c-tooling-sdk/test/operations/mrt/certificate.test.ts new file mode 100644 index 00000000..4c4fc046 --- /dev/null +++ b/packages/b2c-tooling-sdk/test/operations/mrt/certificate.test.ts @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import {expect} from 'chai'; +import {http, HttpResponse} from 'msw'; +import {setupServer} from 'msw/node'; +import {DEFAULT_MRT_ORIGIN} from '../../../src/clients/mrt.js'; +import { + createCertificate, + deleteCertificate, + getCertificate, + listCertificates, + restartCertificateValidation, +} from '../../../src/operations/mrt/certificate.js'; +import {MockAuthStrategy} from '../../helpers/mock-auth.js'; + +describe('operations/mrt/certificate', () => { + const server = setupServer(); + const auth = new MockAuthStrategy(); + + before(() => { + server.listen({onUnhandledRequest: 'error'}); + }); + afterEach(() => { + server.resetHandlers(); + }); + after(() => { + server.close(); + }); + + it('listCertificates returns paginated certs', async () => { + server.use( + http.get(`${DEFAULT_MRT_ORIGIN}/api/organizations/:org/certificates/`, () => + HttpResponse.json({ + count: 1, + next: null, + previous: null, + results: [{id: 7, domain_name: 'shop.example.com', validation_status: 'pending_validation'}], + }), + ), + ); + const result = await listCertificates({organizationSlug: 'org', customOnly: true}, auth); + expect(result.count).to.equal(1); + expect(result.certificates[0].domain_name).to.equal('shop.example.com'); + }); + + it('getCertificate fetches by id', async () => { + server.use( + http.get(`${DEFAULT_MRT_ORIGIN}/api/organizations/:org/certificates/:id/`, ({params}) => + HttpResponse.json({id: Number(params.id), domain_name: 'shop.example.com'}), + ), + ); + const cert = await getCertificate({organizationSlug: 'org', certId: 5}, auth); + expect(cert.id).to.equal(5); + }); + + it('createCertificate sends domain name', async () => { + let receivedBody: any; + server.use( + http.post(`${DEFAULT_MRT_ORIGIN}/api/organizations/:org/certificates/`, async ({request}) => { + receivedBody = await request.json(); + return HttpResponse.json({id: 1, domain_name: receivedBody.domain_name}, {status: 201}); + }), + ); + const cert = await createCertificate({organizationSlug: 'org', domainName: 'shop.example.com'}, auth); + expect(receivedBody.domain_name).to.equal('shop.example.com'); + expect(cert.id).to.equal(1); + }); + + it('deleteCertificate resolves on 204', async () => { + server.use( + http.delete( + `${DEFAULT_MRT_ORIGIN}/api/organizations/:org/certificates/:id/`, + () => new HttpResponse(null, {status: 204}), + ), + ); + await deleteCertificate({organizationSlug: 'org', certId: 1}, auth); + }); + + it('restartCertificateValidation patches the resource', async () => { + server.use( + http.patch(`${DEFAULT_MRT_ORIGIN}/api/organizations/:org/certificates/:id/restart-validation/`, ({params}) => + HttpResponse.json({ + id: Number(params.id), + domain_name: 'shop.example.com', + validation_status: 'pending_validation', + }), + ), + ); + const cert = await restartCertificateValidation({organizationSlug: 'org', certId: 9}, auth); + expect(cert.id).to.equal(9); + }); +}); diff --git a/packages/b2c-tooling-sdk/test/operations/mrt/env.test.ts b/packages/b2c-tooling-sdk/test/operations/mrt/env.test.ts index b3aad3b0..611188c2 100644 --- a/packages/b2c-tooling-sdk/test/operations/mrt/env.test.ts +++ b/packages/b2c-tooling-sdk/test/operations/mrt/env.test.ts @@ -10,7 +10,7 @@ import {http, HttpResponse} from 'msw'; import {setupServer} from 'msw/node'; import {DEFAULT_MRT_ORIGIN} from '../../../src/clients/mrt.js'; import {MockAuthStrategy} from '../../helpers/mock-auth.js'; -import {createEnv, getEnv, deleteEnv, waitForEnv} from '../../../src/operations/mrt/env.js'; +import {createEnv, getEnv, deleteEnv, waitForEnv, cloneEnv} from '../../../src/operations/mrt/env.js'; const DEFAULT_BASE_URL = DEFAULT_MRT_ORIGIN; @@ -385,4 +385,92 @@ describe('operations/mrt/env', () => { } }); }); + + describe('cloneEnv', () => { + it('should clone an environment and return the new target', async () => { + let receivedBody: any; + let receivedPath: string | undefined; + + server.use( + http.post( + `${DEFAULT_BASE_URL}/api/projects/:projectSlug/target/:targetSlug/clone/`, + async ({request, params}) => { + receivedBody = await request.json(); + receivedPath = `${params.projectSlug}/${params.targetSlug}`; + return HttpResponse.json( + {slug: params.targetSlug, name: 'Staging Copy', state: 'CREATE_IN_PROGRESS'}, + {status: 201}, + ); + }, + ), + ); + + const auth = new MockAuthStrategy(); + const result = await cloneEnv( + { + projectSlug: 'my-project', + slug: 'staging-copy', + fromSlug: 'staging', + cloneRedirects: true, + cloneEnvironmentVariables: true, + }, + auth, + ); + + expect(receivedPath).to.equal('my-project/staging-copy'); + expect(receivedBody.from_target_slug).to.equal('staging'); + expect(receivedBody.clone_redirects).to.be.true; + expect(receivedBody.clone_environment_variables).to.be.true; + expect(receivedBody.clone_b2c_target_info).to.be.false; + expect(result.slug).to.equal('staging-copy'); + expect(result.state).to.equal('CREATE_IN_PROGRESS'); + }); + + it('should pass through custom domain options', async () => { + let receivedBody: any; + + server.use( + http.post( + `${DEFAULT_BASE_URL}/api/projects/:projectSlug/target/:targetSlug/clone/`, + async ({request, params}) => { + receivedBody = await request.json(); + return HttpResponse.json({slug: params.targetSlug, name: 'qa', state: 'CREATE_IN_PROGRESS'}, {status: 201}); + }, + ), + ); + + const auth = new MockAuthStrategy(); + await cloneEnv( + { + projectSlug: 'my-project', + slug: 'qa', + fromSlug: 'staging', + externalHostname: 'qa.example.com', + externalDomain: 'example.com', + certificateId: 123, + }, + auth, + ); + + expect(receivedBody.ssr_external_hostname).to.equal('qa.example.com'); + expect(receivedBody.ssr_external_domain).to.equal('example.com'); + expect(receivedBody.certificate_id).to.equal(123); + }); + + it('should throw on API error', async () => { + server.use( + http.post(`${DEFAULT_BASE_URL}/api/projects/:projectSlug/target/:targetSlug/clone/`, () => + HttpResponse.json({message: 'Source target not found'}, {status: 404}), + ), + ); + + const auth = new MockAuthStrategy(); + try { + await cloneEnv({projectSlug: 'p', slug: 's', fromSlug: 'missing'}, auth); + expect.fail('Should have thrown'); + } catch (error: any) { + expect(error.message).to.include('clone environment'); + } + }); + }); }); diff --git a/packages/b2c-tooling-sdk/test/operations/mrt/organization-member.test.ts b/packages/b2c-tooling-sdk/test/operations/mrt/organization-member.test.ts new file mode 100644 index 00000000..26097e55 --- /dev/null +++ b/packages/b2c-tooling-sdk/test/operations/mrt/organization-member.test.ts @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import {expect} from 'chai'; +import {http, HttpResponse} from 'msw'; +import {setupServer} from 'msw/node'; +import {DEFAULT_MRT_ORIGIN} from '../../../src/clients/mrt.js'; +import { + addOrgMember, + getOrgMember, + listOrgMembers, + removeOrgMember, + updateOrgMember, +} from '../../../src/operations/mrt/organization-member.js'; +import {MockAuthStrategy} from '../../helpers/mock-auth.js'; + +describe('operations/mrt/organization-member', () => { + const server = setupServer(); + const auth = new MockAuthStrategy(); + + before(() => { + server.listen({onUnhandledRequest: 'error'}); + }); + afterEach(() => { + server.resetHandlers(); + }); + after(() => { + server.close(); + }); + + it('listOrgMembers returns paginated results', async () => { + server.use( + http.get(`${DEFAULT_MRT_ORIGIN}/api/organizations/:org/members/`, () => + HttpResponse.json({ + count: 2, + next: null, + previous: null, + results: [ + {user: 'a@x.com', email: 'a@x.com', role: 0, can_view_all_projects: true}, + {user: 'b@x.com', email: 'b@x.com', role: 1, can_view_all_projects: false}, + ], + }), + ), + ); + + const result = await listOrgMembers({organizationSlug: 'my-org'}, auth); + expect(result.count).to.equal(2); + expect(result.members).to.have.lengthOf(2); + }); + + it('addOrgMember posts the member and re-fetches the full record', async () => { + let receivedBody: any; + server.use( + http.post(`${DEFAULT_MRT_ORIGIN}/api/organizations/:org/members/`, async ({request}) => { + receivedBody = await request.json(); + return HttpResponse.json(receivedBody, {status: 201}); + }), + http.get(`${DEFAULT_MRT_ORIGIN}/api/organizations/:org/members/:email/`, ({params}) => + HttpResponse.json({ + user: params.email, + email: params.email, + role: 1, + first_name: 'New', + last_name: 'User', + can_view_all_projects: true, + }), + ), + ); + + const result = await addOrgMember( + {organizationSlug: 'my-org', email: 'new@x.com', role: 1, canViewAllProjects: true}, + auth, + ); + expect(receivedBody.user).to.equal('new@x.com'); + expect(receivedBody.role).to.equal(1); + expect(receivedBody.can_view_all_projects).to.be.true; + expect(result.email).to.equal('new@x.com'); + expect(result.first_name).to.equal('New'); + }); + + it('getOrgMember fetches by email', async () => { + server.use( + http.get(`${DEFAULT_MRT_ORIGIN}/api/organizations/:org/members/:email/`, ({params}) => + HttpResponse.json({user: params.email, email: params.email, role: 0}), + ), + ); + const result = await getOrgMember({organizationSlug: 'my-org', email: 'x@y.com'}, auth); + expect(result.email).to.equal('x@y.com'); + }); + + it('updateOrgMember sends only changed fields and re-fetches the full record', async () => { + let receivedBody: any; + server.use( + http.patch(`${DEFAULT_MRT_ORIGIN}/api/organizations/:org/members/:email/`, async ({request}) => { + receivedBody = await request.json(); + return HttpResponse.json(receivedBody); + }), + http.get(`${DEFAULT_MRT_ORIGIN}/api/organizations/:org/members/:email/`, ({params}) => + HttpResponse.json({user: params.email, email: params.email, role: 1, can_view_all_projects: true}), + ), + ); + + const result = await updateOrgMember( + {organizationSlug: 'my-org', email: 'x@y.com', canViewAllProjects: true}, + auth, + ); + expect(receivedBody).to.deep.equal({can_view_all_projects: true}); + expect(result.email).to.equal('x@y.com'); + }); + + it('removeOrgMember resolves on 204', async () => { + server.use( + http.delete( + `${DEFAULT_MRT_ORIGIN}/api/organizations/:org/members/:email/`, + () => new HttpResponse(null, {status: 204}), + ), + ); + await removeOrgMember({organizationSlug: 'my-org', email: 'x@y.com'}, auth); + }); + + it('throws on error response', async () => { + server.use( + http.get(`${DEFAULT_MRT_ORIGIN}/api/organizations/:org/members/:email/`, () => + HttpResponse.json({message: 'Not found'}, {status: 404}), + ), + ); + try { + await getOrgMember({organizationSlug: 'my-org', email: 'x@y.com'}, auth); + expect.fail('Should have thrown'); + } catch (error) { + expect((error as Error).message).to.include('get organization member'); + } + }); +}); diff --git a/packages/b2c-tooling-sdk/test/operations/mrt/push.test.ts b/packages/b2c-tooling-sdk/test/operations/mrt/push.test.ts index f42ee703..7652dd30 100644 --- a/packages/b2c-tooling-sdk/test/operations/mrt/push.test.ts +++ b/packages/b2c-tooling-sdk/test/operations/mrt/push.test.ts @@ -7,7 +7,7 @@ import {expect} from 'chai'; import {http, HttpResponse} from 'msw'; import {setupServer} from 'msw/node'; import {createMrtClient, DEFAULT_MRT_ORIGIN} from '@salesforce/b2c-tooling-sdk/clients'; -import {uploadBundle, listBundles} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {uploadBundle, listBundles, deleteBundle, bulkDeleteBundles} from '@salesforce/b2c-tooling-sdk/operations/mrt'; import type {Bundle} from '@salesforce/b2c-tooling-sdk/operations/mrt'; import {MockAuthStrategy} from '../../helpers/mock-auth.js'; @@ -236,4 +236,63 @@ describe('operations/mrt/push', () => { } }); }); + + describe('deleteBundle', () => { + it('should DELETE the bundle and resolve on 202', async () => { + let receivedPath: string | undefined; + server.use( + http.delete(`${DEFAULT_BASE_URL}/api/projects/:projectSlug/bundles/:bundleId/`, ({params}) => { + receivedPath = `${params.projectSlug}/${params.bundleId}`; + return new HttpResponse(null, {status: 204}); + }), + ); + + const auth = new MockAuthStrategy(); + await deleteBundle({projectSlug: 'my-project', bundleId: 42}, auth); + + expect(receivedPath).to.equal('my-project/42'); + }); + + it('should throw on error response', async () => { + server.use( + http.delete(`${DEFAULT_BASE_URL}/api/projects/:projectSlug/bundles/:bundleId/`, () => + HttpResponse.json({message: 'Bundle in use'}, {status: 403}), + ), + ); + + const auth = new MockAuthStrategy(); + try { + await deleteBundle({projectSlug: 'my-project', bundleId: 42}, auth); + expect.fail('Should have thrown'); + } catch (error) { + expect((error as Error).message).to.include('delete bundle'); + } + }); + }); + + describe('bulkDeleteBundles', () => { + it('should send all bundle IDs and return queued/rejected lists', async () => { + let receivedBody: {bundle_ids?: number[]} | undefined; + server.use( + http.post(`${DEFAULT_BASE_URL}/api/projects/:projectSlug/bundles/bulk-delete/`, async ({request}) => { + receivedBody = (await request.json()) as {bundle_ids?: number[]}; + return HttpResponse.json( + { + bundles_queued_for_cleanup: [1, 3], + rejected_bundles: [{bundle_id: 2, errors: 'Bundle in use'}], + }, + {status: 202}, + ); + }), + ); + + const auth = new MockAuthStrategy(); + const result = await bulkDeleteBundles({projectSlug: 'my-project', bundleIds: [1, 2, 3]}, auth); + + expect(receivedBody?.bundle_ids).to.deep.equal([1, 2, 3]); + expect(result.queued).to.deep.equal([1, 3]); + expect(result.rejected).to.have.lengthOf(1); + expect(result.rejected[0]).to.deep.equal({bundleId: 2, reason: 'Bundle in use'}); + }); + }); }); diff --git a/skills/b2c-cli/skills/b2c-mrt/SKILL.md b/skills/b2c-cli/skills/b2c-mrt/SKILL.md index cca50c00..05daa0b5 100644 --- a/skills/b2c-cli/skills/b2c-mrt/SKILL.md +++ b/skills/b2c-cli/skills/b2c-mrt/SKILL.md @@ -13,15 +13,17 @@ Use the `b2c` CLI to manage Managed Runtime (MRT) projects, environments, bundle ``` mrt -├── org (list, b2c) - Organizations and B2C connections +├── org - Organizations and B2C connections +│ ├── member - Organization-level member management +│ └── cert - Custom domain certificates ├── project - Project management │ ├── member - Team member management │ └── notification - Deployment notifications -├── env - Environment management +├── env - Environment management (incl. clone) │ ├── var - Environment variables │ ├── redirect - URL redirects │ └── access-control - Access control headers -├── bundle - Bundle and deployment management +├── bundle - Bundle and deployment management (incl. delete) └── user - User profile and settings ``` @@ -49,6 +51,9 @@ b2c mrt env list -p my-storefront # Create a new environment b2c mrt env create qa -p my-storefront --name "QA Environment" +# Clone an existing environment (-e is the source; positional arg is the new slug) +b2c mrt env clone qa -p my-storefront -e staging --clone-redirects --clone-env-vars + # Get environment details b2c mrt env get -p my-storefront -e production @@ -80,6 +85,29 @@ b2c mrt bundle history -p my-storefront -e production # Download a bundle artifact b2c mrt bundle download 12345 -p my-storefront + +# Delete one or more bundles (uses bulk-delete for >1) +b2c mrt bundle delete 12345 -p my-storefront +b2c mrt bundle delete 12345 12346 12347 -p my-storefront --force +``` + +### Organization Members and Certificates + +Organization members are distinct from project members; they hold a role at the organization level. Custom-domain certificates are organization-scoped and referenced by `b2c mrt env create`/`update`/`clone` via `--certificate-id`. + +```bash +# List, add, update, remove organization members +b2c mrt org member list --org my-org +b2c mrt org member add alice@example.com --org my-org --role member --view-all-projects +b2c mrt org member update alice@example.com --org my-org --no-cert-permission +b2c mrt org member remove alice@example.com --org my-org + +# Manage custom domain certificates +b2c mrt org cert list --org my-org +b2c mrt org cert create shop.example.com --org my-org # output includes the DNS validation record +b2c mrt org cert get 123 --org my-org +b2c mrt org cert restart-validation 123 --org my-org +b2c mrt org cert delete 123 --org my-org ``` ### Project Management