Skip to content

Commit 3954a91

Browse files
committed
fix: support new package version retrieve error messages (@W-23089623@)
1 parent ccbe328 commit 3954a91

3 files changed

Lines changed: 281 additions & 27 deletions

File tree

messages/package.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ Can't retrieve package version metadata. The specified directory must be relativ
3838

3939
Can't retrieve package metadata. To use this feature, you must first assign yourself the DownloadPackageVersionZips user permission. Then retry retrieving your package metadata.
4040

41+
# packageVersionNotFound
42+
43+
Can't retrieve package metadata. We can't find the package version %s. Verify that the 04t ID is correct and that the package version exists.
44+
45+
# packageVersionNotInDevHub
46+
47+
Can't retrieve package metadata. Package version %s isn't accessible from this Dev Hub org. You can only retrieve package metadata from the Dev Hub that created the package version. Verify that you specified the correct target Dev Hub.
48+
4149
# downloadDeveloperPackageZipHasNoDataNative2GP
4250

4351
Can't retrieve package metadata. The developer package zip for this native 2GP package version is unretrievable. To resolve, create a new package version with the --generate-pkg-zip flag. Then retry retrieving your package metadata.

src/package/packageVersionRetrieve.ts

Lines changed: 105 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { PackageVersionMetadataDownloadOptions, PackageVersionMetadataDownloadRe
2323
import { generatePackageAliasEntry, isPackageDirectoryEffectivelyEmpty } from '../utils/packageUtils';
2424
import { createPackageDirEntry } from './packageCreate';
2525
import { Package } from './package';
26-
import { PackageVersion } from './packageVersion';
26+
import { PackageVersion, Package2VersionFieldTypes } from './packageVersion';
2727

2828
Messages.importMessagesDirectory(__dirname);
2929
const messages = Messages.loadMessages('@salesforce/packaging', 'package');
@@ -60,31 +60,52 @@ export async function retrievePackageVersionMetadata(
6060
const subscriberPackageVersionId =
6161
project.getPackageIdFromAlias(options.subscriberPackageVersionId) ?? options.subscriberPackageVersionId;
6262

63-
// Query Package2Version to get the record by SubscriberPackageVersionId
63+
// Query Package2Version to confirm the row exists in the running user's Dev Hub. We select
64+
// only the minimal columns this flow actually reads (Package2Id for the project.json update,
65+
// ConvertedFromVersionId for the "no data" message variant). We deliberately do NOT select
66+
// DeveloperUsePkgZip here: it is gated by the DownloadPackageVersionZips user permission and
67+
// is invisible to SOQL for users without it, so selecting it would throw "No such column" at
68+
// parse time before we could distinguish "not found" from "wrong Dev Hub" from "missing
69+
// permission". The download URL is fetched separately, only when we're about to download
70+
// (see getDeveloperUsePkgZipUrl below). This mirrors the strip already done in getData().
6471
const queryOptions = {
6572
whereClause: `WHERE SubscriberPackageVersionId = '${subscriberPackageVersionId}'`,
73+
fields: ['Package2Id', 'ConvertedFromVersionId'] as Package2VersionFieldTypes,
6674
};
6775
let versionInfo;
6876
try {
6977
[versionInfo] = await PackageVersion.queryPackage2Version(connection, queryOptions);
7078
} catch (e) {
7179
const msg = e instanceof Error ? e.message : String(e);
72-
if (msg.includes("No such column 'DeveloperUsePkgZip' on entity 'Package2Version'")) {
73-
throw messages.createError('developerUsePkgZipFieldUnavailable');
74-
}
7580
if (msg.includes("sObject type 'Package2Version' is not supported.")) {
7681
throw messages.createError('packagingNotEnabledOnOrg');
7782
}
7883
throw e;
7984
}
8085

81-
if (!versionInfo?.DeveloperUsePkgZip) {
86+
// No Package2Version row visible for this 04t in the running user's Dev Hub.
87+
// Distinguish "doesn't exist anywhere" from "exists but not in this Dev Hub"
88+
// using SubscriberPackageVersion, which any authenticated user can query by ID.
89+
if (!versionInfo) {
90+
const exists = await subscriberPackageVersionExists(connection, subscriberPackageVersionId);
91+
throw messages.createError(exists ? 'packageVersionNotInDevHub' : 'packageVersionNotFound', [
92+
subscriberPackageVersionId,
93+
]);
94+
}
95+
96+
// The row exists. Now fetch the FLS-gated download URL on its own. If the user lacks the
97+
// DownloadPackageVersionZips permission (or the field isn't available on this API version),
98+
// this returns nothing and we surface the actionable "assign the permission" message.
99+
const developerUsePkgZipUrl = await getDeveloperUsePkgZipUrl(connection, subscriberPackageVersionId);
100+
if (!developerUsePkgZipUrl) {
82101
throw messages.createError('developerUsePkgZipFieldUnavailable');
83102
}
84103

85-
const responseBase64 = await connection.tooling.request<string>(versionInfo.DeveloperUsePkgZip, {
86-
encoding: 'base64',
87-
});
104+
const responseBase64 = await downloadDeveloperPackageZip(
105+
connection,
106+
developerUsePkgZipUrl,
107+
subscriberPackageVersionId
108+
);
88109
const buffer = Buffer.from(responseBase64, 'base64');
89110

90111
let tree;
@@ -232,3 +253,78 @@ async function attemptToUpdateProjectJson(
232253
);
233254
}
234255
}
256+
257+
/**
258+
* Download the developer package zip from the DeveloperUsePkgZip URL, mapping late-stage
259+
* access failures to actionable errors. The download servlet performs its own independent
260+
* access check, so a valid-looking URL can still be rejected at fetch time.
261+
*/
262+
async function downloadDeveloperPackageZip(
263+
connection: Connection,
264+
developerUsePkgZipUrl: string,
265+
subscriberPackageVersionId: string
266+
): Promise<string> {
267+
try {
268+
return await connection.tooling.request<string>(developerUsePkgZipUrl, { encoding: 'base64' });
269+
} catch (e) {
270+
const name = e instanceof Error ? e.name : '';
271+
if (name === 'NOT_FOUND') {
272+
throw messages.createError('packageVersionNotFound', [subscriberPackageVersionId]);
273+
}
274+
if (name === 'INSUFFICIENT_ACCESS' || name === 'INSUFFICIENT_ACCESS_OR_READONLY') {
275+
throw messages.createError('packageVersionNotInDevHub', [subscriberPackageVersionId]);
276+
}
277+
throw e;
278+
}
279+
}
280+
281+
/**
282+
* Fetch the FLS-gated DeveloperUsePkgZip download URL for a Package2Version, given that the
283+
* row is already known to exist. DeveloperUsePkgZip is gated by the DownloadPackageVersionZips
284+
* user permission: for a user without it, the column is invisible to SOQL and selecting it
285+
* throws "No such column" (at API versions where the field exists). We isolate that select
286+
* here so the caller can treat both "no such column" and an empty value as the same signal —
287+
* the running user cannot access the download URL — without masking the not-found / wrong-hub
288+
* disambiguation done by the caller's existence query.
289+
*
290+
* @returns the DeveloperUsePkgZip URL, or undefined if the user cannot access it.
291+
*/
292+
async function getDeveloperUsePkgZipUrl(
293+
connection: Connection,
294+
subscriberPackageVersionId: string
295+
): Promise<string | undefined> {
296+
try {
297+
const [record] = await PackageVersion.queryPackage2Version(connection, {
298+
whereClause: `WHERE SubscriberPackageVersionId = '${subscriberPackageVersionId}'`,
299+
fields: ['DeveloperUsePkgZip'] as Package2VersionFieldTypes,
300+
});
301+
return record?.DeveloperUsePkgZip ?? undefined;
302+
} catch (e) {
303+
const msg = e instanceof Error ? e.message : String(e);
304+
if (msg.includes("No such column 'DeveloperUsePkgZip' on entity 'Package2Version'")) {
305+
return undefined;
306+
}
307+
throw e;
308+
}
309+
}
310+
311+
/**
312+
* Returns true if a SubscriberPackageVersion with the given 04t ID exists in the system.
313+
* SubscriberPackageVersion is a global, non-org-scoped view queryable by any authenticated
314+
* user (orgAccess="always", userAccess="isAuthenticatedUser"), so this lets us tell
315+
* "package version not found" apart from "exists but not in the targeted Dev Hub".
316+
*/
317+
async function subscriberPackageVersionExists(
318+
connection: Connection,
319+
subscriberPackageVersionId: string
320+
): Promise<boolean> {
321+
try {
322+
const result = await connection.tooling.query<{ Id: string }>(
323+
`SELECT Id FROM SubscriberPackageVersion WHERE Id = '${subscriberPackageVersionId}' LIMIT 1`
324+
);
325+
return (result.records?.length ?? 0) > 0;
326+
} catch {
327+
// If even the existence probe fails (malformed ID, etc.), fall back to "not found".
328+
return false;
329+
}
330+
}

0 commit comments

Comments
 (0)