feat(backend): admin-panel encryption toggle via initEncryption()
- FIELD_ENCRYPT_ENABLED env var (default: true, fallback only) - initEncryption(productId) polls encryption_enabled from platform-service - Admin panel toggle takes precedence, 3s timeout graceful fallback
This commit is contained in:
parent
253a9db0ea
commit
8c90d863a8
@ -9,6 +9,7 @@ const envSchema = baseBackendConfigSchema.extend({
|
|||||||
PLATFORM_SERVICE_URL: z.string().default('http://localhost:4003'),
|
PLATFORM_SERVICE_URL: z.string().default('http://localhost:4003'),
|
||||||
TELEMETRY_ENABLED: z.coerce.boolean().default(false),
|
TELEMETRY_ENABLED: z.coerce.boolean().default(false),
|
||||||
FEATURE_FLAGS_ENABLED: z.coerce.boolean().default(false),
|
FEATURE_FLAGS_ENABLED: z.coerce.boolean().default(false),
|
||||||
|
FIELD_ENCRYPT_ENABLED: z.coerce.boolean().default(true),
|
||||||
FIELD_ENCRYPT_KEY_PROVIDER: z.enum(['memory', 'env', 'akv']).default('memory'),
|
FIELD_ENCRYPT_KEY_PROVIDER: z.enum(['memory', 'env', 'akv']).default('memory'),
|
||||||
FIELD_ENCRYPT_KEY: z.string().optional(),
|
FIELD_ENCRYPT_KEY: z.string().optional(),
|
||||||
FIELD_ENCRYPT_MEK_NAME: z.string().default('chronomind-mek'),
|
FIELD_ENCRYPT_MEK_NAME: z.string().default('chronomind-mek'),
|
||||||
|
|||||||
@ -3,17 +3,51 @@
|
|||||||
*
|
*
|
||||||
* Encrypts sensitive fields (e.g., timer/routine descriptions, webhook secrets)
|
* Encrypts sensitive fields (e.g., timer/routine descriptions, webhook secrets)
|
||||||
* at rest using AES-256-GCM envelope encryption via @bytelyst/field-encrypt.
|
* at rest using AES-256-GCM envelope encryption via @bytelyst/field-encrypt.
|
||||||
|
*
|
||||||
|
* Toggle precedence:
|
||||||
|
* 1. Admin panel `encryption_enabled` flag (polled at startup via initEncryption)
|
||||||
|
* 2. FIELD_ENCRYPT_ENABLED env var (fallback if platform-service unreachable)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createFieldEncryptor, type FieldEncryptor } from '@bytelyst/field-encrypt';
|
import { createFieldEncryptor, type FieldEncryptor } from '@bytelyst/field-encrypt';
|
||||||
import { config } from './config.js';
|
import { config } from './config.js';
|
||||||
|
|
||||||
let _encryptor: FieldEncryptor | null = null;
|
let _encryptor: FieldEncryptor | null = null;
|
||||||
|
let _enabled: boolean = config.FIELD_ENCRYPT_ENABLED;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Poll the `encryption_enabled` feature flag from platform-service.
|
||||||
|
* Call once during server startup. Falls back to env var if unreachable.
|
||||||
|
*
|
||||||
|
* @param productId — canonical product ID from shared/product.json
|
||||||
|
*/
|
||||||
|
export async function initEncryption(productId: string, logger?: { info: (msg: string) => void }): Promise<void> {
|
||||||
|
const log = logger ?? { info: () => {} };
|
||||||
|
try {
|
||||||
|
const url = `${config.PLATFORM_SERVICE_URL}/flags/poll?platform=backend`;
|
||||||
|
const res = await fetch(url, {
|
||||||
|
signal: AbortSignal.timeout(3000),
|
||||||
|
headers: { 'x-product-id': productId },
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
const data = (await res.json()) as { flags: Record<string, boolean> };
|
||||||
|
if (typeof data.flags?.encryption_enabled === 'boolean') {
|
||||||
|
_enabled = data.flags.encryption_enabled;
|
||||||
|
log.info(`Encryption flag from admin panel: ${_enabled ? 'ON' : 'OFF'}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// platform-service unreachable — use env var
|
||||||
|
}
|
||||||
|
log.info(`Encryption from env var FIELD_ENCRYPT_ENABLED: ${_enabled ? 'ON' : 'OFF'}`);
|
||||||
|
}
|
||||||
|
|
||||||
export function getEncryptor(): FieldEncryptor {
|
export function getEncryptor(): FieldEncryptor {
|
||||||
if (_encryptor) return _encryptor;
|
if (_encryptor) return _encryptor;
|
||||||
|
|
||||||
_encryptor = createFieldEncryptor({
|
_encryptor = createFieldEncryptor({
|
||||||
|
enabled: _enabled,
|
||||||
keyProvider: config.FIELD_ENCRYPT_KEY_PROVIDER,
|
keyProvider: config.FIELD_ENCRYPT_KEY_PROVIDER,
|
||||||
encryptionKey: config.FIELD_ENCRYPT_KEY || undefined,
|
encryptionKey: config.FIELD_ENCRYPT_KEY || undefined,
|
||||||
keyVaultUrl: config.AZURE_KEYVAULT_URL || undefined,
|
keyVaultUrl: config.AZURE_KEYVAULT_URL || undefined,
|
||||||
@ -26,6 +60,7 @@ export function getEncryptor(): FieldEncryptor {
|
|||||||
/** @internal — for testing only. */
|
/** @internal — for testing only. */
|
||||||
export function _resetEncryptor(): void {
|
export function _resetEncryptor(): void {
|
||||||
_encryptor = null;
|
_encryptor = null;
|
||||||
|
_enabled = config.FIELD_ENCRYPT_ENABLED;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { isEncryptedField } from '@bytelyst/field-encrypt';
|
export { isEncryptedField } from '@bytelyst/field-encrypt';
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import { sharedTimerRoutes } from './modules/shared-timers/routes.js';
|
|||||||
import { webhookRoutes } from './modules/webhooks/routes.js';
|
import { webhookRoutes } from './modules/webhooks/routes.js';
|
||||||
import { initCosmosIfNeeded } from './lib/cosmos-init.js';
|
import { initCosmosIfNeeded } from './lib/cosmos-init.js';
|
||||||
import { initDatastore } from './lib/datastore.js';
|
import { initDatastore } from './lib/datastore.js';
|
||||||
|
import { initEncryption } from './lib/field-encrypt.js';
|
||||||
import { config } from './lib/config.js';
|
import { config } from './lib/config.js';
|
||||||
import { getAllFlags } from './lib/feature-flags.js';
|
import { getAllFlags } from './lib/feature-flags.js';
|
||||||
import { getBufferedEvents, flushEvents } from './lib/telemetry.js';
|
import { getBufferedEvents, flushEvents } from './lib/telemetry.js';
|
||||||
@ -74,4 +75,6 @@ app.get('/api/diagnostics/config', async () => ({
|
|||||||
featureFlagsEnabled: config.FEATURE_FLAGS_ENABLED,
|
featureFlagsEnabled: config.FEATURE_FLAGS_ENABLED,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
await initEncryption(PRODUCT_ID, app.log);
|
||||||
|
|
||||||
await startService(app, { port: config.PORT, host: config.HOST });
|
await startService(app, { port: config.PORT, host: config.HOST });
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user