UNPKG

@softlock/sdk

Version:

Official Softlock SDK for access key validation and management

333 lines (327 loc) 11.5 kB
'use strict'; var crossFetch = require('cross-fetch'); // Error types class SoftlockError extends Error { constructor(message, code, statusCode) { super(message); this.code = code; this.statusCode = statusCode; this.name = 'SoftlockError'; } } class ValidationError extends SoftlockError { constructor(message, code) { super(message, code, 400); this.name = 'ValidationError'; } } class NetworkError extends SoftlockError { constructor(message, statusCode) { super(message, 'NETWORK_ERROR', statusCode); this.name = 'NetworkError'; } } class ConfigurationError extends SoftlockError { constructor(message) { super(message, 'CONFIG_ERROR', 500); this.name = 'ConfigurationError'; } } /** * Core Softlock client for access key validation */ class SoftlockClient { constructor(config) { this.cache = new Map(); this.config = { baseUrl: 'https://api.softlock.dev', cacheTtl: 5 * 60 * 1000, // 5 minutes debug: false, ...config, }; if (!this.config.tenantId) { throw new ConfigurationError('tenantId is required'); } } /** * Validate an access key */ async validateKey(keyValue) { if (!keyValue) { throw new ValidationError('Key value is required'); } // Check cache first const cacheKey = `validate:${keyValue}`; const cached = this.cache.get(cacheKey); if (cached && Date.now() - cached.timestamp < cached.ttl) { if (this.config.debug) { console.log('[SoftlockClient] Returning cached result for key:', keyValue); } return { ...cached.result, cached: true }; } try { const url = `${this.config.baseUrl}/api/validate-key`; const headers = { 'Content-Type': 'application/json', }; if (this.config.apiKey) { headers['Authorization'] = `Bearer ${this.config.apiKey}`; } const body = JSON.stringify({ key: keyValue, tenantId: this.config.tenantId, }); if (this.config.debug) { console.log('[SoftlockClient] Validating key:', { url, keyValue, tenantId: this.config.tenantId }); } const response = await crossFetch.fetch(url, { method: 'POST', headers, body, }); if (!response.ok) { const errorText = await response.text(); let errorMessage = `HTTP ${response.status}`; try { const errorJson = JSON.parse(errorText); errorMessage = errorJson.error || errorJson.message || errorMessage; } catch (_a) { errorMessage = errorText || errorMessage; } throw new NetworkError(errorMessage, response.status); } const result = await response.json(); if (this.config.debug) { console.log('[SoftlockClient] Validation result:', result); } // Cache the result this.cache.set(cacheKey, { result, timestamp: Date.now(), ttl: this.config.cacheTtl, }); return result; } catch (error) { if (this.config.debug) { console.error('[SoftlockClient] Validation error:', error); } if (error instanceof Error) { throw error; } throw new NetworkError('Network request failed'); } } /** * Clear validation cache */ clearCache() { this.cache.clear(); } /** * Update configuration */ updateConfig(newConfig) { this.config = { ...this.config, ...newConfig }; } /** * Get current configuration (without sensitive data) */ getConfig() { const { apiKey, ...publicConfig } = this.config; return publicConfig; } } // Default client instance let defaultClient = null; /** * Initialize the default Softlock client */ function initSoftlock(config) { defaultClient = new SoftlockClient(config); return defaultClient; } /** * Get the default client instance */ function getSoftlockClient() { if (!defaultClient) { throw new ConfigurationError('Softlock client not initialized. Call initSoftlock() first.'); } return defaultClient; } /** * Validate a key using the default client */ async function validateKey(keyValue) { return getSoftlockClient().validateKey(keyValue); } /** * Express middleware for protecting routes with Softlock access keys */ function createExpressMiddleware(options = {}) { return async (req, res, next) => { var _a, _b; try { // Extract access key from request let accessKey = null; if (options.extractKey) { accessKey = options.extractKey(req); } else { // Default extraction methods accessKey = req.headers['x-access-key'] || ((_a = req.headers['authorization']) === null || _a === void 0 ? void 0 : _a.replace('Bearer ', '')) || req.query.access_key || ((_b = req.body) === null || _b === void 0 ? void 0 : _b.access_key) || null; } if (!accessKey) { if (options.onUnauthorized) { return options.onUnauthorized(req, res); } return res.status(401).json({ error: 'Access key required', code: 'MISSING_ACCESS_KEY', }); } // Validate the access key const client = getSoftlockClient(); const result = await client.validateKey(accessKey); if (!result.valid) { if (options.onUnauthorized) { return options.onUnauthorized(req, res); } return res.status(403).json({ error: result.error || 'Invalid access key', code: 'INVALID_ACCESS_KEY', }); } // Add validation result to request object req.softlock = { valid: true, key: result.key, user: result.key ? { discordId: result.key.discord_user_id, discordTag: result.key.discord_tag, } : undefined, }; next(); } catch (error) { if (options.onError) { return options.onError(error instanceof Error ? error.message : 'Unknown error', req, res); } return res.status(500).json({ error: 'Internal server error', code: 'VALIDATION_ERROR', }); } }; } /** * Next.js middleware for protecting routes with Softlock access keys */ function createNextMiddleware(options = {}) { return async (request) => { var _a, _b, _c, _d; try { // Extract access key from request let accessKey = null; if (options.extractKey) { accessKey = options.extractKey(request); } else { // Default extraction methods for Next.js accessKey = request.headers.get('x-access-key') || ((_a = request.headers.get('authorization')) === null || _a === void 0 ? void 0 : _a.replace('Bearer ', '')) || request.nextUrl.searchParams.get('access_key') || null; } if (!accessKey) { return new Response(JSON.stringify({ error: 'Access key required', code: 'MISSING_ACCESS_KEY', }), { status: 401, headers: { 'Content-Type': 'application/json' }, }); } // Validate the access key const client = getSoftlockClient(); const result = await client.validateKey(accessKey); if (!result.valid) { return new Response(JSON.stringify({ error: result.error || 'Invalid access key', code: 'INVALID_ACCESS_KEY', }), { status: 403, headers: { 'Content-Type': 'application/json' }, }); } // Add validation result to request headers for downstream handlers const requestHeaders = new Headers(request.headers); requestHeaders.set('x-softlock-valid', 'true'); requestHeaders.set('x-softlock-user-id', ((_b = result.key) === null || _b === void 0 ? void 0 : _b.discord_user_id) || ''); requestHeaders.set('x-softlock-user-tag', ((_c = result.key) === null || _c === void 0 ? void 0 : _c.discord_tag) || ''); requestHeaders.set('x-softlock-key-id', ((_d = result.key) === null || _d === void 0 ? void 0 : _d.id) || ''); // For Next.js middleware, we need to return undefined to continue // The calling middleware should handle the modified headers return undefined; } catch (error) { return new Response(JSON.stringify({ error: 'Internal server error', code: 'VALIDATION_ERROR', }), { status: 500, headers: { 'Content-Type': 'application/json' }, }); } }; } /** * Utility function to extract Softlock data from Next.js request headers */ function getSoftlockDataFromHeaders(headers) { return { valid: headers.get('x-softlock-valid') === 'true', userId: headers.get('x-softlock-user-id') || undefined, userTag: headers.get('x-softlock-user-tag') || undefined, keyId: headers.get('x-softlock-key-id') || undefined, }; } /** * Higher-order function for protecting API routes */ function withSoftlockAuth(handler, options = {}) { return async (req, res) => { const middleware = createExpressMiddleware(options); return new Promise((resolve, reject) => { middleware(req, res, (err) => { if (err) { reject(err); } else { resolve(handler(req, res)); } }); }); }; } exports.ConfigurationError = ConfigurationError; exports.NetworkError = NetworkError; exports.SoftlockClient = SoftlockClient; exports.SoftlockError = SoftlockError; exports.ValidationError = ValidationError; exports.createExpressMiddleware = createExpressMiddleware; exports.createNextMiddleware = createNextMiddleware; exports.getSoftlockClient = getSoftlockClient; exports.getSoftlockDataFromHeaders = getSoftlockDataFromHeaders; exports.initSoftlock = initSoftlock; exports.validateKey = validateKey; exports.withSoftlockAuth = withSoftlockAuth; //# sourceMappingURL=index.js.map