From bd526591f7bb25de3187fb05d11e221adf1c4ea7 Mon Sep 17 00:00:00 2001 From: Sridhar Reddy Shyamala Date: Thu, 27 Aug 2026 20:55:11 +0000 Subject: [PATCH 1/2] feat: add PackageTrustLink.status for Public Secure link state (W-23970571) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a read-only status method that reports the connected authoring org's Public Secure (VerifiedDev) trust-link state — Not Linked when no link exists, otherwise the link's status (Pending/Accepted/Declined/Revoked/ Failed) with the relevant timestamps (requested/established/revoked). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/interfaces/packagingInterfacesAndType.ts | 18 ++++ src/package/packageTrustLink.ts | 47 +++++++++- test/package/packageTrustLink.test.ts | 93 ++++++++++++++++++++ 3 files changed, 156 insertions(+), 2 deletions(-) diff --git a/src/interfaces/packagingInterfacesAndType.ts b/src/interfaces/packagingInterfacesAndType.ts index 1b39bc608..8941fd7af 100644 --- a/src/interfaces/packagingInterfacesAndType.ts +++ b/src/interfaces/packagingInterfacesAndType.ts @@ -225,6 +225,24 @@ export type PackageTrustLinkRequestResult = { Status: string; }; +export type PackageTrustLinkStatusResult = { + // Current state of the authoring org's trust link: 'Not Linked' when no link exists, otherwise one + // of the trust-link statuses (Pending, Accepted, Declined, Revoked, Failed). + Status: string; + // Whether a trust link record exists for the authoring org. false for the 'Not Linked' state. + linked: boolean; + // Id of the trust link record, when one exists. + LinkRequestId?: string; + // Org ID of the Verified PBO the trust link points to, when one exists. + VerifiedOrgId?: string; + // When the link was requested (record created), when one exists. + RequestedDate?: string; + // When the link was accepted/established, when set. + EstablishedDate?: string; + // When the link was revoked, when set. + RevokedDate?: string; +}; + export type PackagePushRequestReportQueryOptions = { packagePushRequestId: string; }; diff --git a/src/package/packageTrustLink.ts b/src/package/packageTrustLink.ts index 588a91f4b..0b879183f 100644 --- a/src/package/packageTrustLink.ts +++ b/src/package/packageTrustLink.ts @@ -15,7 +15,11 @@ */ import type { Schema } from '@jsforce/jsforce-node'; import { Connection, Messages, trimTo15, validateSalesforceId } from '@salesforce/core'; -import { PackageTrustLinkRequestOptions, PackageTrustLinkRequestResult } from '../interfaces'; +import { + PackageTrustLinkRequestOptions, + PackageTrustLinkRequestResult, + PackageTrustLinkStatusResult, +} from '../interfaces'; import { combineSaveErrors } from '../utils/packageUtils'; Messages.importMessagesDirectory(__dirname); @@ -25,10 +29,17 @@ const messages = Messages.loadMessages('@salesforce/packaging', 'package_trust_l // between the connected authoring org and a Verified PBO. See W-23970567 / SPI CLI design doc. const TRUST_LINK_SOBJECT = 'PkgVrfyAuthOrgTrustRela'; +// The absence of a trust link record is a valid state the CLI reports; the SObject Status +// picklist itself only covers the states an existing link can be in. +const NOT_LINKED = 'Not Linked'; + type TrustLinkRecord = { Id: string; VerifiedOrg: string; Status: string; + EstablishedDate: string | null; + RevokedDate: string | null; + CreatedDate: string | null; }; export class PackageTrustLink { @@ -82,10 +93,42 @@ export class PackageTrustLink { Status: 'Pending', }; } + + /** + * Report the connected authoring org's Public Secure (VerifiedDev) trust link state. + * + * Read-only developer/authoring-org side operation. An authoring org holds at most one trust + * relationship, so this returns that org's link if one exists — its status (`Pending`, `Accepted`, + * `Declined`, `Revoked`, or `Failed`) and the relevant timestamps — or the synthetic `Not Linked` + * state when the org has no link at all. It never mutates anything. + * + * @param connection - Connection to the authoring org (the 1GP namespace org or 2GP Dev Hub). + * @returns the current link state and, when a link exists, its Id, verified org ID, and timestamps. + */ + public static async status(connection: Connection): Promise { + // The Tooling API query runs against the connected authoring org (AuthoringOrg is server-set), + // so this returns that org's own link if any. No record means the org was never linked. + const existing = await queryExistingTrustLink(connection); + if (!existing) { + return { Status: NOT_LINKED, linked: false }; + } + + // Only surface timestamps that are actually set — a Pending link has no EstablishedDate, and + // only a Revoked link has a RevokedDate. Emitting undefined keys would leak nulls into --json. + return { + Status: existing.Status, + linked: true, + LinkRequestId: existing.Id, + VerifiedOrgId: existing.VerifiedOrg, + ...(existing.CreatedDate ? { RequestedDate: existing.CreatedDate } : {}), + ...(existing.EstablishedDate ? { EstablishedDate: existing.EstablishedDate } : {}), + ...(existing.RevokedDate ? { RevokedDate: existing.RevokedDate } : {}), + }; + } } async function queryExistingTrustLink(connection: Connection): Promise { - const query = `SELECT Id, VerifiedOrg, Status FROM ${TRUST_LINK_SOBJECT} LIMIT 1`; + const query = `SELECT Id, VerifiedOrg, Status, EstablishedDate, RevokedDate, CreatedDate FROM ${TRUST_LINK_SOBJECT} LIMIT 1`; const result = await connection.autoFetchQuery(query, { tooling: true }); return result.records?.[0]; } diff --git a/test/package/packageTrustLink.test.ts b/test/package/packageTrustLink.test.ts index a1b5d5e36..036c1c3b1 100644 --- a/test/package/packageTrustLink.test.ts +++ b/test/package/packageTrustLink.test.ts @@ -120,4 +120,97 @@ describe('PackageTrustLink', () => { } }); }); + + describe('status', () => { + it('reports Not Linked when the org has no trust link', async () => { + const autoFetchQuery = sinon.stub().resolves({ records: [] }); + const connection = createConnection({ autoFetchQuery }); + + const result = await PackageTrustLink.status(connection); + + expect(result).to.deep.equal({ Status: 'Not Linked', linked: false }); + }); + + it('reports the existing link status, verified org, and timestamps', async () => { + const autoFetchQuery = sinon.stub().resolves({ + records: [ + { + Id: trustLinkId, + VerifiedOrg: verifiedOrgId15, + Status: 'Accepted', + EstablishedDate: '2026-08-20T10:00:00.000+0000', + RevokedDate: null, + CreatedDate: '2026-08-19T09:00:00.000+0000', + }, + ], + }); + const connection = createConnection({ autoFetchQuery }); + + const result = await PackageTrustLink.status(connection); + + expect(result).to.deep.equal({ + Status: 'Accepted', + linked: true, + LinkRequestId: trustLinkId, + VerifiedOrgId: verifiedOrgId15, + RequestedDate: '2026-08-19T09:00:00.000+0000', + EstablishedDate: '2026-08-20T10:00:00.000+0000', + }); + }); + + it('omits unset timestamps (e.g. a Pending link never established)', async () => { + const autoFetchQuery = sinon.stub().resolves({ + records: [ + { + Id: trustLinkId, + VerifiedOrg: verifiedOrgId15, + Status: 'Pending', + EstablishedDate: null, + RevokedDate: null, + CreatedDate: '2026-08-19T09:00:00.000+0000', + }, + ], + }); + const connection = createConnection({ autoFetchQuery }); + + const result = await PackageTrustLink.status(connection); + + expect(result.Status).to.equal('Pending'); + expect(result.linked).to.equal(true); + expect(result.RequestedDate).to.equal('2026-08-19T09:00:00.000+0000'); + expect(result).to.not.have.property('EstablishedDate'); + expect(result).to.not.have.property('RevokedDate'); + }); + + it('surfaces a Revoked link with its revoked timestamp', async () => { + const autoFetchQuery = sinon.stub().resolves({ + records: [ + { + Id: trustLinkId, + VerifiedOrg: verifiedOrgId15, + Status: 'Revoked', + EstablishedDate: '2026-08-20T10:00:00.000+0000', + RevokedDate: '2026-08-25T12:00:00.000+0000', + CreatedDate: '2026-08-19T09:00:00.000+0000', + }, + ], + }); + const connection = createConnection({ autoFetchQuery }); + + const result = await PackageTrustLink.status(connection); + + expect(result.Status).to.equal('Revoked'); + expect(result.RevokedDate).to.equal('2026-08-25T12:00:00.000+0000'); + }); + + it('does not scope the query by verified org (an org holds at most one link)', async () => { + const autoFetchQuery = sinon.stub().resolves({ records: [] }); + const connection = createConnection({ autoFetchQuery }); + + await PackageTrustLink.status(connection); + + expect(autoFetchQuery.firstCall.args[0]).to.not.contain('WHERE'); + expect(autoFetchQuery.firstCall.args[1]).to.deep.equal({ tooling: true }); + }); + }); }); From d65a6f7737b9964e0047d8f61183eba93f1f870f Mon Sep 17 00:00:00 2001 From: svc-cli-bot Date: Fri, 28 Aug 2026 03:05:56 +0000 Subject: [PATCH 2/2] chore(release): 5.0.9-spi.1 [skip ci] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 120e0c936..8b137593f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@salesforce/packaging", - "version": "5.0.9-spi.0", + "version": "5.0.9-spi.1", "description": "Packaging library for the Salesforce packaging platform", "main": "lib/exported", "types": "lib/exported.d.ts",