@@ -23,7 +23,7 @@ import { PackageVersionMetadataDownloadOptions, PackageVersionMetadataDownloadRe
2323import { generatePackageAliasEntry , isPackageDirectoryEffectivelyEmpty } from '../utils/packageUtils' ;
2424import { createPackageDirEntry } from './packageCreate' ;
2525import { Package } from './package' ;
26- import { PackageVersion } from './packageVersion' ;
26+ import { PackageVersion , Package2VersionFieldTypes } from './packageVersion' ;
2727
2828Messages . importMessagesDirectory ( __dirname ) ;
2929const 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