From d9cc50ff10d0ced1bc3f1a1dd44f6e9da85200ae Mon Sep 17 00:00:00 2001 From: Sridhar Reddy Shyamala Date: Fri, 28 Aug 2026 03:10:12 +0000 Subject: [PATCH] feat: add sf package trust link status command (W-23970571) Add the hidden/beta `sf package trust link status` command that reports the connected authoring org's Public Secure (VerifiedDev) trust-link state: Not Linked, or the link's status (Pending/Accepted/Declined/Revoked/Failed) with the timestamps that are set. Thin wrapper over PackageTrustLink.status. Pins @salesforce/packaging 5.0.9-spi.1 (the prerelease that adds status). Co-Authored-By: Claude Opus 4.8 (1M context) --- command-snapshot.json | 20 +++ messages/package_trust_link_status.md | 35 ++++++ package.json | 2 +- schemas/package-trust-link-status.json | 34 ++++++ src/commands/package/trust/link/status.ts | 62 ++++++++++ .../package/packageTrustLinkStatus.test.ts | 115 ++++++++++++++++++ yarn.lock | 8 +- 7 files changed, 271 insertions(+), 5 deletions(-) create mode 100644 messages/package_trust_link_status.md create mode 100644 schemas/package-trust-link-status.json create mode 100644 src/commands/package/trust/link/status.ts create mode 100644 test/commands/package/packageTrustLinkStatus.test.ts diff --git a/command-snapshot.json b/command-snapshot.json index ffc7e2a7..78ccb7bf 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -753,6 +753,26 @@ ], "plugin": "@salesforce/plugin-packaging" }, + { + "alias": [], + "command": "package:trust:link:status", + "flagAliases": [ + "apiversion", + "targetusername", + "u" + ], + "flagChars": [ + "o" + ], + "flags": [ + "api-version", + "flags-dir", + "json", + "loglevel", + "target-org" + ], + "plugin": "@salesforce/plugin-packaging" + }, { "alias": [ "force:package:uninstall" diff --git a/messages/package_trust_link_status.md b/messages/package_trust_link_status.md new file mode 100644 index 00000000..f552829e --- /dev/null +++ b/messages/package_trust_link_status.md @@ -0,0 +1,35 @@ +# summary + +Show your authoring org's Public Secure trust link state with a Verified Partner Business Org (PBO). + +# description + +Reports the current state of the Public Secure (VerifiedDev) trust link on your connected authoring org: Not Linked when no link exists, or one of Pending, Accepted, Declined, Revoked, or Failed when a link does, along with the relevant timestamps. + +Run this command against your connected authoring org—the 1GP namespace org or the 2GP Dev Hub. An authoring org holds at most one trust link at a time, so this reports that single link, if any. This command is read-only and makes no changes; it doesn't change any package's distribution type. + +# examples + +- Show the trust link state on your authoring org: + + <%= config.bin %> <%= command.id %> --target-org myAuthoringOrg + +# output.notLinked + +This org has no Public Secure trust link. It's Not Linked. + +# output.status + +Trust link status: %s (Verified Org %s). + +# output.requested + +Requested: %s + +# output.established + +Established: %s + +# output.revoked + +Revoked: %s diff --git a/package.json b/package.json index 7773cb92..d5bf9643 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "@oclif/core": "^4", "@salesforce/core": "^9.1.0", "@salesforce/kit": "^4.0.0", - "@salesforce/packaging": "5.0.9-spi-dev.0", + "@salesforce/packaging": "5.0.9-spi.1", "@salesforce/sf-plugins-core": "^13.0.0", "@salesforce/ts-types": "^3.0.1", "chalk": "^5.6.2" diff --git a/schemas/package-trust-link-status.json b/schemas/package-trust-link-status.json new file mode 100644 index 00000000..676da6d2 --- /dev/null +++ b/schemas/package-trust-link-status.json @@ -0,0 +1,34 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/PackageTrustLinkStatusResult", + "definitions": { + "PackageTrustLinkStatusResult": { + "type": "object", + "properties": { + "Status": { + "type": "string" + }, + "linked": { + "type": "boolean" + }, + "LinkRequestId": { + "type": "string" + }, + "VerifiedOrgId": { + "type": "string" + }, + "RequestedDate": { + "type": "string" + }, + "EstablishedDate": { + "type": "string" + }, + "RevokedDate": { + "type": "string" + } + }, + "required": ["Status", "linked"], + "additionalProperties": false + } + } +} diff --git a/src/commands/package/trust/link/status.ts b/src/commands/package/trust/link/status.ts new file mode 100644 index 00000000..44351aec --- /dev/null +++ b/src/commands/package/trust/link/status.ts @@ -0,0 +1,62 @@ +/* + * 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 { + loglevel, + orgApiVersionFlagWithDeprecations, + requiredOrgFlagWithDeprecations, + SfCommand, +} from '@salesforce/sf-plugins-core'; +import { Messages } from '@salesforce/core'; +import { PackageTrustLink, PackageTrustLinkStatusResult } from '@salesforce/packaging'; + +Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); +const messages = Messages.loadMessages('@salesforce/plugin-packaging', 'package_trust_link_status'); + +export class PackageTrustLinkStatusCommand extends SfCommand { + 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, + }; + + public async run(): Promise { + const { flags } = await this.parse(PackageTrustLinkStatusCommand); + const connection = flags['target-org'].getConnection(flags['api-version']); + + const result = await PackageTrustLink.status(connection); + + if (!result.linked) { + this.log(messages.getMessage('output.notLinked')); + return result; + } + + this.log(messages.getMessage('output.status', [result.Status, result.VerifiedOrgId])); + const timestamps = [ + result.RequestedDate ? messages.getMessage('output.requested', [result.RequestedDate]) : undefined, + result.EstablishedDate ? messages.getMessage('output.established', [result.EstablishedDate]) : undefined, + result.RevokedDate ? messages.getMessage('output.revoked', [result.RevokedDate]) : undefined, + ].filter((line): line is string => line !== undefined); + timestamps.forEach((line) => this.log(line)); + + return result; + } +} diff --git a/test/commands/package/packageTrustLinkStatus.test.ts b/test/commands/package/packageTrustLinkStatus.test.ts new file mode 100644 index 00000000..4d1a78b5 --- /dev/null +++ b/test/commands/package/packageTrustLinkStatus.test.ts @@ -0,0 +1,115 @@ +/* + * 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 { Config } from '@oclif/core'; +import { TestContext, MockTestOrgData } from '@salesforce/core/testSetup'; +import * as sinon from 'sinon'; +import { expect } from 'chai'; +import { PackageTrustLink, PackageTrustLinkStatusResult } from '@salesforce/packaging'; +import { stubSfCommandUx } from '@salesforce/sf-plugins-core'; +import { PackageTrustLinkStatusCommand } from '../../../src/commands/package/trust/link/status.js'; + +const verifiedOrgId = '00Dxx0000001gPL'; +const linkedResult: PackageTrustLinkStatusResult = { + Status: 'Accepted', + linked: true, + LinkRequestId: '2vtxx0000000001AAA', + VerifiedOrgId: verifiedOrgId, + RequestedDate: '2026-08-19T09:00:00.000+0000', + EstablishedDate: '2026-08-20T10:00:00.000+0000', +}; + +describe('package:trust:link:status - tests', () => { + const $$ = new TestContext(); + const testOrg = new MockTestOrgData(); + let sfCommandStubs: ReturnType; + let statusStub: sinon.SinonStub; + const config = new Config({ root: import.meta.url }); + + beforeEach(async () => { + await $$.stubAuths(testOrg); + await config.load(); + sfCommandStubs = stubSfCommandUx($$.SANDBOX); + statusStub = $$.SANDBOX.stub(PackageTrustLink, 'status'); + }); + + afterEach(() => { + $$.restore(); + }); + + it('reports an existing link and returns the result', async () => { + const cmdArgs = ['--target-org', testOrg.username]; + const cmd = new PackageTrustLinkStatusCommand(cmdArgs, config); + + statusStub.resolves(linkedResult); + const result = await cmd.run(); + + expect(result).to.deep.equal(linkedResult); + expect(statusStub.calledOnce).to.be.true; + // status line + requested + established timestamps + expect(sfCommandStubs.log.callCount).to.equal(3); + const statusLine = sfCommandStubs.log.firstCall.args[0]; + expect(statusLine).to.contain('Accepted'); + expect(statusLine).to.contain(verifiedOrgId); + }); + + it('reports the Not Linked state when the org has no link', async () => { + const cmdArgs = ['-o', testOrg.username]; + const cmd = new PackageTrustLinkStatusCommand(cmdArgs, config); + + const notLinked: PackageTrustLinkStatusResult = { Status: 'Not Linked', linked: false }; + statusStub.resolves(notLinked); + const result = await cmd.run(); + + expect(result).to.deep.equal(notLinked); + expect(sfCommandStubs.log.calledOnce).to.be.true; + expect(sfCommandStubs.log.firstCall.args[0]).to.contain('Not Linked'); + }); + + it('logs only the timestamps that are set (Pending link, no established/revoked)', async () => { + const cmdArgs = ['--target-org', testOrg.username]; + const cmd = new PackageTrustLinkStatusCommand(cmdArgs, config); + + statusStub.resolves({ + Status: 'Pending', + linked: true, + LinkRequestId: '2vtxx0000000001AAA', + VerifiedOrgId: verifiedOrgId, + RequestedDate: '2026-08-19T09:00:00.000+0000', + } satisfies PackageTrustLinkStatusResult); + await cmd.run(); + + // status line + requested only — no established, no revoked + expect(sfCommandStubs.log.callCount).to.equal(2); + const lines = sfCommandStubs.log.getCalls().map((c) => c.args[0] as string); + expect(lines.some((l) => l.includes('Pending'))).to.be.true; + expect(lines.some((l) => l.includes('2026-08-19'))).to.be.true; + }); + + it('surfaces errors thrown by the library', async () => { + const cmdArgs = ['--target-org', testOrg.username]; + const cmd = new PackageTrustLinkStatusCommand(cmdArgs, config); + + statusStub.rejects(new Error('INSUFFICIENT_ACCESS')); + + try { + await cmd.run(); + expect.fail('expected the library error to propagate'); + } catch (err) { + expect((err as Error).message).to.contain('INSUFFICIENT_ACCESS'); + } + expect(sfCommandStubs.log.called).to.be.false; + }); +}); diff --git a/yarn.lock b/yarn.lock index d068acb8..9cb43bc1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1368,10 +1368,10 @@ dependencies: "@salesforce/ts-types" "^3.0.0" -"@salesforce/packaging@5.0.9-spi-dev.0": - version "5.0.9-spi-dev.0" - resolved "https://registry.npmjs.org/@salesforce/packaging/-/packaging-5.0.9-spi-dev.0.tgz#987b0c83de227f76b2b28d3dfd7ba116bbde7961" - integrity sha512-YQFelO233SybpTTIrNneEEUADf06sWZFh7PX9k/EbaUJrSDm79Evxjbah+wK7yzacQwXa1croo2v6sVbAti6xw== +"@salesforce/packaging@5.0.9-spi.1": + version "5.0.9-spi.1" + resolved "https://registry.npmjs.org/@salesforce/packaging/-/packaging-5.0.9-spi.1.tgz#b83c71fee46716e3440380ba1aaf2b0e4cab4198" + integrity sha512-SBuo7tC9MHrHOn58+FKRUnKj632/dnLiZgrMZevYLDzpnf+3Ofm1n3+6/05l1lGxcis97mzXcz672zlMBwoLnA== dependencies: "@jsforce/jsforce-node" "^3.10.19" "@salesforce/core" "^9.0.0"