1
+ import { HapiServer , HapiRequest } from "../types" ;
2
+ import { config } from "../plugins/utils/AdapterConfigurationSchema" ;
3
+ import Boom from "boom" ;
4
+ import wreck from "@hapi/wreck" ;
5
+
6
+ export interface FormHashResponse {
7
+ hash : string ;
8
+ }
9
+
10
+ export interface PublishedFormResponse {
11
+ configuration : any ;
12
+ hash : string ;
13
+ }
14
+
15
+ const LOGGER_DATA = {
16
+ class : "PreAwardApiService" ,
17
+ }
18
+
19
+ export class PreAwardApiService {
20
+ private logger : any ;
21
+ private apiBaseUrl : string ;
22
+ private wreck : typeof wreck ;
23
+
24
+ constructor ( server : HapiServer ) {
25
+ this . logger = server . logger ;
26
+ this . apiBaseUrl = config . preAwardApiUrl ;
27
+ // Create a wreck client with default options for all requests
28
+ this . wreck = wreck . defaults ( {
29
+ timeout : 10000 ,
30
+ headers : {
31
+ 'accept' : 'application/json' ,
32
+ 'content-type' : 'application/json'
33
+ }
34
+ } ) ;
35
+ this . logger . info ( {
36
+ ...LOGGER_DATA ,
37
+ message : `Service initialized with base URL: ${ this . apiBaseUrl } `
38
+ } ) ;
39
+ }
40
+
41
+ /**
42
+ * Extracts and formats authentication headers from the incoming request.
43
+ * The Pre-Award API uses the same authentication mechanism as the Runner,
44
+ * so we forward the user's authentication token to maintain security.
45
+ *
46
+ * This supports both cookie-based JWT tokens and Authorization headers,
47
+ * depending on how the Runner is configured.
48
+ */
49
+ private getAuthHeaders ( request ?: HapiRequest ) : Record < string , string > {
50
+ const headers : Record < string , string > = { } ;
51
+ if ( ! request ) {
52
+ this . logger . warn ( {
53
+ ...LOGGER_DATA ,
54
+ message : "No request context provided for authentication"
55
+ } ) ;
56
+ return headers ;
57
+ }
58
+ // Handle JWT cookie authentication (primary method)
59
+ if ( config . jwtAuthEnabled === "true" ) {
60
+ const cookieName = config . jwtAuthCookieName ;
61
+ // Check for JWT token in cookies
62
+ if ( request . state && request . state [ cookieName ] ) {
63
+ headers [ 'Cookie' ] = `${ cookieName } =${ request . state [ cookieName ] } ` ;
64
+ this . logger . debug ( {
65
+ ...LOGGER_DATA ,
66
+ message : "Using JWT cookie for Pre-Award API authentication"
67
+ } ) ;
68
+ }
69
+ // Fallback to Authorization header if no cookie
70
+ else if ( request . headers . authorization ) {
71
+ headers [ 'Authorization' ] = request . headers . authorization ;
72
+ this . logger . debug ( {
73
+ ...LOGGER_DATA ,
74
+ message : "Using Authorization header for Pre-Award API authentication"
75
+ } ) ;
76
+ }
77
+ else {
78
+ this . logger . warn ( {
79
+ ...LOGGER_DATA ,
80
+ message : "No authentication token found in request"
81
+ } ) ;
82
+ }
83
+ }
84
+ return headers ;
85
+ }
86
+
87
+ /**
88
+ * Fetches the published form data including hash for a specific form.
89
+ * This is the primary method used by the cache service when a form
90
+ * is requested by a user. It only returns forms that have been
91
+ * explicitly published in the Pre-Award system.
92
+ */
93
+ async getPublishedForm ( name : string , request ?: HapiRequest ) : Promise < PublishedFormResponse | null > {
94
+ const url = `${ this . apiBaseUrl } /${ name } /published` ;
95
+ const authHeaders = this . getAuthHeaders ( request ) ;
96
+ this . logger . info ( {
97
+ ...LOGGER_DATA ,
98
+ message : `Fetching published form: ${ name } ` ,
99
+ url : url
100
+ } ) ;
101
+ try {
102
+ const { payload } = await this . wreck . get ( url , {
103
+ headers : {
104
+ ...authHeaders ,
105
+ 'accept' : 'application/json'
106
+ } ,
107
+ json : true
108
+ } ) ;
109
+ this . logger . info ( {
110
+ ...LOGGER_DATA ,
111
+ message : `Successfully fetched published form: ${ name } `
112
+ } ) ;
113
+ return payload as PublishedFormResponse ;
114
+ } catch ( error : any ) {
115
+ // Handle 404 - form doesn't exist or isn't published
116
+ if ( error . output ?. statusCode === 404 ) {
117
+ this . logger . info ( {
118
+ ...LOGGER_DATA ,
119
+ message : `Form ${ name } not found or not published in Pre-Award API`
120
+ } ) ;
121
+ return null ;
122
+ }
123
+ // Handle authentication failures
124
+ if ( error . output ?. statusCode === 401 || error . output ?. statusCode === 403 ) {
125
+ this . logger . error ( {
126
+ ...LOGGER_DATA ,
127
+ message : `Authentication failed when fetching form ${ name } ` ,
128
+ statusCode : error . output ?. statusCode
129
+ } ) ;
130
+ throw Boom . unauthorized ( 'Failed to authenticate with Pre-Award API' ) ;
131
+ }
132
+ // Handle other errors (network, timeout, server errors)
133
+ this . logger . error ( {
134
+ ...LOGGER_DATA ,
135
+ message : `Failed to fetch published form ${ name } ` ,
136
+ error : error . message
137
+ } ) ;
138
+ // Don't expose internal error details to the client
139
+ throw Boom . serverUnavailable ( 'Pre-Award API is temporarily unavailable' ) ;
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Fetches just the hash of a published form.
145
+ * This lightweight endpoint allows us to validate our cache without
146
+ * downloading the entire form definition. We use this for periodic
147
+ * cache freshness checks.
148
+ */
149
+ async getFormHash ( name : string , request ?: HapiRequest ) : Promise < string | null > {
150
+ const url = `${ this . apiBaseUrl } /${ name } /hash` ;
151
+ const authHeaders = this . getAuthHeaders ( request ) ;
152
+ try {
153
+ const { payload } = await this . wreck . get ( url , {
154
+ headers : {
155
+ ...authHeaders ,
156
+ 'accept' : 'application/json'
157
+ } ,
158
+ json : true ,
159
+ timeout : 5000 // Shorter timeout for hash checks
160
+ } ) ;
161
+ const data = payload as FormHashResponse ;
162
+ this . logger . debug ( {
163
+ ...LOGGER_DATA ,
164
+ message : `Retrieved hash for form ${ name } `
165
+ } ) ;
166
+ return data . hash ;
167
+ } catch ( error : any ) {
168
+ if ( error . output ?. statusCode === 404 ) {
169
+ // Form doesn't exist or isn't published - this is normal
170
+ return null ;
171
+ }
172
+ // For hash validation failures, we don't want to fail the entire request.
173
+ // We'll continue using the cached version and try again later.
174
+ this . logger . warn ( {
175
+ ...LOGGER_DATA ,
176
+ message : `Could not fetch hash for form ${ name } , will use cached version` ,
177
+ error : error . message
178
+ } ) ;
179
+ return null ;
180
+ }
181
+ }
182
+ }
0 commit comments