UNPKG

agentlang

Version:

The easiest way to build the most reliable AI agents - enterprise-grade teams of AI agents that collaborate with each other and humans

1,322 lines (1,262 loc) 44 kB
import { Environment, makeEventEvaluator } from '../interpreter.js'; import { logger } from '../logger.js'; import { makeInstance, newInstanceAttributes, RbacPermissionFlag } from '../module.js'; import { makeCoreModuleName } from '../util.js'; import { isSqlTrue } from '../resolvers/sqldb/dbutil.js'; import { AdminUserId, BypassSession, isAuthEnabled, isRbacEnabled, } from '../auth/defs.js'; import { isNodeEnv } from '../../utils/runtime.js'; import { CognitoAuth, getHttpStatusForError } from '../auth/cognito.js'; import { UnauthorisedError, UserNotFoundError, UserNotConfirmedError, PasswordResetRequiredError, TooManyRequestsError, InvalidParameterError, ExpiredCodeError, CodeMismatchError, BadRequestError, } from '../defs.js'; export const CoreAuthModuleName = makeCoreModuleName('auth'); export default `module ${CoreAuthModuleName} import "./modules/auth.js" @as Auth entity User { id UUID @id @default(uuid()) @unique, email Email @unique @indexed, firstName String, lastName String, lastLoginTime DateTime @default(now()), status @enum("Active", "Invited", "Inactive") @default("Active"), @rbac [(allow: [read, delete, update, create], where: auth.user = this.id)], @after {delete AfterDeleteUser} } workflow AfterDeleteUser { {RemoveUserSession {id AfterDeleteUser.User.id}} await Auth.deleteUser(AfterDeleteUser.User.id, AfterDeleteUser.User.email) } @public workflow CreateUser { {User {id CreateUser.id, email CreateUser.email, firstName CreateUser.firstName, lastName CreateUser.lastName, status CreateUser.status}} } @public workflow CreateUsers { for user in CreateUsers.users { {User {email? user.email}} @as [u]; if (u) { {User {id? u.id, firstName user.firstName, lastName user.lastName}} @as [um] um } else { {User {email user.email, firstName user.firstName, lastName user.lastName}} @as [um] {Role {name? user.role}} @as [r]; if (r) { {UserRole {User um, Role r}} } else { {Role {name user.role}} @as [rnew] {UserRole {User um, Role rnew}} } um } } } @public workflow UpdateUser { {User {id UpdateUser.id, firstName UpdateUser.firstName, lastName UpdateUser.lastName}, @upsert} } @public workflow UpdateUserStatus { {User {id UpdateUserStatus.id, status UpdateUserStatus.status}, @upsert} } @public workflow inactivateUser { await Auth.inactivateUser(inactivateUser.userId) } @public workflow activateUser { await Auth.activateUser(activateUser.userId) } workflow UpdateUserLastLogin { {User {id? UpdateUserLastLogin.id, lastLoginTime UpdateUserLastLogin.loginTime}} } @public workflow FindUser { {User {id? FindUser.id}} @as [user]; user } @public workflow FindUserByEmail { {User {email? FindUserByEmail.email}} @as [user]; user } entity Role { name String @id } relationship UserRole between (User, Role) entity Permission { id String @id, resourceFqName String @indexed, c Boolean, r Boolean, u Boolean, d Boolean } relationship RolePermission between(Role, Permission) @public workflow CreateRole { {Role {name CreateRole.name}, @upsert} } @public workflow FindRole { {Role {name? FindRole.name}} @as [role]; role } @public workflow ListRoles { {Role? {}} } @public workflow ListUserRoles { if (ListUserRoles.Role and ListUserRoles.User) { {UserRole {User? ListUserRoles.User, Role? ListUserRoles.Role}} } else if (ListUserRoles.User) { {UserRole {User? ListUserRoles.User}} } else if (ListUserRoles.Role) { {UserRole {Role? ListUserRoles.Role}} } else { {UserRole? {}} } } @public workflow ListPermissions { {Permission? {}} } @public workflow ListRolePermissions { if (ListRolePermissions.Role and ListRolePermissions.Permission) { {RolePermission {Role? ListRolePermissions.Role, Permission? ListRolePermissions.Permission}} } else if (ListRolePermissions.Role) { {RolePermission {Role? ListRolePermissions.Role}} } else if (ListRolePermissions.Permission) { {RolePermission {Permission? ListRolePermissions.Permission}} } else { {RolePermission? {}} } } @public workflow AssignUserToRole { {User {id? AssignUserToRole.userId}} @as [user]; {Role {name? AssignUserToRole.roleName}} @as [role]; {UserRole {User user, Role role}, @upsert} } @public workflow AssignUserToRoleByEmail { {User {email? AssignUserToRoleByEmail.email}} @as [user]; {Role {name? AssignUserToRoleByEmail.roleName}} @as [role]; {UserRole {User user, Role role}, @upsert} } @public workflow FindUserRoles { {User {id? FindUserRoles.userId}, UserRole {Role? {}}} } @public workflow CreatePermission { {Permission {id CreatePermission.id, resourceFqName CreatePermission.resourceFqName, c CreatePermission.c, r CreatePermission.r, u CreatePermission.u, d CreatePermission.d}, RolePermission {Role {name? CreatePermission.roleName}}, @upsert} } @public workflow AddPermissionToRole { {Role {name? AddPermissionToRole.roleName}} @as [role]; {Permission {id? AddPermissionToRole.permissionId}} @as [perm]; {RolePermission {Role role, Permission perm}, @upsert} } @public workflow FindRolePermissions { {Role {name? FindRolePermissions.role}, RolePermission {Permission? {}}} } entity Session { id UUID @id, userId UUID @indexed, authToken String @optional, accessToken String @optional, refreshToken String @optional, isActive Boolean, @rbac [(allow: [read, delete, update, create], where: auth.user = this.userId)] } @public workflow CreateSession { {Session {id CreateSession.id, userId CreateSession.userId, authToken CreateSession.authToken, accessToken CreateSession.accessToken, refreshToken CreateSession.refreshToken, isActive true}} } @public workflow UpdateSession { {Session {id? UpdateSession.id, authToken UpdateSession.authToken, accessToken UpdateSession.accessToken, refreshToken UpdateSession.refreshToken, isActive true}, @upsert} } @public workflow FindSession { {Session {id? FindSession.id}} @as [session]; session } @public workflow FindUserSession { {Session {userId? FindUserSession.userId}} @as [session]; session } @public workflow RemoveSession { purge {Session {id? RemoveSession.id}} } @public workflow RemoveUserSession { {Session {userId? RemoveUserSession.id}} @as [session]; purge {Session {id? session.id}} } @public workflow DeleteRole { purge {UserRole {Role? DeleteRole.name}} purge {Role {name? DeleteRole.name}} } @public workflow DeleteUserRole { purge {UserRole {User? DeleteUserRole.User, Role? DeleteUserRole.Role}} } @public workflow DeletePermission { purge {RolePermission {Permission? DeletePermission.id}} purge {Permission {id? DeletePermission.id}} } @public workflow DeleteRolePermission { purge {RolePermission {Role? DeleteRolePermission.Role, Permission? DeleteRolePermission.Permission}} } @public workflow UpdateRoleAssignment { {User {id? UpdateRoleAssignment.userId}} @as [user] {Role {name? UpdateRoleAssignment.roleName}} @as [role] if (user and role) { {UserRole {__path__? UpdateRoleAssignment.userRole, User user.__path__, Role role.__path__}} } else if (user) { {UserRole {__path__? UpdateRoleAssignment.userRole, User user.__path__}} } else if (role) { {UserRole {__path__? UpdateRoleAssignment.userRole, Role role.__path__}} } } @public workflow UpdatePermissionAssignment { {Role {name? UpdatePermissionAssignment.roleName}} @as [role] {Permission {id? UpdatePermissionAssignment.permissionId}} @as [permission] if (role and permission) { {RolePermission {__path__? UpdatePermissionAssignment.rolePermission, Permission? permission.__path__, Role role.__path__}} } else if (role) { {RolePermission {__path__? UpdatePermissionAssignment.rolePermission, Role role.__path__}} } else if (permission) { {RolePermission {__path__? UpdatePermissionAssignment.rolePermission, Permission? permission.__path__}} } } @public workflow UpdatePermission { if (UpdatePermission.resourceFqName and UpdatePermission.c != undefined and UpdatePermission.r != undefined and UpdatePermission.u != undefined and UpdatePermission.d != undefined) { {Permission {id? UpdatePermission.id, resourceFqName UpdatePermission.resourceFqName, c UpdatePermission.c, r UpdatePermission.r, u UpdatePermission.u, d UpdatePermission.d} } } else if (UpdatePermission.c != undefined and UpdatePermission.r != undefined and UpdatePermission.u != undefined and UpdatePermission.d != undefined) { {Permission {id? UpdatePermission.id, c UpdatePermission.c, r UpdatePermission.r, u UpdatePermission.u, d UpdatePermission.d} } } else if (UpdatePermission.resourceFqName) { {Permission {id? UpdatePermission.id, resourceFqName UpdatePermission.resourceFqName} } } } @public workflow signup { await Auth.signUpUser(signup.firstName, signup.lastName, signup.email, signup.password, signup.userData) } @public workflow confirmSignup { await Auth.confirmSignupUser(confirmSignup.email, confirmSignup.confirmationCode) } @public workflow resendConfirmationCode { await Auth.resendConfirmationCodeUser(resendConfirmationCode.email) } @public workflow login { await Auth.loginUser(login.email, login.password) } @public workflow forgotPassword { await Auth.forgotPasswordUser(forgotPassword.email) } @public workflow confirmForgotPassword { await Auth.confirmForgotPasswordUser( confirmForgotPassword.email, confirmForgotPassword.confirmationCode, confirmForgotPassword.newPassword ) } @public workflow logout { await Auth.logoutUser() } @public workflow changePassword { await Auth.changePassword(changePassword.newPassword, changePassword.password) } @public workflow refreshToken { await Auth.refreshUserToken(refreshToken.refreshToken) } @public workflow getUser { await Auth.getUserInfo(getUser.userId) } @public workflow getUserByEmail { await Auth.getUserInfoByEmail(getUserByEmail.email) } @public workflow getUsersDetail { {User? {}, UserRole {Role? {}}, @into { id User.id, email User.email, firstName User.firstName, lastName User.lastName, lastLoginTime User.lastLoginTime, status User.status, role Role.name} } } @public workflow inviteUser { await Auth.inviteUser(inviteUser.email, inviteUser.firstName, inviteUser.lastName, inviteUser.userData, inviteUser.role) } @public workflow inviteUsers { for u in inviteUsers.users { {inviteUser {email u.email, firstName u.firstName, lastName u.lastName, userData u.userData, role u.role}} } } record ResendInvitationResult { message String } @public workflow resendInvitation { {User {email? resendInvitation.email}} @as [u] if (u and u.status == "Invited") { await Auth.resendInvitationUser(u.email) } else if (u) { {ResendInvitationResult {message "User is not invited"}} } else { {ResendInvitationResult {message "User not found"}} } } @public workflow acceptInvitation { await Auth.acceptInvitationUser(acceptInvitation.email, acceptInvitation.tempPassword, acceptInvitation.newPassword) } @public workflow callback { await Auth.callbackUser(callback.code) } `; const evalEvent = makeEventEvaluator(CoreAuthModuleName); export async function createUser(id, email, firstName, lastName, env, status = 'Active') { return await evalEvent('CreateUser', { id: id, email: email.toLowerCase(), firstName: firstName, lastName: lastName, status: status, }, env); } export async function findUser(id, env) { return await evalEvent('FindUser', { id: id, }, env); } export async function findUserByEmail(email, env) { return await evalEvent('FindUserByEmail', { email: email.toLowerCase(), }, env); } export async function updateUser(userId, firstName, lastName, env) { return await evalEvent('UpdateUser', { id: userId, firstName: firstName, lastName: lastName, }, env); } export async function updateUserStatus(userId, status, env) { return await evalEvent('UpdateUserStatus', { id: userId, status: status, }, env); } export async function inactivateUser(userId, env) { const needCommit = env ? false : true; env = env ? env : new Environment(); const f = async () => { try { // Update user status to 'Inactive' await updateUserStatus(userId, 'Inactive', env); // Disable user in Cognito const user = await findUser(userId, env); if (user) { const email = user.lookup('email'); if (email) { await fetchAuthImpl().disableUser(email, env); } } return { status: 'ok', message: 'User inactivated successfully', }; } catch (err) { logger.error(`Failed to inactivate user ${userId}: ${err.message}`); throw err; } }; if (needCommit) { return await env.callInTransaction(f); } else { return await f(); } } export async function activateUser(userId, env) { const needCommit = env ? false : true; env = env ? env : new Environment(); const f = async () => { try { // Update user status to 'Active' await updateUserStatus(userId, 'Active', env); // Enable user in Cognito const user = await findUser(userId, env); if (user) { const email = user.lookup('email'); if (email) { await fetchAuthImpl().enableUser(email, env); } } return { status: 'ok', message: 'User activated successfully', }; } catch (err) { logger.error(`Failed to activate user ${userId}: ${err.message}`); throw err; } }; if (needCommit) { return await env.callInTransaction(f); } else { return await f(); } } export async function deleteUser(userId, email, env) { const needCommit = env ? false : true; env = env ? env : new Environment(); const f = async () => { try { if (email) { try { await fetchAuthImpl().deleteUser(email, env); } catch (err) { // If user doesn't exist in Cognito, log warning but continue with local deletion if (err.message && err.message.includes('not found')) { logger.warn(`User ${email} not found in Cognito, continuing with local deletion`); } else { logger.error(`Failed to delete user ${email} from Cognito: ${err.message}`); throw err; } } } return { status: 'ok', message: 'User deleted successfully', }; } catch (err) { logger.error(`Failed to delete user ${userId}: ${err.message}`); throw err; } }; if (needCommit) { return await env.callInTransaction(f); } else { return await f(); } } export async function updateUserLastLogin(id, env) { return await evalEvent('UpdateUserLastLogin', { id: id, loginTime: new Date().toISOString(), }, env); } export async function ensureUser(email, firstName, lastName, env, status = 'Active') { const user = await findUserByEmail(email.toLowerCase(), env); if (user) { // Update existing user with latest name information from ID token const userId = user.lookup('id'); await updateUser(userId, firstName, lastName, env).catch((reason) => { logger.error(`Failed to update user ${userId} with latest name information: ${reason}`); }); return user; } return await createUser(crypto.randomUUID(), email.toLowerCase(), firstName, lastName, env, status); } export async function ensureUserRoles(userid, userRoles, env) { const currentRoles = await findUserRoles(userid, env); const currentRoleNames = currentRoles === null || currentRoles === void 0 ? void 0 : currentRoles.map((role) => { const roleName = role.attributes.get('name'); return roleName && roleName !== '*' ? roleName : null; }).filter(Boolean); if (currentRoleNames.length > 0) { logger.info(`User ${userid} already has roles: ${currentRoleNames.join(', ')}, skipping role assignment.`); return; } for (let i = 0; i < userRoles.length; ++i) { const role = userRoles[i]; await createRole(role, env); await assignUserToRole(userid, role, env); } } export async function ensureUserSession(userId, token, accessToken, refreshToken, env) { const sess = await findUserSession(userId, env); if (sess) { // Update existing session instead of deleting and recreating await updateSession(sess.lookup('id'), token, accessToken, refreshToken, env); // Return the updated session by finding it again return await findUserSession(userId, env); } const sessionId = crypto.randomUUID(); await createSession(sessionId, userId, token, accessToken, refreshToken, env); // Return the created session by finding it return await findSession(sessionId, env); } export async function createSession(id, userId, token, accessToken, refreshToken, env) { return await evalEvent('CreateSession', { id: id, userId: userId, authToken: token, accessToken: accessToken, refreshToken: refreshToken, }, env); } export async function findSession(id, env) { return await evalEvent('FindSession', { id: id, }, env); } export async function findUserSession(userId, env) { return await evalEvent('FindUserSession', { userId: userId, }, env); } export async function updateSession(id, token, accessToken, refreshToken, env) { return await evalEvent('UpdateSession', { id: id, authToken: token, accessToken: accessToken, refreshToken: refreshToken, }, env); } export async function removeSession(id, env) { return await evalEvent('RemoveSession', { id: id, }, env); } export async function findRole(name, env) { return await evalEvent('FindRole', { name: name }, env); } export async function createRole(name, env) { await evalEvent('CreateRole', { name: name }, env).catch((reason) => { logger.error(`Failed to create role '${name}' - ${reason}`); }); } export async function createPermission(id, roleName, resourceFqName, c = false, r = false, u = false, d = false, env) { await evalEvent('CreatePermission', { id: id, roleName: roleName, resourceFqName: resourceFqName, c: c, r: r, u: u, d: d, }, env).catch((reason) => { logger.error(`Failed to create permission ${id} - ${reason}`); }); } export async function assignUserToRole(userId, roleName, env) { let r = true; await evalEvent('AssignUserToRole', { userId: userId, roleName: roleName }, env).catch((reason) => { logger.error(`Failed to assign user ${userId} to role ${roleName} - ${reason}`); r = false; }); return r; } export async function assignUserToRoleByEmail(email, roleName, env) { let r = true; await evalEvent('AssignUserToRoleByEmail', { email: email.toLowerCase(), roleName: roleName }, env).catch((reason) => { logger.error(`Failed to assign user ${email} to role ${roleName} - ${reason}`); r = false; }); return r; } let DefaultRoleInstance; export async function findUserRoles(userId, env) { const result = await evalEvent('FindUserRoles', { userId: userId }, env); const inst = result ? result[0] : undefined; if (inst) { let roles = inst.getRelatedInstances('UserRole'); if (roles === undefined) { roles = []; } if (DefaultRoleInstance === undefined) { DefaultRoleInstance = makeInstance(CoreAuthModuleName, 'Role', newInstanceAttributes().set('name', '*')); } roles.push(DefaultRoleInstance); return roles; } return undefined; } const UserRoleCache = new Map(); const RolePermissionsCache = new Map(); async function findRolePermissions(role, env) { return await evalEvent('FindRolePermissions', { role: role }, env); } async function updatePermissionCacheForRole(role, env) { const result = await findRolePermissions(role, env); if (result instanceof Array && result.length > 0) { const roleInst = result[0]; const permInsts = roleInst.getRelatedInstances('RolePermission'); if (permInsts) { RolePermissionsCache.set(role, permInsts.map((inst) => { return inst.cast(); })); } } } export async function userHasPermissions(userId, resourceFqName, perms, env) { if (userId == AdminUserId || !isRbacEnabled()) { return true; } let userRoles = UserRoleCache.get(userId); if (!userRoles) { const roles = await findUserRoles(userId, env); userRoles = []; if (roles) { for (let i = 0; i < roles.length; ++i) { const r = roles[i]; const n = r.attributes.get('name'); userRoles.push(n); if (!RolePermissionsCache.get(n)) { await updatePermissionCacheForRole(n, env); } } } UserRoleCache.set(userId, userRoles); } if (userRoles && userRoles.find((role) => { return role === 'admin'; })) { return true; } const [c, r, u, d] = [ perms.has(RbacPermissionFlag.CREATE), perms.has(RbacPermissionFlag.READ), perms.has(RbacPermissionFlag.UPDATE), perms.has(RbacPermissionFlag.DELETE), ]; if (userRoles !== null) { for (let i = 0; i < userRoles.length; ++i) { const permInsts = RolePermissionsCache.get(userRoles[i]); if (permInsts) { if (permInsts.find((p) => { return (p.resourceFqName == resourceFqName && (c ? isSqlTrue(p.c) : true) && (r ? isSqlTrue(p.r) : true) && (u ? isSqlTrue(p.u) : true) && (d ? isSqlTrue(p.d) : true)); })) return true; } } } return false; } const CreateOperation = new Set([RbacPermissionFlag.CREATE]); const ReadOperation = new Set([RbacPermissionFlag.READ]); const UpdateOperation = new Set([RbacPermissionFlag.UPDATE]); const DeleteOperation = new Set([RbacPermissionFlag.DELETE]); function canUserPerfom(opr) { // TODO: check parent hierarchy // TODO: cache permissions for user async function f(userId, resourceFqName, env) { if (userId == AdminUserId) { return true; } return await userHasPermissions(userId, resourceFqName, opr, env); } return f; } export const canUserCreate = canUserPerfom(CreateOperation); export const canUserRead = canUserPerfom(ReadOperation); export const canUserUpdate = canUserPerfom(UpdateOperation); export const canUserDelete = canUserPerfom(DeleteOperation); let runtimeAuth; if (isNodeEnv) { runtimeAuth = new CognitoAuth(); } function fetchAuthImpl() { if (runtimeAuth) { return runtimeAuth; } else { throw new Error('Auth not initialized'); } } export async function signUpUser(firstName, lastName, username, password, userData, env) { let result; try { await fetchAuthImpl().signUp(firstName, lastName, username.toLowerCase(), password, userData ? new Map(Object.entries(userData)) : undefined, env, (userInfo) => { result = userInfo; }); return result; } catch (err) { logger.error(`Signup failed for ${username}: ${err.message}`); throw err; // Re-throw to preserve error type for HTTP status mapping } } export async function confirmSignupUser(username, confirmationCode, env) { try { await fetchAuthImpl().confirmSignup(username.toLowerCase(), confirmationCode, env); return { status: 'ok', message: 'User confirmed successfully', }; } catch (err) { logger.error(`Confirm signup failed for ${username}: ${err.message}`); throw err; // Re-throw to preserve error type for HTTP status mapping } } export async function resendConfirmationCodeUser(username, env) { try { await fetchAuthImpl().resendConfirmationCode(username.toLowerCase(), env); return { status: 'ok', message: 'Confirmation code resent successfully', }; } catch (err) { logger.error(`Resend confirmation code failed for ${username}: ${err.message}`); throw err; // Re-throw to preserve error type for HTTP status mapping } } export async function forgotPasswordUser(username, env) { try { await fetchAuthImpl().forgotPassword(username.toLowerCase(), env); return { status: 'ok', message: 'Password reset code sent' }; } catch (err) { logger.error(`Forgot password failed for ${username}: ${err.message}`); throw err; } } export async function confirmForgotPasswordUser(username, confirmationCode, newPassword, env) { try { await fetchAuthImpl().confirmForgotPassword(username.toLowerCase(), confirmationCode, newPassword, env); return { status: 'ok', message: 'Password has been reset' }; } catch (err) { logger.error(`Confirm forgot password failed for ${username}: ${err.message}`); throw err; } } export async function loginUser(username, password, env) { let result = ''; try { await fetchAuthImpl().login(username.toLowerCase(), password, env, (r) => { UserRoleCache.set(r.userId, null); updateUserLastLogin(r.userId, env); // Check if Cognito is configured by checking if we have the tokens if (r.idToken && r.accessToken && r.refreshToken) { // Return full token response for Cognito result = { id_token: r.idToken, access_token: r.accessToken, refresh_token: r.refreshToken, token_type: 'Bearer', expires_in: 3600, userId: r.userId, sessionId: r.sessionId, }; } else { // Return string format for non-Cognito authentication result = `${r.userId}/${r.sessionId}`; } }); return result; } catch (err) { logger.error(`Login failed for ${username}: ${err.message}`); throw err; // Re-throw to preserve error type for HTTP status mapping } } export async function callbackUser(code, env) { let result = ''; try { await fetchAuthImpl().callback(code, env, async (r) => { UserRoleCache.set(r.userId, null); updateUserLastLogin(r.userId, env); // Update user status to 'Active' after successful callback await updateUserStatus(r.userId, 'Active', env); if (r.idToken && r.accessToken && r.refreshToken) { result = { id_token: r.idToken, access_token: r.accessToken, refresh_token: r.refreshToken, token_type: 'Bearer', expires_in: 3600, userId: r.userId, sessionId: r.sessionId, }; } else { result = `${r.userId}/${r.sessionId}`; } }); return result; } catch (err) { logger.error(`Callback failed for ${code}: ${err.message}`); throw err; } } async function logoutSession(userId, sess, env) { const sessId = sess.lookup('id'); const tok = sess.lookup('authToken'); await fetchAuthImpl().logout({ sessionId: sessId, userId: userId, authToken: tok, idToken: tok, accessToken: sess.lookup('accessToken'), refreshToken: sess.lookup('refreshToken'), }, env); await removeSession(sessId, env); return { status: 'ok', message: 'Logged out successfully', }; } export async function logoutUser(env) { const user = env.getActiveUser(); const sess = await findUserSession(user, env); if (sess) { return await logoutSession(user, sess, env); } return { status: 'ok', message: 'Logged out successfully', }; } export async function changePassword(newPassword, password, env) { const user = env.getActiveUser(); const sess = await findUserSession(user, env); if (sess) { const sessId = sess.lookup('id'); const tok = sess.lookup('authToken'); const sessInfo = { sessionId: sessId, userId: user, authToken: tok, idToken: tok, accessToken: sess.lookup('accessToken'), refreshToken: sess.lookup('refreshToken'), }; if (await fetchAuthImpl().changePassword(sessInfo, newPassword, password, env)) { return await logoutSession(user, sess, env); } else { return undefined; } } else { throw new UnauthorisedError(`No active session for user ${user}`); } } export async function verifySession(token, env) { if (!isAuthEnabled()) return BypassSession; // Check if token is a JWT (Cognito ID token) or userId/sessionId format if (isJwtToken(token)) { return await verifyJwtToken(token, env); } else { return await verifySessionToken(token, env); } } function isJwtToken(token) { // Simple JWT structure check - JWT tokens have 3 parts separated by dots return !!(token && typeof token === 'string' && token.split('.').length === 3); } async function verifyJwtToken(token, env) { const needCommit = env ? false : true; env = env ? env : new Environment(); const f = async () => { try { // Validate JWT structure first if (!isJwtToken(token)) { throw new UnauthorisedError('Invalid JWT token structure'); } // Verify the JWT token directly with Cognito await fetchAuthImpl().verifyToken(token, env); // Extract user information from JWT payload const parts = token.split('.'); const payload = JSON.parse(atob(parts[1])); // Extract user ID from standard JWT claims (sub or cognito:username) const userId = payload.sub || payload['cognito:username']; const email = payload.email || payload['cognito:username']; if (!userId) { throw new UnauthorisedError('Invalid JWT token: missing user identifier'); } let localUser = null; if (email) { localUser = await findUserByEmail(email.toLowerCase(), env); } if (!localUser && userId) { localUser = await findUser(userId, env); } if (!localUser) { logger.warn(`User not found in local database for JWT token. Email: ${email}, UserId: ${userId}`); throw new UnauthorisedError(`User not found in local database`); } // Use the local user's ID for consistency const localUserId = localUser.lookup('id'); // Check if user status is 'Active' const userStatus = localUser.lookup('status'); if (userStatus !== 'Active') { throw new UnauthorisedError(`User account is not active. Status: ${userStatus}`); } const sess = await findUserSession(localUserId, env); if (!sess) { throw new UnauthorisedError(`No session found for user ${email}, UserId: ${userId}`); } // For JWT tokens, we use the token itself as sessionId for tracking return { sessionId: sess.lookup('id'), userId: localUserId }; } catch (err) { if (err instanceof UnauthorisedError) { throw err; } logger.error(`JWT token verification failed:`, { errorName: err.name, errorMessage: err.message, }); throw new UnauthorisedError('JWT token verification failed'); } }; if (needCommit) { return await env.callInTransaction(f); } else { return await f(); } } async function verifySessionToken(token, env) { const parts = token.split('/'); const sessId = parts[1]; const userId = parts[0]; const needCommit = env ? false : true; env = env ? env : new Environment(); const f = async () => { try { // Check if user status is 'Active' const user = await findUser(userId, env); if (user) { const userStatus = user.lookup('status'); if (userStatus !== 'Active') { throw new UnauthorisedError(`User account is not active. Status: ${userStatus}`); } } const sess = await findSession(sessId, env); if (sess !== undefined) { await fetchAuthImpl().verifyToken(sess.lookup('authToken'), env); return { sessionId: sessId, userId: userId }; } else { logger.warn(`No active session found for user '${userId}'`); throw new UnauthorisedError(`No active session for user '${userId}'`); } } catch (err) { if (err instanceof UnauthorisedError) { throw err; } // Log error details for debugging logger.error(`Session verification failed for user '${parts[0]}':`, { errorName: err.name, errorMessage: err.message, sessionId: sessId, }); throw new UnauthorisedError('Session verification failed'); } }; if (needCommit) { return await env.callInTransaction(f); } else { return await f(); } } export async function getUserInfo(userId, env) { const needCommit = env ? false : true; env = env ? env : new Environment(); const f = async () => { try { return await fetchAuthImpl().getUser(userId, env); } catch (err) { logger.error(`Failed to get user info for ${userId}: ${err.message}`); throw err; // Re-throw to preserve error type } }; if (needCommit) { return await env.callInTransaction(f); } else { return await f(); } } export async function getUserInfoByEmail(email, env) { const needCommit = env ? false : true; env = env ? env : new Environment(); const f = async () => { try { return await fetchAuthImpl().getUserByEmail(email.toLowerCase(), env); } catch (err) { logger.error(`Failed to get user info for email ${email}: ${err.message}`); throw err; // Re-throw to preserve error type } }; if (needCommit) { return await env.callInTransaction(f); } else { return await f(); } } export async function refreshUserToken(refreshToken, env) { const needCommit = env ? false : true; env = env ? env : new Environment(); const f = async () => { try { const sessionInfo = await fetchAuthImpl().refreshToken(refreshToken, env); return { id_token: sessionInfo.idToken, access_token: sessionInfo.accessToken, refresh_token: sessionInfo.refreshToken, token_type: 'Bearer', expires_in: 3600, userId: sessionInfo.userId, sessionId: sessionInfo.sessionId, }; } catch (err) { logger.error(`Token refresh failed: ${err.message}`); throw err; } }; if (needCommit) { return await env.callInTransaction(f); } else { return await f(); } } export async function inviteUser(email, firstName, lastName, userData, role, env) { const needCommit = env ? false : true; env = env ? env : new Environment(); const f = async () => { try { let invitationInfo; await fetchAuthImpl().inviteUser(email, firstName, lastName, userData, role, env, (info) => { invitationInfo = info; }); return { email: invitationInfo.email, firstName: invitationInfo.firstName, lastName: invitationInfo.lastName, invitationId: invitationInfo.invitationId, message: 'User invitation sent successfully', }; } catch (err) { logger.error(`User invitation failed: ${err.message}`); throw err; } }; if (needCommit) { return await env.callInTransaction(f); } else { return await f(); } } export async function resendInvitationUser(email, env) { const needCommit = env ? false : true; env = env ? env : new Environment(); const f = async () => { try { await fetchAuthImpl().resendInvitation(email, env); return { email: email, message: 'Invitation resent successfully', }; } catch (err) { logger.error(`Invitation resend failed: ${err.message}`); throw err; } }; if (needCommit) { return await env.callInTransaction(f); } else { return await f(); } } export async function acceptInvitationUser(email, tempPassword, newPassword, env) { const needCommit = env ? false : true; env = env ? env : new Environment(); const f = async () => { try { await fetchAuthImpl().acceptInvitation(email, tempPassword, newPassword, env); // Update user status to 'Active' after accepting invitation const user = await findUserByEmail(email.toLowerCase(), env); if (user) { const userId = user.lookup('id'); await updateUserStatus(userId, 'Active', env); } return { email: email, message: 'Invitation accepted successfully', }; } catch (err) { logger.error(`Accept invitation failed: ${err.message}`); throw err; } }; if (needCommit) { return await env.callInTransaction(f); } else { return await f(); } } export function requireAuth(moduleName, eventName) { if (isAuthEnabled()) { const f = moduleName == CoreAuthModuleName && (eventName == 'login' || eventName == 'signup' || eventName == 'confirmSignup' || eventName == 'resendConfirmationCode' || eventName == 'forgotPassword' || eventName == 'confirmForgotPassword' || eventName == 'refreshToken' || eventName == 'acceptInvitation' || eventName == 'resendInvitation' || eventName == 'callback'); return !f; } else { return false; } } // Export getHttpStatusForError for use in HTTP handlers export { getHttpStatusForError }; // Helper function to create standardized error responses export function createAuthErrorResponse(error) { const statusCode = getHttpStatusForError(error); let errorType = 'AUTHENTICATION_ERROR'; if (error instanceof UserNotFoundError) { errorType = 'USER_NOT_FOUND'; } else if (error instanceof UnauthorisedError) { errorType = 'UNAUTHORIZED'; } else if (error instanceof UserNotConfirmedError) { errorType = 'USER_NOT_CONFIRMED'; } else if (error instanceof PasswordResetRequiredError) { errorType = 'PASSWORD_RESET_REQUIRED'; } else if (error instanceof TooManyRequestsError) { errorType = 'TOO_MANY_REQUESTS'; } else if (error instanceof InvalidParameterError) { errorType = 'INVALID_PARAMETER'; } else if (error instanceof ExpiredCodeError) { errorType = 'EXPIRED_CODE'; } else if (error instanceof CodeMismatchError) { errorType = 'CODE_MISMATCH'; } else if (error instanceof BadRequestError) { errorType = 'BAD_REQUEST'; } // Log error creation for debugging purposes logger.debug(`Creating auth error response:`, { errorType: errorType, statusCode: statusCode, originalError: error.name, }); return { error: errorType, message: error.message, statusCode: statusCode, }; } // Helper function to check if an error is a known auth error export function isAuthError(error) { return (error instanceof UnauthorisedError || error instanceof UserNotFoundError || error instanceof UserNotConfirmedError || error instanceof PasswordResetRequiredError || error instanceof TooManyRequestsError || error instanceof InvalidParameterError || error instanceof ExpiredCodeError || error instanceof CodeMismatchError || error instanceof BadRequestError); } // Helper function to sanitize error details before logging export function sanitizeErrorForLogging(error) { const sanitizedMessage = error.message .replace(/password/gi, '[REDACTED]') .replace(/token/gi, '[REDACTED]') .replace(/secret/gi, '[REDACTED]') .replace(/key/gi, '[REDACTED]') .replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, '[EMAIL_REDACTED]') .replace(/\b[A-Fa-f0-9]{32,}\b/g, '[TOKEN_REDACTED]') .replace(/\b\d{4,}\b/g, '[NUMBER_REDACTED]'); return { name: error.name, message: error.message, sanitizedMessage: sanitizedMessage, }; } // Helper function to determine if an error should be retried export function isRetryableError(error) { // Only retry on certain types of errors return (error instanceof TooManyRequestsError || (error.message ? error.message.includes('temporarily unavailable') || error.message.includes('service error') || error.message.includes('timeout') : false)); } //# sourceMappingURL=auth.js.map