@@ -25,8 +25,56 @@ import { getConfigFile } from './file';
2525import { validateConfig } from './validators' ;
2626import { handleErrorAndLog , handleErrorAndThrow } from '../utils/errors' ;
2727
28+ // Deprecated compatibility fields are still optional because the defaults do not set them.
29+ type OptionalTopLevelConfigKey = 'proxyUrl' | 'sslCertPemPath' | 'sslKeyPemPath' ;
30+ type RequiredTopLevelConfigKey = Exclude < keyof GitProxyConfig , OptionalTopLevelConfigKey > ;
31+
32+ export type FullGitProxyConfig = Required < Omit < GitProxyConfig , OptionalTopLevelConfigKey > > &
33+ Pick < GitProxyConfig , OptionalTopLevelConfigKey > ;
34+
35+ const REQUIRED_TOP_LEVEL_CONFIG_KEYS = [
36+ 'api' ,
37+ 'apiAuthentication' ,
38+ 'attestationConfig' ,
39+ 'authentication' ,
40+ 'authorisedList' ,
41+ 'commitConfig' ,
42+ 'configurationSources' ,
43+ 'contactEmail' ,
44+ 'cookieSecret' ,
45+ 'csrfProtection' ,
46+ 'domains' ,
47+ 'plugins' ,
48+ 'privateOrganizations' ,
49+ 'rateLimit' ,
50+ 'sessionMaxAgeHours' ,
51+ 'sink' ,
52+ 'tempPassword' ,
53+ 'tls' ,
54+ 'uiRouteAuth' ,
55+ 'upstreamProxy' ,
56+ 'urlShortener' ,
57+ ] as const satisfies readonly RequiredTopLevelConfigKey [ ] ;
58+
59+ type MissingRequiredTopLevelConfigKeys = Exclude <
60+ RequiredTopLevelConfigKey ,
61+ ( typeof REQUIRED_TOP_LEVEL_CONFIG_KEYS ) [ number ]
62+ > ;
63+ type AssertNever < T extends never > = T ;
64+ type _RequiredTopLevelConfigKeysAreExhaustive = AssertNever < MissingRequiredTopLevelConfigKeys > ;
65+
66+ export function assertHasRequiredTopLevelConfig (
67+ config : GitProxyConfig ,
68+ ) : asserts config is FullGitProxyConfig {
69+ const missingKeys = REQUIRED_TOP_LEVEL_CONFIG_KEYS . filter ( ( key ) => config [ key ] === undefined ) ;
70+
71+ if ( missingKeys . length > 0 ) {
72+ throw new Error ( `Missing required top-level configuration values: ${ missingKeys . join ( ', ' ) } ` ) ;
73+ }
74+ }
75+
2876// Cache for current configuration
29- let _currentConfig : GitProxyConfig | null = null ;
77+ let _currentConfig : FullGitProxyConfig | null = null ;
3078let _configLoader : ConfigLoader | null = null ;
3179
3280// Function to invalidate cache - useful for testing
@@ -57,17 +105,17 @@ function cleanUndefinedValues(obj: any): any {
57105
58106/**
59107 * Load and merge default + user configuration with QuickType validation
60- * @return {GitProxyConfig } The merged and validated configuration
108+ * @return {FullGitProxyConfig } The merged and validated configuration
61109 */
62- function loadFullConfiguration ( ) : GitProxyConfig {
110+ function loadFullConfiguration ( ) : FullGitProxyConfig {
63111 if ( _currentConfig ) {
64112 return _currentConfig ;
65113 }
66114
67115 const rawDefaultConfig = Convert . toGitProxyConfig ( JSON . stringify ( defaultSettings ) ) ;
68116
69117 // Clean undefined values from defaultConfig
70- const defaultConfig = cleanUndefinedValues ( rawDefaultConfig ) ;
118+ const defaultConfig = cleanUndefinedValues ( rawDefaultConfig ) as GitProxyConfig ;
71119
72120 let userSettings : Partial < GitProxyConfig > = { } ;
73121 const userConfigFile = process . env . CONFIG_FILE || getConfigFile ( ) ;
@@ -102,12 +150,12 @@ function loadFullConfiguration(): GitProxyConfig {
102150 * Merge configurations with environment variable overrides
103151 * @param {GitProxyConfig } defaultConfig - The default configuration
104152 * @param {Partial<GitProxyConfig> } userSettings - User-provided configuration overrides
105- * @return {GitProxyConfig } The merged configuration
153+ * @return {FullGitProxyConfig } The merged configuration
106154 */
107155function mergeConfigurations (
108156 defaultConfig : GitProxyConfig ,
109157 userSettings : Partial < GitProxyConfig > ,
110- ) : GitProxyConfig {
158+ ) : FullGitProxyConfig {
111159 // Special handling for TLS configuration when legacy fields are used
112160 let tlsConfig = userSettings . tls || defaultConfig . tls ;
113161
@@ -121,7 +169,7 @@ function mergeConfigurations(
121169 } ;
122170 }
123171
124- return {
172+ const config = {
125173 ...defaultConfig ,
126174 ...userSettings ,
127175 // Deep merge for specific objects
@@ -141,6 +189,9 @@ function mergeConfigurations(
141189 userSettings . cookieSecret ||
142190 defaultConfig . cookieSecret ,
143191 } ;
192+
193+ assertHasRequiredTopLevelConfig ( config ) ;
194+ return config ;
144195}
145196
146197// Get configured proxy URL
@@ -169,7 +220,7 @@ export const getUpstreamProxyConfig = () => {
169220// Gets a list of authorised repositories
170221export const getAuthorisedList = ( ) => {
171222 const config = loadFullConfiguration ( ) ;
172- return config . authorisedList || [ ] ;
223+ return config . authorisedList ;
173224} ;
174225
175226// Gets a list of authorised repositories
@@ -181,7 +232,7 @@ export const getTempPasswordConfig = () => {
181232// Gets the configured data sink, defaults to filesystem
182233export const getDatabase = ( ) => {
183234 const config = loadFullConfiguration ( ) ;
184- const databases = config . sink || [ ] ;
235+ const databases = config . sink ;
185236
186237 for ( const db of databases ) {
187238 if ( db . enabled ) {
@@ -204,7 +255,7 @@ export const getDatabase = () => {
204255 */
205256export const getAuthMethods = ( ) => {
206257 const config = loadFullConfiguration ( ) ;
207- const authSources = config . authentication || [ ] ;
258+ const authSources = config . authentication ;
208259
209260 const enabledAuthMethods = authSources . filter ( ( auth ) => auth . enabled ) ;
210261
@@ -223,7 +274,7 @@ export const getAuthMethods = () => {
223274 */
224275export const getAPIAuthMethods = ( ) => {
225276 const config = loadFullConfiguration ( ) ;
226- const apiAuthSources = config . apiAuthentication || [ ] ;
277+ const apiAuthSources = config . apiAuthentication ;
227278
228279 return apiAuthSources . filter ( ( auth : { enabled : any } ) => auth . enabled ) ;
229280} ;
@@ -244,7 +295,7 @@ export const logConfiguration = () => {
244295
245296export const getAPIs = ( ) => {
246297 const config = loadFullConfiguration ( ) ;
247- return config . api || { } ;
298+ return config . api ;
248299} ;
249300
250301export const getCookieSecret = ( ) : string => {
@@ -259,25 +310,25 @@ export const getCookieSecret = (): string => {
259310
260311export const getSessionMaxAgeHours = ( ) : number => {
261312 const config = loadFullConfiguration ( ) ;
262- return config . sessionMaxAgeHours || 24 ;
313+ return config . sessionMaxAgeHours ;
263314} ;
264315
265316// Get commit related configuration
266317export const getCommitConfig = ( ) => {
267318 const config = loadFullConfiguration ( ) ;
268- return config . commitConfig || { } ;
319+ return config . commitConfig ;
269320} ;
270321
271322// Get attestation related configuration
272323export const getAttestationConfig = ( ) => {
273324 const config = loadFullConfiguration ( ) ;
274- return config . attestationConfig || { } ;
325+ return config . attestationConfig ;
275326} ;
276327
277328// Get private organizations related configuration
278329export const getPrivateOrganizations = ( ) => {
279330 const config = loadFullConfiguration ( ) ;
280- return config . privateOrganizations || [ ] ;
331+ return config . privateOrganizations ;
281332} ;
282333
283334// Get URL shortener
@@ -301,7 +352,7 @@ export const getCSRFProtection = (): boolean | undefined => {
301352// Get loadable push plugins
302353export const getPlugins = ( ) => {
303354 const config = loadFullConfiguration ( ) ;
304- return config . plugins || [ ] ;
355+ return config . plugins ;
305356} ;
306357
307358export const getTLSKeyPemPath = ( ) : string | undefined => {
@@ -321,12 +372,12 @@ export const getTLSEnabled = (): boolean => {
321372
322373export const getDomains = ( ) => {
323374 const config = loadFullConfiguration ( ) ;
324- return config . domains || { } ;
375+ return config . domains ;
325376} ;
326377
327378export const getUIRouteAuth = ( ) => {
328379 const config = loadFullConfiguration ( ) ;
329- return config . uiRouteAuth || { } ;
380+ return config . uiRouteAuth ;
330381} ;
331382
332383export const getRateLimit = ( ) => {
@@ -342,12 +393,13 @@ const handleConfigUpdate = async (newConfig: Configuration) => {
342393 const validatedConfig = Convert . toGitProxyConfig ( JSON . stringify ( newConfig ) ) ;
343394
344395 // 2. Get proxy module dynamically to avoid circular dependency
345- const proxy = require ( '../proxy' ) ;
396+ const proxy = ( await import ( '../proxy' ) ) as any ;
346397
347398 // 3. Stop existing services
348399 await proxy . stop ( ) ;
349400
350401 // 4. Update config
402+ assertHasRequiredTopLevelConfig ( validatedConfig ) ;
351403 _currentConfig = validatedConfig ;
352404
353405 // 5. Restart services with new config
@@ -358,7 +410,7 @@ const handleConfigUpdate = async (newConfig: Configuration) => {
358410 handleErrorAndLog ( error , 'Failed to apply new configuration' ) ;
359411 // Attempt to restart with previous config
360412 try {
361- const proxy = require ( '../proxy' ) ;
413+ const proxy = ( await import ( '../proxy' ) ) as any ;
362414 await proxy . start ( ) ;
363415 } catch ( startError : unknown ) {
364416 handleErrorAndLog ( startError , 'Failed to restart services' ) ;
0 commit comments