UNPKG

@datalayer/core

Version:

[![Datalayer](https://assets.datalayer.tech/datalayer-25.svg)](https://datalayer.io)

182 lines (181 loc) 7.99 kB
/* * Copyright (c) 2023-2025 Datalayer, Inc. * Distributed under the terms of the Modified BSD License. */ /** * IAM mixin providing authentication and user management functionality. * @module client/mixins/IAMMixin */ import * as authentication from '../../api/iam/authentication'; import * as profile from '../../api/iam/profile'; import * as usage from '../../api/iam/usage'; import { UserDTO } from '../../models/UserDTO'; import { CreditsDTO } from '../../models/CreditsDTO'; import { HealthCheck } from '../../models/HealthCheck'; /** IAM mixin providing authentication and user management. */ export function IAMMixin(Base) { return class extends Base { // Cache for current user currentUserCache; /** * Get the current user's profile information. * @returns User model instance */ async whoami() { // eslint-disable-next-line @typescript-eslint/no-explicit-any const token = this.getToken(); // eslint-disable-next-line @typescript-eslint/no-explicit-any const iamRunUrl = this.getIamRunUrl(); const response = await profile.whoami(token, iamRunUrl); // Handle the whoami response format let userData; if (!response) { throw new Error(`No response from profile.whoami API`); } // Check if response has the expected wrapper structure with profile if (response.profile) { // Transform the API response to match the User model's expected data structure // Note: whoami returns fields with suffixes like _s, _t userData = { id: response.profile.id, uid: response.profile.uid, // User model expects fields with suffixes from API types handle_s: response.profile.handle_s || response.profile.handle, email_s: response.profile.email_s || response.profile.email, first_name_t: response.profile.first_name_t || response.profile.first_name || '', last_name_t: response.profile.last_name_t || response.profile.last_name || '', // Use avatar_url_s if available, otherwise leave undefined for fallback avatar_url_s: response.profile.avatar_url_s || response.profile.avatar_url, }; } // Fallback for unexpected format else { throw new Error(`Unexpected response format from profile.whoami API: ${JSON.stringify(response)}`); } // Create new User instance (User model is immutable, no update method) this.currentUserCache = new UserDTO(userData, this); return this.currentUserCache; } /** * Authenticate the user with a token. * @param token - Authentication token * @returns User object on successful login * @throws Error if token is invalid */ async login(token) { // For token-based login, we simply set the token and verify it works await this.setToken(token); // Verify the token by calling whoami try { const user = await this.whoami(); return user; } catch (error) { // Clear the invalid token await this.setToken(''); throw new Error(`Invalid token: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** Log out the current user. */ async logout() { const token = this.getToken(); const iamRunUrl = this.getIamRunUrl(); await authentication.logout(token, iamRunUrl); // Clear the token from the SDK and cached user this.setToken(''); this.currentUserCache = undefined; } /** * Get the current user's available credits and usage information. * @returns Credits model instance */ async getCredits() { const token = this.getToken(); const iamRunUrl = this.getIamRunUrl(); const response = await usage.getCredits(token, iamRunUrl); if (!response || !response.credits) { throw new Error('Invalid response from credits API'); } return new CreditsDTO(response.credits, response.reservations || []); } // ======================================================================== // Credits Calculation Utilities // ======================================================================== /** * Calculate the maximum runtime duration in minutes based on available credits and burning rate. * @param availableCredits - The amount of credits available * @param burningRate - The burning rate per second for the environment * @returns Maximum runtime duration in minutes */ calculateMaxRuntimeMinutes(availableCredits, burningRate) { if (!burningRate || burningRate <= 0) return 0; const burningRatePerMinute = burningRate * 60; return Math.floor(availableCredits / burningRatePerMinute); } /** * Calculate the credits required for a given runtime duration. * @param minutes - Runtime duration in minutes * @param burningRate - The burning rate per second for the environment * @returns Credits required (rounded up to nearest integer) */ calculateCreditsRequired(minutes, burningRate) { if (!burningRate || burningRate <= 0 || !minutes || minutes <= 0) return 0; return Math.ceil(minutes * this.calculateBurningRatePerMinute(burningRate)); } /** * Calculate the burning rate per minute from the per-second rate. * @param burningRatePerSecond - The burning rate per second * @returns Burning rate per minute */ calculateBurningRatePerMinute(burningRatePerSecond) { return burningRatePerSecond * 60; } // ======================================================================== // Service Health Checks // ======================================================================== /** * Check the health status of the IAM service. * @returns Health check model instance */ async checkIAMHealth() { const startTime = Date.now(); const errors = []; let status = 'unknown'; let healthy = false; try { // Test basic connectivity and authentication by getting user profile const user = await this.whoami(); const responseTime = Date.now() - startTime; if (user && user.uid) { healthy = true; status = 'operational'; } else { status = 'degraded'; errors.push('Unexpected response format from profile endpoint'); } return new HealthCheck({ healthy, status, responseTime, errors, timestamp: new Date(), }, this); } catch (error) { const responseTime = Date.now() - startTime; status = 'down'; errors.push(`Service unreachable: ${error}`); return new HealthCheck({ healthy: false, status, responseTime, errors, timestamp: new Date(), }, this); } } }; }