@kya-os/provider-registry
v0.1.7
Published
Single source of truth for provider definitions and provider-type mapping
Readme
@kya-os/provider-registry
Single source of truth for provider definitions and provider-type mapping used across the MCP-I framework.
Overview
This package centralizes provider metadata, provider-type detection, and authentication routing logic. It eliminates hardcoded provider lists scattered across the codebase and provides a consistent, testable interface for provider management.
Features
- Single Source of Truth: Centralized provider definitions
- Type Safety: Full TypeScript support with Zod validation
- Dependency Injection: Interface-based design for testability
- Runtime Configuration: Load custom providers from JSON/config
- Default Providers: Pre-populated with common OAuth providers
- oauthProviderId Uniqueness: Enforced uniqueness for OAuth provider IDs
- Seal Capability: Lock registry in production to prevent modifications
- Consistent Fallback Behavior: Documented, predictable behavior for unknown providers
Installation
pnpm add @kya-os/provider-registryUsage
Basic Usage (Default Registry)
import { defaultProviderRegistry } from '@kya-os/provider-registry';
// Check if a provider is OAuth
if (defaultProviderRegistry.isOAuthProvider('github')) {
// Handle OAuth flow
}
// Map auth mode to consent provider type
const providerType = defaultProviderRegistry.mapAuthModeToConsentProvider(
'credentials',
undefined
);
// Returns: 'credential'Dependency Injection (Testing)
import { ProviderRegistry, IProviderRegistry, createDefaultProviderRegistry } from '@kya-os/provider-registry';
// Create isolated registry for tests
const testRegistry = createDefaultProviderRegistry();
// Or create with custom providers
const customRegistry = new ProviderRegistry([
{
id: 'test-provider',
displayName: 'Test Provider',
authType: 'oauth2',
},
]);
// Inject into your class
class MyAgent {
constructor(private providerRegistry: IProviderRegistry = defaultProviderRegistry) {}
handleAuth(provider: string) {
if (this.providerRegistry.isOAuthProvider(provider)) {
// OAuth flow
}
}
}Registering Custom Providers
import { defaultProviderRegistry } from '@kya-os/provider-registry';
// Register a custom credential provider (e.g., for enterprise auth)
defaultProviderRegistry.registerProvider({
id: 'my-company-auth',
displayName: 'My Company Login',
authType: 'password',
ui: {
description: 'Sign in with My Company credentials',
},
});
// Register a custom OAuth provider
defaultProviderRegistry.registerProvider({
id: 'enterprise-sso',
displayName: 'Enterprise SSO',
authType: 'oauth2',
oauthProviderId: 'enterprise-sso',
defaultScopes: ['openid', 'profile', 'email'],
});Loading from Configuration
import { defaultProviderRegistry } from '@kya-os/provider-registry';
// Load from JSON config
const config = {
providers: [
{
id: 'my-provider',
displayName: 'My Provider',
authType: 'oauth2',
},
],
};
defaultProviderRegistry.loadFromConfig(config);Environment Variable Configuration
// Load from environment variable (JSON string)
const envConfig = process.env.MCP_PROVIDER_REGISTRY;
if (envConfig) {
const config = JSON.parse(envConfig);
defaultProviderRegistry.loadFromConfig(config);
}
// Seal registry to prevent further modifications in production
defaultProviderRegistry.seal();Finding Providers by OAuth Provider ID
import { defaultProviderRegistry } from '@kya-os/provider-registry';
// Find provider by oauthProviderId (useful when IDs differ)
defaultProviderRegistry.registerProvider({
id: 'custom-oauth',
authType: 'oauth2',
oauthProviderId: 'my-custom-oauth-id',
});
const provider = defaultProviderRegistry.findByOauthProviderId('my-custom-oauth-id');
// Returns: { id: 'custom-oauth', ... }API Reference
IProviderRegistry Interface
interface IProviderRegistry {
getProvider(id: string): ProviderDefinition | undefined;
findByOauthProviderId(oauthProviderId: string): ProviderDefinition | undefined;
isKnownProvider(id: string): boolean;
isOAuthProvider(id: string): boolean;
isCredentialProvider(id: string): boolean;
mapAuthModeToConsentProvider(authMode?: AuthMode | string, oauthProviderId?: string): ConsentProviderType;
mapProviderToConsentProvider(providerId: string): ConsentProviderType;
determineProviderTypeFromAuthMode(authMode?: string, oauthIdentityProvider?: string): ConsentProviderType; // @deprecated
registerProvider(def: ProviderDefinition, opts?: { overwrite?: boolean }): void;
listProviders(): ProviderDefinition[];
loadFromConfig(config: unknown): void;
seal(): void;
isSealed(): boolean;
}ProviderDefinition Type
interface ProviderDefinition {
id: string; // Unique identifier (e.g., 'github', 'my-company-auth')
displayName?: string; // Human-friendly name
authType: ProviderAuthType; // 'oauth2', 'password', 'verifiable_credential', etc.
oauthProviderId?: string; // OAuth provider ID (must be unique across registry)
defaultScopes?: string[]; // Default OAuth scopes
oauthConfig?: OAuthProviderConfig; // OAuth endpoint configuration
credentialConfig?: CredentialProviderConfig; // Credential flow configuration
ui?: {
icon?: string;
description?: string;
};
metadata?: Record<string, unknown>; // Custom metadata (secret names, not secrets!)
}OAuthProviderConfig Type
Configuration for OAuth provider flows:
interface OAuthProviderConfig {
authorizationEndpoint: string; // OAuth authorization URL
tokenEndpoint: string; // OAuth token URL
userInfoEndpoint?: string; // User info endpoint
defaultScopes?: string[]; // Default scopes to request
supportsPKCE?: boolean; // Whether PKCE is supported
requiresClientSecret?: boolean; // Whether client secret is required
tokenEndpointAuthMethod?: 'client_secret_post' | 'client_secret_basic';
responseType?: string; // OAuth response type (default: 'code')
grantType?: string; // OAuth grant type (default: 'authorization_code')
customParams?: Record<string, string>; // Custom OAuth params (audience, etc.)
authUrlTemplate?: string; // URL template with {placeholders}
}CredentialProviderConfig Type
Configuration for credential/password authentication:
interface CredentialProviderConfig {
authEndpoint: string; // Endpoint to POST credentials
httpMethod?: 'POST' | 'PUT'; // HTTP method
contentType?: 'application/json' | 'application/x-www-form-urlencoded';
requestBodyTemplate?: {
identityField?: string; // Field name for email/username
passwordField?: string; // Field name for password
additionalFields?: Record<string, string>;
};
responseFields?: {
sessionTokenPath?: string; // JSON path to session token
userIdPath?: string; // JSON path to user ID
userEmailPath?: string; // JSON path to user email
userDisplayNamePath?: string; // JSON path to display name
expiresInPath?: string; // JSON path to token expiry
};
successCheck?: {
path?: string; // JSON path to check for success
expectedValue?: string | boolean;
};
useCookieSession?: boolean; // Whether to use cookies for session
cookieNames?: string; // Cookie names to extract
customHeaders?: Record<string, string>;
requiresCsrf?: boolean; // Whether CSRF token is required
}Mapping Semantics
mapAuthModeToConsentProvider(authMode?, oauthProviderId?)
Deterministic precedence rules:
Explicit authMode (non-oauth) → Direct mapping
'credentials','password'→'credential''magic-link'→'magic_link''otp'→'otp''verifiable_credential','idv','mdl'→'verifiable_credential''consent-only','none',''→'none'
If authMode === 'oauth' or oauthProviderId present → Consult registry
- Known provider → Map based on provider's authType
- Unknown provider →
'oauth2'(with warning log)
Fallback →
'none'
mapProviderToConsentProvider(providerId)
- Known provider → Map based on provider's authType
- Unknown provider →
'none'(with warning log)
Fallback Behavior Rationale
The different fallbacks are intentional:
mapAuthModeToConsentProviderwith unknown oauthProviderId →'oauth2': The presence of an oauthProviderId strongly implies an OAuth flow, even if the provider isn't registered.mapProviderToConsentProviderwith unknown providerId →'none': Direct provider lookup should fail safely without assumptions.
Both log warnings to help identify configuration issues.
Default Providers
The registry comes pre-populated with common providers:
OAuth Providers: GitHub, Google, Microsoft, Discord, Slack, Apple, LinkedIn, Twitter, Facebook, Okta, Auth0
Credential Provider: credentials (generic placeholder)
Note: Customer-specific providers (like HardwareWorld) should NOT be in default providers. Register them via:
registerProvider()at runtimeloadFromConfig()with custom configurationMCP_PROVIDER_REGISTRYenvironment variable
Security: Secrets Handling
⚠️ NEVER store actual secrets in provider definitions. Store only secret references (names).
Provider definitions may contain endpoint URLs and configuration, but client secrets, API keys, and other sensitive credentials must NOT be stored in the registry.
The Correct Approach
Store secret names in metadata and read actual secrets from secure storage at runtime:
// ✅ CORRECT: Store secret name, not secret value
defaultProviderRegistry.registerProvider({
id: 'enterprise-oauth',
displayName: 'Enterprise SSO',
authType: 'oauth2',
oauthConfig: {
authorizationEndpoint: 'https://sso.example.com/oauth/authorize',
tokenEndpoint: 'https://sso.example.com/oauth/token',
},
metadata: {
clientSecretName: 'ENTERPRISE_SSO_CLIENT_SECRET', // Name, not value!
clientId: 'my-app-client-id', // Client ID is often public, OK to store
},
});
// At runtime, read secret from secure storage
async function getProviderSecret(providerId: string): Promise<string> {
const provider = defaultProviderRegistry.getProvider(providerId);
const secretName = provider?.metadata?.clientSecretName as string;
// Read from Cloudflare Secrets, AWS Secrets Manager, vault, etc.
return env[secretName] || await secretsManager.getSecret(secretName);
}What Can Be Stored in Registry
| Field | OK to Store | Notes |
|-------|-------------|-------|
| authorizationEndpoint | ✅ Yes | Public URL |
| tokenEndpoint | ✅ Yes | Public URL |
| clientId | ✅ Usually | Often public (check your provider) |
| clientSecret | ❌ NO | Store as metadata.clientSecretName |
| apiKey | ❌ NO | Store as metadata.apiKeyName |
| defaultScopes | ✅ Yes | Configuration, not secret |
Credential Provider Security
For password/credential flows, the credentialConfig describes how to authenticate, not with what credentials:
// ✅ CORRECT: Describe flow structure, not credentials
defaultProviderRegistry.registerProvider({
id: 'legacy-api',
authType: 'password',
credentialConfig: {
authEndpoint: 'https://api.example.com/auth/login',
contentType: 'application/json',
requestBodyTemplate: {
identityField: 'email', // Field name, not value
passwordField: 'password', // Field name, not value
},
responseFields: {
sessionTokenPath: 'data.token',
},
},
metadata: {
// If the API requires static headers like API keys:
apiKeyHeaderName: 'X-API-Key',
apiKeySecretName: 'LEGACY_API_KEY', // Read from env/secrets at runtime
},
});Production Best Practices
1. Load Custom Providers Early
import { defaultProviderRegistry } from '@kya-os/provider-registry';
// In your server initialization
async function initializeRegistry() {
// Load from config/environment
if (process.env.MCP_PROVIDER_REGISTRY) {
defaultProviderRegistry.loadFromConfig(
JSON.parse(process.env.MCP_PROVIDER_REGISTRY)
);
}
// Seal to prevent runtime modifications
defaultProviderRegistry.seal();
}2. Use Dependency Injection for Testing
import { createDefaultProviderRegistry, IProviderRegistry } from '@kya-os/provider-registry';
class MyService {
constructor(private registry: IProviderRegistry = defaultProviderRegistry) {}
}
// In tests
const testRegistry = createDefaultProviderRegistry();
testRegistry.registerProvider({ id: 'mock', authType: 'oauth2' });
const service = new MyService(testRegistry);3. Custom Logger for Metrics
import { createDefaultProviderRegistry, RegistryLogger } from '@kya-os/provider-registry';
const metricsLogger: RegistryLogger = {
warn(message, context) {
myMetricsClient.increment('provider_registry.warning', {
message,
...context,
});
console.warn(message, context);
},
};
const registry = createDefaultProviderRegistry(metricsLogger);4. Registry Lifecycle & Multi-tenant Deployments
Lifecycle expectations for the singleton:
Initialization: The
defaultProviderRegistryis lazily created on first access (no module-load side effects).Configuration: Load project-specific providers early in your application startup:
// At server/worker startup if (env.MCP_PROVIDER_REGISTRY) { defaultProviderRegistry.loadFromConfig(JSON.parse(env.MCP_PROVIDER_REGISTRY)); }Sealing: Call
seal()after configuration to prevent accidental runtime modifications:defaultProviderRegistry.seal(); // After this, registerProvider() and loadFromConfig() will throwRuntime: Use the sealed registry for lookups throughout the application lifecycle.
Multi-tenant deployments:
For multi-tenant scenarios where different projects need different providers, prefer per-project registry instances instead of mutating the global singleton:
import { ProviderRegistry, defaultProviders } from '@kya-os/provider-registry';
function createProjectRegistry(projectConfig: ProviderConfig): ProviderRegistry {
// Start with default providers
const registry = new ProviderRegistry(defaultProviders);
// Add project-specific providers
registry.loadFromConfig(projectConfig);
// Seal and return
registry.seal();
return registry;
}
// Each tenant gets their own isolated registry
const tenant1Registry = createProjectRegistry(tenant1Config);
const tenant2Registry = createProjectRegistry(tenant2Config);Testing:
Use resetDefaultProviderRegistryForTests() in test setup to ensure isolation:
import { resetDefaultProviderRegistryForTests } from '@kya-os/provider-registry';
beforeEach(() => {
resetDefaultProviderRegistryForTests();
});Integration Examples
Cloudflare Agent
import { defaultProviderRegistry, IProviderRegistry } from '@kya-os/provider-registry';
export class MCPICloudflareAgent {
constructor(
private providerRegistry: IProviderRegistry = defaultProviderRegistry
) {}
async handleOAuthRequired(error: OAuthRequiredError) {
const provider = error.provider?.toLowerCase() || '';
if (this.providerRegistry.isCredentialProvider(provider)) {
// Build credential consent URL
} else if (this.providerRegistry.isOAuthProvider(provider)) {
// Build OAuth URL
}
}
}Consent UI
import { defaultProviderRegistry } from '@kya-os/provider-registry';
const providerType = defaultProviderRegistry.mapAuthModeToConsentProvider(
authMode,
oauthIdentity?.provider
);
const provider = defaultProviderRegistry.getProvider(providerId);
if (provider?.ui?.icon) {
// Render provider icon
}Testing
import { ProviderRegistry, createDefaultProviderRegistry } from '@kya-os/provider-registry';
describe('MyComponent', () => {
it('should handle OAuth providers', () => {
const registry = new ProviderRegistry([
{ id: 'github', displayName: 'GitHub', authType: 'oauth2' },
]);
expect(registry.isOAuthProvider('github')).toBe(true);
});
it('should use isolated registry', () => {
const registry = createDefaultProviderRegistry();
registry.registerProvider({ id: 'custom', authType: 'password' });
// Won't affect other tests or global singleton
expect(registry.isCredentialProvider('custom')).toBe(true);
});
});Migration Guide
Replacing Hardcoded Lists
Before:
const KNOWN_OAUTH_PROVIDERS = ['github', 'google', 'microsoft'];
const isOAuth = KNOWN_OAUTH_PROVIDERS.includes(provider);After:
import { defaultProviderRegistry } from '@kya-os/provider-registry';
const isOAuth = defaultProviderRegistry.isOAuthProvider(provider);Replacing Provider Type Detection
Before:
function getProviderType(authMode?: string, oauthProvider?: string) {
if (authMode === 'credentials') return 'credential';
if (oauthProvider) return 'oauth2';
return 'none';
}After:
import { defaultProviderRegistry } from '@kya-os/provider-registry';
const providerType = defaultProviderRegistry.mapAuthModeToConsentProvider(
authMode,
oauthProvider
);Using findByOauthProviderId
Before:
// Manual lookup with potential mismatch
const provider = registry.getProvider(oauthIdentityProvider);After:
// Proper lookup using oauthProviderId index
const provider = registry.findByOauthProviderId(oauthIdentityProvider);License
MIT
