@@ -45,6 +45,18 @@ export type StoredPackageSource = {
4545 kind : string ;
4646 version : number ;
4747 sha256 : string ;
48+ approved ?: boolean ;
49+ } ;
50+
51+ export type SealedPackageAssignment = {
52+ audience : string ;
53+ decisionNeeded ?: string ;
54+ sources : readonly {
55+ artifactId : string ;
56+ kind : string ;
57+ version : number ;
58+ sha256 : string ;
59+ } [ ] ;
4860} ;
4961
5062export type StoredSourceReader = (
@@ -118,11 +130,61 @@ async function verifySourcePin(
118130 message : `${ source . artifactId } @${ String ( source . version ) } ` ,
119131 } ,
120132 ] ) ;
133+ if ( stored . approved === false )
134+ throw new ProjectError ( 422 , "unapproved_package_source" , [
135+ {
136+ path : "manifest.sources" ,
137+ message : `${ source . kind } @${ String ( source . version ) } ` ,
138+ } ,
139+ ] ) ;
140+ }
141+
142+ function verifySealedAssignment (
143+ bundle : ApprovalPackageBundle ,
144+ sealed : SealedPackageAssignment ,
145+ ) : void {
146+ if ( bundle . manifest . audience . trim ( ) !== sealed . audience . trim ( ) ) {
147+ throw new ProjectError ( 422 , "package_assignment_mismatch" , [
148+ {
149+ path : "manifest.audience" ,
150+ message : `expected ${ sealed . audience } ` ,
151+ } ,
152+ ] ) ;
153+ }
154+ if (
155+ sealed . decisionNeeded !== undefined &&
156+ bundle . manifest . decisionNeeded . trim ( ) !== sealed . decisionNeeded . trim ( )
157+ ) {
158+ throw new ProjectError ( 422 , "package_assignment_mismatch" , [
159+ {
160+ path : "manifest.decisionNeeded" ,
161+ message : `expected ${ sealed . decisionNeeded } ` ,
162+ } ,
163+ ] ) ;
164+ }
165+ const actualKeys = bundle . manifest . sources . map (
166+ ( s ) => `${ s . artifactId } :${ s . kind } :${ String ( s . version ) } :${ s . sha256 } ` ,
167+ ) ;
168+ const expectedKeys = sealed . sources . map (
169+ ( s ) => `${ s . artifactId } :${ s . kind } :${ String ( s . version ) } :${ s . sha256 } ` ,
170+ ) ;
171+ if (
172+ actualKeys . length !== expectedKeys . length ||
173+ actualKeys . some ( ( key , idx ) => key !== expectedKeys [ idx ] )
174+ ) {
175+ throw new ProjectError ( 422 , "package_assignment_mismatch" , [
176+ {
177+ path : "manifest.sources" ,
178+ message : "sources do not match the approved hand-off selection" ,
179+ } ,
180+ ] ) ;
181+ }
121182}
122183
123184export async function validateApprovalPackage (
124185 input : string | ApprovalPackageBundle ,
125186 readSource : StoredSourceReader ,
187+ sealedAssignment ?: SealedPackageAssignment ,
126188) : Promise < ApprovalPackageBundle > {
127189 const bundle =
128190 typeof input === "string" ? parseApprovalPackageBundle ( input ) : input ;
@@ -133,6 +195,9 @@ export async function validateApprovalPackage(
133195 ] ) ;
134196 }
135197 scanApprovalPackageForSecrets ( bundle ) ;
198+ if ( sealedAssignment !== undefined ) {
199+ verifySealedAssignment ( bundle , sealedAssignment ) ;
200+ }
136201 for ( const source of bundle . manifest . sources )
137202 await verifySourcePin ( source , readSource ) ;
138203 return bundle ;
@@ -141,17 +206,27 @@ export async function validateApprovalPackage(
141206export function readProjectPackageSource (
142207 db : ArtifactDb ,
143208 tenantId : string ,
209+ isApproved ?: (
210+ artifactId : string ,
211+ kind : string ,
212+ version : number ,
213+ ) => Promise < boolean > ,
144214) : StoredSourceReader {
145215 return async ( artifactId , version ) => {
146216 if ( ! UUID_RE . test ( artifactId ) ) return undefined ;
147217 const row = await getArtifact ( db , artifactId ) ;
148218 if ( ! row || row . tenantId !== tenantId ) return undefined ;
149219 const stored = await getArtifactVersion ( db , artifactId , version ) ;
150220 if ( ! stored ?. contentSha256 ) return undefined ;
221+ const approved =
222+ isApproved === undefined
223+ ? true
224+ : await isApproved ( artifactId , row . kind , stored . version ) ;
151225 return {
152226 kind : row . kind ,
153227 version : stored . version ,
154228 sha256 : stored . contentSha256 ,
229+ approved,
155230 } ;
156231 } ;
157232}
0 commit comments