Skip to content

Commit 1602e54

Browse files
authored
Merge pull request #1514 from dcoric/feat/require-config-defaults
feat(config): require merged top-level defaults
2 parents a60e51a + 4234c16 commit 1602e54

4 files changed

Lines changed: 208 additions & 37 deletions

File tree

src/config/index.ts

Lines changed: 73 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,56 @@ import { getConfigFile } from './file';
2525
import { validateConfig } from './validators';
2626
import { 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;
3078
let _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
*/
107155
function 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
170221
export 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
182233
export 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
*/
205256
export 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
*/
224275
export 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

245296
export const getAPIs = () => {
246297
const config = loadFullConfiguration();
247-
return config.api || {};
298+
return config.api;
248299
};
249300

250301
export const getCookieSecret = (): string => {
@@ -259,25 +310,25 @@ export const getCookieSecret = (): string => {
259310

260311
export const getSessionMaxAgeHours = (): number => {
261312
const config = loadFullConfiguration();
262-
return config.sessionMaxAgeHours || 24;
313+
return config.sessionMaxAgeHours;
263314
};
264315

265316
// Get commit related configuration
266317
export const getCommitConfig = () => {
267318
const config = loadFullConfiguration();
268-
return config.commitConfig || {};
319+
return config.commitConfig;
269320
};
270321

271322
// Get attestation related configuration
272323
export const getAttestationConfig = () => {
273324
const config = loadFullConfiguration();
274-
return config.attestationConfig || {};
325+
return config.attestationConfig;
275326
};
276327

277328
// Get private organizations related configuration
278329
export 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
302353
export const getPlugins = () => {
303354
const config = loadFullConfiguration();
304-
return config.plugins || [];
355+
return config.plugins;
305356
};
306357

307358
export const getTLSKeyPemPath = (): string | undefined => {
@@ -321,12 +372,12 @@ export const getTLSEnabled = (): boolean => {
321372

322373
export const getDomains = () => {
323374
const config = loadFullConfiguration();
324-
return config.domains || {};
375+
return config.domains;
325376
};
326377

327378
export const getUIRouteAuth = () => {
328379
const config = loadFullConfiguration();
329-
return config.uiRouteAuth || {};
380+
return config.uiRouteAuth;
330381
};
331382

332383
export 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');

src/service/index.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,6 @@ const limiter = rateLimit(config.getRateLimit());
3535

3636
const { GIT_PROXY_UI_PORT: uiPort, GIT_PROXY_HTTPS_UI_PORT: uiHttpsPort } = serverConfig;
3737

38-
const DEFAULT_SESSION_MAX_AGE_HOURS = 12;
39-
4038
const app: Express = express();
4139
let _httpServer: http.Server | null = null;
4240
let _httpsServer: https.Server | null = null;
@@ -157,7 +155,7 @@ async function createApp(proxy: Proxy): Promise<Express> {
157155
cookie: {
158156
secure: 'auto',
159157
httpOnly: true,
160-
maxAge: (config.getSessionMaxAgeHours() || DEFAULT_SESSION_MAX_AGE_HOURS) * 60 * 60 * 1000,
158+
maxAge: config.getSessionMaxAgeHours() * 60 * 60 * 1000,
161159
},
162160
}),
163161
);

test/proxy.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { describe, it, beforeEach, afterEach, expect, vi, Mock } from 'vitest';
2020
import fs from 'fs';
2121
import { GitProxyConfig } from '../src/config/generated/config';
2222
import { Proxy } from '../src/proxy';
23-
import { handleAndLogError } from '../src/utils/errors';
23+
import { handleErrorAndLog } from '../src/utils/errors';
2424

2525
describe('Proxy Module TLS Certificate Loading', () => {
2626
let proxyModule: Proxy;
@@ -123,7 +123,7 @@ describe('Proxy Module TLS Certificate Loading', () => {
123123
try {
124124
await proxyModule.stop();
125125
} catch (error: unknown) {
126-
handleAndLogError(error, 'Error occurred when stopping the proxy');
126+
handleErrorAndLog(error, 'Error occurred when stopping the proxy');
127127
}
128128
vi.restoreAllMocks();
129129
});

0 commit comments

Comments
 (0)