UNPKG

@astermind/astermind-premium

Version:

Astermind Premium - Premium ML Toolkit

457 lines 21.3 kB
/** * License management for Astermind Premium * Wraps @astermindai/license-runtime with convenience functions * Also propagates license to Astermind Pro and Astermind Synth for unified licensing * * SECURITY FEATURES: * - Cryptographic JWT signature validation using JWKS from license server * - Token format validation (JWT structure, required claims) * - Issuer validation (must match https://license.astermind.ai) * - Expiration checking * - Fake token rejection (invalid signatures are rejected even if payload looks valid) * - Multiple validation layers to prevent bypass attempts * * The license-runtime library validates tokens using ES256 signatures and public keys * fetched from the license server's JWKS endpoint. Fake tokens cannot bypass this validation. */ import { initLicenseRuntime, setLicenseToken, requireFeature, hasFeature, getLicenseState, base64urlDecodeJson } from '@astermindai/license-runtime'; let initialized = false; let currentToken = null; // SECURITY: Track whether the current token has passed cryptographic validation // This prevents fake tokens from being accepted even if they have valid-looking payloads let tokenValidated = false; /** * Initialize the license runtime singleton for Astermind Premium. * Must be called before any other license functions. * Automatically loads token from ASTERMIND_LICENSE_TOKEN environment variable if present. * * This initializes the license runtime for: * - astermind-premium (primary) * - astermind-pro (fallback) * - astermind-elm (fallback) * - astermind-synth (propagated automatically) * * PROFESSIONAL LICENSING APPROACH: * - Always requires a valid license key (strict mode) * - Trial keys are obtained from the license server and have expiration dates * - No "eval mode" bypass - all usage requires a valid key * - For testing, use a test/dev key or mock the license runtime */ export function initializeLicense() { if (initialized) { return; } // Initialize for astermind-premium // Note: We accept "astermind-premium", "astermind-pro", and "astermind-elm" tokens // The validation in setLicenseTokenFromString handles the audience flexibility initLicenseRuntime({ jwksUrl: 'https://license.astermind.ai/.well-known/astermind-license-keys.json', expectedIss: 'https://license.astermind.ai', expectedAud: 'astermind-premium', // Primary audience, but we'll accept others in setLicenseTokenFromString mode: process.env.NODE_ENV === 'production' ? 'strict' : 'strict', // Always strict for Premium jwksMaxAgeSeconds: 300 }); initialized = true; // Auto-load from environment variable if present const envToken = typeof process !== 'undefined' && process.env?.ASTERMIND_LICENSE_TOKEN; if (envToken) { setLicenseTokenFromString(envToken).catch(err => { console.warn('[Astermind Premium] Failed to load license token from environment:', err); }); } } /** * Require a valid license (throws if not available). * Accepts astermind-premium, astermind-pro, and astermind-elm licenses. * Use this before accessing premium features. * * SECURITY: This function performs multiple validation checks to prevent bypass attempts. * Do not modify or wrap this function in a way that suppresses errors. */ export function requireLicense() { if (!initialized) { initializeLicense(); } // Security: Verify license runtime is actually initialized const state = getLicenseState(); // First try astermind-premium try { requireFeature('astermind-premium'); // Additional security: Verify the state is actually valid if (state.status === 'valid' || hasFeature('astermind-premium')) { return; } } catch (err) { // Continue to check other options } // Try astermind-pro try { requireFeature('astermind-pro'); // Additional security: Verify the state is actually valid if (state.status === 'valid' || hasFeature('astermind-pro')) { return; } } catch (err) { // Continue to check other options } // Try astermind-elm try { requireFeature('astermind-elm'); // Additional security: Verify the state is actually valid if (state.status === 'valid' || hasFeature('astermind-elm')) { return; } } catch (elmErr) { // Continue to check other options } // Try astermind-elm-basic try { requireFeature('astermind-elm-basic'); // Additional security: Verify the state is actually valid if (state.status === 'valid' || hasFeature('astermind-elm-basic')) { return; } } catch (basicErr) { // Continue to check token payload } // SECURITY: If all feature checks fail, but we have a token, // CRITICAL: Only accept it if it passed cryptographic validation // We MUST verify tokenValidated flag to prevent fake tokens from being accepted if (currentToken && tokenValidated) { const payload = parseTokenPayload(currentToken); const validAudiences = ['astermind-premium', 'astermind-pro', 'astermind-elm']; if (payload?.aud && validAudiences.includes(payload.aud)) { const features = payload?.features || []; // Accept any premium ELM features (e.g., "astermind-elm-premium-full") const hasPremiumElmFeature = features.some((f) => f.includes('astermind-elm-premium') || f.includes('premium-full')); if (features.includes('astermind-premium') || features.includes('astermind-pro') || features.includes('astermind-elm') || features.includes('astermind-elm-basic') || hasPremiumElmFeature || features.length > 0) { // SECURITY: Verify token is not expired const now = Math.floor(Date.now() / 1000); if (payload.exp && payload.exp < now) { throw new Error('License token has expired. Please renew your license.'); } // SECURITY: Double-check that the token was actually validated by license-runtime // If state shows "invalid" with reasons other than audience mismatch, reject it // This ensures fake tokens (invalid signature) are rejected even if payload looks valid if (state.status === 'invalid' && state.reason) { const reason = state.reason.toLowerCase(); // Only accept if the ONLY reason is audience mismatch if (!reason.includes('bad audience') && !reason.includes('audience') && !reason.includes('expected audience')) { // Token failed validation for other reasons (signature, issuer, etc.) - reject tokenValidated = false; // Reset validation flag throw new Error('License validation failed. Token signature or format is invalid.'); } } // Token passed cryptographic validation, just audience mismatch - accept it for Premium return; } } } // SECURITY: If we have a token but it hasn't been validated, reject it // This prevents fake tokens from being accepted if (currentToken && !tokenValidated) { throw new Error('License validation failed. Token has not passed cryptographic validation. Fake tokens are rejected.'); } // Security: Final check - if state is explicitly invalid, reject if (state.status === 'invalid' && state.reason && !state.reason.includes('Bad audience') && !state.reason.includes('audience')) { throw new Error('License validation failed. Please provide a valid license token.'); } // If nothing works, throw error throw new Error('Valid license required. Please provide a license token for astermind-premium, astermind-pro, or astermind-elm.'); } /** * Check if license is valid and astermind-premium feature is available (non-blocking). * Also accepts astermind-pro and astermind-elm licenses as valid for Premium. * @returns true if astermind-premium, astermind-pro, or astermind-elm feature is available */ export function checkLicense() { if (!initialized) { initializeLicense(); } // First check for astermind-premium feature (normal case) if (hasFeature('astermind-premium')) { return true; } // Check for astermind-pro feature if (hasFeature('astermind-pro')) { return true; } // If no premium/pro, check for astermind-elm features if (hasFeature('astermind-elm') || hasFeature('astermind-elm-basic')) { return true; } // SECURITY: If license-runtime marked token as invalid due to audience mismatch, // but we have a token, check the token payload directly // CRITICAL: Only accept if token passed cryptographic validation if (currentToken && tokenValidated) { const payload = parseTokenPayload(currentToken); const validAudiences = ['astermind-premium', 'astermind-pro', 'astermind-elm']; if (payload?.aud && validAudiences.includes(payload.aud)) { // Check if token has valid features in its payload const features = payload?.features || []; // Accept any premium ELM features (e.g., "astermind-elm-premium-full") const hasPremiumElmFeature = features.some((f) => f.includes('astermind-elm-premium') || f.includes('premium-full')); if (features.includes('astermind-premium') || features.includes('astermind-pro') || features.includes('astermind-elm') || features.includes('astermind-elm-basic') || hasPremiumElmFeature || features.length > 0) { // SECURITY: Verify token is not expired const now = Math.floor(Date.now() / 1000); if (payload.exp && payload.exp < now) { return false; } // Token passed cryptographic validation, just audience mismatch - accept it for Premium return true; } } } return false; } /** * Check if astermind-synth feature is available (included with Premium subscription). * @returns true if astermind-synth feature is available */ export function checkSynthLicense() { if (!initialized) { initializeLicense(); } return hasFeature('astermind-synth'); } /** * Get detailed license status. * If token has audience "astermind-pro" or "astermind-elm", we'll check if it has valid features * even if the runtime marks it as invalid due to audience mismatch. * @returns LicenseState object with status, reason, payload, etc. */ export function getLicenseStatus() { if (!initialized) { initializeLicense(); } const state = getLicenseState(); // SECURITY: If status is invalid due to audience mismatch but token is for a valid audience, // check if we have valid features anyway // CRITICAL: Only accept if token passed cryptographic validation if (state.status === 'invalid' && state.reason === 'Bad audience' && currentToken && tokenValidated) { const payload = parseTokenPayload(currentToken); const validAudiences = ['astermind-premium', 'astermind-pro', 'astermind-elm']; if (payload?.aud && validAudiences.includes(payload.aud)) { // Check if token has valid features in payload const features = payload?.features || []; // Accept any premium ELM features (e.g., "astermind-elm-premium-full") const hasPremiumElmFeature = features.some((f) => f.includes('astermind-elm-premium') || f.includes('premium-full')); if (features.includes('astermind-premium') || features.includes('astermind-pro') || features.includes('astermind-elm') || features.includes('astermind-elm-basic') || hasPremiumElmFeature || features.length > 0) { // SECURITY: Verify token is not expired const now = Math.floor(Date.now() / 1000); if (payload.exp && payload.exp >= now) { // Return a modified state indicating it's valid for Premium return { ...state, status: 'valid', reason: undefined, }; } } } } return state; } /** * Parse JWT token to extract payload (without verification). * Used to check audience before validation. */ function parseTokenPayload(token) { try { const parts = token.split('.'); if (parts.length !== 3) { return null; } // JWT tokens use base64url encoding - use the license-runtime utility const payload = base64urlDecodeJson(parts[1]); return payload; } catch (err) { return null; } } /** * Set license token from a string. * This will propagate the license to astermind-pro and astermind-synth. * Accepts tokens with audience "astermind-premium", "astermind-pro", or "astermind-elm". * * SECURITY: This function validates the JWT token cryptographically using the license server's * public keys. Fake tokens will be rejected even if they have valid-looking payloads. * * Note: Even if the license-runtime marks tokens as "invalid" due to audience mismatch, * we still accept them for Premium usage IF they pass cryptographic validation. The checkLicense() and * requireLicense() functions will check for all valid features. * * Useful for dynamic token loading from backend services or user input. * @param token The license token string (JWT format) */ export async function setLicenseTokenFromString(token) { if (!initialized) { initializeLicense(); } // SECURITY: Basic token format validation before attempting to set if (!token || typeof token !== 'string') { throw new Error('Invalid license token: token must be a non-empty string'); } // SECURITY: Validate JWT format (must have 3 parts separated by dots) const parts = token.split('.'); if (parts.length !== 3) { throw new Error('Invalid license token: token must be a valid JWT format (header.payload.signature)'); } // SECURITY: Validate that payload can be parsed (basic format check) const payload = parseTokenPayload(token); if (!payload) { throw new Error('Invalid license token: unable to parse token payload'); } // SECURITY: Validate required JWT claims exist if (!payload.iss || !payload.aud || !payload.sub) { throw new Error('Invalid license token: missing required claims (iss, aud, sub)'); } // SECURITY: Validate issuer matches expected issuer if (payload.iss !== 'https://license.astermind.ai') { throw new Error('Invalid license token: token issuer does not match expected issuer'); } // Check token audience - accept "astermind-premium", "astermind-pro", and "astermind-elm" const tokenAudience = payload.aud; const validAudiences = ['astermind-premium', 'astermind-pro', 'astermind-elm']; const isAcceptedToken = validAudiences.includes(tokenAudience); // SECURITY: Try to set the token - this will cryptographically validate the signature // using the license server's public keys (JWKS). Fake tokens will fail here. // CRITICAL: setLicenseToken may not throw - it sets the token and marks it invalid in state // We MUST check the license state AFTER setting to verify cryptographic validation passed try { await setLicenseToken(token); // SECURITY: Check license state immediately after setting token // This is where we detect fake tokens - if signature validation failed, state will be invalid const state = getLicenseState(); // SECURITY: If token failed signature validation, reject it immediately if (state.status === 'invalid' && state.reason) { const reason = state.reason.toLowerCase(); // Check if the reason is signature-related (fake token) const isSignatureError = reason.includes('signature') || reason.includes('jose') || reason.includes('invalid token') || reason.includes('verification failed') || reason.includes('cannot verify'); if (isSignatureError) { // Token failed cryptographic validation - reject fake token currentToken = null; tokenValidated = false; throw new Error(`License validation failed: Invalid token signature. Fake tokens are rejected. Reason: ${state.reason}`); } // Check if it's an audience mismatch (token passed signature validation) const isAudienceError = reason.includes('bad audience') || reason.includes('audience') || reason.includes('expected audience'); if (isAcceptedToken && isAudienceError) { // Token passed signature validation but has audience mismatch // Store it and mark as validated - we'll handle audience in requireLicense/checkLicense currentToken = token; tokenValidated = true; console.warn(`[Astermind Premium] Token audience is "${tokenAudience}" - will be accepted for Premium via feature checking`); } else if (!isAudienceError) { // Token failed validation for other reasons (not signature, not audience) currentToken = null; tokenValidated = false; throw new Error(`License validation failed: ${state.reason || 'Token validation failed'}`); } else { // Invalid audience and not in accepted list currentToken = null; tokenValidated = false; throw new Error(`License validation failed: Invalid token audience. Expected one of: ${validAudiences.join(', ')}`); } } else if (state.status === 'valid') { // Token passed all validation including signature and audience currentToken = token; tokenValidated = true; } else { // Unknown state - reject to be safe currentToken = null; tokenValidated = false; throw new Error(`License validation failed: Unknown validation state`); } } catch (err) { // If setLicenseToken throws an error, check if it's a network/connection error const errorMsg = err?.message || ''; // Network errors might be acceptable if we're offline, but signature errors are not if (errorMsg.includes('network') || errorMsg.includes('fetch') || errorMsg.includes('connection')) { // Network error - don't store token, but don't throw (might be offline) currentToken = null; tokenValidated = false; throw new Error(`License validation failed: Cannot verify token signature. Network error: ${errorMsg}`); } // All other errors should reject the token currentToken = null; tokenValidated = false; throw err; } // Propagate to astermind-pro if available try { // Dynamically import pro's license module to avoid circular dependencies const proLicense = await import('@astermind/astermind-pro'); if (proLicense && typeof proLicense.setLicenseTokenFromString === 'function') { // setLicenseTokenFromString will handle initialization internally await proLicense.setLicenseTokenFromString(token); } } catch (err) { // Pro might not be installed, that's okay console.warn('[Astermind Premium] Could not propagate license to Astermind Pro:', err); } // Propagate to astermind-synth if available try { // Dynamically import synth's license module to avoid circular dependencies const synthLicense = await import('@astermind/astermind-synthetic-data'); if (synthLicense && typeof synthLicense.setLicenseTokenFromString === 'function') { // setLicenseTokenFromString will handle initialization internally await synthLicense.setLicenseTokenFromString(token); } } catch (err) { // Synth might not be installed, that's okay console.warn('[Astermind Premium] Could not propagate license to Astermind Synth:', err); } } /** * Get the current license token (if set). * @returns The current license token or null */ export function getCurrentLicenseToken() { return currentToken; } /** * Check if license runtime has been initialized. * @returns true if initializeLicense() has been called */ export function isLicenseInitialized() { return initialized; } //# sourceMappingURL=license.js.map