Skip to content

Commit 5f91eb5

Browse files
gabemonteroclaude
andcommitted
fix(boost): tighten API surface and harden error paths
- Remove encryptValue/decryptValue from public exports (internal only) - Mark encryptionSecret, set(), remove() as @internal - Auto-delete corrupt JSON rows in getOverride - Handle decryption failures from rotated secrets gracefully - Use knex.fn.now() for DB timestamps instead of client-side Date - Add tests for corrupt JSON and rotated-secret error paths Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: gabemontero <gmontero@redhat.com>
1 parent 016b5b3 commit 5f91eb5

7 files changed

Lines changed: 91 additions & 21 deletions

File tree

workspaces/boost/plugins/boost-backend/report.api.md

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ export class AdminConfigService {
3232
export interface AdminConfigServiceOptions {
3333
// (undocumented)
3434
database: DatabaseService;
35-
encryptionSecret?: string;
3635
// (undocumented)
3736
logger: LoggerService;
3837
}
@@ -161,12 +160,6 @@ export function createAgentResourceLoader(): ResourceLoader;
161160
// @public
162161
export function createToolResourceLoader(): ResourceLoader;
163162

164-
// @public
165-
export function decryptValue(encrypted: string, secret: string): string;
166-
167-
// @public
168-
export function encryptValue(plaintext: string, secret: string): string;
169-
170163
// @public
171164
export function isDbWritable(key: BoostConfigKey): boolean;
172165

@@ -195,10 +188,8 @@ export type ResourceLoader = (req: Request_2) => Promise<
195188
export class RuntimeConfigResolver {
196189
constructor(options: RuntimeConfigResolverOptions);
197190
invalidate(): Promise<void>;
198-
remove(key: BoostConfigKey): Promise<void>;
199191
resolve(key: BoostConfigKey): Promise<unknown | undefined>;
200192
resolveAll(): Promise<Map<string, unknown>>;
201-
set(key: BoostConfigKey, value: unknown): Promise<void>;
202193
}
203194

204195
// @public

workspaces/boost/plugins/boost-backend/src/config/AdminConfigService.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,69 @@ describe('AdminConfigService', () => {
232232
});
233233
});
234234

235+
describe('corrupt data handling', () => {
236+
it('getOverride returns undefined and deletes corrupt JSON row', async () => {
237+
mockKnex._rows.push({
238+
key: 'boost.model.baseUrl',
239+
value: '<<<not-json>>>',
240+
schema_version: 1,
241+
updated_at: new Date().toISOString(),
242+
});
243+
244+
const value = await service.getOverride('boost.model.baseUrl');
245+
expect(value).toBeUndefined();
246+
expect(logger.error).toHaveBeenCalledWith(
247+
expect.stringContaining('Corrupt value'),
248+
);
249+
// Row should be deleted
250+
const row = mockKnex._rows.find(
251+
(r: { key: string }) => r.key === 'boost.model.baseUrl',
252+
);
253+
expect(row).toBeUndefined();
254+
});
255+
256+
it('getAllOverrides skips corrupt JSON rows', async () => {
257+
await service.setOverride('boost.model.name', 'gpt-4');
258+
mockKnex._rows.push({
259+
key: 'boost.model.baseUrl',
260+
value: '<<<not-json>>>',
261+
schema_version: 1,
262+
updated_at: new Date().toISOString(),
263+
});
264+
265+
const overrides = await service.getAllOverrides();
266+
expect(overrides.size).toBe(1);
267+
expect(overrides.get('boost.model.name')).toBe('gpt-4');
268+
expect(overrides.has('boost.model.baseUrl')).toBe(false);
269+
});
270+
271+
it('getOverride returns undefined when decryption fails (rotated secret)', async () => {
272+
// Write with the current secret
273+
await service.setOverride(
274+
'boost.devSpaces.credentials',
275+
'my-secret-token',
276+
);
277+
278+
// Create a new service with a different secret
279+
const database: DatabaseService = {
280+
getClient: async () => mockKnex,
281+
} as unknown as DatabaseService;
282+
const rotatedService = new AdminConfigService({
283+
database,
284+
logger,
285+
encryptionSecret: 'different-secret',
286+
});
287+
288+
const value = await rotatedService.getOverride(
289+
'boost.devSpaces.credentials',
290+
);
291+
expect(value).toBeUndefined();
292+
expect(logger.error).toHaveBeenCalledWith(
293+
expect.stringContaining('Failed to decrypt'),
294+
);
295+
});
296+
});
297+
235298
describe('removeOverride', () => {
236299
it('removes an existing override', async () => {
237300
await service.setOverride(

workspaces/boost/plugins/boost-backend/src/config/AdminConfigService.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,10 @@ interface AdminConfigRow {
5252
export interface AdminConfigServiceOptions {
5353
database: DatabaseService;
5454
logger: LoggerService;
55-
/** Secret used for encrypting sensitive config values. */
55+
/**
56+
* Secret used for encrypting sensitive config values.
57+
* @internal
58+
*/
5659
encryptionSecret?: string;
5760
}
5861

@@ -130,8 +133,9 @@ export class AdminConfigService {
130133
rawValue = JSON.parse(row.value);
131134
} catch {
132135
this.logger.error(
133-
`Corrupt value for config key "${key}" — skipping (invalid JSON)`,
136+
`Corrupt value for config key "${key}" — removing (invalid JSON)`,
134137
);
138+
await knex<AdminConfigRow>(TABLE_NAME).where({ key }).delete();
135139
return undefined;
136140
}
137141

@@ -143,7 +147,14 @@ export class AdminConfigService {
143147
);
144148
return undefined;
145149
}
146-
rawValue = decryptValue(rawValue, this.encryptionSecret);
150+
try {
151+
rawValue = decryptValue(rawValue, this.encryptionSecret);
152+
} catch {
153+
this.logger.error(
154+
`Failed to decrypt sensitive field "${key}" — secret may have been rotated`,
155+
);
156+
return undefined;
157+
}
147158
}
148159

149160
return rawValue;
@@ -183,7 +194,14 @@ export class AdminConfigService {
183194
);
184195
continue;
185196
}
186-
rawValue = decryptValue(rawValue, this.encryptionSecret);
197+
try {
198+
rawValue = decryptValue(rawValue, this.encryptionSecret);
199+
} catch {
200+
this.logger.error(
201+
`Failed to decrypt sensitive field "${key}" — secret may have been rotated`,
202+
);
203+
continue;
204+
}
187205
}
188206

189207
result.set(row.key, rawValue);
@@ -227,20 +245,19 @@ export class AdminConfigService {
227245
}
228246

229247
const knex = await this.getDb();
230-
const now = new Date().toISOString();
231248

232249
await knex<AdminConfigRow>(TABLE_NAME)
233250
.insert({
234251
key,
235252
value: serialized,
236253
schema_version: BOOST_CONFIG_SCHEMA_VERSION,
237-
updated_at: now,
254+
updated_at: knex.fn.now() as unknown as string,
238255
})
239256
.onConflict('key')
240257
.merge({
241258
value: serialized,
242259
schema_version: BOOST_CONFIG_SCHEMA_VERSION,
243-
updated_at: now,
260+
updated_at: knex.fn.now() as unknown as string,
244261
});
245262

246263
this.logger.info(`Config override set: ${key}`);

workspaces/boost/plugins/boost-backend/src/config/RuntimeConfigResolver.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ export class RuntimeConfigResolver {
115115
*
116116
* @param key - The config field key.
117117
* @param value - The value to store.
118+
* @internal
118119
*/
119120
async set(key: BoostConfigKey, value: unknown): Promise<void> {
120121
await this.adminConfigService.setOverride(key, value);
@@ -126,6 +127,7 @@ export class RuntimeConfigResolver {
126127
* baseline is restored.
127128
*
128129
* @param key - The config field key.
130+
* @internal
129131
*/
130132
async remove(key: BoostConfigKey): Promise<void> {
131133
await this.adminConfigService.removeOverride(key);

workspaces/boost/plugins/boost-backend/src/config/encryption.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ function deriveKey(secret: string): Buffer {
4444
* @param secret - The encryption secret (will be derived to a 256-bit key).
4545
* @returns The encrypted value as a base64-encoded string.
4646
*
47-
* @public
47+
* @internal
4848
*/
4949
export function encryptValue(plaintext: string, secret: string): string {
5050
const key = deriveKey(secret);
@@ -71,7 +71,7 @@ export function encryptValue(plaintext: string, secret: string): string {
7171
* @returns The decrypted plaintext string.
7272
* @throws Error if decryption fails (wrong key, tampered data, etc.)
7373
*
74-
* @public
74+
* @internal
7575
*/
7676
export function decryptValue(encrypted: string, secret: string): string {
7777
const key = deriveKey(secret);

workspaces/boost/plugins/boost-backend/src/config/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,3 @@ export {
3232
type ConfigScope,
3333
type ConfigFieldMeta,
3434
} from './schemas';
35-
export { encryptValue, decryptValue } from './encryption';

workspaces/boost/plugins/boost-backend/src/index.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,6 @@ export {
4242
validateConfigValue,
4343
isDbWritable,
4444
isSensitiveField,
45-
encryptValue,
46-
decryptValue,
4745
type AdminConfigServiceOptions,
4846
type RuntimeConfigResolverOptions,
4947
type BoostConfigKey,

0 commit comments

Comments
 (0)