@kibibit/configit
v2.12.0
Published
a general typescript configuration service
Readme
Unless forced to create a new service, this service will return the first created service
Usage
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 }`
);
});
Extending the Configuration Service (Recommended)
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;
Vault Integration
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.
Overview
Vault integration provides:
- Dynamic Secrets: Automatically rotate credentials before expiration
- Centralized Management: Store secrets in HashiCorp Vault
- Automatic Refresh: Background refresh based on TTL (Time To Live)
- Multiple Engines: Support for KV v1/v2, Database, AWS, GCP, Azure, and more
- Source Priority: Vault secrets have highest priority (override env vars and config files)
- Graceful Fallback: Optional secrets can fall back to environment variables
Prerequisites
HashiCorp Vault running and accessible
- Development:
http://127.0.0.1:8200 - Production: HTTPS endpoint (required)
- Development:
Authentication Method configured:
- Token: Simple token-based auth (dev only)
- AppRole: Recommended for production
- GCP IAM: For GCP workloads
- AWS IAM: For AWS workloads
Vault Secrets configured:
- KV secrets:
secret/data/myapp/api_key - Database credentials:
database/creds/my-role - Other engines as needed
- KV secrets:
Installation
Vault integration is included in @kibibit/configit - no additional packages required.
Configuration with Vault
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();Authentication Methods
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: '[email protected]', // 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 Configuration
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
}Refresh Buffer
The refresh buffer determines when secrets are refreshed before expiration:
refreshBuffer: 300 // Refresh 300 seconds (5 minutes) before TTL expiresDefault: min(10% of TTL, 300 seconds) - whichever is smaller.
Vault Decorators
Use composable decorators to mark configuration properties as Vault secrets. Decorators work alongside @ConfigVariable and class-validator decorators.
@VaultPath(path) - Required
Specifies 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) - Optional
Specifies 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) - Optional
Specifies 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) - Optional
Override default refresh buffer for a specific secret.
@VaultRefreshBuffer(600) // Refresh 10 minutes before expiry
DATABASE_PASSWORD: string;@VaultOptional() - Optional
Mark secret as optional - allows fallback to environment variable if Vault unavailable.
@VaultOptional()
FEATURE_FLAG?: boolean;Usage Examples
Basic Example with Token Auth
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 VaultGCP IAM Authentication
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();Dynamic Database Credentials
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:
- Credentials automatically refresh before TTL expires
- Both
usernameandpasswordrefreshed together (same lease) - Background refresh happens seamlessly
Optional Secret with Fallback
@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.
Mixed Vault and Non-Vault Properties
@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;
}Source Hierarchy
Configit uses the following source priority (highest to lowest):
- Vault (if configured) - Highest priority
- CLI arguments (
--key=value) - Environment variables (
KEY=value) - Config files (
.env.development.json, etc.) - Lowest priority
Vault secrets are injected into nconf.overrides() which has the highest priority in the nconf hierarchy.
Health Monitoring
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)
Example: NestJS Integration
See examples/vault-nestjs-sequelize/ for a complete example showing:
- NestJS application setup
- Sequelize with dynamic database credentials
- Automatic credential rotation
- Health check endpoints
// In NestJS onModuleInit
export class ConfigModule implements OnModuleInit {
async onModuleInit() {
await configService.initializeVault();
}
}Troubleshooting
Vault Connection Failed:
- Verify Vault is running:
vault status - Check endpoint URL and TLS configuration
- Verify authentication credentials
Secrets Not Refreshing:
- Check
getVaultHealth()for refresh queue status - Verify TTL is set on secrets (dynamic secrets only)
- Check refresh buffer configuration
Secret Not Found:
- Verify Vault path exists:
vault kv get secret/data/myapp/api_key - Check authentication has read permissions
- Use
@VaultOptional()to allow fallback
For more examples and advanced usage, see the examples/ folder.
Features
- Supports JSON\YAML files\env variables\cli flags as configuration inputs. See
yaml-configin the examples folder - Supports shared configuration files (same file shared for multiple projects)
- initialize a configuration file with
--saveToFileor--init - save configuration files anywhere above your project's package.json
- forced singleton for a single installation (reuse same class)
- testable
- The ability to create json schemas automatically and add descriptions to configuration variables
- Get meaningfull errors when configuration is wrong!
Examples
See the examples folder for a variety of usage examples
Contributors ✨
Thanks goes to these wonderful people (emoji key):
This project follows the all-contributors specification. Contributions of any kind are welcome!
Stay in touch
- Author - Neil Kalman
- Website - https://github.com/kibibit
- StackOverflow - thatkookooguy
- Twitter - @thatkookooguy
- Twitter - @kibibit_opensrc
