Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions command-snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,23 @@
"flags": ["api-version", "flags-dir", "json", "loglevel", "package-id", "target-org"],
"plugin": "@salesforce/plugin-packaging"
},
{
"alias": [],
"command": "package:authorize:add",
"flagAliases": ["apiversion", "targetusername", "u"],
"flagChars": ["o", "p"],
"flags": [
"api-version",
"flags-dir",
"json",
"loglevel",
"package",
"subscriber-org",
"subscriber-org-file-list",
"target-org"
],
"plugin": "@salesforce/plugin-packaging"
},
{
"alias": [],
"command": "package:authorize:list",
Expand Down
49 changes: 49 additions & 0 deletions messages/package_authorize_add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# summary

Authorize subscriber orgs to install a package.

# description

Add subscriber org IDs to the authorization list. Optionally specify a package with --package to scope the authorization to that package.

# examples

- Authorize one subscriber org for a package:

<%= config.bin %> <%= command.id %> --package MyPackage --subscriber-org 00D5e000001CUST --target-org AuthoringOrg

- Authorize multiple subscriber orgs:

<%= config.bin %> <%= command.id %> --subscriber-org 00D5e000001CUST,00D5e000002CUST

- Authorize the subscriber org IDs in a file:

<%= config.bin %> <%= command.id %> --subscriber-org-file-list authorized-orgs.txt

# flags.package.summary

Optional ID or alias of the package to authorize.

# flags.subscriber-org.summary

One or more comma-separated subscriber org IDs to authorize.

# flags.subscriber-org-file-list.summary

Path to a file that contains one subscriber org ID per line.

# errorNoSubscriberOrgs

Provide at least one subscriber org ID.

# columns.subscriber-org

Subscriber Org

# columns.id

Authorization ID

# success

Successfully authorized %s subscriber org(s).
25 changes: 25 additions & 0 deletions schemas/package-authorize-add.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$ref": "#/definitions/PackageAuthorizeAddCommandResult",
"definitions": {
"PackageAuthorizeAddCommandResult": {
"type": "array",
"items": {
"$ref": "#/definitions/PackageAuthorizationAddResult"
}
},
"PackageAuthorizationAddResult": {
"type": "object",
"properties": {
"Id": {
"type": "string"
},
"SubscriberOrg": {
"type": "string"
}
},
"required": ["Id", "SubscriberOrg"],
"additionalProperties": false
}
}
}
86 changes: 86 additions & 0 deletions src/commands/package/authorize/add.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Copyright 2026, Salesforce, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Messages } from '@salesforce/core/messages';
import { PackageAuthorization, PackageAuthorizationAddResult } from '@salesforce/packaging';
import {
Flags,
loglevel,
orgApiVersionFlagWithDeprecations,
requiredOrgFlagWithDeprecations,
SfCommand,
} from '@salesforce/sf-plugins-core';
import { maybeGetProject } from '../../../utils/getProject.js';
import { resolveSubscriberPackageId } from '../../../utils/packageAuthorization.js';
import { parseSubscriberOrgFile, parseSubscriberOrgList } from '../../../utils/subscriberOrg.js';

Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/plugin-packaging', 'package_authorize_add');

export type PackageAuthorizeAddCommandResult = PackageAuthorizationAddResult[];

export class PackageAuthorizeAddCommand extends SfCommand<PackageAuthorizeAddCommandResult> {
public static readonly hidden = true;
public static state = 'beta';
public static readonly summary = messages.getMessage('summary');
public static readonly description = messages.getMessage('description');
public static readonly examples = messages.getMessages('examples');
public static readonly flags = {
loglevel,
'target-org': requiredOrgFlagWithDeprecations,
'api-version': orgApiVersionFlagWithDeprecations,
package: Flags.string({
char: 'p',
summary: messages.getMessage('flags.package.summary'),
}),
'subscriber-org': Flags.string({
summary: messages.getMessage('flags.subscriber-org.summary'),
exactlyOne: ['subscriber-org-file-list'],
}),
'subscriber-org-file-list': Flags.file({
summary: messages.getMessage('flags.subscriber-org-file-list.summary'),
exists: true,
exactlyOne: ['subscriber-org'],
}),
};

public async run(): Promise<PackageAuthorizeAddCommandResult> {
const { flags } = await this.parse(PackageAuthorizeAddCommand);
const connection = flags['target-org'].getConnection(flags['api-version']);
const project = flags.package ? await maybeGetProject() : undefined;
const subscriberPackageId = flags.package
? await resolveSubscriberPackageId({ packageAliasOrId: flags.package, connection, project })
: undefined;
const subscriberOrgs =
typeof flags['subscriber-org'] === 'string'
? parseSubscriberOrgList(flags['subscriber-org'])
: await parseSubscriberOrgFile(flags['subscriber-org-file-list']!);

if (subscriberOrgs.length === 0) {
throw messages.createError('errorNoSubscriberOrgs');
}

const results = await new PackageAuthorization({ connection, subscriberPackageId }).add(subscriberOrgs);
this.table({
data: results,
columns: [
{ key: 'SubscriberOrg', name: messages.getMessage('columns.subscriber-org') },
{ key: 'Id', name: messages.getMessage('columns.id') },
],
});
this.logSuccess(messages.getMessage('success', [results.length]));
return results;
}
}
27 changes: 27 additions & 0 deletions src/utils/subscriberOrg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright 2026, Salesforce, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { readFile } from 'node:fs/promises';

export const parseSubscriberOrgList = (subscriberOrgs: string): string[] =>
subscriberOrgs.split(',').map((subscriberOrg) => subscriberOrg.trim());

export const parseSubscriberOrgFile = async (filePath: string): Promise<string[]> => {
const contents = await readFile(filePath, 'utf8');
return contents
.split(/\r?\n/)
.map((line) => line.replace(/#.*/, '').trim())
.filter(Boolean);
};
172 changes: 172 additions & 0 deletions test/commands/package/authorize/add.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
/*
* Copyright 2026, Salesforce, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { Config } from '@oclif/core';
import { MockTestOrgData, TestContext } from '@salesforce/core/testSetup';
import { Package, PackageAuthorization } from '@salesforce/packaging';
import { stubSfCommandUx } from '@salesforce/sf-plugins-core';
import { expect } from 'chai';
import sinon from 'sinon';
import { PackageAuthorizeAddCommand } from '../../../../src/commands/package/authorize/add.js';

const subscriberOrg = '00D000000000001';
const secondSubscriberOrg = '00D000000000002';
const packageId = '0Ho000000000001';
const subscriberPackageId = '033000000000001';

describe('package:authorize:add', () => {
const $$ = new TestContext();
const testOrg = new MockTestOrgData();
const config = new Config({ root: import.meta.url });
let addStub: sinon.SinonStub;
let getSubscriberPackageIdStub: sinon.SinonStub;
let sfCommandStubs: ReturnType<typeof stubSfCommandUx>;
let tempDirectory: string | undefined;

beforeEach(async () => {
await $$.stubAuths(testOrg);
await config.load();
sfCommandStubs = stubSfCommandUx($$.SANDBOX);
addStub = $$.SANDBOX.stub(PackageAuthorization.prototype, 'add');
getSubscriberPackageIdStub = $$.SANDBOX.stub(Package.prototype, 'getSubscriberPackageId');
});

afterEach(async () => {
$$.restore();
if (tempDirectory) {
await rm(tempDirectory, { recursive: true, force: true });
tempDirectory = undefined;
}
});

it('adds comma-separated subscriber orgs without a package', async () => {
const expectedResults = [
{ Id: '2at000000000001', SubscriberOrg: subscriberOrg },
{ Id: '2at000000000002', SubscriberOrg: secondSubscriberOrg },
];
addStub.resolves(expectedResults);
const command = new PackageAuthorizeAddCommand(
[
'--target-org',
testOrg.username,
'--api-version',
'68.0',
'--subscriber-org',
`${subscriberOrg}, ${secondSubscriberOrg}`,
],
config
);

const result = await command.run();

expect(result).to.deep.equal(expectedResults);
expect(addStub.calledOnceWithExactly([subscriberOrg, secondSubscriberOrg])).to.equal(true);
expect(getSubscriberPackageIdStub.called).to.equal(false);
expect((addStub.thisValues[0] as unknown as { subscriberPackageId?: string }).subscriberPackageId).to.equal(
undefined
);
expect(sfCommandStubs.table.firstCall.args[0].data).to.deep.equal(expectedResults);
expect(sfCommandStubs.logSuccess.calledOnceWithExactly('Successfully authorized 2 subscriber org(s).')).to.equal(
true
);
});

it('resolves a package before adding subscriber orgs', async () => {
getSubscriberPackageIdStub.resolves(subscriberPackageId);
addStub.resolves([{ Id: '2at000000000001', SubscriberOrg: subscriberOrg }]);
const command = new PackageAuthorizeAddCommand(
[
'--target-org',
testOrg.username,
'--api-version',
'68.0',
'--package',
packageId,
'--subscriber-org',
subscriberOrg,
],
config
);

await command.run();

expect(getSubscriberPackageIdStub.calledOnce).to.equal(true);
expect((addStub.thisValues[0] as unknown as { subscriberPackageId?: string }).subscriberPackageId).to.equal(
subscriberPackageId
);
expect(addStub.calledOnceWithExactly([subscriberOrg])).to.equal(true);
});

it('adds subscriber orgs from a file', async () => {
tempDirectory = await mkdtemp(join(tmpdir(), 'package-authorize-add-'));
const subscriberOrgFile = join(tempDirectory, 'subscriber-orgs.txt');
await writeFile(
subscriberOrgFile,
`${subscriberOrg} # Customer A\n\n# comment\n${secondSubscriberOrg} # Customer B\n`,
'utf8'
);
addStub.resolves([
{ Id: '2at000000000001', SubscriberOrg: subscriberOrg },
{ Id: '2at000000000002', SubscriberOrg: secondSubscriberOrg },
]);
const command = new PackageAuthorizeAddCommand(
['--target-org', testOrg.username, '--api-version', '68.0', '--subscriber-org-file-list', subscriberOrgFile],
config
);

await command.run();

expect(addStub.calledOnceWithExactly([subscriberOrg, secondSubscriberOrg])).to.equal(true);
});

it('rejects a subscriber org file with no IDs', async () => {
tempDirectory = await mkdtemp(join(tmpdir(), 'package-authorize-add-'));
const subscriberOrgFile = join(tempDirectory, 'subscriber-orgs.txt');
await writeFile(subscriberOrgFile, '# no subscriber orgs\n\n', 'utf8');
const command = new PackageAuthorizeAddCommand(
['--target-org', testOrg.username, '--api-version', '68.0', '--subscriber-org-file-list', subscriberOrgFile],
config
);

try {
await command.run();
expect.fail('Expected a missing subscriber org error');
} catch (error) {
expect((error as Error).message).to.equal('Provide at least one subscriber org ID.');
}
expect(addStub.called).to.equal(false);
});

it('propagates authorization errors without success output', async () => {
const authorizationError = new Error('Tooling API create failed');
addStub.rejects(authorizationError);
const command = new PackageAuthorizeAddCommand(
['--target-org', testOrg.username, '--api-version', '68.0', '--subscriber-org', subscriberOrg],
config
);

try {
await command.run();
expect.fail('Expected the authorization error');
} catch (error) {
expect(error).to.equal(authorizationError);
}
expect(sfCommandStubs.table.called).to.equal(false);
expect(sfCommandStubs.logSuccess.called).to.equal(false);
});
});
Loading