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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
18 changes: 18 additions & 0 deletions src/interfaces/packagingInterfacesAndType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

/** CLI --status values for `sf package trust link list` (PBO-admin). */
export type PackageTrustLinkListStatusFilter = 'pending' | 'approved' | 'declined' | 'revoked';

Expand Down
42 changes: 41 additions & 1 deletion src/package/packageTrustLink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
PackageTrustLinkRequestOptions,
PackageTrustLinkRequestResult,
PackageTrustLinkStatus,
PackageTrustLinkStatusResult,
PackageTrustLinkUnlinkResult,
} from '../interfaces';
import { combineSaveErrors } from '../utils/packageUtils';
Expand Down Expand Up @@ -53,10 +54,17 @@ const toApiStatus = (status: PackageTrustLinkListStatusFilter): PackageTrustLink
return STATUS_FILTER_TO_API[status];
};

// 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 {
Expand Down Expand Up @@ -111,6 +119,38 @@ export class PackageTrustLink {
};
}

/**
* 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<PackageTrustLinkStatusResult> {
// 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 } : {}),
};
}

/**
* List inbound Public Secure trust-link requests for the connected Verified PBO.
*
Expand Down Expand Up @@ -188,7 +228,7 @@ export class PackageTrustLink {
}

async function queryExistingTrustLink(connection: Connection): Promise<TrustLinkRecord | undefined> {
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<TrustLinkRecord & Schema>(query, { tooling: true });
return result.records?.[0];
}
93 changes: 93 additions & 0 deletions test/package/packageTrustLink.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,99 @@ 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 });
});
});

describe('list', () => {
const verifiedOrg15 = '00D000000000001';
const verifiedOrg18 = '00D000000000001EAA';
Expand Down
Loading