feat: add secret settings manager and migration for legacy settings

- Implemented SecretSettingsManager to handle secret settings with encryption.
- Added functionality to migrate legacy plaintext settings into encrypted storage.
- Introduced methods for setting, getting, and clearing secret settings.
- Created tests for verifying the migration and canonicalization of secret settings.
- Updated app state to handle service updates via socket communication.
- Added interface for push service updates to manage service state changes.
This commit is contained in:
2026-04-19 01:47:06 +00:00
parent 618d4d674f
commit 061ce7c3f2
17 changed files with 413 additions and 73 deletions
+25
View File
@@ -1,6 +1,7 @@
import * as plugins from '../plugins.ts';
import { logger } from '../logging.ts';
import type { Onebox } from '../classes/onebox.ts';
import * as interfaces from '../../ts_interfaces/index.ts';
import * as handlers from './handlers/index.ts';
import { files as bundledFiles } from '../../ts_bundled/bundle.ts';
@@ -77,4 +78,28 @@ export class OpsServer {
logger.success('OpsServer stopped');
}
}
public async pushDashboardEvent(method: string, payload: unknown): Promise<void> {
const typedsocket = (this.server as any)?.typedserver?.typedsocket;
if (!typedsocket) {
return;
}
const connections = await typedsocket.findAllTargetConnectionsByTag('role', 'ops_dashboard');
await Promise.allSettled(
connections.map((connection: any) => typedsocket.createTypedRequest(method, connection).fire(payload)),
);
}
public async broadcastServiceUpdate(
serviceName: string,
action: interfaces.requests.IReq_PushServiceUpdate['request']['action'],
service?: interfaces.data.IService | null,
): Promise<void> {
await this.pushDashboardEvent('pushServiceUpdate', {
action,
serviceName,
service: service || undefined,
});
}
}
+2 -28
View File
@@ -91,21 +91,8 @@ export class PlatformHandler {
line: string,
isError: boolean,
): void {
const typedsocket = (this.opsServerRef.server as any)?.typedserver?.typedsocket;
if (!typedsocket) return;
const entry = this.parseLogLine(line, isError);
typedsocket.findAllTargetConnectionsByTag('role', 'ops_dashboard')
.then((connections: any[]) => {
for (const conn of connections) {
typedsocket.createTypedRequest(
'pushPlatformServiceLog',
conn,
).fire({ serviceType, entry }).catch(() => {});
}
})
.catch(() => {});
void this.opsServerRef.pushDashboardEvent('pushPlatformServiceLog', { serviceType, entry });
}
private pushServiceLogToClients(
@@ -113,21 +100,8 @@ export class PlatformHandler {
line: string,
isError: boolean,
): void {
const typedsocket = (this.opsServerRef.server as any)?.typedserver?.typedsocket;
if (!typedsocket) return;
const entry = this.parseLogLine(line, isError);
typedsocket.findAllTargetConnectionsByTag('role', 'ops_dashboard')
.then((connections: any[]) => {
for (const conn of connections) {
typedsocket.createTypedRequest(
'pushServiceLog',
conn,
).fire({ serviceName, entry }).catch(() => {});
}
})
.catch(() => {});
void this.opsServerRef.pushDashboardEvent('pushServiceLog', { serviceName, entry });
}
private registerHandlers(): void {
+10 -13
View File
@@ -11,12 +11,13 @@ export class SettingsHandler {
this.registerHandlers();
}
private getSettingsObject(): interfaces.data.ISettings {
private async getSettingsObject(): Promise<interfaces.data.ISettings> {
const db = this.opsServerRef.oneboxRef.database;
const settingsMap = db.getAllSettings(); // Returns Record<string, string>
const cloudflareToken = await db.getSecretSetting('cloudflareToken');
const settingsMap = db.getAllSettings();
return {
cloudflareToken: settingsMap['cloudflareToken'] || settingsMap['cloudflareAPIKey'] || '',
cloudflareToken: cloudflareToken || '',
cloudflareZoneId: settingsMap['cloudflareZoneId'] || '',
autoRenewCerts: settingsMap['autoRenewCerts'] === 'true',
renewalThreshold: parseInt(settingsMap['renewalThreshold'] || '30', 10),
@@ -33,7 +34,7 @@ export class SettingsHandler {
'getSettings',
async (dataArg) => {
await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const settings = this.getSettingsObject();
const settings = await this.getSettingsObject();
return { settings };
},
),
@@ -50,16 +51,15 @@ export class SettingsHandler {
// Store each setting as key-value pair
for (const [key, value] of Object.entries(updates)) {
if (value !== undefined) {
if (key === 'cloudflareToken') {
db.setSetting('cloudflareToken', String(value));
db.setSetting('cloudflareAPIKey', String(value));
if (db.isSecretSettingKey(key)) {
await db.setSecretSetting(key, String(value));
} else {
db.setSetting(key, String(value));
}
}
}
const settings = this.getSettingsObject();
const settings = await this.getSettingsObject();
return { settings };
},
),
@@ -70,8 +70,7 @@ export class SettingsHandler {
'setBackupPassword',
async (dataArg) => {
await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
this.opsServerRef.oneboxRef.database.setSetting('backup_encryption_password', dataArg.password);
this.opsServerRef.oneboxRef.database.setSetting('backupPassword', dataArg.password);
await this.opsServerRef.oneboxRef.database.setSecretSetting('backupPassword', dataArg.password);
return { ok: true };
},
),
@@ -82,9 +81,7 @@ export class SettingsHandler {
'getBackupPasswordStatus',
async (dataArg) => {
await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
const backupPassword = this.opsServerRef.oneboxRef.database.getSetting('backupPassword')
|| this.opsServerRef.oneboxRef.database.getSetting('backup_encryption_password');
const isConfigured = !!backupPassword;
const isConfigured = await this.opsServerRef.oneboxRef.database.hasSecretSetting('backupPassword');
return { status: { isConfigured } };
},
),