- Updated all package.json name fields and dependency references - Updated all TypeScript/JavaScript imports - Updated .woodpecker/publish.yml filters and registry paths - Updated tools/install.sh scope default - Updated .npmrc registry paths (worktree + host) - Enhanced update-checker.ts with checkForAllUpdates() multi-package support - Updated CLI update command to show table of all packages - Added KNOWN_PACKAGES, formatAllPackagesTable, getInstallAllCommand - Marked checkForUpdate() with @deprecated JSDoc Closes #391
119 lines
3.8 KiB
TypeScript
119 lines
3.8 KiB
TypeScript
import { Inject, Injectable, Logger } from '@nestjs/common';
|
|
import { eq, and, sql, type Db, preferences as preferencesTable } from '@mosaicstack/db';
|
|
import { DB } from '../database/database.module.js';
|
|
|
|
export const PLATFORM_DEFAULTS: Record<string, unknown> = {
|
|
'agent.defaultModel': null,
|
|
'agent.thinkingLevel': 'auto',
|
|
'agent.streamingEnabled': true,
|
|
'response.language': 'auto',
|
|
'response.codeAnnotations': true,
|
|
'safety.confirmDestructiveTools': true,
|
|
'session.autoCompactThreshold': 0.8,
|
|
'session.autoCompactEnabled': true,
|
|
'limits.maxThinkingLevel': null,
|
|
'limits.rateLimit': null,
|
|
};
|
|
|
|
export const IMMUTABLE_KEYS = new Set<string>(['limits.maxThinkingLevel', 'limits.rateLimit']);
|
|
|
|
@Injectable()
|
|
export class PreferencesService {
|
|
private readonly logger = new Logger(PreferencesService.name);
|
|
|
|
constructor(@Inject(DB) private readonly db: Db) {}
|
|
|
|
/**
|
|
* Returns the effective preference set for a user:
|
|
* Platform defaults → user overrides (mutable keys only) → enforcements re-applied last
|
|
*/
|
|
async getEffective(userId: string): Promise<Record<string, unknown>> {
|
|
const userPrefs = await this.getUserPrefs(userId);
|
|
const result: Record<string, unknown> = { ...PLATFORM_DEFAULTS };
|
|
|
|
for (const [key, value] of Object.entries(userPrefs)) {
|
|
if (!IMMUTABLE_KEYS.has(key)) {
|
|
result[key] = value;
|
|
}
|
|
}
|
|
|
|
// Re-apply immutable keys (enforcements always win)
|
|
for (const key of IMMUTABLE_KEYS) {
|
|
result[key] = PLATFORM_DEFAULTS[key];
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
async set(
|
|
userId: string,
|
|
key: string,
|
|
value: unknown,
|
|
): Promise<{ success: boolean; message: string }> {
|
|
if (IMMUTABLE_KEYS.has(key)) {
|
|
return {
|
|
success: false,
|
|
message: `Cannot override "${key}" — this is a platform enforcement. Contact your admin.`,
|
|
};
|
|
}
|
|
|
|
await this.upsertPref(userId, key, value);
|
|
return { success: true, message: `Preference "${key}" set to ${JSON.stringify(value)}.` };
|
|
}
|
|
|
|
async reset(userId: string, key: string): Promise<{ success: boolean; message: string }> {
|
|
if (IMMUTABLE_KEYS.has(key)) {
|
|
return { success: false, message: `Cannot reset "${key}" — it is a platform enforcement.` };
|
|
}
|
|
|
|
await this.deletePref(userId, key);
|
|
const defaultVal = PLATFORM_DEFAULTS[key];
|
|
return {
|
|
success: true,
|
|
message: `Preference "${key}" reset to default: ${JSON.stringify(defaultVal)}.`,
|
|
};
|
|
}
|
|
|
|
private async getUserPrefs(userId: string): Promise<Record<string, unknown>> {
|
|
const rows = await this.db
|
|
.select({ key: preferencesTable.key, value: preferencesTable.value })
|
|
.from(preferencesTable)
|
|
.where(eq(preferencesTable.userId, userId));
|
|
|
|
const result: Record<string, unknown> = {};
|
|
for (const row of rows) {
|
|
result[row.key] = row.value;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private async upsertPref(userId: string, key: string, value: unknown): Promise<void> {
|
|
// Single-round-trip upsert using INSERT … ON CONFLICT DO UPDATE.
|
|
// Previously this was two queries (SELECT + INSERT/UPDATE), which doubled
|
|
// the DB round-trips and introduced a TOCTOU window under concurrent writes.
|
|
await this.db
|
|
.insert(preferencesTable)
|
|
.values({
|
|
userId,
|
|
key,
|
|
value: value as never,
|
|
mutable: true,
|
|
})
|
|
.onConflictDoUpdate({
|
|
target: [preferencesTable.userId, preferencesTable.key],
|
|
set: {
|
|
value: sql`excluded.value`,
|
|
updatedAt: sql`now()`,
|
|
},
|
|
});
|
|
this.logger.debug(`Upserted preference "${key}" for user ${userId}`);
|
|
}
|
|
|
|
private async deletePref(userId: string, key: string): Promise<void> {
|
|
await this.db
|
|
.delete(preferencesTable)
|
|
.where(and(eq(preferencesTable.userId, userId), eq(preferencesTable.key, key)));
|
|
this.logger.debug(`Deleted preference "${key}" for user ${userId}`);
|
|
}
|
|
}
|