From 8c90d863a8caf29f090fccad44ae2d678543ff83 Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Sat, 21 Mar 2026 15:24:58 -0700 Subject: [PATCH] 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 --- backend/src/lib/config.ts | 1 + backend/src/lib/field-encrypt.ts | 35 ++++++++++++++++++++++++++++++++ backend/src/server.ts | 3 +++ 3 files changed, 39 insertions(+) diff --git a/backend/src/lib/config.ts b/backend/src/lib/config.ts index c075611..9b3fbeb 100644 --- a/backend/src/lib/config.ts +++ b/backend/src/lib/config.ts @@ -9,6 +9,7 @@ const envSchema = baseBackendConfigSchema.extend({ PLATFORM_SERVICE_URL: z.string().default('http://localhost:4003'), TELEMETRY_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: z.string().optional(), FIELD_ENCRYPT_MEK_NAME: z.string().default('chronomind-mek'), diff --git a/backend/src/lib/field-encrypt.ts b/backend/src/lib/field-encrypt.ts index da2d2d4..3e3b715 100644 --- a/backend/src/lib/field-encrypt.ts +++ b/backend/src/lib/field-encrypt.ts @@ -3,17 +3,51 @@ * * Encrypts sensitive fields (e.g., timer/routine descriptions, webhook secrets) * 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 { config } from './config.js'; 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 { + 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 }; + 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 { if (_encryptor) return _encryptor; _encryptor = createFieldEncryptor({ + enabled: _enabled, keyProvider: config.FIELD_ENCRYPT_KEY_PROVIDER, encryptionKey: config.FIELD_ENCRYPT_KEY || undefined, keyVaultUrl: config.AZURE_KEYVAULT_URL || undefined, @@ -26,6 +60,7 @@ export function getEncryptor(): FieldEncryptor { /** @internal — for testing only. */ export function _resetEncryptor(): void { _encryptor = null; + _enabled = config.FIELD_ENCRYPT_ENABLED; } export { isEncryptedField } from '@bytelyst/field-encrypt'; diff --git a/backend/src/server.ts b/backend/src/server.ts index 3d40378..98c5d0e 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -14,6 +14,7 @@ import { sharedTimerRoutes } from './modules/shared-timers/routes.js'; import { webhookRoutes } from './modules/webhooks/routes.js'; import { initCosmosIfNeeded } from './lib/cosmos-init.js'; import { initDatastore } from './lib/datastore.js'; +import { initEncryption } from './lib/field-encrypt.js'; import { config } from './lib/config.js'; import { getAllFlags } from './lib/feature-flags.js'; import { getBufferedEvents, flushEvents } from './lib/telemetry.js'; @@ -74,4 +75,6 @@ app.get('/api/diagnostics/config', async () => ({ featureFlagsEnabled: config.FEATURE_FLAGS_ENABLED, })); +await initEncryption(PRODUCT_ID, app.log); + await startService(app, { port: config.PORT, host: config.HOST });