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,614 lines (1,461 loc) • 44.8 kB
text/typescript
import { Result, Environment, makeEventEvaluator } from '../interpreter.js';
import { logger } from '../logger.js';
import { Instance, makeInstance, newInstanceAttributes, RbacPermissionFlag } from '../module.js';
import { makeCoreModuleName } from '../util.js';
import { isSqlTrue } from '../resolvers/sqldb/dbutil.js';
import { AgentlangAuth, SessionInfo, UserInfo } from '../auth/interface.js';
import {
ActiveSessionInfo,
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" Auth
entity User {
id UUID ,
email Email ,
firstName String,
lastName String,
lastLoginTime DateTime ,
status ,
[(allow: [read, delete, update, create], where: auth.user = this.id)],
{delete AfterDeleteUser}
}
workflow AfterDeleteUser {
{RemoveUserSession {id AfterDeleteUser.User.id}}
await Auth.deleteUser(AfterDeleteUser.User.id, AfterDeleteUser.User.email)
}
workflow CreateUser {
{User {id CreateUser.id,
email CreateUser.email,
firstName CreateUser.firstName,
lastName CreateUser.lastName,
status CreateUser.status}}
}
workflow CreateUsers {
for user in CreateUsers.users {
{User {email? user.email}} [u];
if (u) {
{User {id? u.id,
firstName user.firstName,
lastName user.lastName}}
[um]
um
}
else {
{User {email user.email,
firstName user.firstName,
lastName user.lastName}}
[um]
{Role {name? user.role}}
[r];
if (r) {
{UserRole {User um, Role r}}
} else {
{Role {name user.role}} [rnew]
{UserRole {User um, Role rnew}}
}
um
}
}
}
workflow UpdateUser {
{User {id UpdateUser.id,
firstName UpdateUser.firstName,
lastName UpdateUser.lastName}, }
}
workflow UpdateUserStatus {
{User {id UpdateUserStatus.id,
status UpdateUserStatus.status}, }
}
workflow inactivateUser {
await Auth.inactivateUser(inactivateUser.userId)
}
workflow activateUser {
await Auth.activateUser(activateUser.userId)
}
workflow UpdateUserLastLogin {
{User {id? UpdateUserLastLogin.id, lastLoginTime UpdateUserLastLogin.loginTime}}
}
workflow FindUser {
{User {id? FindUser.id}} [user];
user
}
workflow FindUserByEmail {
{User {email? FindUserByEmail.email}} [user];
user
}
entity Role {
name String
}
relationship UserRole between (User, Role)
entity Permission {
id String ,
resourceFqName String ,
c Boolean,
r Boolean,
u Boolean,
d Boolean
}
relationship RolePermission between(Role, Permission)
workflow CreateRole {
{Role {name CreateRole.name}, }
}
workflow FindRole {
{Role {name? FindRole.name}} [role];
role
}
workflow ListRoles {
{Role? {}}
}
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? {}}
}
}
workflow ListPermissions {
{Permission? {}}
}
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? {}}
}
}
workflow AssignUserToRole {
{User {id? AssignUserToRole.userId}} [user];
{Role {name? AssignUserToRole.roleName}} [role];
{UserRole {User user, Role role}, }
}
workflow AssignUserToRoleByEmail {
{User {email? AssignUserToRoleByEmail.email}} [user];
{Role {name? AssignUserToRoleByEmail.roleName}} [role];
{UserRole {User user, Role role}, }
}
workflow FindUserRoles {
{User {id? FindUserRoles.userId},
UserRole {Role? {}}}
}
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}},
}
}
workflow AddPermissionToRole {
{Role {name? AddPermissionToRole.roleName}} [role];
{Permission {id? AddPermissionToRole.permissionId}} [perm];
{RolePermission {Role role, Permission perm}, }
}
workflow FindRolePermissions {
{Role {name? FindRolePermissions.role},
RolePermission {Permission? {}}}
}
entity Session {
id UUID ,
userId UUID ,
authToken String ,
accessToken String ,
refreshToken String ,
isActive Boolean,
[(allow: [read, delete, update, create], where: auth.user = this.userId)]
}
workflow CreateSession {
{Session {id CreateSession.id, userId CreateSession.userId,
authToken CreateSession.authToken,
accessToken CreateSession.accessToken,
refreshToken CreateSession.refreshToken,
isActive true}}
}
workflow UpdateSession {
{Session {id? UpdateSession.id,
authToken UpdateSession.authToken,
accessToken UpdateSession.accessToken,
refreshToken UpdateSession.refreshToken,
isActive true}, }
}
workflow FindSession {
{Session {id? FindSession.id}} [session];
session
}
workflow FindUserSession {
{Session {userId? FindUserSession.userId}} [session];
session
}
workflow RemoveSession {
purge {Session {id? RemoveSession.id}}
}
workflow RemoveUserSession {
{Session {userId? RemoveUserSession.id}} [session];
purge {Session {id? session.id}}
}
workflow DeleteRole {
purge {UserRole {Role? DeleteRole.name}}
purge {Role {name? DeleteRole.name}}
}
workflow DeleteUserRole {
purge {UserRole {User? DeleteUserRole.User, Role? DeleteUserRole.Role}}
}
workflow DeletePermission {
purge {RolePermission {Permission? DeletePermission.id}}
purge {Permission {id? DeletePermission.id}}
}
workflow DeleteRolePermission {
purge {RolePermission {Role? DeleteRolePermission.Role, Permission? DeleteRolePermission.Permission}}
}
workflow UpdateRoleAssignment {
{User {id? UpdateRoleAssignment.userId}} [user]
{Role {name? UpdateRoleAssignment.roleName}} [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__}}
}
}
workflow UpdatePermissionAssignment {
{Role {name? UpdatePermissionAssignment.roleName}} [role]
{Permission {id? UpdatePermissionAssignment.permissionId}} [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__}}
}
}
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}
}
}
}
workflow signup {
await Auth.signUpUser(signup.firstName, signup.lastName, signup.email, signup.password, signup.userData)
}
workflow confirmSignup {
await Auth.confirmSignupUser(confirmSignup.email, confirmSignup.confirmationCode)
}
workflow resendConfirmationCode {
await Auth.resendConfirmationCodeUser(resendConfirmationCode.email)
}
workflow login {
await Auth.loginUser(login.email, login.password)
}
workflow forgotPassword {
await Auth.forgotPasswordUser(forgotPassword.email)
}
workflow confirmForgotPassword {
await Auth.confirmForgotPasswordUser(
confirmForgotPassword.email,
confirmForgotPassword.confirmationCode,
confirmForgotPassword.newPassword
)
}
workflow logout {
await Auth.logoutUser()
}
workflow changePassword {
await Auth.changePassword(changePassword.newPassword, changePassword.password)
}
workflow refreshToken {
await Auth.refreshUserToken(refreshToken.refreshToken)
}
workflow getUser {
await Auth.getUserInfo(getUser.userId)
}
workflow getUserByEmail {
await Auth.getUserInfoByEmail(getUserByEmail.email)
}
workflow getUsersDetail {
{User? {},
UserRole {Role? {}},
{
id User.id,
email User.email,
firstName User.firstName,
lastName User.lastName,
lastLoginTime User.lastLoginTime,
status User.status,
role Role.name}
}
}
workflow inviteUser {
await Auth.inviteUser(inviteUser.email, inviteUser.firstName, inviteUser.lastName, inviteUser.userData, inviteUser.role)
}
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
}
workflow resendInvitation {
{User {email? resendInvitation.email}} [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"}}
}
}
workflow acceptInvitation {
await Auth.acceptInvitationUser(acceptInvitation.email, acceptInvitation.tempPassword, acceptInvitation.newPassword)
}
workflow callback {
await Auth.callbackUser(callback.code)
}
`;
const evalEvent = makeEventEvaluator(CoreAuthModuleName);
export async function createUser(
id: string,
email: string,
firstName: string,
lastName: string,
env: Environment,
status: string = 'Active'
): Promise<Result> {
return await evalEvent(
'CreateUser',
{
id: id,
email: email.toLowerCase(),
firstName: firstName,
lastName: lastName,
status: status,
},
env
);
}
export async function findUser(id: string, env: Environment): Promise<Result> {
return await evalEvent(
'FindUser',
{
id: id,
},
env
);
}
export async function findUserByEmail(email: string, env: Environment): Promise<Result> {
return await evalEvent(
'FindUserByEmail',
{
email: email.toLowerCase(),
},
env
);
}
export async function updateUser(
userId: string,
firstName: string,
lastName: string,
env: Environment
): Promise<Result> {
return await evalEvent(
'UpdateUser',
{
id: userId,
firstName: firstName,
lastName: lastName,
},
env
);
}
export async function updateUserStatus(
userId: string,
status: string,
env: Environment
): Promise<Result> {
return await evalEvent(
'UpdateUserStatus',
{
id: userId,
status: status,
},
env
);
}
export async function inactivateUser(userId: string, env: Environment): Promise<Result> {
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: any) {
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: string, env: Environment): Promise<Result> {
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: any) {
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: string, email: string, env: Environment): Promise<Result> {
const needCommit = env ? false : true;
env = env ? env : new Environment();
const f = async () => {
try {
if (email) {
try {
await fetchAuthImpl().deleteUser(email, env);
} catch (err: any) {
// 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: any) {
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: string, env: Environment): Promise<Result> {
return await evalEvent(
'UpdateUserLastLogin',
{
id: id,
loginTime: new Date().toISOString(),
},
env
);
}
export async function ensureUser(
email: string,
firstName: string,
lastName: string,
env: Environment,
status: string = '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: any) => {
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: string, userRoles: string[], env: Environment) {
const currentRoles = await findUserRoles(userid, env);
const currentRoleNames = currentRoles
?.map((role: Instance) => {
const roleName = (role as Instance).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: string,
token: string,
accessToken: string,
refreshToken: string,
env: Environment
): Promise<Instance> {
const sess: Instance = 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: string,
userId: string,
token: string,
accessToken: string,
refreshToken: string,
env: Environment
): Promise<Result> {
return await evalEvent(
'CreateSession',
{
id: id,
userId: userId,
authToken: token,
accessToken: accessToken,
refreshToken: refreshToken,
},
env
);
}
export async function findSession(id: string, env: Environment): Promise<Result> {
return await evalEvent(
'FindSession',
{
id: id,
},
env
);
}
export async function findUserSession(userId: string, env: Environment): Promise<Result> {
return await evalEvent(
'FindUserSession',
{
userId: userId,
},
env
);
}
export async function updateSession(
id: string,
token: string,
accessToken: string,
refreshToken: string,
env: Environment
): Promise<Result> {
return await evalEvent(
'UpdateSession',
{
id: id,
authToken: token,
accessToken: accessToken,
refreshToken: refreshToken,
},
env
);
}
export async function removeSession(id: string, env: Environment): Promise<Result> {
return await evalEvent(
'RemoveSession',
{
id: id,
},
env
);
}
export async function findRole(name: string, env: Environment): Promise<Result> {
return await evalEvent('FindRole', { name: name }, env);
}
export async function createRole(name: string, env: Environment) {
await evalEvent('CreateRole', { name: name }, env).catch((reason: any) => {
logger.error(`Failed to create role '${name}' - ${reason}`);
});
}
export async function createPermission(
id: string,
roleName: string,
resourceFqName: string,
c: boolean = false,
r: boolean = false,
u: boolean = false,
d: boolean = false,
env: Environment
) {
await evalEvent(
'CreatePermission',
{
id: id,
roleName: roleName,
resourceFqName: resourceFqName,
c: c,
r: r,
u: u,
d: d,
},
env
).catch((reason: any) => {
logger.error(`Failed to create permission ${id} - ${reason}`);
});
}
export async function assignUserToRole(
userId: string,
roleName: string,
env: Environment
): Promise<boolean> {
let r: boolean = true;
await evalEvent('AssignUserToRole', { userId: userId, roleName: roleName }, env).catch(
(reason: any) => {
logger.error(`Failed to assign user ${userId} to role ${roleName} - ${reason}`);
r = false;
}
);
return r;
}
export async function assignUserToRoleByEmail(
email: string,
roleName: string,
env: Environment
): Promise<boolean> {
let r: boolean = true;
await evalEvent(
'AssignUserToRoleByEmail',
{ email: email.toLowerCase(), roleName: roleName },
env
).catch((reason: any) => {
logger.error(`Failed to assign user ${email} to role ${roleName} - ${reason}`);
r = false;
});
return r;
}
let DefaultRoleInstance: Instance | undefined;
export async function findUserRoles(userId: string, env: Environment): Promise<Result> {
const result: any = await evalEvent('FindUserRoles', { userId: userId }, env);
const inst: Instance | undefined = result ? (result[0] as Instance) : undefined;
if (inst) {
let roles: Instance[] | undefined = inst.getRelatedInstances('UserRole');
if (roles === undefined) {
roles = [];
}
if (DefaultRoleInstance === undefined) {
DefaultRoleInstance = makeInstance(
CoreAuthModuleName,
'Role',
newInstanceAttributes().set('name', '*')
);
}
roles.push(DefaultRoleInstance);
return roles;
}
return undefined;
}
type RbacPermission = {
resourceFqName: string;
c: boolean;
r: boolean;
u: boolean;
d: boolean;
};
const UserRoleCache: Map<string, string[] | null> = new Map();
const RolePermissionsCache: Map<string, RbacPermission[]> = new Map();
async function findRolePermissions(role: string, env: Environment): Promise<Result> {
return await evalEvent('FindRolePermissions', { role: role }, env);
}
async function updatePermissionCacheForRole(role: string, env: Environment) {
const result: any = await findRolePermissions(role, env);
if (result instanceof Array && result.length > 0) {
const roleInst: Instance = result[0] as Instance;
const permInsts: Instance[] | undefined = roleInst.getRelatedInstances('RolePermission');
if (permInsts) {
RolePermissionsCache.set(
role,
permInsts.map((inst: Instance) => {
return inst.cast<RbacPermission>();
})
);
}
}
}
export async function userHasPermissions(
userId: string,
resourceFqName: string,
perms: Set<RbacPermissionFlag>,
env: Environment
): Promise<boolean> {
if (userId == AdminUserId || !isRbacEnabled()) {
return true;
}
let userRoles: string[] | null | undefined = UserRoleCache.get(userId);
if (!userRoles) {
const roles: any = await findUserRoles(userId, env);
userRoles = [];
if (roles) {
for (let i = 0; i < roles.length; ++i) {
const r: Instance = roles[i] as Instance;
const n: string = r.attributes.get('name');
userRoles.push(n);
if (!RolePermissionsCache.get(n)) {
await updatePermissionCacheForRole(n, env);
}
}
}
UserRoleCache.set(userId, userRoles);
}
if (
userRoles &&
userRoles.find((role: string) => {
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: RbacPermission[] | undefined = RolePermissionsCache.get(userRoles[i]);
if (permInsts) {
if (
permInsts.find((p: RbacPermission) => {
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]);
type PermCheckForUser = (
userId: string,
resourceFqName: string,
env: Environment
) => Promise<boolean>;
function canUserPerfom(opr: Set<RbacPermissionFlag>): PermCheckForUser {
// TODO: check parent hierarchy
// TODO: cache permissions for user
async function f(userId: string, resourceFqName: string, env: Environment): Promise<boolean> {
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: AgentlangAuth | undefined;
if (isNodeEnv) {
runtimeAuth = new CognitoAuth();
}
function fetchAuthImpl(): AgentlangAuth {
if (runtimeAuth) {
return runtimeAuth;
} else {
throw new Error('Auth not initialized');
}
}
export async function signUpUser(
firstName: string,
lastName: string,
username: string,
password: string,
userData: object,
env: Environment
): Promise<UserInfo> {
let result: any;
try {
await fetchAuthImpl().signUp(
firstName,
lastName,
username.toLowerCase(),
password,
userData ? new Map(Object.entries(userData)) : undefined,
env,
(userInfo: UserInfo) => {
result = userInfo;
}
);
return result as UserInfo;
} catch (err: any) {
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: string,
confirmationCode: string,
env: Environment
): Promise<Result> {
try {
await fetchAuthImpl().confirmSignup(username.toLowerCase(), confirmationCode, env);
return {
status: 'ok',
message: 'User confirmed successfully',
};
} catch (err: any) {
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: string,
env: Environment
): Promise<Result> {
try {
await fetchAuthImpl().resendConfirmationCode(username.toLowerCase(), env);
return {
status: 'ok',
message: 'Confirmation code resent successfully',
};
} catch (err: any) {
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: string, env: Environment): Promise<Result> {
try {
await fetchAuthImpl().forgotPassword(username.toLowerCase(), env);
return { status: 'ok', message: 'Password reset code sent' };
} catch (err: any) {
logger.error(`Forgot password failed for ${username}: ${err.message}`);
throw err;
}
}
export async function confirmForgotPasswordUser(
username: string,
confirmationCode: string,
newPassword: string,
env: Environment
): Promise<Result> {
try {
await fetchAuthImpl().confirmForgotPassword(
username.toLowerCase(),
confirmationCode,
newPassword,
env
);
return { status: 'ok', message: 'Password has been reset' };
} catch (err: any) {
logger.error(`Confirm forgot password failed for ${username}: ${err.message}`);
throw err;
}
}
export async function loginUser(
username: string,
password: string,
env: Environment
): Promise<string | object> {
let result: string | object = '';
try {
await fetchAuthImpl().login(username.toLowerCase(), password, env, (r: SessionInfo) => {
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: any) {
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: string, env: Environment): Promise<string | object> {
let result: string | object = '';
try {
await fetchAuthImpl().callback(code, env, async (r: SessionInfo) => {
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: any) {
logger.error(`Callback failed for ${code}: ${err.message}`);
throw err;
}
}
async function logoutSession(userId: string, sess: Instance, env: Environment): Promise<Result> {
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: Environment): Promise<Result> {
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: string,
password: string,
env: Environment
): Promise<Result> {
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: string, env?: Environment): Promise<ActiveSessionInfo> {
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: string): boolean {
// 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: string, env?: Environment): Promise<ActiveSessionInfo> {
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: any) {
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: string, env?: Environment): Promise<ActiveSessionInfo> {
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: Instance = 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: any) {
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: string, env: Environment): Promise<UserInfo> {
const needCommit = env ? false : true;
env = env ? env : new Environment();
const f = async () => {
try {
return await fetchAuthImpl().getUser(userId, env);
} catch (err: any) {
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: string, env: Environment): Promise<UserInfo> {
const needCommit = env ? false : true;
env = env ? env : new Environment();
const f = async () => {
try {
return await fetchAuthImpl().getUserByEmail(email.toLowerCase(), env);
} catch (err: any) {
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: string, env: Environment): Promise<object> {
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: any) {
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: string,
firstName: string,
lastName: string,
userData: Map<string, any> | undefined,
role: string | undefined,
env: Environment
): Promise<object> {
const needCommit = env ? false : true;
env = env ? env : new Environment();
const f = async () => {
try {
let invitationInfo: any;
await fetchAuthImpl().inviteUser(
email,
firstName,
lastName,
userData,
role,
env,
(info: any) => {
invitationInfo = info;
}
);
return {
email: invitationInfo.email,
firstName: invitationInfo.firstName,
lastName: invitationInfo.lastName,
invitationId: invitationInfo.invitationId,
message: 'User invitation sent successfully',
};
} catch (err: any) {
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: string, env: Environment): Promise<object> {
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: any) {
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: string,
tempPassword: string,
newPassword: string,
env: Environment
): Promise<object> {
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: any) {
logger.error(`Accept invitation failed: ${err.message}`);
throw err;
}
};
if (needCommit) {
return await env.callInTransaction(f);
} else {
return await f();
}
}
export function requireAuth(moduleName: string, eventName: string): boolean {
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: Error): {
error: string;
message: string;
statusCode: number;
} {
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: any): boolean {
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: Error): {
name: string;
message: string;
sanitizedMessage: string;
} {
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: Error): boolean {
// 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)
);
}