nucleus-core-ts 0.9.963 → 0.9.964
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.build-ok +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/dist/.build-ok
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.9.
|
|
1
|
+
0.9.964
|
package/dist/index.js
CHANGED
|
@@ -442,7 +442,7 @@ ${strMDSAlgs}`)}if(attestationStatementAlg!==void 0&&authenticatorGetInfo?.algor
|
|
|
442
442
|
<p>Your account has been created via ${provider} login. You can optionally set a password to also sign in with your email and password.</p>
|
|
443
443
|
<p><a href="${inviteLink}">Set your password</a></p>
|
|
444
444
|
<p>This link expires in 7 days. If you don't want to set a password, you can always sign in with ${provider}.</p>
|
|
445
|
-
`}),logger2.info("[OAUTH] Invite email sent to new OAuth user",{userId,email:profile.email,provider})}catch(emailErr){logger2.warn("[OAUTH] Failed to send invite email",{userId,error:emailErr})}}{let stateUser=(await db.select().from(usersTable).where(eq50(usersTable.id,userId)).limit(1))[0],state2=stateUser?evaluateAccountState(stateUser):{allowed:!1,reason:"inactive",message:"Account is not active"};if(!state2.allowed)return logger2.warn(`[OAUTH] Login blocked - account ${state2.reason}`,{userId,provider}),ctx.set.status=302,ctx.set.headers.Location=`${errorRedirect}?error=account_${state2.reason}`,null}let defaultRoleNames=resolveDefaultRoleNames(config.authentication);if(defaultRoleNames.length>0){let resolvedForRole=resolveAuthTablesForRequest(ctx.request,config);await assignDefaultRoles({db,userId,roleNames:defaultRoleNames,rolesTable:resolvedForRole.rolesTable??config.rolesTable??null,userRolesTable:resolvedForRole.userRolesTable??config.userRolesTable??null,logger:logger2})}let syncConfig=oauthService.profileSync;if(syncConfig){let syncTables=resolveAuthTablesForRequest(ctx.request,config);await applyProfileSync({db,usersTable,profilesTable:syncTables.schemaTables?.profiles,userId,rawProfile:profile.rawProfile,config:syncConfig,isNewUser:createdNewUser,logger:logger2})}await db.update(usersTable).set({lastLoginAt:new Date}).where(eq50(usersTable.id,userId));let ipAddress=ctx.request.headers.get("cf-connecting-ip")?.trim()||ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||ctx.request.headers.get("x-real-ip")?.trim()||"127.0.0.1",userAgent=ctx.request.headers.get("user-agent")||"Unknown Browser",deviceInfo=parseUserAgentForLogin(userAgent,ipAddress,void 0,deviceContextFromRequest(ctx.request).clientHints),oauthUserRoles=[],oauthUserClaims=[],oauthUserClaimScopes={};if(config.db)try{let resolved2=resolveAuthTablesForRequest(ctx.request,config),rc=await fetchUserRolesAndClaims(config.db,userId,resolved2);oauthUserRoles=rc.roles,oauthUserClaims=rc.claims,oauthUserClaimScopes=rc.claimScopes}catch{}let accessToken=signAccessToken(userId,oauthUserRoles.length>0?oauthUserRoles:void 0,oauthUserClaims.length>0?oauthUserClaims:void 0,ctx.request.headers.get("x-tenant-schema")??void 0,oauthUserClaimScopes),refreshToken=signRefreshToken(userId),requestOrigin=ctx.request.headers.get("origin")||ctx.request.headers.get("referer")?.replace(/\/[^/]*$/,"")||void 0,sessionParams={userId,deviceInfo,loginMethod:`oauth:${provider}`,rememberMe:!0,requestOrigin},sessionId=await createSession(sessionParams),requiresApproval=!1,approvalSessionId=sessionId,deviceTokenCookie;if(saveSessionToDb){let oauthSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0,dtCfg=config.authentication?.deviceTrust;if(dtCfg?.enabled)sessionParams.deviceToken=readDeviceTokenFromCookieHeader(ctx.request.headers.get("cookie"),dtCfg);let saveResult=await saveSessionToDb(sessionId,sessionParams,oauthSchemaName);if(saveResult?.denied)return logger2.warn("[OAUTH] Login blocked by device-trust",{userId,provider}),ctx.set.status=302,ctx.set.headers.Location=`${errorRedirect}?error=device_not_recognized`,null;if(deviceTokenCookie=saveResult?.deviceTokenCookie,saveResult?.requiresApproval){if(requiresApproval=!0,saveResult.sessionId)approvalSessionId=saveResult.sessionId}}if(requiresApproval){if(deviceTokenCookie)ctx.set.headers["Set-Cookie"]=deviceTokenCookie;logger2.info("[OAUTH] Login requires device approval",{userId,provider,approvalSessionId});let pendingUrl=absolutizeRedirect(resolveSafeOAuthRedirect(statePayload.redirectUrl,oauthService.successRedirectUrl,config.authentication?.trustedAppOrigins),portalOrigin);return ctx.set.status=302,ctx.set.headers.Location=`${pendingUrl}?requires_approval=true&provider=${provider}&session_id=${approvalSessionId}`,null}await logger2.audit({entityName:"users",entityId:userId,operation:"LOGIN",userId,summary:`${profile.email??profile.providerAccountId} logged in via OAuth (${provider})`,ipAddress,userAgent,path:`${basePath}/${provider}/callback`,query:""});let securePart=cookies.secure?"; Secure":"",domainPart=cookies.domain?`; Domain=${cookies.domain}`:"",cookieOptions=`; Path=${cookies.path}; HttpOnly; SameSite=${cookies.sameSite}${securePart}${domainPart}`,cookiesToSet=[];if(tokenConfig.accessToken.setHeadersEnabled)cookiesToSet.push(`${cookies.accessTokenName}=${accessToken}${cookieOptions}; Max-Age=${cookies.accessTokenMaxAge}`);if(tokenConfig.refreshToken.setHeadersEnabled)cookiesToSet.push(`${cookies.refreshTokenName}=${refreshToken}${cookieOptions}; Max-Age=${cookies.refreshTokenMaxAge}`);if(tokenConfig.sessionToken.setHeadersEnabled)cookiesToSet.push(`${cookies.sessionTokenName}=${sessionId}${cookieOptions}; Max-Age=${cookies.sessionTokenMaxAge}`);if(deviceTokenCookie)cookiesToSet.push(deviceTokenCookie);let successUrl=absolutizeRedirect(resolveSafeOAuthRedirect(statePayload.redirectUrl,oauthService.successRedirectUrl,config.authentication?.trustedAppOrigins),portalOrigin),headers=new Headers;headers.set("Location",successUrl);for(let cookie of cookiesToSet)headers.append("Set-Cookie",cookie);return new Response(null,{status:302,headers})},{detail:{tags:["Authentication"],summary:"OAuth Callback",description:"Handles the OAuth provider callback, creates session and sets cookies"}}),routes.get(`${basePath}/providers`,()=>{return{success:!0,data:{providers:oauthService.getEnabledProviders()}}},{detail:{tags:["Authentication"],summary:"List OAuth Providers",description:"Returns the list of enabled OAuth providers"}}),routes.get(`${basePath}/link/:provider`,async(ctx)=>{let provider=ctx.params.provider;if(!oauthService.isProviderEnabled(provider))return ctx.set.status=404,{success:!1,message:`OAuth provider "${provider}" is not enabled`};if(!oauthService.allowAccountLinking)return ctx.set.status=403,{success:!1,message:"Account linking is disabled"};let userId=ctx.request.headers.get("x-user-id");if(!userId)return ctx.set.status=401,{success:!1,message:"Authentication required to link accounts"};let redirectUrl=ctx.query.redirect_url,authUrl=await oauthService.buildAuthorizationUrl(provider,userId,redirectUrl),issuedState=new URL(authUrl).searchParams.get("state");if(issuedState)ctx.set.headers["Set-Cookie"]=buildOAuthStateCookie(issuedState,{maxAge:600,secure:cookies.secure,domain:cookies.domain});return ctx.set.status=302,ctx.set.headers.Location=authUrl,null},{detail:{tags:["Authentication"],summary:"Link OAuth Account",description:"Redirects to provider to link an OAuth account to the current user"}}),routes.delete(`${basePath}/unlink/:provider`,async(ctx)=>{let provider=ctx.params.provider,userId=ctx.request.headers.get("x-user-id");if(!userId)return ctx.set.status=401,{success:!1,message:"Authentication required"};if(!db)return ctx.set.status=500,{success:!1,message:"Database not configured"};let oauthTable=config.oauthAccountsTable;if(!oauthTable)return ctx.set.status=500,{success:!1,message:"OAuth accounts table not configured"};if((await db.select().from(oauthTable).where(and18(eq50(oauthTable.userId,userId),eq50(oauthTable.provider,provider))).limit(1)).length===0)return{success:!1,message:`No linked ${provider} account found`};let{usersTable:resolvedUsersTable}=resolveAuthTablesForRequest(ctx.request,config);if(!resolvedUsersTable)return ctx.set.status=500,{success:!1,message:"Users table not configured"};let hasPassword=!!(await db.select().from(resolvedUsersTable).where(eq50(resolvedUsersTable.id,userId)).limit(1))[0]?.password,allOAuthAccounts=await db.select().from(oauthTable).where(eq50(oauthTable.userId,userId));if(!hasPassword&&allOAuthAccounts.length<=1)return ctx.set.status=400,{success:!1,message:"Cannot unlink the only login method. Set a password first."};return await db.delete(oauthTable).where(and18(eq50(oauthTable.userId,userId),eq50(oauthTable.provider,provider))),logger2.info("[OAUTH] Account unlinked",{userId,provider}),{success:!0,message:`${provider} account unlinked successfully`}},{detail:{tags:["Authentication"],summary:"Unlink OAuth Account",description:"Removes a linked OAuth account from the current user"}}),routes.get(`${basePath}/accounts`,async(ctx)=>{let userId=ctx.request.headers.get("x-user-id");if(!userId)return ctx.set.status=401,{success:!1,message:"Authentication required"};if(!db)return ctx.set.status=500,{success:!1,message:"Database not configured"};let oauthTable=config.oauthAccountsTable;if(!oauthTable)return{success:!0,data:{accounts:[]}};return{success:!0,data:{accounts:(await db.select().from(oauthTable).where(eq50(oauthTable.userId,userId))).map((a12)=>({id:a12.id,provider:a12.provider,providerEmail:a12.providerEmail,providerName:a12.providerName,providerAvatarUrl:a12.providerAvatarUrl,isPrimary:a12.isPrimary,lastUsedAt:a12.lastUsedAt,createdAt:a12.createdAt}))}}},{detail:{tags:["Authentication"],summary:"List Linked OAuth Accounts",description:"Returns all OAuth accounts linked to the current user"}}),routes}var OAUTH_STATE_COOKIE="oauth_state",readCookie=(request,name2)=>{let raw=request.headers.get("cookie");if(!raw)return;for(let part of raw.split(";")){let trimmed=part.trim(),eq51=trimmed.indexOf("=");if(eq51>0&&trimmed.slice(0,eq51)===name2)return trimmed.slice(eq51+1)}return},resolveSafeOAuthRedirect=(candidate,fallback,allowedOrigins)=>{if(!candidate)return fallback;let normalized=candidate.replace(/[\t\n\r]/g,"");if(!normalized)return fallback;if(normalized.startsWith("/")&&normalized[1]!=="/"&&normalized[1]!=="\\")return normalized;try{let candHost=new URL(normalized).host.toLowerCase(),trusted=[];try{trusted.push(new URL(fallback).host.toLowerCase())}catch{}for(let o of allowedOrigins??[])try{trusted.push(new URL(o.includes("://")?o:`https://${o}`).host.toLowerCase())}catch{}for(let t33 of trusted)if(t33.startsWith("*.")){let base=t33.slice(2);if(candHost===base||candHost.endsWith(`.${base}`))return normalized}else if(candHost===t33)return normalized}catch{}return fallback},absolutizeRedirect=(target2,portalOrigin)=>portalOrigin&&target2.startsWith("/")&&target2[1]!=="/"?`${portalOrigin}${target2}`:target2;var init_oauth=__esm(()=>{init_DeviceTrust();init_assignDefaultRole();init_fetchUserRolesAndClaims();init_deviceInfo();init_utils7();init_utils8();init_applyProfileSync()});import{t as t33}from"elysia";var PasswordChangeBodySchema,PasswordChangeResponseSchema;var init_types15=__esm(()=>{PasswordChangeBodySchema=t33.Object({currentPassword:t33.Optional(t33.String()),newPassword:t33.String({minLength:8}),confirmPassword:t33.String({minLength:8})}),PasswordChangeResponseSchema=t33.Object({success:t33.Boolean(),message:t33.Optional(t33.String())})});function validatePasswordsMatch(newPassword,confirmPassword){return newPassword===confirmPassword}var exports_store={};__export(exports_store,{userHasNoTrustedDevices:()=>userHasNoTrustedDevices,touchDevice:()=>touchDevice,revokeUserDevices:()=>revokeUserDevices,revokeDevicesBySessionIds:()=>revokeDevicesBySessionIds,revokeDeviceById:()=>revokeDeviceById,rejectDeviceForSession:()=>rejectDeviceForSession,listUserDevices:()=>listUserDevices,insertDeviceRow:()=>insertDeviceRow,findDeviceRowsByRawToken:()=>findDeviceRowsByRawToken,activateDeviceForSession:()=>activateDeviceForSession});import{and as and19,eq as eq51,inArray as inArray16}from"drizzle-orm";async function findDeviceRowsByRawToken(db,table,rawToken){let hash=hashDeviceToken(rawToken);return await db.select().from(table).where(eq51(col14(table,"tokenHash"),hash)).limit(2)}async function insertDeviceRow(db,table,v){let now=new Date;await db.insert(table).values({id:v.id,userId:v.userId,tokenHash:v.tokenHash,status:v.status,originSessionId:v.originSessionId??null,label:v.label??null,deviceFingerprint:v.deviceFingerprint??null,firstSeenIp:v.firstSeenIp??null,lastSeenIp:v.lastSeenIp??v.firstSeenIp??null,lastSeenAt:now,expiresAt:v.expiresAt,isActive:!0,createdAt:now,updatedAt:now})}async function activateDeviceForSession(db,table,sessionId){return(await db.update(table).set({status:"trusted",updatedAt:new Date}).where(and19(eq51(col14(table,"originSessionId"),sessionId),eq51(col14(table,"status"),"pending"))).returning({id:col14(table,"id")})).length}async function rejectDeviceForSession(db,table,sessionId){await db.update(table).set({status:"revoked",revokedAt:new Date,revokedReason:"replaced",isActive:!1,updatedAt:new Date}).where(and19(eq51(col14(table,"originSessionId"),sessionId),eq51(col14(table,"status"),"pending")))}async function touchDevice(db,table,id,ip){await db.update(table).set({lastSeenAt:new Date,lastSeenIp:ip??null,updatedAt:new Date}).where(eq51(col14(table,"id"),id))}async function revokeWhere(db,table,where,reason){await db.update(table).set({status:"revoked",revokedAt:new Date,revokedReason:reason,isActive:!1,updatedAt:new Date}).where(where)}async function revokeUserDevices(db,table,userId,reason){if(!userId)return;let notAlready=and19(eq51(col14(table,"userId"),userId),eq51(col14(table,"status"),"trusted")),alsoPending=and19(eq51(col14(table,"userId"),userId),eq51(col14(table,"status"),"pending"));await revokeWhere(db,table,notAlready,reason),await revokeWhere(db,table,alsoPending,reason)}async function revokeDevicesBySessionIds(db,table,sessionIds,reason){if(!sessionIds?.length)return;await revokeWhere(db,table,inArray16(col14(table,"originSessionId"),sessionIds),reason)}async function revokeDeviceById(db,table,id,userId,reason){return(await db.update(table).set({status:"revoked",revokedAt:new Date,revokedReason:reason,isActive:!1,updatedAt:new Date}).where(and19(eq51(col14(table,"id"),id),eq51(col14(table,"userId"),userId))).returning({id:col14(table,"id")})).length}async function listUserDevices(db,table,userId){return(await db.select().from(table).where(eq51(col14(table,"userId"),userId))).map((r2)=>{let{tokenHash:_omit,...safe}=r2;return safe})}async function userHasNoTrustedDevices(db,table,userId){return(await db.select({id:col14(table,"id")}).from(table).where(and19(eq51(col14(table,"userId"),userId),eq51(col14(table,"status"),"trusted"))).limit(1)).length===0}var col14=(t34,name2)=>t34[name2];var init_store2=__esm(()=>{init_DeviceTrust()});import{eq as eq52}from"drizzle-orm";import{Elysia as Elysia39}from"elysia";function createPasswordChangeRoute(config,passwordChangeConfig){let{db,logger:logger2}=config,log4=logger2.scoped(AUTH_PASSWORD_CHANGE),route=passwordChangeConfig.route||"/auth/password-change",passwordChangeRoutes=new Elysia39;if(!passwordChangeConfig.enabled)return passwordChangeRoutes;return passwordChangeRoutes.post(route,async(ctx)=>{let{usersTable,sessionsTable}=resolveAuthTablesForRequest(ctx.request,config);if(!db||!usersTable)return ctx.set.status=500,{success:!1,message:"Database not configured"};let userId=ctx.request.headers.get("x-user-id");if(!userId)return ctx.set.status=401,{success:!1,message:"Authentication required"};let{currentPassword,newPassword,confirmPassword}=ctx.body;if(!validatePasswordsMatch(newPassword,confirmPassword))return ctx.set.status=422,{success:!1,message:"New passwords do not match"};let strength=validatePasswordStrength(newPassword);if(!strength.valid)return ctx.set.status=400,{success:!1,message:"Password too weak",errors:strength.errors};let user=(await db.select().from(usersTable).where(eq52(usersTable.id,userId)).limit(1))[0];if(!user)return ctx.set.status=404,{success:!1,message:"User not found"};let existingHash=user.password??"",userHasPassword=existingHash.length>0,isInitialSet=!userHasPassword,reqUrl=new URL(ctx.request.url),auditIp=ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||ctx.request.headers.get("x-real-ip")?.trim()||"unknown",auditUa=ctx.request.headers.get("user-agent")||"unknown";if(userHasPassword){if(!currentPassword)return ctx.set.status=400,{success:!1,message:"Current password is required"};if(!await verifyPassword(currentPassword,existingHash))return log4.warn("Password change failed - invalid current password",{userId}),logger2.audit({entityName:"users",entityId:userId,operation:"PASSWORD_CHANGE_FAILED",userId,summary:"Password change failed: invalid current password",ipAddress:auditIp,userAgent:auditUa,path:reqUrl.pathname,query:reqUrl.search}),ctx.set.status=400,{success:!1,message:"Current password is incorrect"}}let hashedNewPassword=await hashPassword(newPassword);if(await db.update(usersTable).set({password:hashedNewPassword,updatedAt:new Date}).where(eq52(usersTable.id,userId)),!isInitialSet&&sessionsTable)try{await db.update(sessionsTable).set({isActive:!1,revokedAt:new Date,revokedReason:"password_change"}).where(eq52(sessionsTable.userId,userId))}catch(err){log4.warn("Failed to revoke sessions after password change",{userId,error:err instanceof Error?err.message:String(err)})}if(!isInitialSet&&config.authentication?.deviceTrust?.enabled)try{let{schemaTables}=resolveAuthTablesForRequest(ctx.request,config),dtTable=schemaTables?.trustedDevices??schemaTables?.trusted_devices;if(dtTable&&db){let{revokeUserDevices:revokeUserDevices2}=await Promise.resolve().then(() => (init_store2(),exports_store));await revokeUserDevices2(db,dtTable,userId,"password_reset")}}catch(err){log4.warn("Failed to revoke device tokens after password change",{userId,error:err instanceof Error?err.message:String(err)})}return log4.info(isInitialSet?"Initial password set successful":"Password change successful",{userId}),logger2.audit({entityName:"users",entityId:userId,operation:isInitialSet?"PASSWORD_SET":"PASSWORD_CHANGE",userId,summary:isInitialSet?"Initial password set successfully":"Password changed successfully",ipAddress:auditIp,userAgent:auditUa,path:reqUrl.pathname,query:reqUrl.search}),{success:!0,message:isInitialSet?"Password set successfully":"Password changed successfully"}},{body:PasswordChangeBodySchema,detail:{tags:["Authentication"],summary:"Change Password",description:"Change password for authenticated user"}}),passwordChangeRoutes}var init_passwordChange=__esm(()=>{init_scopes();init_utils7();init_utils6();init_types15();init_types15()});function describeResetEmail(outcome){if(!outcome||outcome.sent)return{sent:!0,level:"info",message:"Password reset email sent"};let base=SENTENCES[outcome.reason],detail=outcome.detail?.trim();return{sent:!1,level:outcome.reason==="exempt"?"info":"warn",message:detail?`${base} (${detail})`:base}}var SENTENCES;var init_outcome=__esm(()=>{SENTENCES={no_provider:"Password reset email NOT sent \u2014 no email provider is configured. The token was stored, so a link exists, but nothing carried it.",exempt:"Password reset email not sent \u2014 the address is on an exempt domain this install never mails.",rejected:"Password reset email NOT sent \u2014 the provider refused the recipient. The address is accepted at login but not deliverable.",error:"Password reset email NOT sent \u2014 the send failed."}});import{t as t34}from"elysia";var PasswordResetRequestSchema,PasswordResetConfirmSchema,PasswordResetResponseSchema;var init_types16=__esm(()=>{PasswordResetRequestSchema=t34.Object({email:t34.String({format:"email"})}),PasswordResetConfirmSchema=t34.Object({token:t34.String(),newPassword:t34.String({minLength:8}),confirmPassword:t34.String({minLength:8})}),PasswordResetResponseSchema=t34.Object({success:t34.Boolean(),message:t34.Optional(t34.String())})});import{randomBytes as randomBytes7}from"crypto";function generateResetToken(){return randomBytes7(32).toString("hex")}function isTokenExpired(expiresAt){return new Date>expiresAt}var init_utils9=()=>{};import{Elysia as Elysia40}from"elysia";function createPasswordResetRoute(config,passwordResetConfig,storeResetToken,getResetToken,deleteResetToken,sendResetEmail){let{db,logger:logger2}=config,log4=logger2.scoped(AUTH_PASSWORD_RESET),baseRoute=passwordResetConfig.route||"/auth/password-reset",passwordResetRoutes=new Elysia40;if(!passwordResetConfig.enabled)return passwordResetRoutes;return passwordResetRoutes.post(`${baseRoute}/request`,async(ctx)=>{let{usersTable}=resolveAuthTablesForRequest(ctx.request,config);if(!db||!usersTable)return{success:!1,message:"Database not configured"};let{email:rawEmail}=ctx.body,email=rawEmail.toLowerCase(),{eq:eq53}=await import("drizzle-orm"),user=(await db.select().from(usersTable).where(eq53(usersTable.email,email)).limit(1))[0];if(!user)return{success:!0,message:"If email exists, reset link will be sent"};let token=generateResetToken(),expiresAt=new Date(Date.now()+3600000),reqSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0;await storeResetToken(user.id,token,expiresAt,reqSchemaName);let delivery={sent:!1,level:"warn",message:"Password reset email NOT sent \u2014 no send helper is configured."};if(sendResetEmail)try{delivery=describeResetEmail(await sendResetEmail(email,token,ctx.request))}catch(error3){delivery=describeResetEmail({sent:!1,reason:"error",detail:error3 instanceof Error?error3.message:String(error3)})}if(delivery.level==="warn")log4.warn(delivery.message,{email});else log4.info(delivery.message,{email});log4.info("Password reset requested",{userId:user.id,email});let reqUrl=new URL(ctx.request.url);return logger2.audit({entityName:"users",entityId:user.id,operation:"PASSWORD_RESET_REQUEST",userId:user.id,summary:`Password reset requested for ${email} \u2014 ${delivery.sent?"e-posta g\xF6nderildi":"E-POSTA G\xD6NDER\u0130LEMED\u0130"}`,ipAddress:ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||ctx.request.headers.get("x-real-ip")?.trim()||"unknown",userAgent:ctx.request.headers.get("user-agent")||"unknown",path:reqUrl.pathname,query:reqUrl.search}),{success:!0,message:"If email exists, reset link will be sent"}},{body:PasswordResetRequestSchema,detail:{tags:["Authentication"],summary:"Request Password Reset",description:"Request a password reset email"}}),passwordResetRoutes.post(`${baseRoute}/confirm`,async(ctx)=>{let{usersTable,sessionsTable}=resolveAuthTablesForRequest(ctx.request,config);if(!db||!usersTable)return{success:!1,message:"Database not configured"};let{token,newPassword,confirmPassword}=ctx.body;if(newPassword!==confirmPassword)return{success:!1,message:"Passwords do not match"};let strength=validatePasswordStrength(newPassword);if(!strength.valid)return ctx.set.status=400,{success:!1,message:"Password too weak",errors:strength.errors};let reqSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0,resetData=await getResetToken(token,reqSchemaName);if(!resetData)return{success:!1,message:"Invalid or expired reset token"};if(isTokenExpired(resetData.expiresAt))return await deleteResetToken(token,reqSchemaName),{success:!1,message:"Reset token has expired"};let hashedPassword=await hashPassword(newPassword),{eq:eq53}=await import("drizzle-orm");if(await db.update(usersTable).set({password:hashedPassword}).where(eq53(usersTable.id,resetData.userId)),await deleteResetToken(token,reqSchemaName),sessionsTable)try{await db.update(sessionsTable).set({isActive:!1,revokedAt:new Date,revokedReason:"password_reset"}).where(eq53(sessionsTable.userId,resetData.userId))}catch(err){log4.warn("Failed to revoke sessions after password reset",{userId:resetData.userId,error:err instanceof Error?err.message:String(err)})}try{if(config.authentication?.deviceTrust?.enabled){let{schemaTables}=resolveAuthTablesForRequest(ctx.request,config),dtTable=schemaTables?.trustedDevices??schemaTables?.trusted_devices;if(dtTable&&db){let{revokeUserDevices:revokeUserDevices2}=await Promise.resolve().then(() => (init_store2(),exports_store));await revokeUserDevices2(db,dtTable,resetData.userId,"password_reset")}}}catch(err){log4.warn("Failed to revoke device tokens after password reset",{userId:resetData.userId,error:err instanceof Error?err.message:String(err)})}log4.info("Password reset successful",{userId:resetData.userId});let reqUrl=new URL(ctx.request.url);return logger2.audit({entityName:"users",entityId:resetData.userId,operation:"PASSWORD_RESET",userId:resetData.userId,summary:"Password reset completed successfully",ipAddress:ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||ctx.request.headers.get("x-real-ip")?.trim()||"unknown",userAgent:ctx.request.headers.get("user-agent")||"unknown",path:reqUrl.pathname,query:reqUrl.search}),{success:!0,message:"Password has been reset"}},{body:PasswordResetConfirmSchema,detail:{tags:["Authentication"],summary:"Confirm Password Reset",description:"Reset password with token"}}),passwordResetRoutes}var init_passwordReset=__esm(()=>{init_scopes();init_utils6();init_outcome();init_types16();init_utils9();init_types16()});import{t as t35}from"elysia";var PasswordSetBodySchema,PasswordSetResponseSchema;var init_types17=__esm(()=>{PasswordSetBodySchema=t35.Object({newPassword:t35.String({minLength:8}),userId:t35.Optional(t35.String()),token:t35.Optional(t35.String())}),PasswordSetResponseSchema=t35.Object({success:t35.Boolean(),message:t35.String()})});import{eq as eq53}from"drizzle-orm";import{Elysia as Elysia41}from"elysia";function createPasswordSetRoute(config,passwordSetConfig,getMagicToken,deleteMagicToken){let{db,logger:logger2}=config,log4=logger2.scoped(AUTH_PASSWORD_SET),route=passwordSetConfig.route||"/auth/password-set",passwordSetRoutes=new Elysia41;if(!passwordSetConfig.enabled)return passwordSetRoutes;return passwordSetRoutes.post(route,async(ctx)=>{log4.info("Password set request received");let{usersTable}=resolveAuthTablesForRequest(ctx.request,config);if(!db||!usersTable)return log4.error("Password set failed - database not configured"),{success:!1,message:"Database not configured"};let{newPassword,token}=ctx.body,strength=validatePasswordStrength(newPassword);if(!strength.valid)return ctx.set.status=400,{success:!1,message:"Password too weak",errors:strength.errors};let userId=null;if(token&&getMagicToken&&deleteMagicToken){let reqSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0,tokenHash=hashToken(token,TOKEN_PURPOSE.invite),storedToken=await getMagicToken(tokenHash,reqSchemaName);if(!storedToken)return log4.warn("Password set failed - invalid token"),{success:!1,message:"Invalid or expired token"};if(new Date>storedToken.expiresAt)return await deleteMagicToken(tokenHash,reqSchemaName),log4.warn("Password set failed - expired token",{email:storedToken.email}),{success:!1,message:"Invalid or expired token"};userId=storedToken.userId,log4.info("Password set - userId resolved from token",{userId,email:storedToken.email})}else userId=ctx.request.headers.get("x-user-id");if(!userId)return log4.warn("Password set failed - no token and no authenticated user"),{success:!1,message:"Valid token or authentication required"};let user=(await db.select().from(usersTable).where(eq53(usersTable.id,userId)).limit(1))[0];if(log4.info("Password set - user found",{found:!!user,hasPassword:!!user?.password}),!user)return{success:!1,message:"User not found"};if(user.password)return log4.warn("Password set failed - user already has password",{userId}),{success:!1,message:"Password already set. Use password change instead."};let hashedPassword=await hashPassword(newPassword);log4.info("Password set - updating user with verifiedAt",{userId});let updateResult=await db.update(usersTable).set({password:hashedPassword,verifiedAt:new Date,updatedAt:new Date}).where(eq53(usersTable.id,userId));if(log4.info("Password set successful for invited user",{userId,updateResult}),token&&deleteMagicToken){let setSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0,tokenHash=hashToken(token,TOKEN_PURPOSE.invite);await deleteMagicToken(tokenHash,setSchemaName),log4.info("Invite token consumed after password set",{userId})}return{success:!0,message:"Password set successfully"}},{body:PasswordSetBodySchema,detail:{tags:["Authentication"],summary:"Set Password",description:"Set password for the first time (for invited users who do not have a password yet)"}}),passwordSetRoutes}var init_passwordSet=__esm(()=>{init_scopes();init_utils8();init_utils6();init_types17();init_types17()});function parseTimeToSeconds3(time2){let match=time2.match(/^(\d+)([smhd])$/);if(!match)return 900;let value2=match[1],unit=match[2];if(!value2||!unit)return 900;let num3=parseInt(value2,10);switch(unit){case"s":return num3;case"m":return num3*60;case"h":return num3*3600;case"d":return num3*86400;default:return 900}}import{t as t36}from"elysia";var RefreshResponseSchema;var init_types18=__esm(()=>{RefreshResponseSchema=t36.Object({success:t36.Boolean(),message:t36.Optional(t36.String()),data:t36.Optional(t36.Object({accessToken:t36.String()}))})});import{eq as eq54}from"drizzle-orm";import{Elysia as Elysia42}from"elysia";function createRefreshRoute(config,refreshConfig,verifyRefreshToken,signAccessToken,signRefreshToken,tokenResponseConfig,cookieMaxAgeBufferSeconds,cookieConfig){let{logger:logger2,authentication}=config,route=refreshConfig.route||"/auth/refresh",refreshRoutes=new Elysia42;if(!refreshConfig.enabled)return refreshRoutes;return refreshRoutes.post(route,async(ctx)=>{let refreshTokenName=authentication?.refreshToken?.name||"refresh_token",accessTokenName=authentication?.accessToken?.name||"access_token",refreshToken=ctx.request.headers.get("x-refresh-token");if(!refreshToken){let cookieHeader=ctx.request.headers.get("cookie");if(cookieHeader)refreshToken=cookieHeader.split(";").reduce((acc,cookie)=>{let trimmed=cookie.trim(),eqIndex=trimmed.indexOf("=");if(eqIndex>0)acc[trimmed.slice(0,eqIndex)]=trimmed.slice(eqIndex+1);return acc},{})[refreshTokenName]||null}if(!refreshToken)return ctx.set.status=401,{success:!1,message:"Refresh token required"};let result=verifyRefreshToken(refreshToken);if(!result.valid||!result.payload)return logger2.warn("[AUTH] Refresh failed - invalid token"),ctx.set.status=401,{success:!1,message:"Invalid refresh token"};let userId=result.payload.sub,userRoles=[],userClaims=[],userClaimScopes={},resolved=resolveAuthTablesForRequest(ctx.request,config);if(config.db){try{let rc=await fetchUserRolesAndClaims(config.db,userId,resolved);userRoles=rc.roles,userClaims=rc.claims,userClaimScopes=rc.claimScopes}catch(roleError){logger2.error("[AUTH] Failed to fetch roles/claims during refresh",{userId,error:roleError instanceof Error?roleError.message:String(roleError)})}try{let usersTable=resolved.usersTable;if(usersTable){let user=(await config.db.select().from(usersTable).where(eq54(usersTable.id,userId)).limit(1))[0];if(!user||user.isActive===!1)return logger2.warn("[AUTH] Refresh blocked - account inactive",{userId}),ctx.set.status=401,{success:!1,message:"Account is not active"};let lockedUntilRaw=user.lockedUntil,lockedUntil=lockedUntilRaw?new Date(lockedUntilRaw):null;if(user.isLocked===!0&&(!lockedUntil||lockedUntil>new Date))return logger2.warn("[AUTH] Refresh blocked - account locked",{userId}),ctx.set.status=401,{success:!1,message:"Account is locked"};let userCohortId=user.cohortId;if(userCohortId){let cohortsTable=resolved.schemaTables?.userCohorts??resolved.schemaTables?.user_cohorts;if(cohortsTable){let cohortExpiresAt=(await config.db.select().from(cohortsTable).where(eq54(cohortsTable.id,userCohortId)).limit(1))[0]?.expiresAt;if(cohortExpiresAt&&new Date(cohortExpiresAt)<new Date)return logger2.warn("[AUTH] Refresh blocked - cohort expired",{userId,cohortId:userCohortId}),ctx.set.status=401,{success:!1,message:"Your access has expired. Please contact your administrator."}}}}}catch(cohortErr){logger2.error("[AUTH] Failed cohort expiry check during refresh",{userId,error:cohortErr instanceof Error?cohortErr.message:String(cohortErr)})}}let newAccessToken=signAccessToken(userId,userRoles.length>0?userRoles:void 0,userClaims.length>0?userClaims:void 0,ctx.request.headers.get("x-tenant-schema")??void 0,userClaimScopes),newRefreshToken=signRefreshToken?signRefreshToken(userId):null,accessSetHeaders=tokenResponseConfig?.accessToken?.setHeadersEnabled??!0,accessReturnJson=tokenResponseConfig?.accessToken?.returnJson??!0,refreshSetHeaders=tokenResponseConfig?.refreshToken?.setHeadersEnabled??!0,refreshReturnJson=tokenResponseConfig?.refreshToken?.returnJson??!1,accessExpiresIn=authentication?.accessToken?.expiresIn||"15m",accessMaxAge=Math.max(0,parseTimeToSeconds3(accessExpiresIn)-(cookieMaxAgeBufferSeconds??0)),refreshTtl=lifetimeSeconds(authentication?.refreshToken?.expiresIn,604800),refreshMaxAge=cookieMaxAge(refreshTtl),headers=new Headers;headers.set("Content-Type","application/json");let cookiePath=cookieConfig?.path||"/",cookieSameSite=cookieConfig?.sameSite||"Strict",securePart=cookieConfig?.secure!==!1?"; Secure":"",domainPart=cookieConfig?.domain?`; Domain=${cookieConfig.domain}`:"",cookieOpts=`; Path=${cookiePath}; HttpOnly; SameSite=${cookieSameSite}${securePart}${domainPart}`;if(accessSetHeaders)headers.append("Set-Cookie",`${accessTokenName}=${newAccessToken}${cookieOpts}; Max-Age=${accessMaxAge}`);if(newRefreshToken&&refreshSetHeaders)headers.append("Set-Cookie",`${refreshTokenName}=${newRefreshToken}${cookieOpts}; Max-Age=${refreshMaxAge}`);logger2.info("[AUTH] Token refresh successful",{userId,rotatedRefreshToken:!!newRefreshToken});let responseData={};if(accessReturnJson)responseData.accessToken=newAccessToken;if(newRefreshToken&&refreshReturnJson)responseData.refreshToken=newRefreshToken;let jsonBody=JSON.stringify({success:!0,data:responseData});return new Response(jsonBody,{status:200,headers})},{detail:{tags:["Authentication"],summary:"Refresh Token",description:"Get new access token using refresh token"}}),refreshRoutes}var init_refresh=__esm(()=>{init_lifetime();init_fetchUserRolesAndClaims();init_types18()});import{t as t37}from"elysia";var RegisterBodySchema,RegisterResponseSchema;var init_types19=__esm(()=>{RegisterBodySchema=t37.Object({email:t37.String({format:"email"}),password:t37.String({minLength:8}),confirmPassword:t37.Optional(t37.String({minLength:8})),firstName:t37.Optional(t37.String({maxLength:100})),lastName:t37.Optional(t37.String({maxLength:100})),createProfile:t37.Optional(t37.Boolean())}),RegisterResponseSchema=t37.Object({success:t37.Boolean(),message:t37.Optional(t37.String()),data:t37.Optional(t37.Object({user:t37.Object({id:t37.String(),email:t37.String()})}))})});import{eq as eq55}from"drizzle-orm";import{Elysia as Elysia43}from"elysia";function createRegisterRoute(config,registerConfig,sendWelcomeEmail,signAccessToken,signRefreshToken,createSession,cookieConfig,tokenResponseConfig,getEmailService,appName,saveSessionToDb){let{db,logger:logger2}=config,log4=logger2.scoped(AUTH_REGISTER),route=registerConfig.route||"/auth/register",cookies={accessTokenName:cookieConfig?.accessTokenName||"access_token",refreshTokenName:cookieConfig?.refreshTokenName||"refresh_token",sessionTokenName:cookieConfig?.sessionTokenName||"session_token",accessTokenMaxAge:cookieConfig?.accessTokenMaxAge||900,refreshTokenMaxAge:cookieConfig?.refreshTokenMaxAge||604800,sessionTokenMaxAge:cookieConfig?.sessionTokenMaxAge||900,secure:cookieConfig?.secure??!0,httpOnly:cookieConfig?.httpOnly??!0,sameSite:cookieConfig?.sameSite||"strict",path:cookieConfig?.path||"/",domain:cookieConfig?.domain},registerRoutes=new Elysia43;if(!registerConfig.enabled)return registerRoutes;return registerRoutes.post(route,async(ctx)=>{let resolved=resolveAuthTablesForRequest(ctx.request,config),usersTable=resolved.usersTable;if(!db||!usersTable)return ctx.set.status=500,{success:!1,message:"Database not configured"};let{email:rawEmail,password:password4,confirmPassword,firstName,lastName,createProfile}=ctx.body,email=rawEmail.toLowerCase();if(confirmPassword&&password4!==confirmPassword)return ctx.set.status=400,{success:!1,message:"Passwords do not match"};let passwordValidation=validatePasswordStrength(password4);if(!passwordValidation.valid)return ctx.set.status=400,{success:!1,message:"Password too weak",errors:passwordValidation.errors};let existingUsers=await db.select().from(usersTable).where(eq55(usersTable.email,email)).limit(1),reqUrl=new URL(ctx.request.url),auditIp=ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||ctx.request.headers.get("x-real-ip")?.trim()||"unknown",auditUa=ctx.request.headers.get("user-agent")||"unknown";if(existingUsers.length>0){log4.warn("Registration failed - email exists",{email}),logger2.audit({entityName:"users",operation:"REGISTER_FAILED",summary:`Registration failed: email already exists (${email})`,ipAddress:auditIp,userAgent:auditUa,path:reqUrl.pathname,query:reqUrl.search});let verifGatesSignup=registerConfig.emailVerification?.enabled&&getEmailService?.()?.isAvailable();if(config.authentication?.preventUserEnumeration&&verifGatesSignup)return{success:!0,message:"Registration successful. Please check your email to verify your account."};return ctx.set.status=409,{success:!1,message:"Email already registered"}}let hashedPassword=await hashPassword(password4),domainExempt=isEmailExempt(email,config.authentication?.emailExemptDomains),emailService=getEmailService?.()??null,emailVerificationEnabled=!domainExempt&®isterConfig.emailVerification?.enabled&&emailService?.isAvailable(),verificationToken=emailVerificationEnabled?generateVerificationToken():null,tokenExpiresIn=registerConfig.emailVerification?.tokenExpiresIn||"24h",tokenExpiresAt=emailVerificationEnabled?new Date(Date.now()+parseTimeToMs(tokenExpiresIn)):null,insertValues={email,password:hashedPassword};if(emailVerificationEnabled&&verificationToken)insertValues.emailVerificationToken=hashVerificationToken(verificationToken),insertValues.emailVerificationTokenExpiresAt=tokenExpiresAt,insertValues.emailVerificationSentAt=new Date,insertValues.emailVerificationAttempts=1;let newUser=(await db.insert(usersTable).values(insertValues).returning())[0],registerDefaultRoles=resolveDefaultRoleNames(config.authentication);if(registerDefaultRoles.length>0)await assignDefaultRoles({db,userId:newUser.id,roleNames:registerDefaultRoles,rolesTable:resolved.rolesTable??config.rolesTable??null,userRolesTable:resolved.userRolesTable??config.userRolesTable??null,logger:logger2});let wantsProfile=registerConfig.createProfileOnRegister||createProfile,profilesTable=resolved.schemaTables?.profiles;if(wantsProfile&&profilesTable&&(firstName||lastName))try{let profileNow=new Date;await db.insert(profilesTable).values({userId:newUser.id,firstName:firstName??"",lastName:lastName??"",createdAt:profileNow,updatedAt:profileNow})}catch(err){log4.warn("Failed to create profile on register \u2014 continuing",{userId:newUser.id,error:err instanceof Error?err.message:String(err)})}if(log4.info("Registration successful",{userId:newUser.id,email,emailVerificationEnabled}),logger2.audit({entityName:"users",entityId:newUser.id,operation:"REGISTER",userId:newUser.id,summary:`New user registered: ${email}`,ipAddress:auditIp,userAgent:auditUa,path:reqUrl.pathname,query:reqUrl.search}),emailVerificationEnabled&&emailService&&verificationToken){let verificationLink=buildEmailActionLink({request:ctx.request,configuredUrl:registerConfig.emailVerification?.redirectUrl,path:"/verify-email",query:{token:verificationToken},allowedOrigins:config.authentication?.trustedAppOrigins});emailService.sendVerificationEmail(email,email.split("@")[0]||"User",verificationLink,appName||"Nucleus").then((result2)=>{if(result2.success)log4.info("Verification email sent",{email});else log4.error("Failed to send verification email",{email,error:result2.error})}).catch((err)=>{log4.error("Failed to send verification email",{email,error:err})});let resendCooldown=registerConfig.emailVerification?.resendCooldown||"60s",cooldownSeconds=parseTimeToMs(resendCooldown)/1000,maxResendAttempts=registerConfig.emailVerification?.maxResendAttempts||3;if(config.authentication?.preventUserEnumeration)return{success:!0,message:"Registration successful. Please check your email to verify your account."};return{success:!0,message:"Registration successful. Please check your email to verify your account.",data:{user:{id:newUser.id,email:newUser.email},emailVerificationRequired:!0,verification:{cooldownSeconds,canResendAt:new Date(Date.now()+cooldownSeconds*1000).toISOString(),attemptsRemaining:maxResendAttempts-1,maxAttempts:maxResendAttempts}}}}if(sendWelcomeEmail)sendWelcomeEmail(email,firstName||email.split("@")[0]||"User").catch((err)=>{log4.error("Failed to send welcome email",{email,error:err})});if(signAccessToken&&signRefreshToken&&createSession){let ipAddress=ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||ctx.request.headers.get("x-real-ip")?.trim()||"unknown",userAgent=ctx.request.headers.get("user-agent")||"",accessToken=signAccessToken(newUser.id,void 0,void 0,ctx.request.headers.get("x-tenant-schema")??void 0),refreshToken=signRefreshToken(newUser.id),sessionParams={userId:newUser.id,deviceInfo:ensureDeviceInfo({ipAddress,userAgent},deviceContextFromRequest(ctx.request).clientHints),loginMethod:"register",rememberMe:!0},sessionId=await createSession(sessionParams),deviceTokenCookie;if(saveSessionToDb){let reqSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0,dtCfg=config.authentication?.deviceTrust;if(dtCfg?.enabled)sessionParams.deviceToken=readDeviceTokenFromCookieHeader(ctx.request.headers.get("cookie"),dtCfg);deviceTokenCookie=(await saveSessionToDb(sessionId,sessionParams,reqSchemaName))?.deviceTokenCookie}let tokenConfig={accessToken:{setHeadersEnabled:tokenResponseConfig?.accessToken?.setHeadersEnabled??!0,returnJson:tokenResponseConfig?.accessToken?.returnJson??!0},refreshToken:{setHeadersEnabled:tokenResponseConfig?.refreshToken?.setHeadersEnabled??!0,returnJson:tokenResponseConfig?.refreshToken?.returnJson??!0},sessionToken:{setHeadersEnabled:tokenResponseConfig?.sessionToken?.setHeadersEnabled??!0,returnJson:tokenResponseConfig?.sessionToken?.returnJson??!0}},securePart=cookies.secure?"; Secure":"",domainPart=cookies.domain?`; Domain=${cookies.domain}`:"",cookieOptions=`; Path=${cookies.path}; HttpOnly; SameSite=${cookies.sameSite}${securePart}${domainPart}`,cookiesToSet=[];if(tokenConfig.accessToken.setHeadersEnabled)cookiesToSet.push(`${cookies.accessTokenName}=${accessToken}${cookieOptions}; Max-Age=${cookies.accessTokenMaxAge}`);if(tokenConfig.refreshToken.setHeadersEnabled)cookiesToSet.push(`${cookies.refreshTokenName}=${refreshToken}${cookieOptions}; Max-Age=${cookies.refreshTokenMaxAge}`);if(tokenConfig.sessionToken.setHeadersEnabled)cookiesToSet.push(`${cookies.sessionTokenName}=${sessionId}${cookieOptions}; Max-Age=${cookies.sessionTokenMaxAge}`);if(deviceTokenCookie)cookiesToSet.push(deviceTokenCookie);let responseData={user:{id:newUser.id,email:newUser.email}};if(tokenConfig.accessToken.returnJson)responseData.accessToken=accessToken;if(tokenConfig.refreshToken.returnJson)responseData.refreshToken=refreshToken;if(tokenConfig.sessionToken.returnJson)responseData.sessionId=sessionId;let jsonBody=JSON.stringify({success:!0,data:responseData}),headers=new Headers;headers.set("Content-Type","application/json"),headers.set("x-session-id",sessionId);for(let cookie of cookiesToSet)headers.append("Set-Cookie",cookie);return new Response(jsonBody,{status:200,headers})}return{success:!0,data:{user:{id:newUser.id,email:newUser.email}}}},{body:RegisterBodySchema,detail:{tags:["Authentication"],summary:"Register",description:"Register a new user account"}}),registerRoutes}var init_register=__esm(()=>{init_DeviceTrust();init_scopes();init_assignDefaultRole();init_deviceInfo();init_types19();init_utils6();init_types19()});import{t as t38}from"elysia";var SessionResponseSchema,SessionListResponseSchema,RevokeSessionBodySchema,RevokeAllSessionsBodySchema,SessionActivityResponseSchema,AdminRevokeSessionsBodySchema;var init_types20=__esm(()=>{SessionResponseSchema=t38.Object({id:t38.String(),deviceName:t38.Optional(t38.String()),deviceType:t38.Optional(t38.String()),browserName:t38.Optional(t38.String()),browserVersion:t38.Optional(t38.String()),osName:t38.Optional(t38.String()),osVersion:t38.Optional(t38.String()),ipAddress:t38.String(),locationCountry:t38.Optional(t38.String()),locationCity:t38.Optional(t38.String()),lastActivityAt:t38.String(),createdAt:t38.String(),isCurrent:t38.Boolean(),loginMethod:t38.Optional(t38.String()),trustScore:t38.Optional(t38.Number())}),SessionListResponseSchema=t38.Object({success:t38.Boolean(),data:t38.Optional(t38.Object({sessions:t38.Array(SessionResponseSchema),currentSessionId:t38.Optional(t38.String()),totalCount:t38.Number()})),message:t38.Optional(t38.String())}),RevokeSessionBodySchema=t38.Object({reason:t38.Optional(t38.String())}),RevokeAllSessionsBodySchema=t38.Object({excludeCurrent:t38.Optional(t38.Boolean()),reason:t38.Optional(t38.String())}),SessionActivityResponseSchema=t38.Object({success:t38.Boolean(),data:t38.Optional(t38.Object({recentActivity:t38.Array(t38.Object({sessionId:t38.String(),action:t38.String(),ipAddress:t38.String(),timestamp:t38.String(),deviceInfo:t38.Optional(t38.String())}))})),message:t38.Optional(t38.String())}),AdminRevokeSessionsBodySchema=t38.Optional(t38.Object({sessionId:t38.Optional(t38.String()),reason:t38.Optional(t38.String())}))});function formatSessionForResponse(session){let get=(snake,camel3)=>session[snake]??session[camel3],getDate=(snake,camel3)=>{let val=session[snake]??session[camel3];if(!val)return;return val instanceof Date?val.toISOString():String(val)};return{id:session.id,deviceName:get("device_name","deviceName"),deviceType:get("device_type","deviceType"),deviceFingerprint:get("device_fingerprint","deviceFingerprint"),browserName:get("browser_name","browserName"),browserVersion:get("browser_version","browserVersion"),osName:get("os_name","osName"),osVersion:get("os_version","osVersion"),ipAddress:get("ip_address","ipAddress"),locationCountry:get("location_country","locationCountry"),locationCity:get("location_city","locationCity"),lastActivityAt:getDate("last_activity_at","lastActivityAt"),createdAt:getDate("created_at","createdAt"),isCurrent:get("is_current","isCurrent"),loginMethod:get("login_method","loginMethod"),trustScore:get("trust_score","trustScore")}}var init_utils10=()=>{};import{and as and20,desc as desc6,eq as eq56,isNull as isNull3}from"drizzle-orm";import{Elysia as Elysia44}from"elysia";function createSessionsRoute(config,sessionsConfig,sessionsTable){let{db,logger:logger2}=config,log4=logger2.scoped(AUTH_SESSION),baseRoute=sessionsConfig.route||"/auth/sessions",sessionsRoutes=new Elysia44;if(!sessionsConfig.enabled||!sessionsTable)return sessionsRoutes;let tableColumns=sessionsTable,_col=(name2)=>{let snakeCase=name2.replace(/([A-Z])/g,"_$1").toLowerCase();return tableColumns[name2]||tableColumns[snakeCase]},getSessionsForRequest=(request)=>{let resolved=resolveAuthTablesForRequest(request,config),reqTable=resolved.sessionsTable||sessionsTable,reqCols=reqTable,reqCol=(name2)=>{let snakeCase=name2.replace(/([A-Z])/g,"_$1").toLowerCase();return reqCols[name2]||reqCols[snakeCase]},trustedDevicesTable=resolved.schemaTables?.trustedDevices??resolved.schemaTables?.trusted_devices;return{sessionsTable:reqTable,col:reqCol,trustedDevicesTable}};sessionsRoutes.get(baseRoute,async(ctx)=>{let{sessionsTable:sessionsTable2,col:col15}=getSessionsForRequest(ctx.request);if(!db)return{success:!1,message:"Database not configured"};let userId=ctx.request.headers.get("x-user-id");if(!userId)return{success:!1,message:"Authentication required"};let currentSessionId=ctx.request.headers.get("x-session-id"),sessions=await db.select().from(sessionsTable2).where(and20(eq56(col15("userId"),userId),eq56(col15("isActive"),!0),isNull3(col15("revokedAt")))).orderBy(desc6(col15("lastActivityAt"))),realSessions=sessions.filter((s)=>{let sess=s,fp=(sess.deviceFingerprint||"").toLowerCase(),ip=sess.ipAddress||"";return!((!fp||fp==="--"||fp==="--unknown"||fp.includes("bot/crawler")||fp.includes("headless")||fp.includes("unknown-unknown"))&&(ip==="127.0.0.1"||ip==="::1"||ip==="localhost"||!ip))}),formattedSessions=realSessions.map((s)=>{let session=s,formatted=formatSessionForResponse(session);return formatted.isCurrent=session.id===currentSessionId,formatted});return log4.info("Sessions list retrieved",{userId,totalInDb:sessions.length,filteredCount:realSessions.length,hiddenBotSessions:sessions.length-realSessions.length}),{success:!0,data:{sessions:formattedSessions,currentSessionId,totalCount:realSessions.length}}},{detail:{tags:["Authentication"],summary:"List active sessions",description:"Get all active sessions for the current user"}}),sessionsRoutes.get(`${baseRoute}/current`,async(ctx)=>{let{sessionsTable:sessionsTable2,col:col15}=getSessionsForRequest(ctx.request);if(!db)return{success:!1,message:"Database not configured"};let userId=ctx.request.headers.get("x-user-id"),sessionId=ctx.request.headers.get("x-session-id");if(!userId||!sessionId)return{success:!1,message:"Authentication required"};let sessions=await db.select().from(sessionsTable2).where(eq56(col15("id"),sessionId)).limit(1);if(sessions.length===0)return{success:!1,message:"Session not found"};let session=sessions[0],formatted=formatSessionForResponse(session);return formatted.isCurrent=!0,{success:!0,data:formatted}},{detail:{tags:["Authentication"],summary:"Get current session",description:"Get details of the current session"}}),sessionsRoutes.delete(`${baseRoute}/:sessionId`,async(ctx)=>{let{sessionsTable:sessionsTable2,col:col15}=getSessionsForRequest(ctx.request);if(!db)return{success:!1,message:"Database not configured"};let userId=ctx.request.headers.get("x-user-id");if(!userId)return{success:!1,message:"Authentication required"};let{sessionId}=ctx.params,body=ctx.body,currentSessionId=ctx.request.headers.get("x-session-id");if((await db.select().from(sessionsTable2).where(and20(eq56(col15("id"),sessionId),eq56(col15("userId"),userId))).limit(1)).length===0)return{success:!1,message:"Session not found"};let isCurrentSession=sessionId===currentSessionId;await db.update(sessionsTable2).set({isActive:!1,revokedAt:new Date,revokedReason:isCurrentSession?"user_logout":body.reason||"user_revoked"}).where(eq56(col15("id"),sessionId)),log4.info("Session revoked",{userId,sessionId,isCurrentSession,reason:body.reason||"user_revoked"});let reqUrl=new URL(ctx.request.url);return logger2.audit({entityName:"user_sessions",entityId:sessionId,operation:isCurrentSession?"LOGOUT":"SESSION_REVOKE",userId:userId||void 0,summary:isCurrentSession?"User logged out via session revoke":`Session revoked (${sessionId.substring(0,8)}...)`,ipAddress:ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||ctx.request.headers.get("x-real-ip")?.trim()||"unknown",userAgent:ctx.request.headers.get("user-agent")||"unknown",path:reqUrl.pathname,query:reqUrl.search}),{success:!0,message:isCurrentSession?"Logged out successfully":"Session revoked successfully"}},{body:RevokeSessionBodySchema,detail:{tags:["Authentication"],summary:"Revoke session",description:"Revoke a specific session by ID"}}),sessionsRoutes.delete(`${baseRoute}/all`,async(ctx)=>{let{sessionsTable:sessionsTable2,col:col15}=getSessionsForRequest(ctx.request);if(!db)return{success:!1,message:"Database not configured"};let userId=ctx.request.headers.get("x-user-id");if(!userId)return{success:!1,message:"Authentication required"};let body=ctx.body,currentSessionId=ctx.request.headers.get("x-session-id"),query=db.update(sessionsTable2).set({isActive:!1,revokedAt:new Date,revokedReason:body.reason||"user_revoked"}).where(and20(eq56(col15("userId"),userId),eq56(col15("isActive"),!0)));if(body.excludeCurrent&¤tSessionId){let{ne:ne3}=await import("drizzle-orm");query=db.update(sessionsTable2).set({isActive:!1,revokedAt:new Date,revokedReason:body.reason||"user_revoked"}).where(and20(eq56(col15("userId"),userId),eq56(col15("isActive"),!0),ne3(col15("id"),currentSessionId)))}await query,log4.info("All sessions revoked",{userId,excludeCurrent:body.excludeCurrent,reason:body.reason||"user_revoked"});let reqUrl=new URL(ctx.request.url);return logger2.audit({entityName:"user_sessions",operation:"SESSION_REVOKE_ALL",userId:userId||void 0,summary:body.excludeCurrent?"All other sessions revoked":"All sessions revoked",ipAddress:ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||ctx.request.headers.get("x-real-ip")?.trim()||"unknown",userAgent:ctx.request.headers.get("user-agent")||"unknown",path:reqUrl.pathname,query:reqUrl.search}),{success:!0,message:body.excludeCurrent?"All other sessions revoked successfully":"All sessions revoked successfully"}},{body:RevokeAllSessionsBodySchema,detail:{tags:["Authentication"],summary:"Revoke all sessions",description:"Revoke all sessions for the current user (optionally exclude current)"}}),sessionsRoutes.get(`${baseRoute}/stats`,async(ctx)=>{let{sessionsTable:sessionsTable2,col:col15}=getSessionsForRequest(ctx.request);if(!db)return{success:!1,message:"Database not configured"};let userId=ctx.request.headers.get("x-user-id");if(!userId)return{success:!1,message:"Authentication required"};let{count:count3,countDistinct}=await import("drizzle-orm"),activeSessions=await db.select({count:count3()}).from(sessionsTable2).where(and20(eq56(col15("userId"),userId),eq56(col15("isActive"),!0),isNull3(col15("revokedAt")))),uniqueDevices=await db.select({count:countDistinct(col15("deviceFingerprint"))}).from(sessionsTable2).where(and20(eq56(col15("userId"),userId),eq56(col15("isActive"),!0))),uniqueIps=await db.select({count:countDistinct(col15("ipAddress"))}).from(sessionsTable2).where(and20(eq56(col15("userId"),userId),eq56(col15("isActive"),!0)));return{success:!0,data:{activeSessions:activeSessions[0]?.count||0,uniqueDevices:uniqueDevices[0]?.count||0,uniqueIpAddresses:uniqueIps[0]?.count||0}}},{detail:{tags:["Authentication"],summary:"Session statistics",description:"Get session statistics for the current user"}});let APPROVAL_TOKEN_TTL_MS=86400000,buildResultHtml=(action,success,message)=>{let icon=success?action==="approve"?"\u2705":"\uD83D\uDEAB":"\u274C",title=success?action==="approve"?"Device Approved":"Device Rejected":"Action Failed",bName=sessionsConfig.brandName||"Nucleus";return`<!DOCTYPE html>
|
|
445
|
+
`}),logger2.info("[OAUTH] Invite email sent to new OAuth user",{userId,email:profile.email,provider})}catch(emailErr){logger2.warn("[OAUTH] Failed to send invite email",{userId,error:emailErr})}}{let stateUser=(await db.select().from(usersTable).where(eq50(usersTable.id,userId)).limit(1))[0],state2=stateUser?evaluateAccountState(stateUser):{allowed:!1,reason:"inactive",message:"Account is not active"};if(!state2.allowed)return logger2.warn(`[OAUTH] Login blocked - account ${state2.reason}`,{userId,provider}),ctx.set.status=302,ctx.set.headers.Location=`${errorRedirect}?error=account_${state2.reason}`,null}let defaultRoleNames=resolveDefaultRoleNames(config.authentication);if(defaultRoleNames.length>0){let resolvedForRole=resolveAuthTablesForRequest(ctx.request,config);await assignDefaultRoles({db,userId,roleNames:defaultRoleNames,rolesTable:resolvedForRole.rolesTable??config.rolesTable??null,userRolesTable:resolvedForRole.userRolesTable??config.userRolesTable??null,logger:logger2})}let syncConfig=oauthService.profileSync;if(syncConfig){let syncTables=resolveAuthTablesForRequest(ctx.request,config);await applyProfileSync({db,usersTable,profilesTable:syncTables.schemaTables?.profiles,userId,rawProfile:profile.rawProfile,config:syncConfig,isNewUser:createdNewUser,logger:logger2})}await db.update(usersTable).set({lastLoginAt:new Date}).where(eq50(usersTable.id,userId));let ipAddress=ctx.request.headers.get("cf-connecting-ip")?.trim()||ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||ctx.request.headers.get("x-real-ip")?.trim()||"127.0.0.1",userAgent=ctx.request.headers.get("user-agent")||"Unknown Browser",deviceInfo=parseUserAgentForLogin(userAgent,ipAddress,void 0,deviceContextFromRequest(ctx.request).clientHints),oauthUserRoles=[],oauthUserClaims=[],oauthUserClaimScopes={};if(config.db)try{let resolved2=resolveAuthTablesForRequest(ctx.request,config),rc=await fetchUserRolesAndClaims(config.db,userId,resolved2);oauthUserRoles=rc.roles,oauthUserClaims=rc.claims,oauthUserClaimScopes=rc.claimScopes}catch{}let accessToken=signAccessToken(userId,oauthUserRoles.length>0?oauthUserRoles:void 0,oauthUserClaims.length>0?oauthUserClaims:void 0,ctx.request.headers.get("x-tenant-schema")??void 0,oauthUserClaimScopes),refreshToken=signRefreshToken(userId),requestOrigin=ctx.request.headers.get("origin")||ctx.request.headers.get("referer")?.replace(/\/[^/]*$/,"")||void 0,sessionParams={userId,deviceInfo,loginMethod:`oauth:${provider}`,rememberMe:!0,requestOrigin},sessionId=await createSession(sessionParams),requiresApproval=!1,approvalSessionId=sessionId,deviceTokenCookie;if(saveSessionToDb){let oauthSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0,dtCfg=config.authentication?.deviceTrust;if(dtCfg?.enabled)sessionParams.deviceToken=readDeviceTokenFromCookieHeader(ctx.request.headers.get("cookie"),dtCfg);let saveResult=await saveSessionToDb(sessionId,sessionParams,oauthSchemaName);if(saveResult?.denied)return logger2.warn("[OAUTH] Login blocked by device-trust",{userId,provider}),ctx.set.status=302,ctx.set.headers.Location=`${errorRedirect}?error=device_not_recognized`,null;if(deviceTokenCookie=saveResult?.deviceTokenCookie,saveResult?.requiresApproval){if(requiresApproval=!0,saveResult.sessionId)approvalSessionId=saveResult.sessionId}}if(requiresApproval){if(deviceTokenCookie)ctx.set.headers["Set-Cookie"]=deviceTokenCookie;logger2.info("[OAUTH] Login requires device approval",{userId,provider,approvalSessionId});let pendingUrl=absolutizeRedirect(resolveSafeOAuthRedirect(statePayload.redirectUrl,oauthService.successRedirectUrl,config.authentication?.trustedAppOrigins),portalOrigin);return ctx.set.status=302,ctx.set.headers.Location=`${pendingUrl}?requires_approval=true&provider=${provider}&session_id=${approvalSessionId}`,null}await logger2.audit({entityName:"users",entityId:userId,operation:"LOGIN",userId,summary:`${profile.email??profile.providerAccountId} logged in via OAuth (${provider})`,ipAddress,userAgent,path:`${basePath}/${provider}/callback`,query:""});let securePart=cookies.secure?"; Secure":"",domainPart=cookies.domain?`; Domain=${cookies.domain}`:"",cookieOptions=`; Path=${cookies.path}; HttpOnly; SameSite=${cookies.sameSite}${securePart}${domainPart}`,cookiesToSet=[];if(tokenConfig.accessToken.setHeadersEnabled)cookiesToSet.push(`${cookies.accessTokenName}=${accessToken}${cookieOptions}; Max-Age=${cookies.accessTokenMaxAge}`);if(tokenConfig.refreshToken.setHeadersEnabled)cookiesToSet.push(`${cookies.refreshTokenName}=${refreshToken}${cookieOptions}; Max-Age=${cookies.refreshTokenMaxAge}`);if(tokenConfig.sessionToken.setHeadersEnabled)cookiesToSet.push(`${cookies.sessionTokenName}=${sessionId}${cookieOptions}; Max-Age=${cookies.sessionTokenMaxAge}`);if(deviceTokenCookie)cookiesToSet.push(deviceTokenCookie);let successUrl=absolutizeRedirect(resolveSafeOAuthRedirect(statePayload.redirectUrl,oauthService.successRedirectUrl,config.authentication?.trustedAppOrigins),portalOrigin),headers=new Headers;headers.set("Location",successUrl);for(let cookie of cookiesToSet)headers.append("Set-Cookie",cookie);return new Response(null,{status:302,headers})},{detail:{tags:["Authentication"],summary:"OAuth Callback",description:"Handles the OAuth provider callback, creates session and sets cookies"}}),routes.get(`${basePath}/providers`,()=>{return{success:!0,data:{providers:oauthService.getEnabledProviders()}}},{detail:{tags:["Authentication"],summary:"List OAuth Providers",description:"Returns the list of enabled OAuth providers"}}),routes.get(`${basePath}/link/:provider`,async(ctx)=>{let provider=ctx.params.provider;if(!oauthService.isProviderEnabled(provider))return ctx.set.status=404,{success:!1,message:`OAuth provider "${provider}" is not enabled`};if(!oauthService.allowAccountLinking)return ctx.set.status=403,{success:!1,message:"Account linking is disabled"};let userId=ctx.request.headers.get("x-user-id");if(!userId)return ctx.set.status=401,{success:!1,message:"Authentication required to link accounts"};let redirectUrl=ctx.query.redirect_url,authUrl=await oauthService.buildAuthorizationUrl(provider,userId,redirectUrl),issuedState=new URL(authUrl).searchParams.get("state");if(issuedState)ctx.set.headers["Set-Cookie"]=buildOAuthStateCookie(issuedState,{maxAge:600,secure:cookies.secure,domain:cookies.domain});return ctx.set.status=302,ctx.set.headers.Location=authUrl,null},{detail:{tags:["Authentication"],summary:"Link OAuth Account",description:"Redirects to provider to link an OAuth account to the current user"}}),routes.delete(`${basePath}/unlink/:provider`,async(ctx)=>{let provider=ctx.params.provider,userId=ctx.request.headers.get("x-user-id");if(!userId)return ctx.set.status=401,{success:!1,message:"Authentication required"};if(!db)return ctx.set.status=500,{success:!1,message:"Database not configured"};let oauthTable=config.oauthAccountsTable;if(!oauthTable)return ctx.set.status=500,{success:!1,message:"OAuth accounts table not configured"};if((await db.select().from(oauthTable).where(and18(eq50(oauthTable.userId,userId),eq50(oauthTable.provider,provider))).limit(1)).length===0)return{success:!1,message:`No linked ${provider} account found`};let{usersTable:resolvedUsersTable}=resolveAuthTablesForRequest(ctx.request,config);if(!resolvedUsersTable)return ctx.set.status=500,{success:!1,message:"Users table not configured"};let hasPassword=!!(await db.select().from(resolvedUsersTable).where(eq50(resolvedUsersTable.id,userId)).limit(1))[0]?.password,allOAuthAccounts=await db.select().from(oauthTable).where(eq50(oauthTable.userId,userId));if(!hasPassword&&allOAuthAccounts.length<=1)return ctx.set.status=400,{success:!1,message:"Cannot unlink the only login method. Set a password first."};return await db.delete(oauthTable).where(and18(eq50(oauthTable.userId,userId),eq50(oauthTable.provider,provider))),logger2.info("[OAUTH] Account unlinked",{userId,provider}),{success:!0,message:`${provider} account unlinked successfully`}},{detail:{tags:["Authentication"],summary:"Unlink OAuth Account",description:"Removes a linked OAuth account from the current user"}}),routes.get(`${basePath}/accounts`,async(ctx)=>{let userId=ctx.request.headers.get("x-user-id");if(!userId)return ctx.set.status=401,{success:!1,message:"Authentication required"};if(!db)return ctx.set.status=500,{success:!1,message:"Database not configured"};let oauthTable=config.oauthAccountsTable;if(!oauthTable)return{success:!0,data:{accounts:[]}};return{success:!0,data:{accounts:(await db.select().from(oauthTable).where(eq50(oauthTable.userId,userId))).map((a12)=>({id:a12.id,provider:a12.provider,providerEmail:a12.providerEmail,providerName:a12.providerName,providerAvatarUrl:a12.providerAvatarUrl,isPrimary:a12.isPrimary,lastUsedAt:a12.lastUsedAt,createdAt:a12.createdAt}))}}},{detail:{tags:["Authentication"],summary:"List Linked OAuth Accounts",description:"Returns all OAuth accounts linked to the current user"}}),routes}var OAUTH_STATE_COOKIE="oauth_state",readCookie=(request,name2)=>{let raw=request.headers.get("cookie");if(!raw)return;for(let part of raw.split(";")){let trimmed=part.trim(),eq51=trimmed.indexOf("=");if(eq51>0&&trimmed.slice(0,eq51)===name2)return trimmed.slice(eq51+1)}return},resolveSafeOAuthRedirect=(candidate,fallback,allowedOrigins)=>{if(!candidate)return fallback;let normalized=candidate.replace(/[\t\n\r]/g,"");if(!normalized)return fallback;if(normalized.startsWith("/")&&normalized[1]!=="/"&&normalized[1]!=="\\")return normalized;try{let candHost=new URL(normalized).host.toLowerCase(),trusted=[];try{trusted.push(new URL(fallback).host.toLowerCase())}catch{}for(let o of allowedOrigins??[])try{trusted.push(new URL(o.includes("://")?o:`https://${o}`).host.toLowerCase())}catch{}for(let t33 of trusted)if(t33.startsWith("*.")){let base=t33.slice(2);if(candHost===base||candHost.endsWith(`.${base}`))return normalized}else if(candHost===t33)return normalized}catch{}return fallback},absolutizeRedirect=(target2,portalOrigin)=>portalOrigin&&target2.startsWith("/")&&target2[1]!=="/"?`${portalOrigin}${target2}`:target2;var init_oauth=__esm(()=>{init_DeviceTrust();init_assignDefaultRole();init_fetchUserRolesAndClaims();init_deviceInfo();init_utils7();init_utils8();init_applyProfileSync()});import{t as t33}from"elysia";var PasswordChangeBodySchema,PasswordChangeResponseSchema;var init_types15=__esm(()=>{PasswordChangeBodySchema=t33.Object({currentPassword:t33.Optional(t33.String()),newPassword:t33.String({minLength:8}),confirmPassword:t33.String({minLength:8})}),PasswordChangeResponseSchema=t33.Object({success:t33.Boolean(),message:t33.Optional(t33.String())})});function validatePasswordsMatch(newPassword,confirmPassword){return newPassword===confirmPassword}var exports_store={};__export(exports_store,{userHasNoTrustedDevices:()=>userHasNoTrustedDevices,touchDevice:()=>touchDevice,revokeUserDevices:()=>revokeUserDevices,revokeDevicesBySessionIds:()=>revokeDevicesBySessionIds,revokeDeviceById:()=>revokeDeviceById,rejectDeviceForSession:()=>rejectDeviceForSession,listUserDevices:()=>listUserDevices,insertDeviceRow:()=>insertDeviceRow,findDeviceRowsByRawToken:()=>findDeviceRowsByRawToken,activateDeviceForSession:()=>activateDeviceForSession});import{and as and19,eq as eq51,inArray as inArray16}from"drizzle-orm";async function findDeviceRowsByRawToken(db,table,rawToken){let hash=hashDeviceToken(rawToken);return await db.select().from(table).where(eq51(col14(table,"tokenHash"),hash)).limit(2)}async function insertDeviceRow(db,table,v){let now=new Date;await db.insert(table).values({id:v.id,userId:v.userId,tokenHash:v.tokenHash,status:v.status,originSessionId:v.originSessionId??null,label:v.label??null,deviceFingerprint:v.deviceFingerprint??null,firstSeenIp:v.firstSeenIp??null,lastSeenIp:v.lastSeenIp??v.firstSeenIp??null,lastSeenAt:now,expiresAt:v.expiresAt,isActive:!0,createdAt:now,updatedAt:now})}async function activateDeviceForSession(db,table,sessionId){return(await db.update(table).set({status:"trusted",updatedAt:new Date}).where(and19(eq51(col14(table,"originSessionId"),sessionId),eq51(col14(table,"status"),"pending"))).returning({id:col14(table,"id")})).length}async function rejectDeviceForSession(db,table,sessionId){await db.update(table).set({status:"revoked",revokedAt:new Date,revokedReason:"replaced",isActive:!1,updatedAt:new Date}).where(and19(eq51(col14(table,"originSessionId"),sessionId),eq51(col14(table,"status"),"pending")))}async function touchDevice(db,table,id,ip){await db.update(table).set({lastSeenAt:new Date,lastSeenIp:ip??null,updatedAt:new Date}).where(eq51(col14(table,"id"),id))}async function revokeWhere(db,table,where,reason){await db.update(table).set({status:"revoked",revokedAt:new Date,revokedReason:reason,isActive:!1,updatedAt:new Date}).where(where)}async function revokeUserDevices(db,table,userId,reason){if(!userId)return;let notAlready=and19(eq51(col14(table,"userId"),userId),eq51(col14(table,"status"),"trusted")),alsoPending=and19(eq51(col14(table,"userId"),userId),eq51(col14(table,"status"),"pending"));await revokeWhere(db,table,notAlready,reason),await revokeWhere(db,table,alsoPending,reason)}async function revokeDevicesBySessionIds(db,table,sessionIds,reason){if(!sessionIds?.length)return;await revokeWhere(db,table,inArray16(col14(table,"originSessionId"),sessionIds),reason)}async function revokeDeviceById(db,table,id,userId,reason){return(await db.update(table).set({status:"revoked",revokedAt:new Date,revokedReason:reason,isActive:!1,updatedAt:new Date}).where(and19(eq51(col14(table,"id"),id),eq51(col14(table,"userId"),userId))).returning({id:col14(table,"id")})).length}async function listUserDevices(db,table,userId){return(await db.select().from(table).where(eq51(col14(table,"userId"),userId))).map((r2)=>{let{tokenHash:_omit,...safe}=r2;return safe})}async function userHasNoTrustedDevices(db,table,userId){return(await db.select({id:col14(table,"id")}).from(table).where(and19(eq51(col14(table,"userId"),userId),eq51(col14(table,"status"),"trusted"))).limit(1)).length===0}var col14=(t34,name2)=>t34[name2];var init_store2=__esm(()=>{init_DeviceTrust()});import{eq as eq52}from"drizzle-orm";import{Elysia as Elysia39}from"elysia";function createPasswordChangeRoute(config,passwordChangeConfig){let{db,logger:logger2}=config,log4=logger2.scoped(AUTH_PASSWORD_CHANGE),route=passwordChangeConfig.route||"/auth/password-change",passwordChangeRoutes=new Elysia39;if(!passwordChangeConfig.enabled)return passwordChangeRoutes;return passwordChangeRoutes.post(route,async(ctx)=>{let{usersTable,sessionsTable}=resolveAuthTablesForRequest(ctx.request,config);if(!db||!usersTable)return ctx.set.status=500,{success:!1,message:"Database not configured"};let userId=ctx.request.headers.get("x-user-id");if(!userId)return ctx.set.status=401,{success:!1,message:"Authentication required"};let{currentPassword,newPassword,confirmPassword}=ctx.body;if(!validatePasswordsMatch(newPassword,confirmPassword))return ctx.set.status=422,{success:!1,message:"New passwords do not match"};let strength=validatePasswordStrength(newPassword);if(!strength.valid)return ctx.set.status=400,{success:!1,message:"Password too weak",errors:strength.errors};let user=(await db.select().from(usersTable).where(eq52(usersTable.id,userId)).limit(1))[0];if(!user)return ctx.set.status=404,{success:!1,message:"User not found"};let existingHash=user.password??"",userHasPassword=existingHash.length>0,isInitialSet=!userHasPassword,reqUrl=new URL(ctx.request.url),auditIp=ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||ctx.request.headers.get("x-real-ip")?.trim()||"unknown",auditUa=ctx.request.headers.get("user-agent")||"unknown";if(userHasPassword){if(!currentPassword)return ctx.set.status=400,{success:!1,message:"Current password is required"};if(!await verifyPassword(currentPassword,existingHash))return log4.warn("Password change failed - invalid current password",{userId}),logger2.audit({entityName:"users",entityId:userId,operation:"PASSWORD_CHANGE_FAILED",userId,summary:"Password change failed: invalid current password",ipAddress:auditIp,userAgent:auditUa,path:reqUrl.pathname,query:reqUrl.search}),ctx.set.status=400,{success:!1,message:"Current password is incorrect"}}let hashedNewPassword=await hashPassword(newPassword);if(await db.update(usersTable).set({password:hashedNewPassword,updatedAt:new Date}).where(eq52(usersTable.id,userId)),!isInitialSet&&sessionsTable)try{await db.update(sessionsTable).set({isActive:!1,revokedAt:new Date,revokedReason:"password_change"}).where(eq52(sessionsTable.userId,userId))}catch(err){log4.warn("Failed to revoke sessions after password change",{userId,error:err instanceof Error?err.message:String(err)})}if(!isInitialSet&&config.authentication?.deviceTrust?.enabled)try{let{schemaTables}=resolveAuthTablesForRequest(ctx.request,config),dtTable=schemaTables?.trustedDevices??schemaTables?.trusted_devices;if(dtTable&&db){let{revokeUserDevices:revokeUserDevices2}=await Promise.resolve().then(() => (init_store2(),exports_store));await revokeUserDevices2(db,dtTable,userId,"password_reset")}}catch(err){log4.warn("Failed to revoke device tokens after password change",{userId,error:err instanceof Error?err.message:String(err)})}return log4.info(isInitialSet?"Initial password set successful":"Password change successful",{userId}),logger2.audit({entityName:"users",entityId:userId,operation:isInitialSet?"PASSWORD_SET":"PASSWORD_CHANGE",userId,summary:isInitialSet?"Initial password set successfully":"Password changed successfully",ipAddress:auditIp,userAgent:auditUa,path:reqUrl.pathname,query:reqUrl.search}),{success:!0,message:isInitialSet?"Password set successfully":"Password changed successfully"}},{body:PasswordChangeBodySchema,detail:{tags:["Authentication"],summary:"Change Password",description:"Change password for authenticated user"}}),passwordChangeRoutes}var init_passwordChange=__esm(()=>{init_scopes();init_utils7();init_utils6();init_types15();init_types15()});function describeResetEmail(outcome){if(!outcome||outcome.sent)return{sent:!0,level:"info",message:"Password reset email sent"};let base=SENTENCES[outcome.reason],detail=outcome.detail?.trim();return{sent:!1,level:outcome.reason==="exempt"?"info":"warn",message:detail?`${base} (${detail})`:base}}var SENTENCES;var init_outcome=__esm(()=>{SENTENCES={no_provider:"Password reset email NOT sent \u2014 no email provider is configured. The token was stored, so a link exists, but nothing carried it.",exempt:"Password reset email not sent \u2014 the address is on an exempt domain this install never mails.",rejected:"Password reset email NOT sent \u2014 the provider refused the recipient. The address is accepted at login but not deliverable.",error:"Password reset email NOT sent \u2014 the send failed."}});import{t as t34}from"elysia";var PasswordResetRequestSchema,PasswordResetConfirmSchema,PasswordResetResponseSchema;var init_types16=__esm(()=>{PasswordResetRequestSchema=t34.Object({email:t34.String({format:"email"})}),PasswordResetConfirmSchema=t34.Object({token:t34.String(),newPassword:t34.String({minLength:8}),confirmPassword:t34.String({minLength:8})}),PasswordResetResponseSchema=t34.Object({success:t34.Boolean(),message:t34.Optional(t34.String())})});import{randomBytes as randomBytes7}from"crypto";function generateResetToken(){return randomBytes7(32).toString("hex")}function isTokenExpired(expiresAt){return new Date>expiresAt}var init_utils9=()=>{};import{Elysia as Elysia40}from"elysia";function createPasswordResetRoute(config,passwordResetConfig,storeResetToken,getResetToken,deleteResetToken,sendResetEmail){let{db,logger:logger2}=config,log4=logger2.scoped(AUTH_PASSWORD_RESET),baseRoute=passwordResetConfig.route||"/auth/password-reset",passwordResetRoutes=new Elysia40;if(!passwordResetConfig.enabled)return passwordResetRoutes;return passwordResetRoutes.post(`${baseRoute}/request`,async(ctx)=>{let{usersTable}=resolveAuthTablesForRequest(ctx.request,config);if(!db||!usersTable)return{success:!1,message:"Database not configured"};let{email:rawEmail}=ctx.body,email=rawEmail.toLowerCase(),{eq:eq53}=await import("drizzle-orm"),user=(await db.select().from(usersTable).where(eq53(usersTable.email,email)).limit(1))[0];if(!user)return{success:!0,message:"If email exists, reset link will be sent"};let token=generateResetToken(),expiresAt=new Date(Date.now()+3600000),reqSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0;await storeResetToken(user.id,token,expiresAt,reqSchemaName);let delivery={sent:!1,level:"warn",message:"Password reset email NOT sent \u2014 no send helper is configured."};if(sendResetEmail)try{delivery=describeResetEmail(await sendResetEmail(email,token,ctx.request))}catch(error3){delivery=describeResetEmail({sent:!1,reason:"error",detail:error3 instanceof Error?error3.message:String(error3)})}if(delivery.level==="warn")log4.warn(delivery.message,{email});else log4.info(delivery.message,{email});log4.info("Password reset requested",{userId:user.id,email});let reqUrl=new URL(ctx.request.url);return logger2.audit({entityName:"users",entityId:user.id,operation:"PASSWORD_RESET_REQUEST",userId:user.id,summary:`Password reset requested for ${email} \u2014 ${delivery.sent?"e-posta g\xF6nderildi":"E-POSTA G\xD6NDER\u0130LEMED\u0130"}`,ipAddress:ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||ctx.request.headers.get("x-real-ip")?.trim()||"unknown",userAgent:ctx.request.headers.get("user-agent")||"unknown",path:reqUrl.pathname,query:reqUrl.search}),{success:!0,message:"If email exists, reset link will be sent"}},{body:PasswordResetRequestSchema,detail:{tags:["Authentication"],summary:"Request Password Reset",description:"Request a password reset email"}}),passwordResetRoutes.post(`${baseRoute}/confirm`,async(ctx)=>{let{usersTable,sessionsTable}=resolveAuthTablesForRequest(ctx.request,config);if(!db||!usersTable)return{success:!1,message:"Database not configured"};let{token,newPassword,confirmPassword}=ctx.body;if(newPassword!==confirmPassword)return{success:!1,message:"Passwords do not match"};let strength=validatePasswordStrength(newPassword);if(!strength.valid)return ctx.set.status=400,{success:!1,message:"Password too weak",errors:strength.errors};let reqSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0,resetData=await getResetToken(token,reqSchemaName);if(!resetData)return{success:!1,message:"Invalid or expired reset token"};if(isTokenExpired(resetData.expiresAt))return await deleteResetToken(token,reqSchemaName),{success:!1,message:"Reset token has expired"};let hashedPassword=await hashPassword(newPassword),{eq:eq53}=await import("drizzle-orm");if(await db.update(usersTable).set({password:hashedPassword}).where(eq53(usersTable.id,resetData.userId)),await deleteResetToken(token,reqSchemaName),sessionsTable)try{await db.update(sessionsTable).set({isActive:!1,revokedAt:new Date,revokedReason:"password_reset"}).where(eq53(sessionsTable.userId,resetData.userId))}catch(err){log4.warn("Failed to revoke sessions after password reset",{userId:resetData.userId,error:err instanceof Error?err.message:String(err)})}try{if(config.authentication?.deviceTrust?.enabled){let{schemaTables}=resolveAuthTablesForRequest(ctx.request,config),dtTable=schemaTables?.trustedDevices??schemaTables?.trusted_devices;if(dtTable&&db){let{revokeUserDevices:revokeUserDevices2}=await Promise.resolve().then(() => (init_store2(),exports_store));await revokeUserDevices2(db,dtTable,resetData.userId,"password_reset")}}}catch(err){log4.warn("Failed to revoke device tokens after password reset",{userId:resetData.userId,error:err instanceof Error?err.message:String(err)})}log4.info("Password reset successful",{userId:resetData.userId});let reqUrl=new URL(ctx.request.url);return logger2.audit({entityName:"users",entityId:resetData.userId,operation:"PASSWORD_RESET",userId:resetData.userId,summary:"Password reset completed successfully",ipAddress:ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||ctx.request.headers.get("x-real-ip")?.trim()||"unknown",userAgent:ctx.request.headers.get("user-agent")||"unknown",path:reqUrl.pathname,query:reqUrl.search}),{success:!0,message:"Password has been reset"}},{body:PasswordResetConfirmSchema,detail:{tags:["Authentication"],summary:"Confirm Password Reset",description:"Reset password with token"}}),passwordResetRoutes}var init_passwordReset=__esm(()=>{init_scopes();init_utils6();init_outcome();init_types16();init_utils9();init_types16()});import{t as t35}from"elysia";var PasswordSetBodySchema,PasswordSetResponseSchema;var init_types17=__esm(()=>{PasswordSetBodySchema=t35.Object({newPassword:t35.String({minLength:8}),userId:t35.Optional(t35.String()),token:t35.Optional(t35.String())}),PasswordSetResponseSchema=t35.Object({success:t35.Boolean(),message:t35.String()})});import{eq as eq53}from"drizzle-orm";import{Elysia as Elysia41}from"elysia";function createPasswordSetRoute(config,passwordSetConfig,getMagicToken,deleteMagicToken){let{db,logger:logger2}=config,log4=logger2.scoped(AUTH_PASSWORD_SET),route=passwordSetConfig.route||"/auth/password-set",passwordSetRoutes=new Elysia41;if(!passwordSetConfig.enabled)return passwordSetRoutes;return passwordSetRoutes.post(route,async(ctx)=>{log4.info("Password set request received");let{usersTable}=resolveAuthTablesForRequest(ctx.request,config);if(!db||!usersTable)return log4.error("Password set failed - database not configured"),{success:!1,message:"Database not configured"};let{newPassword,token}=ctx.body,strength=validatePasswordStrength(newPassword);if(!strength.valid)return ctx.set.status=400,{success:!1,message:"Password too weak",errors:strength.errors};let userId=null;if(token&&getMagicToken&&deleteMagicToken){let reqSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0,tokenHash=hashToken(token,TOKEN_PURPOSE.invite),storedToken=await getMagicToken(tokenHash,reqSchemaName);if(!storedToken)return log4.warn("Password set failed - invalid token"),{success:!1,message:"Invalid or expired token"};if(new Date>storedToken.expiresAt)return await deleteMagicToken(tokenHash,reqSchemaName),log4.warn("Password set failed - expired token",{email:storedToken.email}),{success:!1,message:"Invalid or expired token"};userId=storedToken.userId,log4.info("Password set - userId resolved from token",{userId,email:storedToken.email})}else userId=ctx.request.headers.get("x-user-id");if(!userId)return log4.warn("Password set failed - no token and no authenticated user"),{success:!1,message:"Valid token or authentication required"};let user=(await db.select().from(usersTable).where(eq53(usersTable.id,userId)).limit(1))[0];if(log4.info("Password set - user found",{found:!!user,hasPassword:!!user?.password}),!user)return{success:!1,message:"User not found"};if(user.password)return log4.warn("Password set failed - user already has password",{userId}),{success:!1,message:"Password already set. Use password change instead."};let hashedPassword=await hashPassword(newPassword);log4.info("Password set - updating user with verifiedAt",{userId});let updateResult=await db.update(usersTable).set({password:hashedPassword,verifiedAt:new Date,updatedAt:new Date}).where(eq53(usersTable.id,userId));if(log4.info("Password set successful for invited user",{userId,updateResult}),token&&deleteMagicToken){let setSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0,tokenHash=hashToken(token,TOKEN_PURPOSE.invite);await deleteMagicToken(tokenHash,setSchemaName),log4.info("Invite token consumed after password set",{userId})}return{success:!0,message:"Password set successfully"}},{body:PasswordSetBodySchema,detail:{tags:["Authentication"],summary:"Set Password",description:"Set password for the first time (for invited users who do not have a password yet)"}}),passwordSetRoutes}var init_passwordSet=__esm(()=>{init_scopes();init_utils8();init_utils6();init_types17();init_types17()});function parseTimeToSeconds3(time2){let match=time2.match(/^(\d+)([smhd])$/);if(!match)return 900;let value2=match[1],unit=match[2];if(!value2||!unit)return 900;let num3=parseInt(value2,10);switch(unit){case"s":return num3;case"m":return num3*60;case"h":return num3*3600;case"d":return num3*86400;default:return 900}}import{t as t36}from"elysia";var RefreshResponseSchema;var init_types18=__esm(()=>{RefreshResponseSchema=t36.Object({success:t36.Boolean(),message:t36.Optional(t36.String()),data:t36.Optional(t36.Object({accessToken:t36.String()}))})});import{eq as eq54}from"drizzle-orm";import{Elysia as Elysia42}from"elysia";function createRefreshRoute(config,refreshConfig,verifyRefreshToken,signAccessToken,signRefreshToken,tokenResponseConfig,cookieMaxAgeBufferSeconds,cookieConfig){let{logger:logger2,authentication}=config,route=refreshConfig.route||"/auth/refresh",refreshRoutes=new Elysia42;if(!refreshConfig.enabled)return refreshRoutes;return refreshRoutes.post(route,async(ctx)=>{let refreshTokenName=authentication?.refreshToken?.name||"refresh_token",accessTokenName=authentication?.accessToken?.name||"access_token",refreshToken=ctx.request.headers.get("x-refresh-token");if(!refreshToken){let cookieHeader=ctx.request.headers.get("cookie");if(cookieHeader)refreshToken=cookieHeader.split(";").reduce((acc,cookie)=>{let trimmed=cookie.trim(),eqIndex=trimmed.indexOf("=");if(eqIndex>0)acc[trimmed.slice(0,eqIndex)]=trimmed.slice(eqIndex+1);return acc},{})[refreshTokenName]||null}if(!refreshToken)return logger2.debug("[AUTH] Refresh skipped - no refresh cookie on the request"),ctx.set.status=401,{success:!1,message:"Refresh token required"};let result=verifyRefreshToken(refreshToken);if(!result.valid||!result.payload)return logger2.warn("[AUTH] Refresh failed - invalid token"),ctx.set.status=401,{success:!1,message:"Invalid refresh token"};let userId=result.payload.sub,userRoles=[],userClaims=[],userClaimScopes={},resolved=resolveAuthTablesForRequest(ctx.request,config);if(config.db){try{let rc=await fetchUserRolesAndClaims(config.db,userId,resolved);userRoles=rc.roles,userClaims=rc.claims,userClaimScopes=rc.claimScopes}catch(roleError){logger2.error("[AUTH] Failed to fetch roles/claims during refresh",{userId,error:roleError instanceof Error?roleError.message:String(roleError)})}try{let usersTable=resolved.usersTable;if(usersTable){let user=(await config.db.select().from(usersTable).where(eq54(usersTable.id,userId)).limit(1))[0];if(!user||user.isActive===!1)return logger2.warn("[AUTH] Refresh blocked - account inactive",{userId}),ctx.set.status=401,{success:!1,message:"Account is not active"};let lockedUntilRaw=user.lockedUntil,lockedUntil=lockedUntilRaw?new Date(lockedUntilRaw):null;if(user.isLocked===!0&&(!lockedUntil||lockedUntil>new Date))return logger2.warn("[AUTH] Refresh blocked - account locked",{userId}),ctx.set.status=401,{success:!1,message:"Account is locked"};let userCohortId=user.cohortId;if(userCohortId){let cohortsTable=resolved.schemaTables?.userCohorts??resolved.schemaTables?.user_cohorts;if(cohortsTable){let cohortExpiresAt=(await config.db.select().from(cohortsTable).where(eq54(cohortsTable.id,userCohortId)).limit(1))[0]?.expiresAt;if(cohortExpiresAt&&new Date(cohortExpiresAt)<new Date)return logger2.warn("[AUTH] Refresh blocked - cohort expired",{userId,cohortId:userCohortId}),ctx.set.status=401,{success:!1,message:"Your access has expired. Please contact your administrator."}}}}}catch(cohortErr){logger2.error("[AUTH] Failed cohort expiry check during refresh",{userId,error:cohortErr instanceof Error?cohortErr.message:String(cohortErr)})}}let newAccessToken=signAccessToken(userId,userRoles.length>0?userRoles:void 0,userClaims.length>0?userClaims:void 0,ctx.request.headers.get("x-tenant-schema")??void 0,userClaimScopes),newRefreshToken=signRefreshToken?signRefreshToken(userId):null,accessSetHeaders=tokenResponseConfig?.accessToken?.setHeadersEnabled??!0,accessReturnJson=tokenResponseConfig?.accessToken?.returnJson??!0,refreshSetHeaders=tokenResponseConfig?.refreshToken?.setHeadersEnabled??!0,refreshReturnJson=tokenResponseConfig?.refreshToken?.returnJson??!1,accessExpiresIn=authentication?.accessToken?.expiresIn||"15m",accessMaxAge=Math.max(0,parseTimeToSeconds3(accessExpiresIn)-(cookieMaxAgeBufferSeconds??0)),refreshTtl=lifetimeSeconds(authentication?.refreshToken?.expiresIn,604800),refreshMaxAge=cookieMaxAge(refreshTtl),headers=new Headers;headers.set("Content-Type","application/json");let cookiePath=cookieConfig?.path||"/",cookieSameSite=cookieConfig?.sameSite||"Strict",securePart=cookieConfig?.secure!==!1?"; Secure":"",domainPart=cookieConfig?.domain?`; Domain=${cookieConfig.domain}`:"",cookieOpts=`; Path=${cookiePath}; HttpOnly; SameSite=${cookieSameSite}${securePart}${domainPart}`;if(accessSetHeaders)headers.append("Set-Cookie",`${accessTokenName}=${newAccessToken}${cookieOpts}; Max-Age=${accessMaxAge}`);if(newRefreshToken&&refreshSetHeaders)headers.append("Set-Cookie",`${refreshTokenName}=${newRefreshToken}${cookieOpts}; Max-Age=${refreshMaxAge}`);logger2.info("[AUTH] Token refresh successful",{userId,rotatedRefreshToken:!!newRefreshToken});let responseData={};if(accessReturnJson)responseData.accessToken=newAccessToken;if(newRefreshToken&&refreshReturnJson)responseData.refreshToken=newRefreshToken;let jsonBody=JSON.stringify({success:!0,data:responseData});return new Response(jsonBody,{status:200,headers})},{detail:{tags:["Authentication"],summary:"Refresh Token",description:"Get new access token using refresh token"}}),refreshRoutes}var init_refresh=__esm(()=>{init_lifetime();init_fetchUserRolesAndClaims();init_types18()});import{t as t37}from"elysia";var RegisterBodySchema,RegisterResponseSchema;var init_types19=__esm(()=>{RegisterBodySchema=t37.Object({email:t37.String({format:"email"}),password:t37.String({minLength:8}),confirmPassword:t37.Optional(t37.String({minLength:8})),firstName:t37.Optional(t37.String({maxLength:100})),lastName:t37.Optional(t37.String({maxLength:100})),createProfile:t37.Optional(t37.Boolean())}),RegisterResponseSchema=t37.Object({success:t37.Boolean(),message:t37.Optional(t37.String()),data:t37.Optional(t37.Object({user:t37.Object({id:t37.String(),email:t37.String()})}))})});import{eq as eq55}from"drizzle-orm";import{Elysia as Elysia43}from"elysia";function createRegisterRoute(config,registerConfig,sendWelcomeEmail,signAccessToken,signRefreshToken,createSession,cookieConfig,tokenResponseConfig,getEmailService,appName,saveSessionToDb){let{db,logger:logger2}=config,log4=logger2.scoped(AUTH_REGISTER),route=registerConfig.route||"/auth/register",cookies={accessTokenName:cookieConfig?.accessTokenName||"access_token",refreshTokenName:cookieConfig?.refreshTokenName||"refresh_token",sessionTokenName:cookieConfig?.sessionTokenName||"session_token",accessTokenMaxAge:cookieConfig?.accessTokenMaxAge||900,refreshTokenMaxAge:cookieConfig?.refreshTokenMaxAge||604800,sessionTokenMaxAge:cookieConfig?.sessionTokenMaxAge||900,secure:cookieConfig?.secure??!0,httpOnly:cookieConfig?.httpOnly??!0,sameSite:cookieConfig?.sameSite||"strict",path:cookieConfig?.path||"/",domain:cookieConfig?.domain},registerRoutes=new Elysia43;if(!registerConfig.enabled)return registerRoutes;return registerRoutes.post(route,async(ctx)=>{let resolved=resolveAuthTablesForRequest(ctx.request,config),usersTable=resolved.usersTable;if(!db||!usersTable)return ctx.set.status=500,{success:!1,message:"Database not configured"};let{email:rawEmail,password:password4,confirmPassword,firstName,lastName,createProfile}=ctx.body,email=rawEmail.toLowerCase();if(confirmPassword&&password4!==confirmPassword)return ctx.set.status=400,{success:!1,message:"Passwords do not match"};let passwordValidation=validatePasswordStrength(password4);if(!passwordValidation.valid)return ctx.set.status=400,{success:!1,message:"Password too weak",errors:passwordValidation.errors};let existingUsers=await db.select().from(usersTable).where(eq55(usersTable.email,email)).limit(1),reqUrl=new URL(ctx.request.url),auditIp=ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||ctx.request.headers.get("x-real-ip")?.trim()||"unknown",auditUa=ctx.request.headers.get("user-agent")||"unknown";if(existingUsers.length>0){log4.warn("Registration failed - email exists",{email}),logger2.audit({entityName:"users",operation:"REGISTER_FAILED",summary:`Registration failed: email already exists (${email})`,ipAddress:auditIp,userAgent:auditUa,path:reqUrl.pathname,query:reqUrl.search});let verifGatesSignup=registerConfig.emailVerification?.enabled&&getEmailService?.()?.isAvailable();if(config.authentication?.preventUserEnumeration&&verifGatesSignup)return{success:!0,message:"Registration successful. Please check your email to verify your account."};return ctx.set.status=409,{success:!1,message:"Email already registered"}}let hashedPassword=await hashPassword(password4),domainExempt=isEmailExempt(email,config.authentication?.emailExemptDomains),emailService=getEmailService?.()??null,emailVerificationEnabled=!domainExempt&®isterConfig.emailVerification?.enabled&&emailService?.isAvailable(),verificationToken=emailVerificationEnabled?generateVerificationToken():null,tokenExpiresIn=registerConfig.emailVerification?.tokenExpiresIn||"24h",tokenExpiresAt=emailVerificationEnabled?new Date(Date.now()+parseTimeToMs(tokenExpiresIn)):null,insertValues={email,password:hashedPassword};if(emailVerificationEnabled&&verificationToken)insertValues.emailVerificationToken=hashVerificationToken(verificationToken),insertValues.emailVerificationTokenExpiresAt=tokenExpiresAt,insertValues.emailVerificationSentAt=new Date,insertValues.emailVerificationAttempts=1;let newUser=(await db.insert(usersTable).values(insertValues).returning())[0],registerDefaultRoles=resolveDefaultRoleNames(config.authentication);if(registerDefaultRoles.length>0)await assignDefaultRoles({db,userId:newUser.id,roleNames:registerDefaultRoles,rolesTable:resolved.rolesTable??config.rolesTable??null,userRolesTable:resolved.userRolesTable??config.userRolesTable??null,logger:logger2});let wantsProfile=registerConfig.createProfileOnRegister||createProfile,profilesTable=resolved.schemaTables?.profiles;if(wantsProfile&&profilesTable&&(firstName||lastName))try{let profileNow=new Date;await db.insert(profilesTable).values({userId:newUser.id,firstName:firstName??"",lastName:lastName??"",createdAt:profileNow,updatedAt:profileNow})}catch(err){log4.warn("Failed to create profile on register \u2014 continuing",{userId:newUser.id,error:err instanceof Error?err.message:String(err)})}if(log4.info("Registration successful",{userId:newUser.id,email,emailVerificationEnabled}),logger2.audit({entityName:"users",entityId:newUser.id,operation:"REGISTER",userId:newUser.id,summary:`New user registered: ${email}`,ipAddress:auditIp,userAgent:auditUa,path:reqUrl.pathname,query:reqUrl.search}),emailVerificationEnabled&&emailService&&verificationToken){let verificationLink=buildEmailActionLink({request:ctx.request,configuredUrl:registerConfig.emailVerification?.redirectUrl,path:"/verify-email",query:{token:verificationToken},allowedOrigins:config.authentication?.trustedAppOrigins});emailService.sendVerificationEmail(email,email.split("@")[0]||"User",verificationLink,appName||"Nucleus").then((result2)=>{if(result2.success)log4.info("Verification email sent",{email});else log4.error("Failed to send verification email",{email,error:result2.error})}).catch((err)=>{log4.error("Failed to send verification email",{email,error:err})});let resendCooldown=registerConfig.emailVerification?.resendCooldown||"60s",cooldownSeconds=parseTimeToMs(resendCooldown)/1000,maxResendAttempts=registerConfig.emailVerification?.maxResendAttempts||3;if(config.authentication?.preventUserEnumeration)return{success:!0,message:"Registration successful. Please check your email to verify your account."};return{success:!0,message:"Registration successful. Please check your email to verify your account.",data:{user:{id:newUser.id,email:newUser.email},emailVerificationRequired:!0,verification:{cooldownSeconds,canResendAt:new Date(Date.now()+cooldownSeconds*1000).toISOString(),attemptsRemaining:maxResendAttempts-1,maxAttempts:maxResendAttempts}}}}if(sendWelcomeEmail)sendWelcomeEmail(email,firstName||email.split("@")[0]||"User").catch((err)=>{log4.error("Failed to send welcome email",{email,error:err})});if(signAccessToken&&signRefreshToken&&createSession){let ipAddress=ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||ctx.request.headers.get("x-real-ip")?.trim()||"unknown",userAgent=ctx.request.headers.get("user-agent")||"",accessToken=signAccessToken(newUser.id,void 0,void 0,ctx.request.headers.get("x-tenant-schema")??void 0),refreshToken=signRefreshToken(newUser.id),sessionParams={userId:newUser.id,deviceInfo:ensureDeviceInfo({ipAddress,userAgent},deviceContextFromRequest(ctx.request).clientHints),loginMethod:"register",rememberMe:!0},sessionId=await createSession(sessionParams),deviceTokenCookie;if(saveSessionToDb){let reqSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0,dtCfg=config.authentication?.deviceTrust;if(dtCfg?.enabled)sessionParams.deviceToken=readDeviceTokenFromCookieHeader(ctx.request.headers.get("cookie"),dtCfg);deviceTokenCookie=(await saveSessionToDb(sessionId,sessionParams,reqSchemaName))?.deviceTokenCookie}let tokenConfig={accessToken:{setHeadersEnabled:tokenResponseConfig?.accessToken?.setHeadersEnabled??!0,returnJson:tokenResponseConfig?.accessToken?.returnJson??!0},refreshToken:{setHeadersEnabled:tokenResponseConfig?.refreshToken?.setHeadersEnabled??!0,returnJson:tokenResponseConfig?.refreshToken?.returnJson??!0},sessionToken:{setHeadersEnabled:tokenResponseConfig?.sessionToken?.setHeadersEnabled??!0,returnJson:tokenResponseConfig?.sessionToken?.returnJson??!0}},securePart=cookies.secure?"; Secure":"",domainPart=cookies.domain?`; Domain=${cookies.domain}`:"",cookieOptions=`; Path=${cookies.path}; HttpOnly; SameSite=${cookies.sameSite}${securePart}${domainPart}`,cookiesToSet=[];if(tokenConfig.accessToken.setHeadersEnabled)cookiesToSet.push(`${cookies.accessTokenName}=${accessToken}${cookieOptions}; Max-Age=${cookies.accessTokenMaxAge}`);if(tokenConfig.refreshToken.setHeadersEnabled)cookiesToSet.push(`${cookies.refreshTokenName}=${refreshToken}${cookieOptions}; Max-Age=${cookies.refreshTokenMaxAge}`);if(tokenConfig.sessionToken.setHeadersEnabled)cookiesToSet.push(`${cookies.sessionTokenName}=${sessionId}${cookieOptions}; Max-Age=${cookies.sessionTokenMaxAge}`);if(deviceTokenCookie)cookiesToSet.push(deviceTokenCookie);let responseData={user:{id:newUser.id,email:newUser.email}};if(tokenConfig.accessToken.returnJson)responseData.accessToken=accessToken;if(tokenConfig.refreshToken.returnJson)responseData.refreshToken=refreshToken;if(tokenConfig.sessionToken.returnJson)responseData.sessionId=sessionId;let jsonBody=JSON.stringify({success:!0,data:responseData}),headers=new Headers;headers.set("Content-Type","application/json"),headers.set("x-session-id",sessionId);for(let cookie of cookiesToSet)headers.append("Set-Cookie",cookie);return new Response(jsonBody,{status:200,headers})}return{success:!0,data:{user:{id:newUser.id,email:newUser.email}}}},{body:RegisterBodySchema,detail:{tags:["Authentication"],summary:"Register",description:"Register a new user account"}}),registerRoutes}var init_register=__esm(()=>{init_DeviceTrust();init_scopes();init_assignDefaultRole();init_deviceInfo();init_types19();init_utils6();init_types19()});import{t as t38}from"elysia";var SessionResponseSchema,SessionListResponseSchema,RevokeSessionBodySchema,RevokeAllSessionsBodySchema,SessionActivityResponseSchema,AdminRevokeSessionsBodySchema;var init_types20=__esm(()=>{SessionResponseSchema=t38.Object({id:t38.String(),deviceName:t38.Optional(t38.String()),deviceType:t38.Optional(t38.String()),browserName:t38.Optional(t38.String()),browserVersion:t38.Optional(t38.String()),osName:t38.Optional(t38.String()),osVersion:t38.Optional(t38.String()),ipAddress:t38.String(),locationCountry:t38.Optional(t38.String()),locationCity:t38.Optional(t38.String()),lastActivityAt:t38.String(),createdAt:t38.String(),isCurrent:t38.Boolean(),loginMethod:t38.Optional(t38.String()),trustScore:t38.Optional(t38.Number())}),SessionListResponseSchema=t38.Object({success:t38.Boolean(),data:t38.Optional(t38.Object({sessions:t38.Array(SessionResponseSchema),currentSessionId:t38.Optional(t38.String()),totalCount:t38.Number()})),message:t38.Optional(t38.String())}),RevokeSessionBodySchema=t38.Object({reason:t38.Optional(t38.String())}),RevokeAllSessionsBodySchema=t38.Object({excludeCurrent:t38.Optional(t38.Boolean()),reason:t38.Optional(t38.String())}),SessionActivityResponseSchema=t38.Object({success:t38.Boolean(),data:t38.Optional(t38.Object({recentActivity:t38.Array(t38.Object({sessionId:t38.String(),action:t38.String(),ipAddress:t38.String(),timestamp:t38.String(),deviceInfo:t38.Optional(t38.String())}))})),message:t38.Optional(t38.String())}),AdminRevokeSessionsBodySchema=t38.Optional(t38.Object({sessionId:t38.Optional(t38.String()),reason:t38.Optional(t38.String())}))});function formatSessionForResponse(session){let get=(snake,camel3)=>session[snake]??session[camel3],getDate=(snake,camel3)=>{let val=session[snake]??session[camel3];if(!val)return;return val instanceof Date?val.toISOString():String(val)};return{id:session.id,deviceName:get("device_name","deviceName"),deviceType:get("device_type","deviceType"),deviceFingerprint:get("device_fingerprint","deviceFingerprint"),browserName:get("browser_name","browserName"),browserVersion:get("browser_version","browserVersion"),osName:get("os_name","osName"),osVersion:get("os_version","osVersion"),ipAddress:get("ip_address","ipAddress"),locationCountry:get("location_country","locationCountry"),locationCity:get("location_city","locationCity"),lastActivityAt:getDate("last_activity_at","lastActivityAt"),createdAt:getDate("created_at","createdAt"),isCurrent:get("is_current","isCurrent"),loginMethod:get("login_method","loginMethod"),trustScore:get("trust_score","trustScore")}}var init_utils10=()=>{};import{and as and20,desc as desc6,eq as eq56,isNull as isNull3}from"drizzle-orm";import{Elysia as Elysia44}from"elysia";function createSessionsRoute(config,sessionsConfig,sessionsTable){let{db,logger:logger2}=config,log4=logger2.scoped(AUTH_SESSION),baseRoute=sessionsConfig.route||"/auth/sessions",sessionsRoutes=new Elysia44;if(!sessionsConfig.enabled||!sessionsTable)return sessionsRoutes;let tableColumns=sessionsTable,_col=(name2)=>{let snakeCase=name2.replace(/([A-Z])/g,"_$1").toLowerCase();return tableColumns[name2]||tableColumns[snakeCase]},getSessionsForRequest=(request)=>{let resolved=resolveAuthTablesForRequest(request,config),reqTable=resolved.sessionsTable||sessionsTable,reqCols=reqTable,reqCol=(name2)=>{let snakeCase=name2.replace(/([A-Z])/g,"_$1").toLowerCase();return reqCols[name2]||reqCols[snakeCase]},trustedDevicesTable=resolved.schemaTables?.trustedDevices??resolved.schemaTables?.trusted_devices;return{sessionsTable:reqTable,col:reqCol,trustedDevicesTable}};sessionsRoutes.get(baseRoute,async(ctx)=>{let{sessionsTable:sessionsTable2,col:col15}=getSessionsForRequest(ctx.request);if(!db)return{success:!1,message:"Database not configured"};let userId=ctx.request.headers.get("x-user-id");if(!userId)return{success:!1,message:"Authentication required"};let currentSessionId=ctx.request.headers.get("x-session-id"),sessions=await db.select().from(sessionsTable2).where(and20(eq56(col15("userId"),userId),eq56(col15("isActive"),!0),isNull3(col15("revokedAt")))).orderBy(desc6(col15("lastActivityAt"))),realSessions=sessions.filter((s)=>{let sess=s,fp=(sess.deviceFingerprint||"").toLowerCase(),ip=sess.ipAddress||"";return!((!fp||fp==="--"||fp==="--unknown"||fp.includes("bot/crawler")||fp.includes("headless")||fp.includes("unknown-unknown"))&&(ip==="127.0.0.1"||ip==="::1"||ip==="localhost"||!ip))}),formattedSessions=realSessions.map((s)=>{let session=s,formatted=formatSessionForResponse(session);return formatted.isCurrent=session.id===currentSessionId,formatted});return log4.info("Sessions list retrieved",{userId,totalInDb:sessions.length,filteredCount:realSessions.length,hiddenBotSessions:sessions.length-realSessions.length}),{success:!0,data:{sessions:formattedSessions,currentSessionId,totalCount:realSessions.length}}},{detail:{tags:["Authentication"],summary:"List active sessions",description:"Get all active sessions for the current user"}}),sessionsRoutes.get(`${baseRoute}/current`,async(ctx)=>{let{sessionsTable:sessionsTable2,col:col15}=getSessionsForRequest(ctx.request);if(!db)return{success:!1,message:"Database not configured"};let userId=ctx.request.headers.get("x-user-id"),sessionId=ctx.request.headers.get("x-session-id");if(!userId||!sessionId)return{success:!1,message:"Authentication required"};let sessions=await db.select().from(sessionsTable2).where(eq56(col15("id"),sessionId)).limit(1);if(sessions.length===0)return{success:!1,message:"Session not found"};let session=sessions[0],formatted=formatSessionForResponse(session);return formatted.isCurrent=!0,{success:!0,data:formatted}},{detail:{tags:["Authentication"],summary:"Get current session",description:"Get details of the current session"}}),sessionsRoutes.delete(`${baseRoute}/:sessionId`,async(ctx)=>{let{sessionsTable:sessionsTable2,col:col15}=getSessionsForRequest(ctx.request);if(!db)return{success:!1,message:"Database not configured"};let userId=ctx.request.headers.get("x-user-id");if(!userId)return{success:!1,message:"Authentication required"};let{sessionId}=ctx.params,body=ctx.body,currentSessionId=ctx.request.headers.get("x-session-id");if((await db.select().from(sessionsTable2).where(and20(eq56(col15("id"),sessionId),eq56(col15("userId"),userId))).limit(1)).length===0)return{success:!1,message:"Session not found"};let isCurrentSession=sessionId===currentSessionId;await db.update(sessionsTable2).set({isActive:!1,revokedAt:new Date,revokedReason:isCurrentSession?"user_logout":body.reason||"user_revoked"}).where(eq56(col15("id"),sessionId)),log4.info("Session revoked",{userId,sessionId,isCurrentSession,reason:body.reason||"user_revoked"});let reqUrl=new URL(ctx.request.url);return logger2.audit({entityName:"user_sessions",entityId:sessionId,operation:isCurrentSession?"LOGOUT":"SESSION_REVOKE",userId:userId||void 0,summary:isCurrentSession?"User logged out via session revoke":`Session revoked (${sessionId.substring(0,8)}...)`,ipAddress:ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||ctx.request.headers.get("x-real-ip")?.trim()||"unknown",userAgent:ctx.request.headers.get("user-agent")||"unknown",path:reqUrl.pathname,query:reqUrl.search}),{success:!0,message:isCurrentSession?"Logged out successfully":"Session revoked successfully"}},{body:RevokeSessionBodySchema,detail:{tags:["Authentication"],summary:"Revoke session",description:"Revoke a specific session by ID"}}),sessionsRoutes.delete(`${baseRoute}/all`,async(ctx)=>{let{sessionsTable:sessionsTable2,col:col15}=getSessionsForRequest(ctx.request);if(!db)return{success:!1,message:"Database not configured"};let userId=ctx.request.headers.get("x-user-id");if(!userId)return{success:!1,message:"Authentication required"};let body=ctx.body,currentSessionId=ctx.request.headers.get("x-session-id"),query=db.update(sessionsTable2).set({isActive:!1,revokedAt:new Date,revokedReason:body.reason||"user_revoked"}).where(and20(eq56(col15("userId"),userId),eq56(col15("isActive"),!0)));if(body.excludeCurrent&¤tSessionId){let{ne:ne3}=await import("drizzle-orm");query=db.update(sessionsTable2).set({isActive:!1,revokedAt:new Date,revokedReason:body.reason||"user_revoked"}).where(and20(eq56(col15("userId"),userId),eq56(col15("isActive"),!0),ne3(col15("id"),currentSessionId)))}await query,log4.info("All sessions revoked",{userId,excludeCurrent:body.excludeCurrent,reason:body.reason||"user_revoked"});let reqUrl=new URL(ctx.request.url);return logger2.audit({entityName:"user_sessions",operation:"SESSION_REVOKE_ALL",userId:userId||void 0,summary:body.excludeCurrent?"All other sessions revoked":"All sessions revoked",ipAddress:ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||ctx.request.headers.get("x-real-ip")?.trim()||"unknown",userAgent:ctx.request.headers.get("user-agent")||"unknown",path:reqUrl.pathname,query:reqUrl.search}),{success:!0,message:body.excludeCurrent?"All other sessions revoked successfully":"All sessions revoked successfully"}},{body:RevokeAllSessionsBodySchema,detail:{tags:["Authentication"],summary:"Revoke all sessions",description:"Revoke all sessions for the current user (optionally exclude current)"}}),sessionsRoutes.get(`${baseRoute}/stats`,async(ctx)=>{let{sessionsTable:sessionsTable2,col:col15}=getSessionsForRequest(ctx.request);if(!db)return{success:!1,message:"Database not configured"};let userId=ctx.request.headers.get("x-user-id");if(!userId)return{success:!1,message:"Authentication required"};let{count:count3,countDistinct}=await import("drizzle-orm"),activeSessions=await db.select({count:count3()}).from(sessionsTable2).where(and20(eq56(col15("userId"),userId),eq56(col15("isActive"),!0),isNull3(col15("revokedAt")))),uniqueDevices=await db.select({count:countDistinct(col15("deviceFingerprint"))}).from(sessionsTable2).where(and20(eq56(col15("userId"),userId),eq56(col15("isActive"),!0))),uniqueIps=await db.select({count:countDistinct(col15("ipAddress"))}).from(sessionsTable2).where(and20(eq56(col15("userId"),userId),eq56(col15("isActive"),!0)));return{success:!0,data:{activeSessions:activeSessions[0]?.count||0,uniqueDevices:uniqueDevices[0]?.count||0,uniqueIpAddresses:uniqueIps[0]?.count||0}}},{detail:{tags:["Authentication"],summary:"Session statistics",description:"Get session statistics for the current user"}});let APPROVAL_TOKEN_TTL_MS=86400000,buildResultHtml=(action,success,message)=>{let icon=success?action==="approve"?"\u2705":"\uD83D\uDEAB":"\u274C",title=success?action==="approve"?"Device Approved":"Device Rejected":"Action Failed",bName=sessionsConfig.brandName||"Nucleus";return`<!DOCTYPE html>
|
|
446
446
|
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
447
447
|
<title>${title} \u2014 ${bName}</title>
|
|
448
448
|
<style>*{box-sizing:border-box}body{margin:0;padding:0;background-color:#f4f4f5;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;}</style>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nucleus-core-ts",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.964",
|
|
4
4
|
"description": "Production-ready, enterprise-grade TypeScript framework for building multi-tenant APIs",
|
|
5
5
|
"author": "Hidayet Can Özcan <hidayetcan@gmail.com>",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|