a general typescript configuration service
A general typescript configuration service
Unless forced to create a new service, this service will return the first created service
Create a new class to define your configuration.
The class should extend the Config class from this repo
import { IsNumber, IsString } from 'class-validator';
import { BaseConfig, Configuration, ConfigVariable } from '@kibibit/configit';
@Configuration()
export class ProjectConfig extends BaseConfig {
@ConfigVariable('Server port')
@IsNumber()
PORT: number;
@ConfigVariable([
'This is the slack API to talk and report to channel "hello"'
])
@IsString()
SLACK_API_KEY: string;
}
}
Then, in your code, initialize the config service when you bootstrap your application
import express from 'express';
import { ConfigService } from '@kibibit/configit';
import { ProjectConfig } from './project-config.model';
export const configService = new ConfigService<ProjectConfig>(ProjectConfig);
const app = express();
app.get( '/', ( req, res ) => {
res.send( 'Hello world!' );
} );
app.listen(configService.config.PORT, () => {
console.log(
`server started at http://localhost:${ configService.config.PORT }`
);
});
You can extend the configuration to add your own customization and functions!
import { chain } from 'lodash';
import { ConfigService, IConfigServiceOptions } from '@kibibit/configit';
import { WinstonLogger } from '@kibibit/nestjs-winston';
import { ExtProjectConfig } from './ext-project-config.model';
import { initializeWinston } from './winston.config';
export class ExtConfigService extends ConfigService<ExtProjectConfig> {
public logger: WinstonLogger;
constructor(passedConfig?: Partial<ExtProjectConfig>, options: IConfigServiceOptions = {}) {
super(ExtProjectConfig, passedConfig, options);
initializeWinston(this.appRoot);
this.logger = new WinstonLogger('');
}
getSlackApiObject() {
const slackApiObject = chain(this.toPlainObject())
.pickBy((value, key) => key.startsWith('SLACK_'))
.mapKeys((value, key) => key.replace(/^SLACK_/i, ''))
.mapKeys((value, key) => key.toLowerCase())
.value();
return slackApiObject;
}
}
export const configService = new ExtConfigService() as ExtConfigService;
Configit supports HashiCorp Vault integration for dynamic secrets management with automatic TTL-based refresh. This enables secure, centralized secret management with automatic rotation for database credentials, API keys, and other sensitive configuration values.
Vault integration provides:
http://127.0.0.1:8200secret/data/myapp/api_keydatabase/creds/my-roleVault integration is included in @kibibit/configit - no additional packages required.
Configure Vault integration by passing vault options to ConfigService:
import { ConfigService, IVaultConfigOptions } from '@kibibit/configit';
const vaultOptions: IVaultConfigOptions = {
endpoint: process.env.VAULT_ADDR || 'http://127.0.0.1:8200',
auth: {
method: 'token',
config: {
token: process.env.VAULT_TOKEN
}
},
tls: {
enabled: true,
verifyCertificate: true
},
refreshBuffer: 300, // Refresh 5 minutes before expiry (default)
fallback: {
required: true, // Fail fast if Vault unavailable
useCacheOnFailure: true,
maxCacheAge: 3600000 // 1 hour
}
};
const configService = new ConfigService(ProjectConfig, undefined, {
vault: vaultOptions
});
// Initialize Vault (async - call before accessing config)
await configService.initializeVault();
Token Authentication (Development):
auth: {
method: 'token',
config: {
token: process.env.VAULT_TOKEN
}
}
AppRole Authentication (Production):
auth: {
method: 'approle',
config: {
roleId: process.env.VAULT_ROLE_ID,
secretId: process.env.VAULT_SECRET_ID, // From secure source
mountPath: 'approle' // Optional, defaults to 'approle'
}
}
GCP IAM Authentication:
auth: {
method: 'gcp',
config: {
role: 'my-vault-role',
serviceAccountKeyFile: '/path/to/key.json', // Optional, uses ADC if not provided
serviceAccountEmail: 'my-service@project.iam.gserviceaccount.com', // Optional
jwtExpiration: 900 // Optional, default 15 minutes
}
}
AWS IAM Authentication:
auth: {
method: 'aws',
config: {
role: 'my-vault-role'
// Uses instance profile or environment credentials automatically
}
}
Multiple Auth Methods (Fallback Chain):
auth: {
methods: [
{
type: 'gcp',
config: { role: 'my-role' }
},
{
type: 'approle',
config: {
roleId: process.env.VAULT_ROLE_ID,
secretId: process.env.VAULT_SECRET_ID
}
}
]
}
tls: {
enabled: true, // Required (cannot be disabled)
verifyCertificate: true, // Verify server certificate
certificateFingerprint: 'sha256:...', // Optional: pin certificate
caCert: '-----BEGIN CERTIFICATE-----\n...', // Optional: custom CA
minVersion: 'TLSv1.2' // Optional: minimum TLS version
}
The refresh buffer determines when secrets are refreshed before expiration:
refreshBuffer: 300 // Refresh 300 seconds (5 minutes) before TTL expires
Default: min(10% of TTL, 300 seconds) - whichever is smaller.
Use composable decorators to mark configuration properties as Vault secrets. Decorators work alongside @ConfigVariable and class-validator decorators.
@VaultPath(path) - RequiredSpecifies the Vault path for a property. Any property with @VaultPath is treated as a Vault secret.
@VaultPath('secret/data/myapp/api_key')
API_KEY: string;
@VaultEngine(type) - OptionalSpecifies the Vault secrets engine type. Auto-detected from path if not provided.
@VaultEngine('database') // 'kv-v1', 'kv-v2', 'database', 'aws', 'gcp', etc.
DATABASE_PASSWORD: string;
Supported engines:
kv-v1, kv-v2: Key-Value stores (default: kv-v2)database: Database secrets engine (dynamic credentials)aws, azure, gcp: Cloud provider secrets enginestransit, pki: Encryption and certificate enginescustom: Custom engines (requires custom extraction logic)@VaultKey(key) - OptionalSpecifies the key name within the secret. Only used for KV v1/v2 engines. Defaults to property name in kebab-case.
@VaultKey('api_key') // If key name differs from property name
API_KEY: string;
@VaultRefreshBuffer(seconds) - OptionalOverride default refresh buffer for a specific secret.
@VaultRefreshBuffer(600) // Refresh 10 minutes before expiry
DATABASE_PASSWORD: string;
@VaultOptional() - OptionalMark secret as optional - allows fallback to environment variable if Vault unavailable.
@VaultOptional()
FEATURE_FLAG?: boolean;
import { BaseConfig, Configuration, ConfigVariable, VaultPath, VaultKey } from '@kibibit/configit';
import { IsString } from 'class-validator';
@Configuration()
export class AppConfig extends BaseConfig {
@VaultPath('secret/data/myapp/api_key')
@VaultKey('api_key')
@ConfigVariable('API key for external service')
@IsString()
API_KEY: string;
@ConfigVariable('Server port')
@IsNumber()
PORT: number;
}
// Initialize with Vault
const configService = new ConfigService(AppConfig, undefined, {
vault: {
endpoint: process.env.VAULT_ADDR || 'http://127.0.0.1:8200',
auth: {
method: 'token',
config: {
token: process.env.VAULT_TOKEN
}
}
}
});
await configService.initializeVault();
console.log(configService.config.API_KEY); // Loaded from Vault
const configService = new ConfigService(AppConfig, undefined, {
vault: {
endpoint: 'https://vault.example.com',
auth: {
method: 'gcp',
config: {
role: 'my-vault-role'
// Uses Application Default Credentials (ADC)
}
},
tls: {
enabled: true,
verifyCertificate: true
}
}
});
await configService.initializeVault();
import { BaseConfig, Configuration, ConfigVariable, VaultPath, VaultEngine, VaultKey } from '@kibibit/configit';
import { IsString } from 'class-validator';
@Configuration()
export class DatabaseConfig extends BaseConfig {
@VaultPath('database/creds/my-role')
@VaultEngine('database')
@VaultKey('username')
@ConfigVariable('Database username')
@IsString()
DATABASE_USERNAME: string;
@VaultPath('database/creds/my-role')
@VaultEngine('database')
@VaultKey('password')
@ConfigVariable('Database password')
@IsString()
DATABASE_PASSWORD: string;
}
Behavior:
username and password refreshed together (same lease)@Configuration()
export class OptionalConfig extends BaseConfig {
@VaultPath('secret/data/myapp/feature_flag')
@VaultKey('enabled')
@VaultOptional()
@ConfigVariable('Optional feature flag')
@IsOptional()
@IsBoolean()
FEATURE_FLAG_ENABLED?: boolean;
}
If Vault is unavailable, falls back to FEATURE_FLAG_ENABLED environment variable.
@Configuration()
export class MixedConfig extends BaseConfig {
// From Vault
@VaultPath('database/creds/my-role')
@VaultEngine('database')
@VaultKey('password')
@ConfigVariable('Database password')
@IsString()
DATABASE_PASSWORD: string;
// From environment variable (no Vault decorators)
@ConfigVariable('Database host')
@IsString()
DATABASE_HOST: string;
// From Vault
@VaultPath('secret/data/myapp/api_key')
@ConfigVariable('API key')
@IsString()
API_KEY: string;
}
Configit uses the following source priority (highest to lowest):
--key=value)KEY=value).env.development.json, etc.) - Lowest priorityVault secrets are injected into nconf.overrides() which has the highest priority in the nconf hierarchy.
Monitor Vault connection status and secret refresh schedule:
const health = configService.getVaultHealth();
if (health) {
console.log('Connected:', health.connected);
console.log('Authenticated:', health.authenticated);
console.log('Cached secrets:', health.cacheSize);
console.log('Scheduled refreshes:', health.refreshQueueSize);
console.log('Last refresh:', new Date(health.lastRefreshTime));
console.log('Recent errors:', health.errors);
}
Health Status Fields:
connected: Whether connected to Vaultauthenticated: Whether authenticated successfullycacheSize: Number of cached secretsrefreshQueueSize: Number of scheduled refresheslastRefreshTime: Timestamp of last refresherrors: Array of recent errors (last 10)See examples/vault-nestjs-sequelize/ for a complete example showing:
// In NestJS onModuleInit
export class ConfigModule implements OnModuleInit {
async onModuleInit() {
await configService.initializeVault();
}
}
Vault Connection Failed:
vault statusSecrets Not Refreshing:
getVaultHealth() for refresh queue statusSecret Not Found:
vault kv get secret/data/myapp/api_key@VaultOptional() to allow fallbackFor more examples and advanced usage, see the examples/ folder.
yaml-config in the examples folder--saveToFile or --initSee the examples folder for a variety of usage examples
Thanks goes to these wonderful people (emoji key):
Neil Kalman π» π π¨ π§ π β οΈ |
Nitzan Madar π» |
Dafna Assaf π» |
This project follows the all-contributors specification. Contributions of any kind are welcome!