@datalayer/core
Version:
[](https://datalayer.io)
182 lines (181 loc) • 7.99 kB
JavaScript
/*
* 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);
}
}
};
}