From 2e65ee78530b656ed7394b68841724c61fdd31e3 Mon Sep 17 00:00:00 2001 From: "b.tran" Date: Wed, 26 Aug 2026 10:24:32 -0400 Subject: [PATCH 1/5] feat: add sf package link list for Public Secure PBO requests Add the PBO-admin list command on top of PackageLink.list so verified orgs can inspect inbound VerifiedDev trust requests. --- command-snapshot.json | 8 ++ messages/package_link_list.md | 33 ++++++++ package.json | 5 +- schemas/package-link-list.json | 57 +++++++++++++ src/commands/package/link/list.ts | 72 +++++++++++++++++ test/commands/package/packageLinkList.test.ts | 80 +++++++++++++++++++ 6 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 messages/package_link_list.md create mode 100644 schemas/package-link-list.json create mode 100644 src/commands/package/link/list.ts create mode 100644 test/commands/package/packageLinkList.test.ts diff --git a/command-snapshot.json b/command-snapshot.json index 6a908640..3540a861 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -299,6 +299,14 @@ "flags": ["api-version", "flags-dir", "json", "loglevel", "target-dev-hub", "verbose"], "plugin": "@salesforce/plugin-packaging" }, + { + "alias": [], + "command": "package:link:list", + "flagAliases": ["apiversion", "targetusername", "u"], + "flagChars": ["o", "s"], + "flags": ["api-version", "flags-dir", "json", "loglevel", "status", "target-org"], + "plugin": "@salesforce/plugin-packaging" + }, { "alias": [], "command": "package:push-upgrade:abort", diff --git a/messages/package_link_list.md b/messages/package_link_list.md new file mode 100644 index 00000000..65376d05 --- /dev/null +++ b/messages/package_link_list.md @@ -0,0 +1,33 @@ +# summary + +List Public Secure (VerifiedDev) trust link requests for this verified org. + +# description + +Run this command against a verified packaging org (PBO). It lists inbound trust requests from authoring orgs (1GP namespace orgs or 2GP Dev Hubs). + +Results include the request ID, requesting user, authoring org ID, status, and request date. Use --status to filter. Status "approved" maps to an Accepted trust. + +Authoring org name and that org's packages are not returned by the Tooling API for this entity; use the request ID with approve, deny, or revoke. + +# examples + +- List all inbound Public Secure link requests in the target verified org: + + <%= config.bin %> <%= command.id %> --target-org pbo@example.com + +- List only pending requests: + + <%= config.bin %> <%= command.id %> --target-org pbo@example.com --status pending + +- List accepted (approved) links as JSON: + + <%= config.bin %> <%= command.id %> --target-org pbo@example.com --status approved --json + +# flags.status.summary + +Filter results by request status: pending, approved, declined, or revoked. + +# flags.status.description + +"approved" selects Accepted records. Failed requests are included only when this flag is omitted. diff --git a/package.json b/package.json index 0f217430..e87597c0 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.4", + "@salesforce/packaging": "^5.1.0", "@salesforce/sf-plugins-core": "^13.0.0", "@salesforce/ts-types": "^3.0.1", "chalk": "^5.6.2" @@ -109,6 +109,9 @@ }, "installed": { "description": "Command to list installed packages." + }, + "link": { + "description": "Commands to manage Public Secure (VerifiedDev) package trust links." } } } diff --git a/schemas/package-link-list.json b/schemas/package-link-list.json new file mode 100644 index 00000000..4f9a0186 --- /dev/null +++ b/schemas/package-link-list.json @@ -0,0 +1,57 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/PackageLinkListCommandResult", + "definitions": { + "PackageLinkListCommandResult": { + "type": "array", + "items": { + "$ref": "#/definitions/PackageLinkRecord" + } + }, + "PackageLinkRecord": { + "type": "object", + "properties": { + "Id": { + "type": "string" + }, + "AuthoringOrg": { + "type": "string" + }, + "VerifiedOrg": { + "type": "string" + }, + "Status": { + "$ref": "#/definitions/PackageLinkStatus" + }, + "RequestedBy": { + "type": ["string", "null"] + }, + "CreatedDate": { + "type": "string" + }, + "EstablishedDate": { + "type": ["string", "null"] + }, + "RevokedDate": { + "type": ["string", "null"] + } + }, + "required": [ + "Id", + "AuthoringOrg", + "VerifiedOrg", + "Status", + "RequestedBy", + "CreatedDate", + "EstablishedDate", + "RevokedDate" + ], + "additionalProperties": false + }, + "PackageLinkStatus": { + "type": "string", + "enum": ["Pending", "Accepted", "Declined", "Revoked", "Failed"], + "description": "Tooling API values for PkgVrfyAuthOrgTrustRela.Status" + } + } +} diff --git a/src/commands/package/link/list.ts b/src/commands/package/link/list.ts new file mode 100644 index 00000000..963ece88 --- /dev/null +++ b/src/commands/package/link/list.ts @@ -0,0 +1,72 @@ +/* + * 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 { + Flags, + loglevel, + orgApiVersionFlagWithDeprecations, + requiredOrgFlagWithDeprecations, + SfCommand, +} from '@salesforce/sf-plugins-core'; +import { Messages } from '@salesforce/core/messages'; +import { PackageLink, PackageLinkListStatusFilter, PackageLinkRecord } from '@salesforce/packaging'; + +Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); +const messages = Messages.loadMessages('@salesforce/plugin-packaging', 'package_link_list'); + +export type PackageLinkListCommandResult = PackageLinkRecord[]; + +export class PackageLinkListCommand extends SfCommand { + 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, + status: Flags.custom({ + options: ['pending', 'approved', 'declined', 'revoked'], + })({ + char: 's', + summary: messages.getMessage('flags.status.summary'), + description: messages.getMessage('flags.status.description'), + }), + }; + + public async run(): Promise { + const { flags } = await this.parse(PackageLinkListCommand); + const connection = flags['target-org'].getConnection(flags['api-version']); + const results = await new PackageLink({ connection }).list(flags.status); + + if (results.length === 0) { + this.warn('No results found'); + } else { + this.table({ + data: results.map((r) => ({ + Id: r.Id, + 'Requested By': r.RequestedBy ?? '', + 'Authoring Org': r.AuthoringOrg, + Status: r.Status, + 'Request Date': r.CreatedDate, + })), + title: `Link Requests [${results.length}]`, + overflow: 'wrap', + }); + } + + return results; + } +} diff --git a/test/commands/package/packageLinkList.test.ts b/test/commands/package/packageLinkList.test.ts new file mode 100644 index 00000000..5231818f --- /dev/null +++ b/test/commands/package/packageLinkList.test.ts @@ -0,0 +1,80 @@ +/* + * 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 { PackageLink, PackageLinkRecord } from '@salesforce/packaging'; +import { stubSfCommandUx } from '@salesforce/sf-plugins-core'; +import { PackageLinkListCommand } from '../../../src/commands/package/link/list.js'; + +const linkListSuccess: PackageLinkRecord[] = [ + { + Id: '2vt000000000001AAA', + AuthoringOrg: '00D000000000002', + VerifiedOrg: '00D000000000001', + Status: 'Pending', + RequestedBy: 'Ada Lovelace', + CreatedDate: '2026-08-24T00:00:00.000Z', + EstablishedDate: null, + RevokedDate: null, + }, +]; + +describe('package:link:list - tests', () => { + const $$ = new TestContext(); + const testOrg = new MockTestOrgData(); + let sfCommandStubs: ReturnType; + let listStub: sinon.SinonStub; + const config = new Config({ root: import.meta.url }); + + beforeEach(async () => { + await $$.stubAuths(testOrg); + await config.load(); + sfCommandStubs = stubSfCommandUx($$.SANDBOX); + listStub = $$.SANDBOX.stub(PackageLink.prototype, 'list').resolves(linkListSuccess); + }); + + afterEach(() => { + $$.restore(); + }); + + it('lists inbound Public Secure link requests', async () => { + const cmd = new PackageLinkListCommand(['-o', testOrg.username, '--api-version', '68.0'], config); + const result = await cmd.run(); + + expect(result).to.deep.equal(linkListSuccess); + expect(listStub.calledOnceWithExactly(undefined)).to.equal(true); + expect(sfCommandStubs.table.called).to.equal(true); + }); + + it('passes the status filter to PackageLink.list', async () => { + const cmd = new PackageLinkListCommand( + ['-o', testOrg.username, '--api-version', '68.0', '--status', 'pending'], + config + ); + await cmd.run(); + expect(listStub.calledOnceWithExactly('pending')).to.equal(true); + }); + + it('warns when there are no results', async () => { + listStub.resolves([]); + const cmd = new PackageLinkListCommand(['-o', testOrg.username, '--api-version', '68.0'], config); + const result = await cmd.run(); + expect(result).to.deep.equal([]); + expect(sfCommandStubs.warn.called).to.equal(true); + }); +}); From c1bc3b5ccb2dcb9ec5ea6be44f48caedcf4d7c13 Mon Sep 17 00:00:00 2001 From: "b.tran" Date: Wed, 26 Aug 2026 10:56:50 -0400 Subject: [PATCH 2/5] fix: decouple link list from unreleased packaging version Keep the plugin installable from npm while retaining the same Tooling query behind an internal service until the shared library release is available. --- messages/package_link.md | 7 ++ package.json | 2 +- schemas/package-link-list.json | 3 +- src/commands/package/link/list.ts | 11 ++- src/utils/packageLink.ts | 85 ++++++++++++++++ test/commands/package/packageLinkList.test.ts | 4 +- test/utils/packageLink.test.ts | 99 +++++++++++++++++++ 7 files changed, 203 insertions(+), 8 deletions(-) create mode 100644 messages/package_link.md create mode 100644 src/utils/packageLink.ts create mode 100644 test/utils/packageLink.test.ts diff --git a/messages/package_link.md b/messages/package_link.md new file mode 100644 index 00000000..07542079 --- /dev/null +++ b/messages/package_link.md @@ -0,0 +1,7 @@ +# apiVersionTooLow + +Package link requires API version %s or later. + +# missingOrgId + +Unable to determine the target org ID from the current connection. diff --git a/package.json b/package.json index e87597c0..f4aef598 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.1.0", + "@salesforce/packaging": "^5.0.4", "@salesforce/sf-plugins-core": "^13.0.0", "@salesforce/ts-types": "^3.0.1", "chalk": "^5.6.2" diff --git a/schemas/package-link-list.json b/schemas/package-link-list.json index 4f9a0186..81d6aa8f 100644 --- a/schemas/package-link-list.json +++ b/schemas/package-link-list.json @@ -50,8 +50,7 @@ }, "PackageLinkStatus": { "type": "string", - "enum": ["Pending", "Accepted", "Declined", "Revoked", "Failed"], - "description": "Tooling API values for PkgVrfyAuthOrgTrustRela.Status" + "enum": ["Pending", "Accepted", "Declined", "Revoked", "Failed"] } } } diff --git a/src/commands/package/link/list.ts b/src/commands/package/link/list.ts index 963ece88..054c0325 100644 --- a/src/commands/package/link/list.ts +++ b/src/commands/package/link/list.ts @@ -22,7 +22,12 @@ import { SfCommand, } from '@salesforce/sf-plugins-core'; import { Messages } from '@salesforce/core/messages'; -import { PackageLink, PackageLinkListStatusFilter, PackageLinkRecord } from '@salesforce/packaging'; +import { + PACKAGE_LINK_LIST_STATUS_OPTIONS, + PackageLinkListStatusFilter, + PackageLinkRecord, + PackageLinkService, +} from '../../../utils/packageLink.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-packaging', 'package_link_list'); @@ -38,7 +43,7 @@ export class PackageLinkListCommand extends SfCommand({ - options: ['pending', 'approved', 'declined', 'revoked'], + options: PACKAGE_LINK_LIST_STATUS_OPTIONS, })({ char: 's', summary: messages.getMessage('flags.status.summary'), @@ -49,7 +54,7 @@ export class PackageLinkListCommand extends SfCommand { const { flags } = await this.parse(PackageLinkListCommand); const connection = flags['target-org'].getConnection(flags['api-version']); - const results = await new PackageLink({ connection }).list(flags.status); + const results = await new PackageLinkService(connection).list(flags.status); if (results.length === 0) { this.warn('No results found'); diff --git a/src/utils/packageLink.ts b/src/utils/packageLink.ts new file mode 100644 index 00000000..338afcba --- /dev/null +++ b/src/utils/packageLink.ts @@ -0,0 +1,85 @@ +/* + * 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 type { Schema } from '@jsforce/jsforce-node'; +import { Connection, Messages, trimTo15 } from '@salesforce/core'; + +const PACKAGE_LINK_SOBJECT = 'PkgVrfyAuthOrgTrustRela'; +const MINIMUM_API_VERSION = '68.0'; +const ORGANIZATION_TYPE_VERIFIED = 'Verified'; + +export type PackageLinkListStatusFilter = 'pending' | 'approved' | 'declined' | 'revoked'; +export type PackageLinkStatus = 'Pending' | 'Accepted' | 'Declined' | 'Revoked' | 'Failed'; + +export type PackageLinkRecord = { + Id: string; + AuthoringOrg: string; + VerifiedOrg: string; + Status: PackageLinkStatus; + RequestedBy: string | null; + CreatedDate: string; + EstablishedDate: string | null; + RevokedDate: string | null; +}; + +const STATUS_FILTER_TO_API: Record = { + pending: 'Pending', + approved: 'Accepted', + declined: 'Declined', + revoked: 'Revoked', +}; +export const PACKAGE_LINK_LIST_STATUS_OPTIONS = Object.keys(STATUS_FILTER_TO_API) as PackageLinkListStatusFilter[]; + +type PackageLinkQueryRecord = PackageLinkRecord & Schema; + +Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); +const messages = Messages.loadMessages('@salesforce/plugin-packaging', 'package_link'); + +export class PackageLinkService { + private readonly verifiedOrgId: string; + + public constructor(private readonly connection: Connection) { + if (!(Number(connection.getApiVersion()) >= Number(MINIMUM_API_VERSION))) { + throw messages.createError('apiVersionTooLow', [MINIMUM_API_VERSION]); + } + const orgId = connection.getAuthInfoFields()?.orgId; + if (!orgId) { + throw messages.createError('missingOrgId'); + } + this.verifiedOrgId = trimTo15(orgId); + } + + public async list(status?: PackageLinkListStatusFilter): Promise { + const statusClause = status ? ` AND Status = '${STATUS_FILTER_TO_API[status]}'` : ''; + const query = + 'SELECT Id, AuthoringOrg, VerifiedOrg, Status, RequestedBy, CreatedDate, EstablishedDate, RevokedDate FROM ' + + `${PACKAGE_LINK_SOBJECT} WHERE VerifiedOrg = '${this.verifiedOrgId}' AND OrganizationType = '${ORGANIZATION_TYPE_VERIFIED}' ` + + `AND AuthoringOrg != '${this.verifiedOrgId}'${statusClause} ORDER BY CreatedDate DESC`; + + const result = await this.connection.autoFetchQuery(query, { tooling: true }); + return (result.records ?? []).map( + ({ Id, AuthoringOrg, VerifiedOrg, Status, RequestedBy, CreatedDate, EstablishedDate, RevokedDate }) => ({ + Id, + AuthoringOrg, + VerifiedOrg, + Status, + RequestedBy: RequestedBy ?? null, + CreatedDate, + EstablishedDate: EstablishedDate ?? null, + RevokedDate: RevokedDate ?? null, + }) + ); + } +} diff --git a/test/commands/package/packageLinkList.test.ts b/test/commands/package/packageLinkList.test.ts index 5231818f..e29177fe 100644 --- a/test/commands/package/packageLinkList.test.ts +++ b/test/commands/package/packageLinkList.test.ts @@ -17,9 +17,9 @@ import { Config } from '@oclif/core'; import { TestContext, MockTestOrgData } from '@salesforce/core/testSetup'; import * as sinon from 'sinon'; import { expect } from 'chai'; -import { PackageLink, PackageLinkRecord } from '@salesforce/packaging'; import { stubSfCommandUx } from '@salesforce/sf-plugins-core'; import { PackageLinkListCommand } from '../../../src/commands/package/link/list.js'; +import { PackageLinkRecord, PackageLinkService } from '../../../src/utils/packageLink.js'; const linkListSuccess: PackageLinkRecord[] = [ { @@ -45,7 +45,7 @@ describe('package:link:list - tests', () => { await $$.stubAuths(testOrg); await config.load(); sfCommandStubs = stubSfCommandUx($$.SANDBOX); - listStub = $$.SANDBOX.stub(PackageLink.prototype, 'list').resolves(linkListSuccess); + listStub = $$.SANDBOX.stub(PackageLinkService.prototype, 'list').resolves(linkListSuccess); }); afterEach(() => { diff --git a/test/utils/packageLink.test.ts b/test/utils/packageLink.test.ts new file mode 100644 index 00000000..4730a50e --- /dev/null +++ b/test/utils/packageLink.test.ts @@ -0,0 +1,99 @@ +/* + * 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 { Connection } from '@salesforce/core'; +import { expect } from 'chai'; +import sinon from 'sinon'; +import { PackageLinkService } from '../../src/utils/packageLink.js'; + +const verifiedOrg15 = '00D000000000001'; +const verifiedOrg18 = '00D000000000001EAA'; + +const createConnection = ({ + autoFetchQuery = sinon.stub().resolves({ records: [] }), + apiVersion = '68.0', + orgId = verifiedOrg18, +}: { + autoFetchQuery?: sinon.SinonStub; + apiVersion?: string; + orgId?: string; +} = {}) => + ({ + autoFetchQuery, + getApiVersion: sinon.stub().returns(apiVersion), + getAuthInfoFields: sinon.stub().returns({ orgId }), + } as unknown as Connection); + +describe('PackageLinkService', () => { + it('queries inbound VerifiedDev requests and excludes self-trust', async () => { + const autoFetchQuery = sinon.stub().resolves({ + records: [ + { + Id: '2vt000000000001AAA', + AuthoringOrg: '00D000000000002', + VerifiedOrg: verifiedOrg15, + Status: 'Pending', + RequestedBy: 'Ada Lovelace', + CreatedDate: '2026-08-24T00:00:00.000Z', + EstablishedDate: null, + RevokedDate: null, + attributes: { type: 'PkgVrfyAuthOrgTrustRela' }, + }, + ], + }); + + const result = await new PackageLinkService(createConnection({ autoFetchQuery })).list(); + + expect(result).to.deep.equal([ + { + Id: '2vt000000000001AAA', + AuthoringOrg: '00D000000000002', + VerifiedOrg: verifiedOrg15, + Status: 'Pending', + RequestedBy: 'Ada Lovelace', + CreatedDate: '2026-08-24T00:00:00.000Z', + EstablishedDate: null, + RevokedDate: null, + }, + ]); + expect(autoFetchQuery.calledOnce).to.equal(true); + expect(autoFetchQuery.firstCall.args[0]).to.equal( + "SELECT Id, AuthoringOrg, VerifiedOrg, Status, RequestedBy, CreatedDate, EstablishedDate, RevokedDate FROM PkgVrfyAuthOrgTrustRela WHERE VerifiedOrg = '00D000000000001' AND OrganizationType = 'Verified' AND AuthoringOrg != '00D000000000001' ORDER BY CreatedDate DESC" + ); + expect(autoFetchQuery.firstCall.args[1]).to.deep.equal({ tooling: true }); + }); + + it('maps approved to the Accepted API status', async () => { + const autoFetchQuery = sinon.stub().resolves({ records: [] }); + await new PackageLinkService(createConnection({ autoFetchQuery })).list('approved'); + expect(autoFetchQuery.firstCall.args[0]).to.contain("AND Status = 'Accepted'"); + }); + + it('compares API versions numerically', () => { + expect(() => new PackageLinkService(createConnection({ apiVersion: '100.0' }))).not.to.throw(); + expect(() => new PackageLinkService(createConnection({ apiVersion: '67.0' }))).to.throw( + 'Package link requires API version 68.0 or later.' + ); + expect(() => new PackageLinkService(createConnection({ apiVersion: 'invalid' }))).to.throw( + 'Package link requires API version 68.0 or later.' + ); + }); + + it('requires an org ID', () => { + expect(() => new PackageLinkService(createConnection({ orgId: '' }))).to.throw( + 'Unable to determine the target org ID' + ); + }); +}); From ea5c6d7499fb9987037b3702cf867c72397e4167 Mon Sep 17 00:00:00 2001 From: "b.tran" Date: Wed, 26 Aug 2026 17:41:53 -0400 Subject: [PATCH 3/5] feat: call PackageTrustLink.list from the packaging library Drop the in-plugin Tooling duplicate now that list lives on the 918 class. --- messages/package_link.md | 7 -- src/commands/package/link/list.ts | 17 ++-- src/utils/packageLink.ts | 85 ---------------- test/commands/package/packageLinkList.test.ts | 16 +-- test/utils/packageLink.test.ts | 99 ------------------- 5 files changed, 16 insertions(+), 208 deletions(-) delete mode 100644 messages/package_link.md delete mode 100644 src/utils/packageLink.ts delete mode 100644 test/utils/packageLink.test.ts diff --git a/messages/package_link.md b/messages/package_link.md deleted file mode 100644 index 07542079..00000000 --- a/messages/package_link.md +++ /dev/null @@ -1,7 +0,0 @@ -# apiVersionTooLow - -Package link requires API version %s or later. - -# missingOrgId - -Unable to determine the target org ID from the current connection. diff --git a/src/commands/package/link/list.ts b/src/commands/package/link/list.ts index 054c0325..0ba6d12e 100644 --- a/src/commands/package/link/list.ts +++ b/src/commands/package/link/list.ts @@ -22,17 +22,14 @@ import { SfCommand, } from '@salesforce/sf-plugins-core'; import { Messages } from '@salesforce/core/messages'; -import { - PACKAGE_LINK_LIST_STATUS_OPTIONS, - PackageLinkListStatusFilter, - PackageLinkRecord, - PackageLinkService, -} from '../../../utils/packageLink.js'; +import { PackageTrustLink, PackageTrustLinkListStatusFilter, PackageTrustLinkRecord } from '@salesforce/packaging'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-packaging', 'package_link_list'); -export type PackageLinkListCommandResult = PackageLinkRecord[]; +const STATUS_OPTIONS: PackageTrustLinkListStatusFilter[] = ['pending', 'approved', 'declined', 'revoked']; + +export type PackageLinkListCommandResult = PackageTrustLinkRecord[]; export class PackageLinkListCommand extends SfCommand { public static readonly summary = messages.getMessage('summary'); @@ -42,8 +39,8 @@ export class PackageLinkListCommand extends SfCommand({ - options: PACKAGE_LINK_LIST_STATUS_OPTIONS, + status: Flags.custom({ + options: STATUS_OPTIONS, })({ char: 's', summary: messages.getMessage('flags.status.summary'), @@ -54,7 +51,7 @@ export class PackageLinkListCommand extends SfCommand { const { flags } = await this.parse(PackageLinkListCommand); const connection = flags['target-org'].getConnection(flags['api-version']); - const results = await new PackageLinkService(connection).list(flags.status); + const results = await PackageTrustLink.list(connection, flags.status); if (results.length === 0) { this.warn('No results found'); diff --git a/src/utils/packageLink.ts b/src/utils/packageLink.ts deleted file mode 100644 index 338afcba..00000000 --- a/src/utils/packageLink.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - * 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 type { Schema } from '@jsforce/jsforce-node'; -import { Connection, Messages, trimTo15 } from '@salesforce/core'; - -const PACKAGE_LINK_SOBJECT = 'PkgVrfyAuthOrgTrustRela'; -const MINIMUM_API_VERSION = '68.0'; -const ORGANIZATION_TYPE_VERIFIED = 'Verified'; - -export type PackageLinkListStatusFilter = 'pending' | 'approved' | 'declined' | 'revoked'; -export type PackageLinkStatus = 'Pending' | 'Accepted' | 'Declined' | 'Revoked' | 'Failed'; - -export type PackageLinkRecord = { - Id: string; - AuthoringOrg: string; - VerifiedOrg: string; - Status: PackageLinkStatus; - RequestedBy: string | null; - CreatedDate: string; - EstablishedDate: string | null; - RevokedDate: string | null; -}; - -const STATUS_FILTER_TO_API: Record = { - pending: 'Pending', - approved: 'Accepted', - declined: 'Declined', - revoked: 'Revoked', -}; -export const PACKAGE_LINK_LIST_STATUS_OPTIONS = Object.keys(STATUS_FILTER_TO_API) as PackageLinkListStatusFilter[]; - -type PackageLinkQueryRecord = PackageLinkRecord & Schema; - -Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); -const messages = Messages.loadMessages('@salesforce/plugin-packaging', 'package_link'); - -export class PackageLinkService { - private readonly verifiedOrgId: string; - - public constructor(private readonly connection: Connection) { - if (!(Number(connection.getApiVersion()) >= Number(MINIMUM_API_VERSION))) { - throw messages.createError('apiVersionTooLow', [MINIMUM_API_VERSION]); - } - const orgId = connection.getAuthInfoFields()?.orgId; - if (!orgId) { - throw messages.createError('missingOrgId'); - } - this.verifiedOrgId = trimTo15(orgId); - } - - public async list(status?: PackageLinkListStatusFilter): Promise { - const statusClause = status ? ` AND Status = '${STATUS_FILTER_TO_API[status]}'` : ''; - const query = - 'SELECT Id, AuthoringOrg, VerifiedOrg, Status, RequestedBy, CreatedDate, EstablishedDate, RevokedDate FROM ' + - `${PACKAGE_LINK_SOBJECT} WHERE VerifiedOrg = '${this.verifiedOrgId}' AND OrganizationType = '${ORGANIZATION_TYPE_VERIFIED}' ` + - `AND AuthoringOrg != '${this.verifiedOrgId}'${statusClause} ORDER BY CreatedDate DESC`; - - const result = await this.connection.autoFetchQuery(query, { tooling: true }); - return (result.records ?? []).map( - ({ Id, AuthoringOrg, VerifiedOrg, Status, RequestedBy, CreatedDate, EstablishedDate, RevokedDate }) => ({ - Id, - AuthoringOrg, - VerifiedOrg, - Status, - RequestedBy: RequestedBy ?? null, - CreatedDate, - EstablishedDate: EstablishedDate ?? null, - RevokedDate: RevokedDate ?? null, - }) - ); - } -} diff --git a/test/commands/package/packageLinkList.test.ts b/test/commands/package/packageLinkList.test.ts index e29177fe..9a1f5bb5 100644 --- a/test/commands/package/packageLinkList.test.ts +++ b/test/commands/package/packageLinkList.test.ts @@ -7,7 +7,7 @@ * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software + * Unless required by applicable law or authorized 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 @@ -18,10 +18,10 @@ import { TestContext, MockTestOrgData } from '@salesforce/core/testSetup'; import * as sinon from 'sinon'; import { expect } from 'chai'; import { stubSfCommandUx } from '@salesforce/sf-plugins-core'; +import { PackageTrustLink, PackageTrustLinkRecord } from '@salesforce/packaging'; import { PackageLinkListCommand } from '../../../src/commands/package/link/list.js'; -import { PackageLinkRecord, PackageLinkService } from '../../../src/utils/packageLink.js'; -const linkListSuccess: PackageLinkRecord[] = [ +const linkListSuccess: PackageTrustLinkRecord[] = [ { Id: '2vt000000000001AAA', AuthoringOrg: '00D000000000002', @@ -45,7 +45,7 @@ describe('package:link:list - tests', () => { await $$.stubAuths(testOrg); await config.load(); sfCommandStubs = stubSfCommandUx($$.SANDBOX); - listStub = $$.SANDBOX.stub(PackageLinkService.prototype, 'list').resolves(linkListSuccess); + listStub = $$.SANDBOX.stub(PackageTrustLink, 'list').resolves(linkListSuccess); }); afterEach(() => { @@ -57,17 +57,19 @@ describe('package:link:list - tests', () => { const result = await cmd.run(); expect(result).to.deep.equal(linkListSuccess); - expect(listStub.calledOnceWithExactly(undefined)).to.equal(true); + expect(listStub.calledOnce).to.equal(true); + expect(listStub.firstCall.args[1]).to.equal(undefined); expect(sfCommandStubs.table.called).to.equal(true); }); - it('passes the status filter to PackageLink.list', async () => { + it('passes the status filter to PackageTrustLink.list', async () => { const cmd = new PackageLinkListCommand( ['-o', testOrg.username, '--api-version', '68.0', '--status', 'pending'], config ); await cmd.run(); - expect(listStub.calledOnceWithExactly('pending')).to.equal(true); + expect(listStub.calledOnce).to.equal(true); + expect(listStub.firstCall.args[1]).to.equal('pending'); }); it('warns when there are no results', async () => { diff --git a/test/utils/packageLink.test.ts b/test/utils/packageLink.test.ts deleted file mode 100644 index 4730a50e..00000000 --- a/test/utils/packageLink.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -/* - * 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 { Connection } from '@salesforce/core'; -import { expect } from 'chai'; -import sinon from 'sinon'; -import { PackageLinkService } from '../../src/utils/packageLink.js'; - -const verifiedOrg15 = '00D000000000001'; -const verifiedOrg18 = '00D000000000001EAA'; - -const createConnection = ({ - autoFetchQuery = sinon.stub().resolves({ records: [] }), - apiVersion = '68.0', - orgId = verifiedOrg18, -}: { - autoFetchQuery?: sinon.SinonStub; - apiVersion?: string; - orgId?: string; -} = {}) => - ({ - autoFetchQuery, - getApiVersion: sinon.stub().returns(apiVersion), - getAuthInfoFields: sinon.stub().returns({ orgId }), - } as unknown as Connection); - -describe('PackageLinkService', () => { - it('queries inbound VerifiedDev requests and excludes self-trust', async () => { - const autoFetchQuery = sinon.stub().resolves({ - records: [ - { - Id: '2vt000000000001AAA', - AuthoringOrg: '00D000000000002', - VerifiedOrg: verifiedOrg15, - Status: 'Pending', - RequestedBy: 'Ada Lovelace', - CreatedDate: '2026-08-24T00:00:00.000Z', - EstablishedDate: null, - RevokedDate: null, - attributes: { type: 'PkgVrfyAuthOrgTrustRela' }, - }, - ], - }); - - const result = await new PackageLinkService(createConnection({ autoFetchQuery })).list(); - - expect(result).to.deep.equal([ - { - Id: '2vt000000000001AAA', - AuthoringOrg: '00D000000000002', - VerifiedOrg: verifiedOrg15, - Status: 'Pending', - RequestedBy: 'Ada Lovelace', - CreatedDate: '2026-08-24T00:00:00.000Z', - EstablishedDate: null, - RevokedDate: null, - }, - ]); - expect(autoFetchQuery.calledOnce).to.equal(true); - expect(autoFetchQuery.firstCall.args[0]).to.equal( - "SELECT Id, AuthoringOrg, VerifiedOrg, Status, RequestedBy, CreatedDate, EstablishedDate, RevokedDate FROM PkgVrfyAuthOrgTrustRela WHERE VerifiedOrg = '00D000000000001' AND OrganizationType = 'Verified' AND AuthoringOrg != '00D000000000001' ORDER BY CreatedDate DESC" - ); - expect(autoFetchQuery.firstCall.args[1]).to.deep.equal({ tooling: true }); - }); - - it('maps approved to the Accepted API status', async () => { - const autoFetchQuery = sinon.stub().resolves({ records: [] }); - await new PackageLinkService(createConnection({ autoFetchQuery })).list('approved'); - expect(autoFetchQuery.firstCall.args[0]).to.contain("AND Status = 'Accepted'"); - }); - - it('compares API versions numerically', () => { - expect(() => new PackageLinkService(createConnection({ apiVersion: '100.0' }))).not.to.throw(); - expect(() => new PackageLinkService(createConnection({ apiVersion: '67.0' }))).to.throw( - 'Package link requires API version 68.0 or later.' - ); - expect(() => new PackageLinkService(createConnection({ apiVersion: 'invalid' }))).to.throw( - 'Package link requires API version 68.0 or later.' - ); - }); - - it('requires an org ID', () => { - expect(() => new PackageLinkService(createConnection({ orgId: '' }))).to.throw( - 'Unable to determine the target org ID' - ); - }); -}); From 326eac4ea8d3c2c962dffd325c635b8167823cb3 Mon Sep 17 00:00:00 2001 From: "b.tran" Date: Wed, 26 Aug 2026 17:42:03 -0400 Subject: [PATCH 4/5] fix: restore Apache header wording in link list tests --- test/commands/package/packageLinkList.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/commands/package/packageLinkList.test.ts b/test/commands/package/packageLinkList.test.ts index 9a1f5bb5..182fdc99 100644 --- a/test/commands/package/packageLinkList.test.ts +++ b/test/commands/package/packageLinkList.test.ts @@ -7,7 +7,7 @@ * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or authorized to in writing, software + * 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 From d8aae90d2a20891d105ef62670a93baf387585e7 Mon Sep 17 00:00:00 2001 From: "b.tran" Date: Wed, 26 Aug 2026 17:45:39 -0400 Subject: [PATCH 5/5] feat: rename list command to sf package trust link list Match the 918 CLI path (package/trust/link) instead of package/link. --- command-snapshot.json | 16 ++++++++-------- ...link_list.md => package_trust_link_list.md} | 2 +- package.json | 3 --- ...-list.json => package-trust-link-list.json} | 12 ++++++------ src/commands/package/{ => trust}/link/list.ts | 14 ++++++++------ ...st.test.ts => packageTrustLinkList.test.ts} | 18 +++++++++--------- 6 files changed, 32 insertions(+), 33 deletions(-) rename messages/{package_link_list.md => package_trust_link_list.md} (93%) rename schemas/{package-link-list.json => package-trust-link-list.json} (78%) rename src/commands/package/{ => trust}/link/list.ts (83%) rename test/commands/package/{packageLinkList.test.ts => packageTrustLinkList.test.ts} (79%) diff --git a/command-snapshot.json b/command-snapshot.json index 3540a861..5d3c0987 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -299,14 +299,6 @@ "flags": ["api-version", "flags-dir", "json", "loglevel", "target-dev-hub", "verbose"], "plugin": "@salesforce/plugin-packaging" }, - { - "alias": [], - "command": "package:link:list", - "flagAliases": ["apiversion", "targetusername", "u"], - "flagChars": ["o", "s"], - "flags": ["api-version", "flags-dir", "json", "loglevel", "status", "target-org"], - "plugin": "@salesforce/plugin-packaging" - }, { "alias": [], "command": "package:push-upgrade:abort", @@ -358,6 +350,14 @@ ], "plugin": "@salesforce/plugin-packaging" }, + { + "alias": [], + "command": "package:trust:link:list", + "flagAliases": ["apiversion", "targetusername", "u"], + "flagChars": ["o", "s"], + "flags": ["api-version", "flags-dir", "json", "loglevel", "status", "target-org"], + "plugin": "@salesforce/plugin-packaging" + }, { "alias": ["force:package:uninstall"], "command": "package:uninstall", diff --git a/messages/package_link_list.md b/messages/package_trust_link_list.md similarity index 93% rename from messages/package_link_list.md rename to messages/package_trust_link_list.md index 65376d05..2d0c5d6d 100644 --- a/messages/package_link_list.md +++ b/messages/package_trust_link_list.md @@ -12,7 +12,7 @@ Authoring org name and that org's packages are not returned by the Tooling API f # examples -- List all inbound Public Secure link requests in the target verified org: +- List all inbound Public Secure trust link requests in the target verified org: <%= config.bin %> <%= command.id %> --target-org pbo@example.com diff --git a/package.json b/package.json index f4aef598..0f217430 100644 --- a/package.json +++ b/package.json @@ -109,9 +109,6 @@ }, "installed": { "description": "Command to list installed packages." - }, - "link": { - "description": "Commands to manage Public Secure (VerifiedDev) package trust links." } } } diff --git a/schemas/package-link-list.json b/schemas/package-trust-link-list.json similarity index 78% rename from schemas/package-link-list.json rename to schemas/package-trust-link-list.json index 81d6aa8f..c5f3101c 100644 --- a/schemas/package-link-list.json +++ b/schemas/package-trust-link-list.json @@ -1,14 +1,14 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$ref": "#/definitions/PackageLinkListCommandResult", + "$ref": "#/definitions/PackageTrustLinkListCommandResult", "definitions": { - "PackageLinkListCommandResult": { + "PackageTrustLinkListCommandResult": { "type": "array", "items": { - "$ref": "#/definitions/PackageLinkRecord" + "$ref": "#/definitions/PackageTrustLinkRecord" } }, - "PackageLinkRecord": { + "PackageTrustLinkRecord": { "type": "object", "properties": { "Id": { @@ -21,7 +21,7 @@ "type": "string" }, "Status": { - "$ref": "#/definitions/PackageLinkStatus" + "$ref": "#/definitions/PackageTrustLinkStatus" }, "RequestedBy": { "type": ["string", "null"] @@ -48,7 +48,7 @@ ], "additionalProperties": false }, - "PackageLinkStatus": { + "PackageTrustLinkStatus": { "type": "string", "enum": ["Pending", "Accepted", "Declined", "Revoked", "Failed"] } diff --git a/src/commands/package/link/list.ts b/src/commands/package/trust/link/list.ts similarity index 83% rename from src/commands/package/link/list.ts rename to src/commands/package/trust/link/list.ts index 0ba6d12e..1b85537c 100644 --- a/src/commands/package/link/list.ts +++ b/src/commands/package/trust/link/list.ts @@ -25,13 +25,15 @@ import { Messages } from '@salesforce/core/messages'; import { PackageTrustLink, PackageTrustLinkListStatusFilter, PackageTrustLinkRecord } from '@salesforce/packaging'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); -const messages = Messages.loadMessages('@salesforce/plugin-packaging', 'package_link_list'); +const messages = Messages.loadMessages('@salesforce/plugin-packaging', 'package_trust_link_list'); const STATUS_OPTIONS: PackageTrustLinkListStatusFilter[] = ['pending', 'approved', 'declined', 'revoked']; -export type PackageLinkListCommandResult = PackageTrustLinkRecord[]; +export type PackageTrustLinkListCommandResult = PackageTrustLinkRecord[]; -export class PackageLinkListCommand extends SfCommand { +export class PackageTrustLinkListCommand 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'); @@ -48,8 +50,8 @@ export class PackageLinkListCommand extends SfCommand { - const { flags } = await this.parse(PackageLinkListCommand); + public async run(): Promise { + const { flags } = await this.parse(PackageTrustLinkListCommand); const connection = flags['target-org'].getConnection(flags['api-version']); const results = await PackageTrustLink.list(connection, flags.status); @@ -64,7 +66,7 @@ export class PackageLinkListCommand extends SfCommand { +describe('package:trust:link:list - tests', () => { const $$ = new TestContext(); const testOrg = new MockTestOrgData(); let sfCommandStubs: ReturnType; @@ -45,25 +45,25 @@ describe('package:link:list - tests', () => { await $$.stubAuths(testOrg); await config.load(); sfCommandStubs = stubSfCommandUx($$.SANDBOX); - listStub = $$.SANDBOX.stub(PackageTrustLink, 'list').resolves(linkListSuccess); + listStub = $$.SANDBOX.stub(PackageTrustLink, 'list').resolves(trustLinkListSuccess); }); afterEach(() => { $$.restore(); }); - it('lists inbound Public Secure link requests', async () => { - const cmd = new PackageLinkListCommand(['-o', testOrg.username, '--api-version', '68.0'], config); + it('lists inbound Public Secure trust link requests', async () => { + const cmd = new PackageTrustLinkListCommand(['-o', testOrg.username, '--api-version', '68.0'], config); const result = await cmd.run(); - expect(result).to.deep.equal(linkListSuccess); + expect(result).to.deep.equal(trustLinkListSuccess); expect(listStub.calledOnce).to.equal(true); expect(listStub.firstCall.args[1]).to.equal(undefined); expect(sfCommandStubs.table.called).to.equal(true); }); it('passes the status filter to PackageTrustLink.list', async () => { - const cmd = new PackageLinkListCommand( + const cmd = new PackageTrustLinkListCommand( ['-o', testOrg.username, '--api-version', '68.0', '--status', 'pending'], config ); @@ -74,7 +74,7 @@ describe('package:link:list - tests', () => { it('warns when there are no results', async () => { listStub.resolves([]); - const cmd = new PackageLinkListCommand(['-o', testOrg.username, '--api-version', '68.0'], config); + const cmd = new PackageTrustLinkListCommand(['-o', testOrg.username, '--api-version', '68.0'], config); const result = await cmd.run(); expect(result).to.deep.equal([]); expect(sfCommandStubs.warn.called).to.equal(true);