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:
saravanakumardb1 2026-03-21 15:25:41 -07:00
parent e85cfeb0f1
commit 20cc3e4e49
3 changed files with 34 additions and 0 deletions

View File

@ -15,6 +15,7 @@ const envSchema = baseBackendConfigSchema.extend({
TELEMETRY_ENABLED: z.coerce.boolean().default(false),
FEATURE_FLAGS_ENABLED: z.coerce.boolean().default(false),
// ── Field Encryption (@bytelyst/field-encrypt) ──
FIELD_ENCRYPT_ENABLED: z.coerce.boolean().default(true),
FIELD_ENCRYPT_KEY_PROVIDER: z.enum(['akv', 'env', 'memory']).default('memory'),
FIELD_ENCRYPT_KEY: z.string().default(''),
FIELD_ENCRYPT_MEK_NAME: z.string().default('notelett-mek'),

View File

@ -1,16 +1,45 @@
/**
* Field encryption singleton for NoteLett backend.
*
* 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 encryption_enabled flag from platform-service at startup. */
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 {
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,
@ -23,4 +52,5 @@ export function getEncryptor(): FieldEncryptor {
/** @internal — for testing only. */
export function _resetEncryptor(): void {
_encryptor = null;
_enabled = config.FIELD_ENCRYPT_ENABLED;
}

View File

@ -8,6 +8,7 @@ import { noteTaskRoutes } from './modules/note-tasks/routes.js';
import { savedViewRoutes } from './modules/saved-views/routes.js';
import { workspaceRoutes } from './modules/workspaces/routes.js';
import { initCosmosIfNeeded } from './lib/cosmos-init.js';
import { initEncryption } from './lib/field-encrypt.js';
import { initDatastore } from './lib/datastore.js';
import { config } from './lib/config.js';
import { getAllFlags } from './lib/feature-flags.js';
@ -76,4 +77,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 });