defarm-sdk
Version:
DeFarm SDK - On-premise blockchain data processing and tokenization engine for agriculture supply chain
340 lines (295 loc) • 9.19 kB
JavaScript
/**
* API Key Manager - Authentication and Authorization
*
* Controls access to DeFarm SDK features based on API keys and subscription plans
*/
const crypto = require('crypto');
const axios = require('axios');
class APIKeyManager {
constructor(config = {}) {
this.config = {
apiEndpoint: config.apiEndpoint || process.env.DEFARM_API_ENDPOINT || 'https://api.defarm.io',
cacheTimeout: config.cacheTimeout || 300000, // 5 minutes cache
offlineMode: config.offlineMode || false,
...config
};
// Cache validated API keys
this.keyCache = new Map();
// Usage tracking
this.usageTracker = new Map();
// Subscription plans and their limits
this.plans = {
trial: {
name: 'Trial',
tokenizations: 10,
processings: 100,
validityDays: 14,
features: ['basic_processing', 'basic_validation']
},
starter: {
name: 'Starter',
tokenizations: 1000,
processings: 10000,
validityDays: 30,
features: ['basic_processing', 'validation', 'duplicate_detection', 'basic_analytics']
},
professional: {
name: 'Professional',
tokenizations: 10000,
processings: 100000,
validityDays: 30,
features: ['all_processing', 'validation', 'duplicate_detection', 'analytics', 'provenance', 'compliance']
},
enterprise: {
name: 'Enterprise',
tokenizations: 100000,
processings: -1, // unlimited
validityDays: 30,
features: ['all'] // all features
},
government: {
name: 'Government',
tokenizations: -1, // unlimited
processings: -1, // unlimited
validityDays: 365,
features: ['all']
}
};
}
/**
* Validate API key with DeFarm servers
*/
async validateAPIKey(apiKey) {
if (!apiKey) {
throw new Error('API key is required');
}
// Check cache first
const cached = this.keyCache.get(apiKey);
if (cached && cached.expiresAt > Date.now()) {
return cached.data;
}
// In offline mode, check if key format is valid
if (this.config.offlineMode) {
return this.validateOffline(apiKey);
}
try {
// Validate with DeFarm API
const response = await axios.post(
`${this.config.apiEndpoint}/v1/auth/validate`,
{},
{
headers: {
'X-API-Key': apiKey,
'X-SDK-Version': '1.0.0',
'X-SDK-Platform': process.platform
},
timeout: 5000
}
);
const validationData = response.data;
// Cache the result
this.keyCache.set(apiKey, {
data: validationData,
expiresAt: Date.now() + this.config.cacheTimeout
});
// Initialize usage tracking
if (!this.usageTracker.has(apiKey)) {
this.usageTracker.set(apiKey, {
tokenizations: 0,
processings: 0,
lastReset: Date.now()
});
}
return validationData;
} catch (error) {
// If API is unreachable, check cache or use offline mode
if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT') {
console.warn('⚠️ DeFarm API unreachable, using offline validation');
return this.validateOffline(apiKey);
}
if (error.response?.status === 401) {
throw new Error('Invalid API key');
} else if (error.response?.status === 403) {
throw new Error('API key expired or suspended');
} else if (error.response?.status === 429) {
throw new Error('Rate limit exceeded');
}
throw error;
}
}
/**
* Offline validation (basic format check)
*/
validateOffline(apiKey) {
// Accept different key formats:
// - Production: sk_live_ or sk_test_ followed by 32+ chars
// - Demo: demo_ followed by any chars
// - Development: dev_ followed by any chars
const patterns = [
/^sk_(live|test)_[a-zA-Z0-9]{32,}$/, // Production keys
/^demo_[a-zA-Z0-9_]{8,}$/, // Demo keys
/^dev_[a-zA-Z0-9_]{8,}$/ // Development keys
];
const isValidFormat = patterns.some(pattern => pattern.test(apiKey));
if (!isValidFormat) {
throw new Error('Invalid API key format');
}
// Determine plan based on key type
let plan = 'professional';
if (apiKey.startsWith('demo_')) {
plan = 'trial';
} else if (apiKey.startsWith('dev_')) {
plan = 'starter';
} else if (apiKey.includes('live')) {
plan = 'professional';
}
// In offline mode, provide appropriate plan features
return {
valid: true,
plan: plan,
organization: 'offline-mode',
features: ['basic_processing', 'validation', 'tokenization'],
limits: {
tokenizations: plan === 'trial' ? 10 : plan === 'starter' ? 100 : 1000, // Limited in offline mode
processings: 1000
},
offline: true
};
}
/**
* Check if feature is available for API key
*/
async hasFeature(apiKey, feature) {
const validation = await this.validateAPIKey(apiKey);
if (!validation.valid) {
return false;
}
const plan = this.plans[validation.plan];
if (!plan) {
return false;
}
return plan.features.includes('all') || plan.features.includes(feature);
}
/**
* Check and update usage limits
*/
async checkUsageLimit(apiKey, usageType = 'processings') {
const validation = await this.validateAPIKey(apiKey);
if (!validation.valid) {
throw new Error('Invalid API key');
}
const plan = this.plans[validation.plan];
if (!plan) {
throw new Error('Unknown plan');
}
// Get current usage
let usage = this.usageTracker.get(apiKey);
if (!usage) {
usage = { tokenizations: 0, processings: 0, lastReset: Date.now() };
this.usageTracker.set(apiKey, usage);
}
// Check if we need to reset monthly usage
const daysSinceReset = (Date.now() - usage.lastReset) / (1000 * 60 * 60 * 24);
if (daysSinceReset >= 30) {
usage.tokenizations = 0;
usage.processings = 0;
usage.lastReset = Date.now();
}
// Check limits
const limit = plan[usageType];
if (limit === -1) {
return true; // Unlimited
}
if (usage[usageType] >= limit) {
throw new Error(`${usageType} limit exceeded for plan ${plan.name}. Upgrade at https://defarm.io/pricing`);
}
// Increment usage
usage[usageType]++;
// Report usage to server (async, don't wait)
this.reportUsage(apiKey, usageType).catch(console.error);
return true;
}
/**
* Report usage to DeFarm servers
*/
async reportUsage(apiKey, usageType) {
if (this.config.offlineMode) {
return;
}
try {
await axios.post(
`${this.config.apiEndpoint}/v1/usage/report`,
{
type: usageType,
timestamp: Date.now(),
metadata: {
sdk_version: '1.0.0',
platform: process.platform,
node_version: process.version
}
},
{
headers: {
'X-API-Key': apiKey
},
timeout: 2000 // Quick timeout for usage reporting
}
);
} catch (error) {
// Silently fail - don't interrupt operations
console.debug('Usage reporting failed:', error.message);
}
}
/**
* Get current usage stats
*/
async getUsageStats(apiKey) {
const validation = await this.validateAPIKey(apiKey);
if (!validation.valid) {
throw new Error('Invalid API key');
}
const usage = this.usageTracker.get(apiKey) || {
tokenizations: 0,
processings: 0,
lastReset: Date.now()
};
const plan = this.plans[validation.plan];
return {
plan: plan.name,
usage: {
tokenizations: {
used: usage.tokenizations,
limit: plan.tokenizations,
remaining: plan.tokenizations === -1 ? 'unlimited' : Math.max(0, plan.tokenizations - usage.tokenizations)
},
processings: {
used: usage.processings,
limit: plan.processings,
remaining: plan.processings === -1 ? 'unlimited' : Math.max(0, plan.processings - usage.processings)
}
},
resetDate: new Date(usage.lastReset + (30 * 24 * 60 * 60 * 1000)).toISOString()
};
}
/**
* Generate a test API key for development
*/
static generateTestKey() {
const randomBytes = crypto.randomBytes(24).toString('hex');
return `sk_test_${randomBytes}`;
}
/**
* Validate webhook signature from DeFarm
*/
validateWebhookSignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(payload))
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
}
module.exports = { APIKeyManager };