@quicore/credentials
v1.0.0
Published
Configurable, pluggable credential management module with support for multiple storage providers, encryption profiles, and authentication methods.
Readme
@quicore/credentials
A configurable, pluggable credential management module for Node.js. Supports multiple storage backends, encryption profiles, and authentication method types — all injectable and extensible.
Features
- Modular architecture with full dependency injection
- Multiple storage providers: File, MongoDB, S3, AWS KMS
- Encryption profiles: abstract interface — bring your own implementation
- Authentication methods: API Key, Bearer, OAuth2, Basic, AWS Signature, Custom
- Separate body storage: credential metadata and secret bodies stored independently
- Express HTTP controller with configurable routes
- In-memory default store for development and testing
- All layers independently extensible via subclassing
Installation
npm install @quicore/credentialsExpress is an optional peer dependency, required only if using the HTTP controller:
npm install expressStorage provider SDKs are optional — install only what you use:
# MongoDB
npm install mongodb
# AWS S3
npm install @aws-sdk/client-s3
# AWS KMS
npm install @aws-sdk/client-kmsQuick Start
Minimal — In-Memory Store
import { CredentialModule } from '@quicore/credentials';
const module = new CredentialModule();
const service = module.getService();
await service.createCredential({
credentialKey: 'my-api-key',
type: 'ApiKey',
body: {
apiKey: 'secret-value'
}
});
const credential = await service.getCredential('my-api-key');With MongoDB Storage
import {
CredentialModule,
CredentialConfig,
MongoCredentialBodyProvider
} from '@quicore/credentials';
import { MongoClient } from 'mongodb';
const mongo = new MongoClient(process.env.MONGO_URI);
await mongo.connect();
const provider = new MongoCredentialBodyProvider(mongo.db('app'), 'credential_bodies');
const config = new CredentialConfig({
providers: [provider],
defaultProvider: 'mongo'
});
const module = new CredentialModule({ config });
const service = module.getService();With Express Routes
import express from 'express';
import { CredentialModule } from '@quicore/credentials';
const app = express();
app.use(express.json());
const module = new CredentialModule();
for (const route of module.getRoutes()) {
app[route.method](route.path, route.handler);
}
app.listen(3000);Architecture
CredentialModule
CredentialConfig -- encryption profiles, providers, routes, pagination
CredentialStore -- metadata persistence (extend for production)
CredentialBusinessService -- business logic, body routing
CredentialController -- Express HTTP layer (optional)
Body Storage Separation
Credential collection -- metadata only (body: null when provider is set)
Provider -- actual credential secret
Providers
FileCredentialBodyProvider -- local filesystem
MongoCredentialBodyProvider -- MongoDB collection
S3CredentialBodyProvider -- AWS S3 bucket
KMSCredentialBodyProvider -- AWS KMS encrypted storageBody Storage
When a provider is configured, credential bodies are stored only in the provider, not in the main credential collection. The credential record stores only metadata and a source reference pointing to the provider and location.
credential.source = {
provider: 'mongo',
location: 'credential_bodies', // auto-populated by provider.getLocation()
encryptionProfileRef: 'kms-prod',
isEncrypted: true
}
credential.body = null // not stored in collectionOn fetch, the body is loaded from the provider and optionally decrypted before being returned.
If no provider is configured, the body is stored inline in the credential collection.
Configuration
import {
CredentialConfig,
EncryptionProfile,
MongoCredentialBodyProvider,
S3CredentialBodyProvider
} from '@quicore/credentials';
const config = new CredentialConfig({
// Encryption profiles (bring your own implementation)
encryptionProfiles: [
new EncryptionProfile({
name: 'kms-prod',
algorithm: 'AES-256-GCM',
keyReference: 'arn:aws:kms:us-east-1:123456789:key/abc-123',
provider: 'kms'
})
],
defaultEncryptionProfile: 'kms-prod',
// Body storage providers
providers: [
new MongoCredentialBodyProvider(mongoDb, 'credential_bodies'),
new S3CredentialBodyProvider(s3Client, 'my-bucket', { prefix: 'credentials/' })
],
defaultProvider: 'mongo',
// HTTP routing
routePrefix: '/api/v1/credentials',
pageSize: 50,
// Caching strategy
cachingStrategy: 'credential', // 'api-call' | 'integration-id' | 'credential'
// Restrict credential types
allowedAuthMethods: ['ApiKey', 'Bearer', 'OAuth2']
});CredentialModule
The module wires all components together and supports full dependency injection:
import {
CredentialModule,
CredentialConfig,
CredentialStore,
CredentialBusinessService,
CredentialController
} from '@quicore/credentials';
const module = new CredentialModule({
store: customStore, // optional: must extend CredentialStore
service: customService, // optional: must extend CredentialBusinessService
controller: customController, // optional: must extend CredentialController
config: credentialConfig // optional: must be instance of CredentialConfig
});
module.getStore();
module.getService();
module.getController();
module.getConfig();
module.getRoutes(); // returns Express route definitionsExtending the Store
The default CredentialStore is in-memory and intended for development only. For production, extend it to back onto a real database:
import { CredentialStore } from '@quicore/credentials';
export class MongoCredentialStore extends CredentialStore {
constructor(db) {
super();
this.collection = db.collection('credentials');
}
async save(credential) {
await this.collection.replaceOne(
{ credentialKey: credential.credentialKey },
credential.toJSON(),
{ upsert: true }
);
return credential;
}
async findByKey(credentialKey) {
const doc = await this.collection.findOne({ credentialKey });
return doc ? Credential.fromJSON(doc) : null;
}
async findAll() {
const docs = await this.collection.find({}).toArray();
return docs.map(doc => Credential.fromJSON(doc));
}
async delete(credentialKey) {
const result = await this.collection.deleteOne({ credentialKey });
return result.deletedCount > 0;
}
async exists(credentialKey) {
const count = await this.collection.countDocuments({ credentialKey });
return count > 0;
}
}Extending the Business Service
Override specific methods to add custom logic:
import { CredentialBusinessService } from '@quicore/credentials';
export class AuditedCredentialService extends CredentialBusinessService {
async createCredential(data) {
const result = await super.createCredential(data);
await auditLog.record('credential.created', result.data.credentialKey);
return result;
}
async deleteCredential(credentialKey) {
const result = await super.deleteCredential(credentialKey);
await auditLog.record('credential.deleted', credentialKey);
return result;
}
}Extending a Provider
Implement the CredentialBodyProvider interface to add a custom storage backend:
import { CredentialBodyProvider } from '@quicore/credentials';
export class VaultCredentialBodyProvider extends CredentialBodyProvider {
constructor(vaultClient, mountPath) {
super();
this.name = 'vault';
this.vaultClient = vaultClient;
this.mountPath = mountPath;
}
async save(credentialKey, body) {
await this.vaultClient.write(`${this.mountPath}/${credentialKey}`, { data: body });
}
async fetch(credentialKey) {
const result = await this.vaultClient.read(`${this.mountPath}/${credentialKey}`);
return result.data;
}
async delete(credentialKey) {
await this.vaultClient.delete(`${this.mountPath}/${credentialKey}`);
}
getLocation() {
return `vault://${this.mountPath}`;
}
getProviderType() {
return 'vault';
}
validate() {
if (!this.vaultClient) throw new Error('vaultClient is required');
if (!this.mountPath) throw new Error('mountPath is required');
}
}Encryption Profiles
EncryptionProfile is a black-box interface. You provide the implementation via setEncryptionImplementation():
import { EncryptionProfile } from '@quicore/credentials';
const profile = new EncryptionProfile({
name: 'aes-local',
algorithm: 'AES-256-GCM',
keyReference: './keys/local.key',
provider: 'local'
});
profile.setEncryptionImplementation({
async encrypt(plainText, keyRef, algorithm) {
// your crypto logic here
return encryptedBuffer;
},
async decrypt(cipherText, keyRef, algorithm) {
// your crypto logic here
return decryptedString;
}
});Authentication Methods
Each method validates and normalizes a credential body for its authentication type:
import { APIKeyMethod, BearerMethod, OAuth2Method } from '@quicore/credentials';
// API Key
const method = new APIKeyMethod({
body: { apiKey: 'my-key' },
headers: { 'X-Api-Key': ':{apiKey}' }
});
const validated = method.validate();
// OAuth2
const oauth = new OAuth2Method({
body: {
clientId: 'client-123',
clientSecret: 'secret',
tokenUrl: 'https://auth.example.com/token',
scope: 'read write'
}
});Service API
const service = module.getService();
// Create
await service.createCredential({ credentialKey, type, body, ...opts });
// Read
await service.getCredential(credentialKey);
// Update
await service.updateCredential(credentialKey, { name, body, tags, ... });
// Delete
await service.deleteCredential(credentialKey);
// List with filters
await service.listCredentials({ tag, type, includeExpired });
// Search
await service.searchCredentials({ nameContains, types, tags, expiredOnly });
// Bulk
await service.getMultipleCredentials([key1, key2]);
// Status
await service.checkCredentialStatus(credentialKey);
// Export (sensitive fields redacted)
await service.exportCredentials([key1, key2]);HTTP Routes (Express)
Default routes mounted by module.getRoutes():
| Method | Path | Description | |--------|------|-------------| | GET | /credentials | List all credentials | | POST | /credentials | Create a credential | | GET | /credentials/:credentialKey | Get a credential | | PATCH | /credentials/:credentialKey | Update a credential | | DELETE | /credentials/:credentialKey | Delete a credential | | GET | /credentials/search | Search credentials | | GET | /credentials/:credentialKey/status | Check expiry status | | POST | /credentials/export | Export (redacted) |
The route prefix and individual paths are configurable via CredentialConfig.
Credential Schema
{
credentialKey: string, // unique identifier, alphanumeric/hyphens/underscores
type: string, // 'ApiKey' | 'Bearer' | 'OAuth2' | 'Basic' | 'Custom'
name: string,
description: string,
tags: string[],
expiryAt: string | null, // ISO 8601 date string
cacheLevel: string, // 'api-call' | 'integration-id' | 'credential'
version: number,
body: object | null, // null when external provider is used
headers: object | null,
meta: object | null,
source: {
provider: string, // provider name
location: string, // auto-populated from provider.getLocation()
isEncrypted: boolean,
encryptionProfileRef: string
} | null
}Token Set
TokenSet manages OAuth2 token state with built-in expiry checks and single-flight refresh:
import { TokenSet } from '@quicore/credentials';
const token = new TokenSet({
access_token: 'abc123',
expires_in: 3600,
refresh_token: 'refresh-xyz'
});
token.expired(); // boolean
token.expiresIn(); // seconds remaining
token.isRefreshable(); // boolean
await token.refresh(async () => {
// call your token refresh endpoint
return new TokenSet(responseData);
});Requirements
- Node.js >= 18.0.0
Docs
Additional documentation is available in the docs/ directory:
docs/API_REFERENCE.md— full API surfacedocs/QUICK_START.md— quick start guidedocs/CREDENTIAL_CONFIG_ARCHITECTURE.md— configuration architecturedocs/BODY_STORAGE_SEPARATION.md— body storage separation strategydocs/PROVIDER_LOCATION_GUIDE.md— provider location referencedocs/MODULE_CONFIGURATION.md— module configuration optionsdocs/ARCHITECTURE_MIGRATION_GUIDE.md— migration from static implementations
License
MIT
