@softlock/sdk
Version:
Official Softlock SDK for access key validation and management
333 lines (327 loc) • 11.5 kB
JavaScript
;
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