nucleus-core-ts 0.10.99 → 0.10.101

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 CHANGED
@@ -1 +1 @@
1
- 0.10.99
1
+ 0.10.101
package/dist/index.js CHANGED
@@ -444,7 +444,7 @@ ${strMDSAlgs}`)}if(attestationStatementAlg!==void 0&&authenticatorGetInfo?.algor
444
444
  AND c2.column_name = 'owner_type'
445
445
  AND c1.table_schema = ${effectiveSchema}
446
446
  AND c1.data_type = 'uuid'
447
- `),polyTables=(Array.isArray(polyResult)?polyResult:polyResult.rows||[]).map((r)=>String(r.table_name||""));logger2.info("[AUTH] Change User ID - discovered schema references",{fkConstraints:fkConstraints.map((f)=>`${f.table_name}.${f.column_name} (${f.constraint_name})`),userIdColumns:userIdColumns.map((u)=>`${u.table_name}.${u.column_name}`),auditColumns:auditColumns.map((a)=>`${a.table_name}.${a.column_name}`),polyTables});let allUpdates=new Map,addUpdate=(tableName,columnName)=>{if(!allUpdates.has(tableName))allUpdates.set(tableName,new Set);allUpdates.get(tableName)?.add(columnName)};for(let fk of fkConstraints)addUpdate(fk.table_name,fk.column_name);for(let col13 of userIdColumns)addUpdate(col13.table_name,col13.column_name);for(let col13 of auditColumns)addUpdate(col13.table_name,col13.column_name);logger2.info("[AUTH] Change User ID - all column updates to execute",{updates:Array.from(allUpdates.entries()).map(([t,cols])=>`${t}: [${Array.from(cols).join(", ")}]`)}),await db.transaction(async(tx)=>{for(let fk of fkConstraints)await tx.execute(sql9.raw(`ALTER TABLE "${effectiveSchema}"."${fk.table_name}" DROP CONSTRAINT "${fk.constraint_name}"`));await tx.execute(sql9.raw(`UPDATE "${effectiveSchema}"."users" SET "id" = '${newId}' WHERE "id" = '${currentId}'`));for(let[tableName,columns]of allUpdates.entries())for(let columnName of columns)await tx.execute(sql9.raw(`UPDATE "${effectiveSchema}"."${tableName}" SET "${columnName}" = '${newId}' WHERE "${columnName}" = '${currentId}'`));for(let tableName of polyTables)await tx.execute(sql9.raw(`UPDATE "${effectiveSchema}"."${tableName}" SET "owner_id" = '${newId}' WHERE "owner_id" = '${currentId}' AND "owner_type" IN ('user', 'users', 'User')`));for(let fk of fkConstraints){let onDelete=fk.delete_rule==="CASCADE"?"ON DELETE CASCADE":fk.delete_rule==="SET NULL"?"ON DELETE SET NULL":"";await tx.execute(sql9.raw(`ALTER TABLE "${effectiveSchema}"."${fk.table_name}" ADD CONSTRAINT "${fk.constraint_name}" FOREIGN KEY ("${fk.column_name}") REFERENCES "${effectiveSchema}"."users" ("id") ${onDelete}`))}});let allAffectedTables=[...new Set([...fkConstraints.map((f)=>f.table_name),...userIdColumns.map((u)=>u.table_name),...auditColumns.map((a)=>a.table_name),...polyTables])];return logger2.info("[AUTH] User ID changed successfully",{godminId:requestingUserId,currentId,newId,email:targetUser.email,affectedTables:allAffectedTables,fkConstraintsDroppedAndReadded:fkConstraints.length}),await logger2.audit({entityName:"users",entityId:newId,operation:"CHANGE_USER_ID",userId:requestingUserId,summary:`Godmin changed user ID: ${currentId} \u2192 ${newId} (${targetUser.email})`,oldValues:{id:currentId},newValues:{id:newId},ipAddress:ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||"unknown",userAgent:ctx.request.headers.get("user-agent")||"unknown",path:new URL(ctx.request.url).pathname,query:""}),{success:!0,data:{oldId:currentId,newId,email:targetUser.email,affectedTables:allAffectedTables}}}catch(error3){return logger2.error("[AUTH] Failed to change user ID",{error:error3 instanceof Error?error3.message:String(error3),stack:error3 instanceof Error?error3.stack:void 0,currentId,newId}),ctx.set.status=500,{success:!1,message:error3 instanceof Error?error3.message:"Failed to change user ID"}}}})];return collectRoutes([{routes:declared}])}var init_changeUserId=__esm(()=>{init_esm3();init_routeCollector();init_requireGodmin();init_resolveMutationSchema()});function decideCreateUserAuthz(params){let{callerIsGod,callerRoleNames,allowRoles,requestedRoleNames,godminRoleName}=params,callerIsGodmin=callerIsGod||callerRoleNames.includes(godminRoleName);if(!(callerIsGodmin||callerRoleNames.some((n)=>allowRoles.includes(n))))return{allowed:!1,status:403,reason:"not_privileged"};if(requestedRoleNames.includes(godminRoleName)&&!callerIsGodmin)return{allowed:!1,status:403,reason:"godmin_assignment_forbidden"};if(!callerIsGodmin){let heldByCaller=new Set(callerRoleNames);if(!requestedRoleNames.every((r)=>heldByCaller.has(r)))return{allowed:!1,status:403,reason:"role_above_caller"}}return{allowed:!0,callerIsGodmin}}import{eq as eq43,inArray as inArray17}from"drizzle-orm";function createCreateUserRoute(config,createUserConfig){let{db,logger:logger2}=config;if(createUserConfig?.enabled===!1)return collectRoutes();let route=createUserConfig?.route||"/auth/admin/create-user",allowRoles=createUserConfig?.allowRoles??[],declared=[defineRoute({method:"POST",path:"/",body:CreateUserBodySchema,detail:{tags:["Authentication","Admin"],summary:"Admin Create User",description:"Create a fully active user with a password, roles, and optional profile. Admin-only. No email is sent."},handler:async(ctx)=>{let resolved=resolveAuthTablesForRequest(ctx.request,config),{usersTable,rolesTable:resolvedRolesTable,userRolesTable:resolvedUserRolesTable,schemaTables}=resolved,profilesTable=schemaTables?.profiles,rolesTable=resolvedRolesTable??config.rolesTable??null,userRolesTable=resolvedUserRolesTable??config.userRolesTable??null;if(!db||!usersTable)return ctx.set.status=500,{success:!1,message:"Database not configured"};let requestingUserId=ctx.request.headers.get("x-user-id");if(!requestingUserId)return ctx.set.status=401,{success:!1,message:"Unauthorized"};let requestingUser=(await db.select().from(usersTable).where(eq43(usersTable.id,requestingUserId)).limit(1))[0];if(!requestingUser)return ctx.set.status=401,{success:!1,message:"Unauthorized"};let callerIsGod=Boolean(requestingUser.isGod),callerRoleNames=[];if(!callerIsGod&&rolesTable&&userRolesTable){let rolesCols=rolesTable,userRolesCols=userRolesTable;callerRoleNames=(await db.select({name:rolesCols.name}).from(userRolesTable).innerJoin(rolesTable,eq43(rolesCols.id,userRolesCols.roleId)).where(eq43(userRolesCols.userId,requestingUserId))).map((r)=>r.name)}let requestedRoleIds=ctx.body.roleIds??[],foundRoles=[];if(requestedRoleIds.length>0&&rolesTable){let rolesCols=rolesTable;foundRoles=(await db.select({id:rolesCols.id,name:rolesCols.name}).from(rolesTable).where(inArray17(rolesCols.id,requestedRoleIds))).map((r)=>({id:r.id,name:r.name}))}let decision=decideCreateUserAuthz({callerIsGod,callerRoleNames,allowRoles,requestedRoleNames:foundRoles.map((r)=>r.name),godminRoleName:GODMIN_ROLE_NAME});if(!decision.allowed)return logger2.warn("[AUTH] create-user denied",{callerId:requestingUserId,reason:decision.reason}),ctx.set.status=decision.status,{success:!1,message:decision.reason==="godmin_assignment_forbidden"?"Forbidden: assigning the godmin role requires godmin privileges":"Forbidden"};let{email:rawEmail,password:password4,profile}=ctx.body,email=rawEmail.toLowerCase().trim(),passwordValidation=validatePasswordStrength(password4,config.authentication?.passwordPolicy,email);if(!passwordValidation.valid)return ctx.set.status=400,{success:!1,message:"Password too weak",errors:passwordValidation.errors};if((await db.select().from(usersTable).where(eq43(usersTable.email,email)).limit(1)).length>0)return logger2.warn("[AUTH] create-user failed \u2014 email already exists",{email}),ctx.set.status=409,{success:!1,message:"Email already registered"};let now=new Date,hashedPassword=await hashPassword(password4),insertValues={email,password:hashedPassword,emailVerified:!0,verifiedAt:now,isLocked:!1,createdAt:now,updatedAt:now},newUser=(await db.insert(usersTable).values(insertValues).returning())[0];if(!newUser)return logger2.error("[AUTH] create-user insert returned nothing",{email}),ctx.set.status=500,{success:!1,message:"Failed to create user"};let newUserId=newUser.id;if(requestedRoleIds.length>0&&rolesTable&&userRolesTable)try{let validRoleIds=foundRoles.map((r)=>r.id);if(validRoleIds.length>0)await db.insert(userRolesTable).values(validRoleIds.map((roleId)=>({userId:newUserId,roleId})));let skipped=requestedRoleIds.filter((id)=>!validRoleIds.includes(id));if(skipped.length>0)logger2.warn("[AUTH] create-user: some roleIds were not found",{userId:newUserId,skipped});if(foundRoles.some((r)=>r.name===GODMIN_ROLE_NAME))await db.update(usersTable).set({isGod:!0}).where(eq43(usersTable.id,newUserId))}catch(err){logger2.warn("[AUTH] create-user: failed to assign roles \u2014 continuing",{userId:newUserId,error:err instanceof Error?err.message:String(err)})}else{let createDefaultRoles=resolveDefaultRoleNames(config.authentication);if(createDefaultRoles.length>0)await assignDefaultRoles({db,userId:newUserId,roleNames:createDefaultRoles,rolesTable,userRolesTable,logger:logger2})}if(profile&&profilesTable)try{await db.insert(profilesTable).values({userId:newUserId,firstName:profile.firstName??null,lastName:profile.lastName??null,phoneNumber:profile.phoneNumber??null,createdAt:now,updatedAt:now})}catch(err){logger2.warn("[AUTH] create-user: failed to create profile \u2014 continuing",{userId:newUserId,error:err instanceof Error?err.message:String(err)})}let 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";return logger2.audit({entityName:"users",entityId:newUserId,operation:"ADMIN_CREATE_USER",userId:requestingUserId,summary:`Admin ${requestingUserId} created user ${email}`,ipAddress:auditIp,userAgent:auditUa,path:reqUrl.pathname,query:reqUrl.search}),logger2.info("[AUTH] Admin created user",{newUserId,email,createdBy:requestingUserId}),{success:!0,message:"User created successfully",data:{id:newUserId,email}}}})];return collectRoutes([{prefix:route,routes:declared}])}var CreateUserBodySchema;var init_createUser=__esm(()=>{init_esm3();init_Authorization();init_routeCollector();init_assignDefaultRole();init_utils6();CreateUserBodySchema=Type.Object({email:Type.String({format:"email"}),password:Type.String({minLength:8}),roleIds:Type.Optional(Type.Array(Type.String())),profile:Type.Optional(Type.Object({firstName:Type.Optional(Type.String()),lastName:Type.Optional(Type.String()),phoneNumber:Type.Optional(Type.String())}))})});import{eq as eq44}from"drizzle-orm";function createHardDeleteUserRoute(config){let{db,logger:logger2}=config,declared=[defineRoute({method:"DELETE",path:"/auth/admin/hard-delete/:userId",params:HardDeleteUserParamsSchema,detail:{tags:["Authentication","Admin"],summary:"Hard Delete User",description:"Permanently delete a user and all related data (roles, sessions, profile, files, etc.). Godmin-only. Irreversible."},handler:async(ctx)=>{let resolved=resolveAuthTablesForRequest(ctx.request,config),{usersTable,userRolesTable,sessionsTable,oauthAccountsTable,apiKeysTable,schemaTables}=resolved;if(!db||!usersTable)return ctx.set.status=500,{success:!1,message:"Database not configured"};let requestingUserId=ctx.request.headers.get("x-user-id");if(!requestingUserId)return ctx.set.status=401,{success:!1,message:"Unauthorized"};let caller=(await db.select().from(usersTable).where(eq44(usersTable.id,requestingUserId)).limit(1))[0],callerIsGodmin=Boolean(caller?.isGod)||hasGodminRole(ctx.request)||await userHasGodminRoleInDb(db,config.schemaTables??{},requestingUserId,config.logger);if(!caller||!callerIsGodmin)return ctx.set.status=403,{success:!1,message:"Forbidden \u2014 only godmin can hard-delete users"};let{userId:targetUserId}=ctx.params;if(targetUserId===requestingUserId)return ctx.set.status=400,{success:!1,message:"Cannot hard-delete yourself"};let targetUser=(await db.select().from(usersTable).where(eq44(usersTable.id,targetUserId)).limit(1))[0];if(!targetUser)return ctx.set.status=404,{success:!1,message:"User not found"};if(targetUser.isGod)return ctx.set.status=403,{success:!1,message:"Cannot hard-delete a godmin user"};let deletedRelations=[],col13=(table,name2)=>table[name2],profilesTable=schemaTables?.profiles,addressesTable=schemaTables?.addresses,phonesTable=schemaTables?.phones,filesTable=schemaTables?.files,runCascade=async(exec)=>{deletedRelations.length=0;let del=async(table,columnName,label)=>{if(!table)return;await exec.delete(table).where(eq44(col13(table,columnName),targetUserId)),deletedRelations.push(label)};await del(userRolesTable,"userId","user_roles"),await del(sessionsTable,"userId","sessions"),await del(oauthAccountsTable,"userId","oauth_accounts"),await del(apiKeysTable,"userId","api_keys"),await del(profilesTable,"userId","profiles"),await del(addressesTable,"ownerId","addresses"),await del(phonesTable,"ownerId","phones"),await del(filesTable,"uploadedBy","files"),await exec.delete(usersTable).where(eq44(usersTable.id,targetUserId))};try{let txDb=db;if(typeof txDb.transaction==="function")await txDb.transaction(async(tx)=>{await runCascade(tx)});else await runCascade(db)}catch(err){return logger2.error("[AUTH] hard-delete failed; transaction rolled back",{userId:targetUserId,error:err instanceof Error?err.message:String(err)}),ctx.set.status=500,{success:!1,message:"Hard delete failed and was rolled back"}}let reqUrl=new URL(ctx.request.url);return logger2.audit({entityName:"users",entityId:targetUserId,operation:"ADMIN_HARD_DELETE_USER",userId:requestingUserId,summary:`Godmin ${requestingUserId} hard-deleted user ${targetUser.email} (${targetUserId}). Cleaned: ${deletedRelations.join(", ")}`,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}),logger2.info("[AUTH] Hard-deleted user",{targetUserId,email:targetUser.email,deletedBy:requestingUserId,cleanedRelations:deletedRelations}),{success:!0,message:"User permanently deleted",data:{userId:targetUserId,email:targetUser.email,cleanedRelations:deletedRelations}}}})];return collectRoutes([{routes:declared}])}var HardDeleteUserParamsSchema;var init_hardDeleteUser=__esm(()=>{init_esm3();init_routeCollector();init_adminGuard();HardDeleteUserParamsSchema=Type.Object({userId:Type.String()})});var exports_fetchUserRolesAndClaims={};__export(exports_fetchUserRolesAndClaims,{fetchUserRolesAndClaims:()=>fetchUserRolesAndClaims});import{and as and23,eq as eq45,inArray as inArray18}from"drizzle-orm";var noopLogger,fetchUserRolesAndClaims=async(db,userId,resolved,logger2=noopLogger)=>{let{userRolesTable,rolesTable,roleClaimsTable,claimsTable,usersTable}=resolved,result={roles:[],claims:[],claimScopes:{}};if(!userRolesTable||!rolesTable)return result;let roleIds=(await db.select().from(userRolesTable).where(and23(eq45(userRolesTable.userId,userId),activeRowsPredicate(userRolesTable)))).map((r)=>r.roleId);if(roleIds.length===0)return result;let roleRows=await db.select().from(rolesTable).where(inArray18(rolesTable.id,roleIds));if(result.roles=roleRows.map((r)=>r.name),roleClaimsTable&&claimsTable){let rows=(await db.select({action:claimsTable.action,scope:roleClaimsTable.scope}).from(roleClaimsTable).innerJoin(claimsTable,eq45(roleClaimsTable.claimId,claimsTable.id)).where(and23(inArray18(roleClaimsTable.roleId,roleIds),activeRowsPredicate(roleClaimsTable)))).filter((r)=>typeof r.action==="string"&&r.action.length>0).map((r)=>({action:r.action,scope:r.scope??null})),userData;if(rows.some((r)=>!!r.scope)&&usersTable)try{userData=(await db.select().from(usersTable).where(eq45(usersTable.id,userId)))[0]}catch(err){logger2.warn("[Authorization] Failed to load user row for scope resolution",{userId,error:err instanceof Error?err.message:String(err)})}let{claimScopes,dropped}=resolveClaimScopes(rows,userData,logger2),droppedSet=new Set(dropped);result.claims=Array.from(new Set(rows.map((r)=>r.action))).filter((a)=>!droppedSet.has(a)),result.claimScopes=claimScopes}return result};var init_fetchUserRolesAndClaims=__esm(()=>{init_activeRows();init_Middleware();noopLogger={debug:()=>{},info:()=>{},warn:()=>{},error:()=>{}}});var IMPERSONATION_COOKIE_NAMES;var init_impersonationCookies=__esm(()=>{IMPERSONATION_COOKIE_NAMES=["nucleus_impersonating_as","nucleus_godmin_id","nucleus_impersonating_email","nucleus_imp_sig"]});import{eq as eq46}from"drizzle-orm";function createImpersonateRoute(config,signAccessToken,signRefreshToken,verifyRefreshToken,createSession,saveSessionToDb,cookieConfig){let{db,logger:logger2}=config,requireGodmin2=createRequireGodmin(config),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,sameSite:cookieConfig?.sameSite||"lax",path:cookieConfig?.path||"/",domain:cookieConfig?.domain},routePostAuthAdminImpersonate=defineRoute({method:"POST",path:"/auth/admin/impersonate",body:Type.Object({targetUserId:Type.String()}),detail:{tags:["Authentication"],summary:"Impersonate User",description:"Godmin: start a session as another user"},handler:async(ctx)=>{let{usersTable}=resolveAuthTablesForRequest(ctx.request,config);if(!db||!usersTable)return ctx.set.status=500,{success:!1,message:"Database not configured"};let{targetUserId}=ctx.body,gate=await requireGodmin2(ctx.request);if(!gate.ok)return ctx.set.status=gate.status,{success:!1,message:gate.message};let{callerId:requestingUserId,caller:requestingUser}=gate;if(requestingUserId===targetUserId)return ctx.set.status=400,{success:!1,message:"Cannot impersonate yourself"};let targetUser=(await db.select().from(usersTable).where(eq46(usersTable.id,targetUserId)).limit(1))[0];if(!targetUser)return ctx.set.status=404,{success:!1,message:"Target user not found"};let impRoles=[],impClaims=[],impClaimScopes={};if(config.db)try{let resolved=resolveAuthTablesForRequest(ctx.request,config),rc=await fetchUserRolesAndClaims(config.db,targetUserId,resolved);impRoles=rc.roles,impClaims=rc.claims,impClaimScopes=rc.claimScopes}catch{}let reqIp=ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||"unknown",sessionParams={userId:targetUserId,deviceInfo:{deviceName:"Impersonation Session",browserName:"Admin",osName:"Admin",ipAddress:reqIp},loginMethod:"impersonation"},sessionId=await createSession(sessionParams),accessToken=signAccessToken(targetUserId,impRoles.length>0?impRoles:void 0,impClaims.length>0?impClaims:void 0,ctx.request.headers.get("x-tenant-schema")??void 0,impClaimScopes,sessionId),refreshToken=signRefreshToken(targetUserId,void 0,sessionId);if(saveSessionToDb){let impSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0;await saveSessionToDb(sessionId,sessionParams,impSchemaName)}logger2.info("[AUTH] Impersonation started",{godminId:requestingUserId,targetUserId,targetEmail:targetUser.email}),await logger2.audit({entityName:"users",entityId:targetUserId,operation:"IMPERSONATE_START",userId:requestingUserId,summary:`Godmin ${requestingUser.email} started impersonating ${targetUser.email}`,ipAddress:ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||"unknown",userAgent:ctx.request.headers.get("user-agent")||"unknown",path:new URL(ctx.request.url).pathname,query:""});let securePart=cookies.secure?"; Secure":"",domainPart=cookies.domain?`; Domain=${cookies.domain}`:"",cookieOptions=`; Path=${cookies.path}; HttpOnly; SameSite=${cookies.sameSite}${securePart}${domainPart}`,cookiesToSet=[`${cookies.accessTokenName}=${accessToken}${cookieOptions}; Max-Age=${cookies.accessTokenMaxAge}`,`${cookies.refreshTokenName}=${refreshToken}${cookieOptions}; Max-Age=${cookies.refreshTokenMaxAge}`,`${cookies.sessionTokenName}=${sessionId}${cookieOptions}; Max-Age=${cookies.sessionTokenMaxAge}`,`nucleus_impersonating_as=${targetUserId}${cookieOptions}; Max-Age=${cookies.sessionTokenMaxAge}`,`nucleus_godmin_id=${requestingUserId}${cookieOptions}; Max-Age=${cookies.sessionTokenMaxAge}`,`nucleus_impersonating_email=${encodeURIComponent(String(targetUser.email))}${cookieOptions}; Max-Age=${cookies.sessionTokenMaxAge}`,`nucleus_imp_sig=${signRefreshToken(requestingUserId)}${cookieOptions}; Max-Age=${cookies.refreshTokenMaxAge}`],jsonBody=JSON.stringify(withEnvelopeFlags({success:!0,data:{targetUser:{id:targetUser.id,email:targetUser.email},godminId:requestingUserId,sessionId}})),headers=new Headers;headers.set("Content-Type","application/json");for(let cookie of cookiesToSet)headers.append("Set-Cookie",cookie);return new Response(jsonBody,{status:200,headers})}}),routePostAuthAdminImpersonateStop=defineRoute({method:"POST",path:"/auth/admin/impersonate/stop",detail:{tags:["Authentication"],summary:"Stop Impersonation",description:"Godmin: restore own session after impersonation"},handler:async(ctx)=>{let{usersTable}=resolveAuthTablesForRequest(ctx.request,config);if(!db||!usersTable)return ctx.set.status=500,{success:!1,message:"Database not configured"};let cookieHeader=ctx.request.headers.get("cookie")||"",impSig=Object.fromEntries(cookieHeader.split(";").map((c)=>{let[k,...v]=c.trim().split("=");return[k?.trim(),v.join("=")]})).nucleus_imp_sig,verified=impSig?verifyRefreshToken(impSig):{valid:!1};if(!verified.valid||!verified.payload?.sub)return ctx.set.status=401,{success:!1,message:"No valid impersonation session"};let godminId=verified.payload.sub,godminUser=(await db.select().from(usersTable).where(eq46(usersTable.id,godminId)).limit(1))[0];if(!godminUser)return ctx.set.status=404,{success:!1,message:"Godmin user not found"};if(godminUser.isGod!==!0)return ctx.set.status=403,{success:!1,message:"Forbidden: godmin privileges required"};let stateRefusal=accountStateRefusal(godminUser);if(stateRefusal)return ctx.set.status=401,{success:!1,message:stateRefusal};let godminRoles=[],godminClaims=[],godminClaimScopes={};if(config.db)try{let resolvedStop=resolveAuthTablesForRequest(ctx.request,config),rcStop=await fetchUserRolesAndClaims(config.db,godminId,resolvedStop);godminRoles=rcStop.roles,godminClaims=rcStop.claims,godminClaimScopes=rcStop.claimScopes}catch{}let stopReqIp=ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||"unknown",sessionParams={userId:godminId,deviceInfo:{deviceName:"Restored Admin Session",browserName:"Admin",osName:"Admin",ipAddress:stopReqIp},loginMethod:"impersonation_stop"},sessionId=await createSession(sessionParams),accessToken=signAccessToken(godminId,godminRoles.length>0?godminRoles:void 0,godminClaims.length>0?godminClaims:void 0,ctx.request.headers.get("x-tenant-schema")??void 0,godminClaimScopes,sessionId),refreshToken=signRefreshToken(godminId,void 0,sessionId);if(saveSessionToDb){let stopSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0;await saveSessionToDb(sessionId,sessionParams,stopSchemaName)}logger2.info("[AUTH] Impersonation stopped",{godminId}),await logger2.audit({entityName:"users",entityId:godminId,operation:"IMPERSONATE_STOP",userId:godminId,summary:`Godmin ${godminUser.email} stopped impersonation`,ipAddress:ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||"unknown",userAgent:ctx.request.headers.get("user-agent")||"unknown",path:new URL(ctx.request.url).pathname,query:""});let securePart=cookies.secure?"; Secure":"",domainPart=cookies.domain?`; Domain=${cookies.domain}`:"",cookieOptions=`; Path=${cookies.path}; HttpOnly; SameSite=${cookies.sameSite}${securePart}${domainPart}`,expiredOptions=`; Path=${cookies.path}; HttpOnly; SameSite=${cookies.sameSite}${securePart}${domainPart}; Max-Age=0`,cookiesToSet=[`${cookies.accessTokenName}=${accessToken}${cookieOptions}; Max-Age=${cookies.accessTokenMaxAge}`,`${cookies.refreshTokenName}=${refreshToken}${cookieOptions}; Max-Age=${cookies.refreshTokenMaxAge}`,`${cookies.sessionTokenName}=${sessionId}${cookieOptions}; Max-Age=${cookies.sessionTokenMaxAge}`,...IMPERSONATION_COOKIE_NAMES.map((name2)=>`${name2}=deleted${expiredOptions}`)],jsonBody=JSON.stringify(withEnvelopeFlags({success:!0,data:{godminUser:{id:godminUser.id,email:godminUser.email},sessionId}})),headers=new Headers;headers.set("Content-Type","application/json");for(let cookie of cookiesToSet)headers.append("Set-Cookie",cookie);return new Response(jsonBody,{status:200,headers})}});return collectRoutes([{routes:[routePostAuthAdminImpersonate,routePostAuthAdminImpersonateStop]}])}var init_impersonate=__esm(()=>{init_esm3();init_response();init_routeCollector();init_fetchUserRolesAndClaims();init_impersonationCookies();init_requireGodmin()});function clearedLockoutState(){return{failedLoginAttempts:0,isLocked:!1,lockedUntil:null}}function planPasswordRotation(input){let protectedRoles=new Set(input.protectedRoles.filter((name2)=>name2.trim()!=="")),plan={rotate:[],skipped:[]};for(let user of input.users){if(user.id===input.callerId){plan.skipped.push({id:user.id,reason:"caller"});continue}if(user.isGod){plan.skipped.push({id:user.id,reason:"godmin"});continue}if(user.roleNames.some((name2)=>protectedRoles.has(name2))){plan.skipped.push({id:user.id,reason:"protected_role"});continue}if(input.includeInactive!==!0&&user.isActive===!1){plan.skipped.push({id:user.id,reason:"inactive"});continue}plan.rotate.push(user.id)}return plan}function splitByRecoverability(users,rotate){let rotating=new Set(rotate),selfService=[],needsAdmin=[];for(let user of users){if(!rotating.has(user.id))continue;if(String(user.email??"").trim()==="")needsAdmin.push(user.id);else selfService.push(user.id)}return{selfService,needsAdmin}}import{randomBytes as randomBytes5}from"crypto";import{eq as eq47}from"drizzle-orm";function createPasswordAdminRoutes(config,options){let{db,logger:logger2}=config,log4=logger2.scoped(AUTH_LOGIN),passwordToolsEnabled=options?.passwordTools===!0,requireGodmin2=createRequireGodmin(config),routePostAuthAdminRotatePasswords=defineRoute({method:"POST",path:"/auth/admin/rotate-passwords",body:Type.Object({confirm:Type.Optional(Type.Boolean()),protectedRoles:Type.Optional(Type.Array(Type.String())),includeInactive:Type.Optional(Type.Boolean())}),detail:{tags:["Authentication"],summary:"Rotate every password except the ones named",description:"Without `confirm: true` this only reports what it would do. Confirmed, it gives every selected account a different random password, signs those sessions out, and tells nobody the new values \u2014 recovery is the reset link or an administrator."},handler:async(ctx)=>{if(!passwordToolsEnabled)return notEnabled(ctx);let gate=await requireGodmin2(ctx.request);if(!gate.ok)return ctx.set.status=gate.status,{success:!1,message:gate.message};let resolved=resolveAuthTablesForRequest(ctx.request,config),usersTable=resolved.usersTable,body=ctx.body,users=await db?.select().from(usersTable),roleNamesByUser=await loadRoleNames(resolved,users),candidates=users.map((row)=>({id:String(row.id),roleNames:roleNamesByUser.get(String(row.id))??[],isGod:row.isGod===!0,email:row.email??null,isActive:row.isActive})),plan=planPasswordRotation({users:candidates,callerId:gate.callerId,protectedRoles:body.protectedRoles??[],includeInactive:body.includeInactive===!0}),recovery=splitByRecoverability(candidates,plan.rotate);if(body.confirm!==!0)return{success:!0,data:{preview:!0,willRotate:plan.rotate.length,skipped:plan.skipped.length,skippedReasons:countReasons(plan.skipped),canResetThemselves:recovery.selfService.length,needAdministrator:recovery.needsAdmin.length}};if(plan.rotate.length===0)return{success:!0,data:{preview:!1,rotated:0,signedOut:0}};let rotated=0;for(let userId of plan.rotate){let hashed=await hashPassword(randomBytes5(24).toString("base64url"));await db?.update(usersTable).set({password:hashed,updatedAt:new Date}).where(eq47(usersTable.id,userId)),rotated+=1}let signedOut=await revokeSessions(resolved,plan.rotate);return log4.warn("Bulk password rotation",{by:gate.callerId,rotated,signedOut,skipped:plan.skipped.length}),{success:!0,data:{preview:!1,rotated,signedOut,canResetThemselves:recovery.selfService.length,needAdministrator:recovery.needsAdmin.length}}}}),routePostAuthAdminSetUserPassword=defineRoute({method:"POST",path:"/auth/admin/set-user-password",body:Type.Object({userId:Type.String(),password:Type.String()}),detail:{tags:["Authentication"],summary:"Set one account's password",description:"For the person who has no email address and cannot use the reset link. Applies the same strength policy as a self-service change and ends that account\u2019s existing sessions."},handler:async(ctx)=>{if(!passwordToolsEnabled)return notEnabled(ctx);let gate=await requireGodmin2(ctx.request);if(!gate.ok)return ctx.set.status=gate.status,{success:!1,message:gate.message};let resolved=resolveAuthTablesForRequest(ctx.request,config),usersTable=resolved.usersTable,{userId,password:password4}=ctx.body,strength=validatePasswordStrength(password4);if(!strength.valid)return ctx.set.status=400,{success:!1,message:strength.errors?.join(" ")??"Password is too weak"};let rows=await db?.select().from(usersTable).where(eq47(usersTable.id,userId)).limit(1);if(!rows||rows.length===0)return ctx.set.status=404,{success:!1,message:"User not found"};await db?.update(usersTable).set({password:await hashPassword(password4),updatedAt:new Date,...clearedLockoutState()}).where(eq47(usersTable.id,userId));let signedOut=await revokeSessions(resolved,[userId]);return log4.warn("Password set by administrator",{by:gate.callerId,userId,signedOut}),{success:!0,data:{signedOut,lockoutCleared:!0}}}});return collectRoutes([{routes:[routePostAuthAdminRotatePasswords,routePostAuthAdminSetUserPassword]}]);function notEnabled(ctx){return ctx.set.status=404,{success:!1,message:"Not found"}}async function loadRoleNames(resolved,users){let byUser=new Map,userRoles=resolved.userRolesTable,roles=resolved.rolesTable;if(!db||!userRoles||!roles||users.length===0)return byUser;let roleRows=await db.select().from(roles),roleNameById=new Map(roleRows.map((row)=>[String(row.id),String(row.name??"")])),links=await db.select().from(userRoles);for(let link of links){let userId=String(link.userId??""),name2=roleNameById.get(String(link.roleId??""));if(!userId||!name2)continue;byUser.set(userId,[...byUser.get(userId)??[],name2])}return byUser}async function revokeSessions(resolved,userIds){return revokeUserSessions({db,sessionsTable:resolved.sessionsTable,onError:(error3)=>log4.error("Sessions could not be revoked after a password change",{error:error3})},userIds)}}function countReasons(skipped){let counts={};for(let item of skipped)counts[item.reason]=(counts[item.reason]??0)+1;return counts}var init_passwordAdmin=__esm(()=>{init_esm3();init_scopes();init_routeCollector();init_utils6();init_requireGodmin();init_revokeUserSessions()});import{eq as eq48}from"drizzle-orm";function createUserStateRoutes(config){let{db,logger:logger2}=config,log4=logger2.scoped(AUTH_LOGIN),requireGodmin2=createRequireGodmin(config);async function loadTarget(request,userId){let resolved=resolveAuthTablesForRequest(request,config),usersTable=resolved.usersTable;if(!db||!usersTable)return{ok:!1,status:500,message:"Database not configured"};let user=(await db.select().from(usersTable).where(eq48(usersTable.id,userId)).limit(1))?.[0];if(!user)return{ok:!1,status:404,message:"User not found"};return{ok:!0,usersTable,sessionsTable:resolved.sessionsTable??null,user}}async function writeState(usersTable,userId,changes){await db?.update(usersTable).set({...changes,updatedAt:new Date}).where(eq48(usersTable.id,userId))}let routePostAuthAdminLockUser=defineRoute({method:"POST",path:"/auth/admin/lock-user",body:Type.Object({userId:Type.String(),until:Type.Optional(Type.String())}),detail:{tags:["Authentication"],summary:"Lock one account",description:"Refuses further login attempts. `until` is an ISO timestamp for a temporary lock; omit it to lock indefinitely. Cannot be used on the calling administrator."},handler:async(ctx)=>{let gate=await requireGodmin2(ctx.request);if(!gate.ok)return ctx.set.status=gate.status,{success:!1,message:gate.message};let{userId,until}=ctx.body,target2=await loadTarget(ctx.request,userId);if(!target2.ok)return ctx.set.status=target2.status,{success:!1,message:target2.message};if(userId===gate.callerId)return ctx.set.status=400,{success:!1,message:"An administrator cannot lock their own account"};let lockedUntil=until?new Date(until):null;if(until&&Number.isNaN(lockedUntil?.getTime()))return ctx.set.status=400,{success:!1,message:"`until` is not a valid date"};await writeState(target2.usersTable,userId,{isLocked:!0,lockedUntil});let signedOut=await revokeUserSessions({db,sessionsTable:target2.sessionsTable,onError:(error3)=>log4.error("Sessions could not be revoked after locking an account",{error:error3,userId})},[userId]);return log4.warn("Account locked by administrator",{by:gate.callerId,userId,until:lockedUntil,signedOut}),{success:!0,data:{isLocked:!0,lockedUntil,signedOut}}}}),routePostAuthAdminUnlockUser=defineRoute({method:"POST",path:"/auth/admin/unlock-user",body:Type.Object({userId:Type.String()}),detail:{tags:["Authentication"],summary:"Release one account from lockout",description:"Clears isLocked, lockedUntil AND failedLoginAttempts together, without touching the password. Clearing only isLocked is not a release: the login route sends any account at or above the captcha threshold through the captcha gate before it checks the password, and the counter only clears on a login that succeeds."},handler:async(ctx)=>{let gate=await requireGodmin2(ctx.request);if(!gate.ok)return ctx.set.status=gate.status,{success:!1,message:gate.message};let{userId}=ctx.body,target2=await loadTarget(ctx.request,userId);if(!target2.ok)return ctx.set.status=target2.status,{success:!1,message:target2.message};let wasLocked=target2.user.isLocked===!0,clearedFailedAttempts=Number(target2.user.failedLoginAttempts??0);return await writeState(target2.usersTable,userId,clearedLockoutState()),log4.warn("Account lockout cleared by administrator",{by:gate.callerId,userId,wasLocked,clearedFailedAttempts}),{success:!0,data:{wasLocked,clearedFailedAttempts}}}}),routePostAuthAdminMarkEmailVerified=defineRoute({method:"POST",path:"/auth/admin/mark-email-verified",body:Type.Object({userId:Type.String()}),detail:{tags:["Authentication"],summary:"Mark one account as email-verified",description:"For an installation with no working mail transport, where the verification message can never arrive. Idempotent: an account that is already verified keeps its original timestamp."},handler:async(ctx)=>{let gate=await requireGodmin2(ctx.request);if(!gate.ok)return ctx.set.status=gate.status,{success:!1,message:gate.message};let{userId}=ctx.body,target2=await loadTarget(ctx.request,userId);if(!target2.ok)return ctx.set.status=target2.status,{success:!1,message:target2.message};let alreadyVerified=Boolean(target2.user.verifiedAt),verifiedAt=alreadyVerified?target2.user.verifiedAt:new Date;if(!alreadyVerified)await writeState(target2.usersTable,userId,{verifiedAt,emailVerified:!0});return log4.warn("Email marked verified by administrator",{by:gate.callerId,userId,alreadyVerified}),{success:!0,data:{verifiedAt,alreadyVerified}}}});return collectRoutes([{routes:[routePostAuthAdminLockUser,routePostAuthAdminUnlockUser,routePostAuthAdminMarkEmailVerified]}])}var init_userState=__esm(()=>{init_esm3();init_scopes();init_routeCollector();init_requireGodmin();init_revokeUserSessions()});import{and as and24,count as count2,eq as eq49,sql as sql10}from"drizzle-orm";function createApiKeyRoutes(app,config,apiKeysConfig){let basePath=apiKeysConfig.route||"/auth/api-keys",apiKeysTable=config.apiKeysTable,{db,logger:logger2}=config;if(!apiKeysTable||!db){logger2.warn("[API_KEYS] api_keys table or database not available. Skipping API key routes.");return}let getResolvedConfig=(request)=>{let resolved=resolveAuthTablesForRequest(request,config),reqApiKeysTable=resolved.apiKeysTable??apiKeysTable;return{config:{...config,usersTable:resolved.usersTable??config.usersTable,userRolesTable:resolved.userRolesTable,rolesTable:resolved.rolesTable,roleClaimsTable:resolved.roleClaimsTable,claimsTable:resolved.claimsTable,apiKeysTable:reqApiKeysTable,schemaTables:resolved.schemaTables},apiKeysTable:reqApiKeysTable}},keyPrefix=apiKeysConfig.keyPrefix||"nk_live",maxKeysPerUser=apiKeysConfig.maxKeysPerUser||10,preventManagement=apiKeysConfig.preventApiKeyManagement??!1,routePostRoot=defineRoute({method:"POST",path:"/",handler:async({request,set:set2,body:rawBody})=>{let resolved=getResolvedConfig(request),reqApiKeysTable=resolved.apiKeysTable,reqConfig=resolved.config;if(preventManagement)return set2.status=403,{isSuccess:!1,message:"API key management is disabled via configuration",data:null};if(request.headers.get("x-auth-type")==="api_key")return set2.status=403,{isSuccess:!1,message:"API keys cannot be created using another API key",data:null};let userId=request.headers.get("x-user-id");if(!userId)return set2.status=401,{isSuccess:!1,message:"Unauthenticated",data:null};let body;try{body=rawBody??{}}catch{return set2.status=400,{isSuccess:!1,message:"Invalid request body",data:null}}if(!body.name||typeof body.name!=="string"||body.name.trim().length===0)return set2.status=400,{isSuccess:!1,message:"Name is required",data:null};if(body.ownerType==="application"){if(!(apiKeysConfig.allowApplicationKeys??!0))return set2.status=403,{isSuccess:!1,message:"Application API keys are not allowed",data:null};if(!body.applicationName||body.applicationName.trim().length===0)return set2.status=400,{isSuccess:!1,message:"Application name is required for application keys",data:null}}if(((await db.select({count:count2()}).from(reqApiKeysTable).where(and24(eq49(col13(reqApiKeysTable,"userId"),userId),eq49(col13(reqApiKeysTable,"isActive"),!0))))[0]?.count||0)>=maxKeysPerUser)return set2.status=400,{isSuccess:!1,message:`Maximum API key limit reached (${maxKeysPerUser})`,data:null};let userRoles=await getUserRoleNames(reqConfig,userId),userClaims=await getUserClaimActions(reqConfig,userId),isGodmin4=userRoles.some((r)=>isGodminRole(r)),requestedRoles=body.allowedRoles||[],requestedClaims=body.allowedClaims||[],requestedScopes=body.allowedScopes||[],stripGodmin=(roles)=>roles.filter((r)=>!isGodminRole(r)),finalRoles,finalClaims;if(isGodmin4)finalRoles=requestedRoles.length>0?stripGodmin(requestedRoles):[],finalClaims=requestedClaims.length>0?requestedClaims:userClaims;else{let validRoles=intersectPermissions(userRoles,requestedRoles),validClaims=intersectPermissions(userClaims,requestedClaims);if(requestedRoles.length>0&&validRoles.length!==requestedRoles.length){let disallowed=requestedRoles.filter((r)=>!userRoles.includes(r));return set2.status=403,{isSuccess:!1,message:`Cannot assign roles you do not have: ${disallowed.join(", ")}`,data:null}}if(requestedClaims.length>0&&validClaims.length!==requestedClaims.length){let disallowed=requestedClaims.filter((c)=>!userClaims.includes(c));return set2.status=403,{isSuccess:!1,message:`Cannot assign claims you do not have: ${disallowed.join(", ")}`,data:null}}if(requestedScopes.length>0){let userScopes=await getUserScopes(reqConfig,userId);if(intersectPermissions(userScopes,requestedScopes).length!==requestedScopes.length){let disallowed=requestedScopes.filter((s)=>!userScopes.includes(s));return set2.status=403,{isSuccess:!1,message:`Cannot assign scopes you do not have: ${disallowed.join(", ")}`,data:null}}}finalRoles=stripGodmin(requestedRoles.length>0?validRoles:userRoles),finalClaims=requestedClaims.length>0?validClaims:userClaims}let expiresAt=null,expiresInStr=body.expiresIn||apiKeysConfig.defaultExpiresIn;if(expiresInStr){let seconds=parseTimeToSeconds2(expiresInStr);expiresAt=new Date(Date.now()+seconds*1000)}let generated=generateApiKey(keyPrefix);try{let createdId=(await db.insert(reqApiKeysTable).values({userId,name:body.name.trim(),description:body.description?.trim()||null,keyHash:generated.keyHash,keyPreview:generated.keyPreview,ownerType:body.ownerType||"personal",applicationName:body.applicationName?.trim()||null,expiresAt,isActive:!0,usageCount:0,createdAt:new Date,updatedAt:new Date}).returning())[0]?.id;if(finalRoles.length>0||finalClaims.length>0||requestedScopes.length>0){let jsonbUpdate={};if(finalRoles.length>0)jsonbUpdate.allowedRoles=JSON.stringify(finalRoles);if(finalClaims.length>0)jsonbUpdate.allowedClaims=JSON.stringify(finalClaims);if(requestedScopes.length>0)jsonbUpdate.allowedScopes=JSON.stringify(requestedScopes);await db.update(reqApiKeysTable).set(jsonbUpdate).where(eq49(col13(reqApiKeysTable,"keyHash"),generated.keyHash))}return logger2.info("[API_KEYS] API key created",{keyId:createdId,userId,ownerType:body.ownerType||"personal",applicationName:body.applicationName,rolesCount:finalRoles.length,claimsCount:finalClaims.length,expiresAt:expiresAt?.toISOString()||"never"}),{isSuccess:!0,message:"API key created successfully. Save this key \u2014 it will not be shown again.",data:{id:createdId,key:generated.rawKey,keyPreview:generated.keyPreview,name:body.name.trim(),ownerType:body.ownerType||"personal",applicationName:body.applicationName||null,allowedRoles:finalRoles,allowedClaims:finalClaims,allowedScopes:requestedScopes,expiresAt:expiresAt?.toISOString()||null}}}catch(err){let errDetail;try{errDetail=JSON.stringify(err,Object.getOwnPropertyNames(err))}catch{errDetail=err instanceof Error?err.message:String(err)}return logger2.error("[API_KEYS] Failed to create API key",{error:errDetail,userId}),set2.status=500,{isSuccess:!1,message:"Failed to create API key",data:null}}}}),routeGetRoot=defineRoute({method:"GET",path:"/",detail:{tags:["API keys"],summary:"List the caller's API keys",description:"Every key the caller owns, revoked ones included \u2014 a revoked key is history, not "+"garbage, and it is the only place `revokedAt` / `revokedReason` can be read. Filter on `isActive` for the live set; that is also what `maxKeysPerUser` counts."},handler:async({request,set:set2})=>{let{apiKeysTable:reqApiKeysTable}=getResolvedConfig(request),userId=request.headers.get("x-user-id");if(!userId)return set2.status=401,{isSuccess:!1,message:"Unauthenticated",data:null};let items=(await db.select().from(reqApiKeysTable).where(eq49(col13(reqApiKeysTable,"userId"),userId)).orderBy(sql10`created_at DESC`)).map((row)=>sanitizeKeyForResponse(row));return{isSuccess:!0,data:{items,totalCount:items.length}}}}),routeGetId=defineRoute({method:"GET",path:"/:id",detail:{tags:["API keys"],summary:"Read one API key",description:"A REVOKED key still answers 200 here \u2014 revoking does not delete the row. Read "+"`isActive` / `revokedAt` rather than the status to decide whether the key is live. 404 means no such key, or a key belonging to somebody else."},handler:async({params,request,set:set2})=>{let{apiKeysTable:reqApiKeysTable}=getResolvedConfig(request),userId=request.headers.get("x-user-id");if(!userId)return set2.status=401,{isSuccess:!1,message:"Unauthenticated",data:null};let keyId=params.id,record3=(await db.select().from(reqApiKeysTable).where(and24(eq49(col13(reqApiKeysTable,"id"),keyId),eq49(col13(reqApiKeysTable,"userId"),userId))).limit(1))[0];if(!record3)return set2.status=404,{isSuccess:!1,message:"API key not found",data:null};return{isSuccess:!0,data:sanitizeKeyForResponse(record3)}}}),routePatchId=defineRoute({method:"PATCH",path:"/:id",handler:async({params,request,set:set2,body:rawBody})=>{let resolved=getResolvedConfig(request),reqApiKeysTable=resolved.apiKeysTable,reqConfig=resolved.config;if(preventManagement)return set2.status=403,{isSuccess:!1,message:"API key management is disabled via configuration",data:null};if(request.headers.get("x-auth-type")==="api_key")return set2.status=403,{isSuccess:!1,message:"API keys cannot be modified using another API key",data:null};let userId=request.headers.get("x-user-id");if(!userId)return set2.status=401,{isSuccess:!1,message:"Unauthenticated",data:null};let keyId=params.id,body;try{body=rawBody??{}}catch{return set2.status=400,{isSuccess:!1,message:"Invalid request body",data:null}}let existing=(await db.select().from(reqApiKeysTable).where(and24(eq49(col13(reqApiKeysTable,"id"),keyId),eq49(col13(reqApiKeysTable,"userId"),userId))).limit(1))[0];if(!existing)return set2.status=404,{isSuccess:!1,message:"API key not found",data:null};if(existing.revokedAt)return set2.status=400,{isSuccess:!1,message:"Cannot modify a revoked API key",data:null};let userRoles=await getUserRoleNames(reqConfig,userId),userClaims=await getUserClaimActions(reqConfig,userId),updatePayload={updatedAt:new Date};if(body.name!==void 0){if(typeof body.name!=="string"||body.name.trim().length===0)return set2.status=400,{isSuccess:!1,message:"Name cannot be empty",data:null};updatePayload.name=body.name.trim()}if(body.description!==void 0)updatePayload.description=body.description?.trim()||null;if(body.allowedRoles!==void 0){if(intersectPermissions(userRoles,body.allowedRoles).length!==body.allowedRoles.length){let disallowed=body.allowedRoles.filter((r)=>!userRoles.includes(r));return set2.status=403,{isSuccess:!1,message:`Cannot assign roles you do not have: ${disallowed.join(", ")}`,data:null}}updatePayload.allowedRoles=body.allowedRoles.filter((r)=>!isGodminRole(r))}if(body.allowedClaims!==void 0){if(intersectPermissions(userClaims,body.allowedClaims).length!==body.allowedClaims.length){let disallowed=body.allowedClaims.filter((c)=>!userClaims.includes(c));return set2.status=403,{isSuccess:!1,message:`Cannot assign claims you do not have: ${disallowed.join(", ")}`,data:null}}updatePayload.allowedClaims=body.allowedClaims}if(body.allowedScopes!==void 0){if(body.allowedScopes.length>0){let userScopes=await getUserScopes(reqConfig,userId);if(intersectPermissions(userScopes,body.allowedScopes).length!==body.allowedScopes.length){let disallowed=body.allowedScopes.filter((s)=>!userScopes.includes(s));return set2.status=403,{isSuccess:!1,message:`Cannot assign scopes you do not have: ${disallowed.join(", ")}`,data:null}}}updatePayload.allowedScopes=body.allowedScopes}try{await db.update(reqApiKeysTable).set(updatePayload).where(eq49(col13(reqApiKeysTable,"id"),keyId)),logger2.info("[API_KEYS] API key updated",{keyId,userId});let updated=await db.select().from(reqApiKeysTable).where(eq49(col13(reqApiKeysTable,"id"),keyId)).limit(1);return{isSuccess:!0,message:"API key updated successfully",data:sanitizeKeyForResponse(updated[0])}}catch(err){return logger2.error("[API_KEYS] Failed to update API key",{error:err,keyId,userId}),set2.status=500,{isSuccess:!1,message:"Failed to update API key",data:null}}}}),revokeApiKey=async({params,request,set:set2,body:rawBody})=>{let{apiKeysTable:reqApiKeysTable}=getResolvedConfig(request);if(request.headers.get("x-auth-type")==="api_key")return set2.status=403,{isSuccess:!1,message:"API keys cannot be revoked using another API key",data:null};let userId=request.headers.get("x-user-id");if(!userId)return set2.status=401,{isSuccess:!1,message:"Unauthenticated",data:null};let keyId=params.id,body={};try{if(rawBody)body=rawBody}catch{}let record3=(await db.select().from(reqApiKeysTable).where(and24(eq49(col13(reqApiKeysTable,"id"),keyId),eq49(col13(reqApiKeysTable,"userId"),userId))).limit(1))[0];if(!record3)return set2.status=404,{isSuccess:!1,message:"API key not found",data:null};if(record3.revokedAt)return set2.status=400,{isSuccess:!1,message:"API key is already revoked",data:null};let revokedAt=new Date,revokedReason=body.reason||"user_revoked";try{await db.update(reqApiKeysTable).set({isActive:!1,revokedAt,revokedReason,updatedAt:revokedAt}).where(eq49(col13(reqApiKeysTable,"id"),keyId)),logger2.info("[API_KEYS] API key revoked",{keyId,userId,reason:revokedReason});let revoked=(await db.select().from(reqApiKeysTable).where(eq49(col13(reqApiKeysTable,"id"),keyId)).limit(1))[0]??{...record3,isActive:!1,revokedAt,revokedReason,updatedAt:revokedAt};return{isSuccess:!0,message:"API key revoked. It can no longer authenticate. This is a revoke, not a delete \u2014 "+`the record is retained as history and still reads at GET ${basePath}/${keyId}.`,data:sanitizeKeyForResponse(revoked)}}catch(err){return logger2.error("[API_KEYS] Failed to revoke API key",{error:err,keyId,userId}),set2.status=500,{isSuccess:!1,message:"Failed to revoke API key",data:null}}},revokeDetail=(summary)=>({tags:["API keys"],summary,description:"Revokes the key: it stops authenticating immediately, on this request. It is NOT a "+"delete \u2014 the row is retained as the audit record (`revokedAt`, `revokedReason`, "+"`usageCount`, `lastUsedAt`), so `GET /:id` still answers 200 with `isActive:false` and the list still carries it. The answer carries the revoked record. Revoking an "+"already-revoked key answers 400, not 404 \u2014 the row is still there. Revoking is "+"deliberately NOT blocked by `preventApiKeyManagement`: being unable to kill a leaked key because management is off would be worse than the rule it enforces."}),routePostIdRevoke=defineRoute({method:"POST",path:"/:id/revoke",detail:revokeDetail("Revoke an API key"),handler:(ctx)=>revokeApiKey(ctx)}),routeDeleteId=defineRoute({method:"DELETE",path:"/:id",detail:revokeDetail("Revoke an API key (alias of POST /:id/revoke)"),handler:(ctx)=>revokeApiKey(ctx)});mountRoutes(app,[{prefix:basePath,routes:[routePostRoot,routeGetRoot,routeGetId,routePatchId,routePostIdRevoke,routeDeleteId]}]),logger2.info(`[API_KEYS] Routes registered at ${basePath}`)}var col13=(table,name2)=>table[name2],getUserRoleNames=async(config,userId)=>{if(!config.db||!config.userRolesTable||!config.rolesTable)return[];return(await config.db.select({name:col13(config.rolesTable,"name")}).from(config.userRolesTable).innerJoin(config.rolesTable,eq49(col13(config.rolesTable,"id"),col13(config.userRolesTable,"roleId"))).where(eq49(col13(config.userRolesTable,"userId"),userId))).map((r)=>r.name).filter((n)=>n!==void 0)},getUserClaimActions=async(config,userId)=>{if(!config.db||!config.userRolesTable||!config.roleClaimsTable||!config.claimsTable)return[];let rows=await config.db.select({action:col13(config.claimsTable,"action")}).from(config.userRolesTable).innerJoin(config.roleClaimsTable,eq49(col13(config.roleClaimsTable,"roleId"),col13(config.userRolesTable,"roleId"))).innerJoin(config.claimsTable,eq49(col13(config.claimsTable,"id"),col13(config.roleClaimsTable,"claimId"))).where(eq49(col13(config.userRolesTable,"userId"),userId)),uniqueActions=new Set(rows.map((r)=>r.action));return Array.from(uniqueActions).filter((a)=>a!==void 0)},getUserScopes=async(config,userId)=>{if(!config.db||!config.userRolesTable||!config.roleClaimsTable)return[];let rows=await config.db.select({scope:col13(config.roleClaimsTable,"scope")}).from(config.userRolesTable).innerJoin(config.roleClaimsTable,eq49(col13(config.roleClaimsTable,"roleId"),col13(config.userRolesTable,"roleId"))).where(eq49(col13(config.userRolesTable,"userId"),userId)),uniqueScopes=new Set(rows.map((r)=>r.scope).filter((s)=>s!==null&&s!==void 0&&s.length>0));return Array.from(uniqueScopes)},sanitizeKeyForResponse=(record3)=>{let{keyHash:_keyHash,...safe}=record3;return safe};var init_apiKeys=__esm(()=>{init_ApiKey();init_GodminSetup();init_routeMount();init_utils5()});function createCaptchaRoutes(config){let{captchaService,logger:logger2,basePath="/auth/captcha"}=config;if(!captchaService.isEnabled())return collectRoutes();let warnedUnattributable=!1,warnUnattributableOnce=()=>{if(warnedUnattributable)return;warnedUnattributable=!0,logger2.warn("[CAPTCHA] No client address on the request \u2014 the per-IP generate limit is OFF. "+"Every caller would otherwise share one bucket, which locks out an entire office behind one egress. Configure the proxy or ingress to forward X-Forwarded-For.")},routeGetBasepathGenerate=defineRoute({method:"GET",path:"/generate",query:GenerateCaptchaQuerySchema,detail:{tags:["Captcha"],summary:"Generate Captcha",description:"Generate a new captcha challenge"},handler:async(ctx)=>{let{type:type4,difficulty}=ctx.query,ipAddress=ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||ctx.request.headers.get("x-real-ip")?.trim()||ctx.request.headers.get("cf-connecting-ip")?.trim()||void 0;if(!ipAddress)warnUnattributableOnce();let result=await captchaService.generate(type4,difficulty,ipAddress);if(result.rateLimited)return ctx.set.status=429,{success:!1,message:result.message??"Too many requests. Please try again later."};return logger2.info("[CAPTCHA] Challenge generated via endpoint",{challengeId:result.challengeId,type:result.type,ipAddress}),{success:!0,data:{challengeId:result.challengeId,type:result.type,question:result.question,expiresAt:result.expiresAt,...result.imageData?{imageData:result.imageData}:{},...result.puzzleData?{puzzleData:{pieces:result.puzzleData.pieces}}:{}}}}}),routePostBasepathValidate=defineRoute({method:"POST",path:"/validate",body:ValidateCaptchaBodySchema,detail:{tags:["Captcha"],summary:"Validate Captcha",description:"Validate a captcha answer"},handler:async(ctx)=>{let{challengeId,answer}=ctx.body,result=await captchaService.validate(challengeId,answer);if(!result.valid)ctx.set.status=400;return result}});return collectRoutes([{prefix:basePath,routes:[routeGetBasepathGenerate,routePostBasepathValidate]}])}var GenerateCaptchaQuerySchema,ValidateCaptchaBodySchema;var init_captcha=__esm(()=>{init_esm3();init_routeCollector();GenerateCaptchaQuerySchema=Type.Object({type:Type.Optional(Type.Union([Type.Literal("math"),Type.Literal("image"),Type.Literal("puzzle"),Type.Literal("text")])),difficulty:Type.Optional(Type.Union([Type.Literal("easy"),Type.Literal("medium"),Type.Literal("hard")]))}),ValidateCaptchaBodySchema=Type.Object({challengeId:Type.String(),answer:Type.String()})});import{eq as eq50}from"drizzle-orm";function createCheckRoute(config,checkConfig){let{db,logger:logger2}=config,route=checkConfig.route||"/auth/check",routePostCheck=defineRoute({method:"POST",path:"/",detail:{tags:["Auth"],summary:"Check authorization for a given entity and method",description:"Allows consumer/resource servers to perform full claim checks (including scope) against the IDP's authorization system."},handler:async(ctx)=>{let{body,request,set:set2}=ctx,userId=request.headers.get("x-user-id");if(!userId)return set2.status=401,{isSuccess:!1,message:"Unauthenticated",data:null};if(!db)return set2.status=503,{isSuccess:!1,message:"Authorization service unavailable",data:null};let{schemaTables}=resolveAuthTablesForRequest(request,config);if(!schemaTables)return set2.status=503,{isSuccess:!1,message:"Authorization tables not available",data:null};let{entity,method,fields,relations}=body;if(!entity||!method)return set2.status=400,{isSuccess:!1,message:"entity and method are required",data:null};let result=await checkAuthorization({userId,method,entity,requestedFields:fields,requestedRelations:relations,db,schemaTables,logger:logger2,getUserData:async()=>{let usersTbl=schemaTables[AUTHORIZATION_TABLE_KEYS.users];if(!usersTbl||!db)return;let idCol=usersTbl.id;if(!idCol)return;return(await db.select().from(usersTbl).where(eq50(idCol,userId)).limit(1))[0]}});return{isSuccess:!0,message:result.authorized?"Authorized":"Forbidden",data:{authorized:result.authorized,reason:result.reason,allowedFields:result.allowedFields,allowedRelations:result.allowedRelations,scopeFilters:result.scopeFilters}}}});return collectRoutes([{prefix:route,routes:[routePostCheck]}])}var init_check=__esm(()=>{init_Authorization();init_routeCollector()});import{sql as sql11}from"drizzle-orm";function createEmailVerificationRoutes(config){let{authConfig,registerConfig,getEmailService,appName}=config,{db,logger:logger2}=authConfig;if(!registerConfig.enabled||!registerConfig.emailVerification?.enabled)return collectRoutes();let verifyRoute="/verify-email",resendRoute="/resend-verification",routeGetVerifyroute=defineRoute({method:"GET",path:"/",query:Type.Object({token:Type.String()}),detail:{tags:["Authentication"],summary:"Verify Email",description:"Verify user email address using the verification token"},handler:async(ctx)=>{let{usersTable}=resolveAuthTablesForRequest(ctx.request,authConfig);if(!db||!usersTable)return ctx.set.status=500,{success:!1,message:"Database not configured"};let token=ctx.query.token;if(!token)return ctx.set.status=400,{success:!1,message:"Verification token is required"};let users=await db.select().from(usersTable).where(sql11`email_verification_token = ${hashVerificationToken(token)}`).limit(1);if(users.length===0)return logger2.warn("[AUTH] Email verification failed - invalid token"),ctx.set.status=400,{success:!1,message:"Invalid or expired verification token"};let user=users[0],tokenExpiresAt=user.emailVerificationTokenExpiresAt;if(tokenExpiresAt&&new Date>new Date(tokenExpiresAt))return logger2.warn("[AUTH] Email verification failed - token expired",{userId:user.id,email:user.email}),ctx.set.status=400,{success:!1,message:"Verification token has expired. Please request a new one."};await db.update(usersTable).set({verifiedAt:new Date,emailVerificationToken:null,emailVerificationTokenExpiresAt:null}).where(sql11`id = ${user.id}`),logger2.info("[AUTH] Email verified successfully",{userId:user.id,email:user.email});let emailService=getEmailService?.()??null;if(registerConfig.emailVerification?.templates?.welcome?.enabled&&emailService?.isAvailable())emailService.sendWelcomeEmail(user.email,user.email.split("@")[0]||"User",appName||"Nucleus").catch((err)=>{logger2.error("[AUTH] Failed to send welcome email after verification",{email:user.email,error:err})});return{success:!0,message:"Email verified successfully. You can now log in.",data:{redirectUrl:registerConfig.emailVerification?.redirectUrl||"/login",verified:!0}}}}),routePostResendroute=defineRoute({method:"POST",path:"/",body:Type.Object({email:Type.String({format:"email"})}),detail:{tags:["Authentication"],summary:"Resend Verification Email",description:"Resend the email verification link to the user"},handler:async(ctx)=>{let{usersTable}=resolveAuthTablesForRequest(ctx.request,authConfig);if(!db||!usersTable)return ctx.set.status=500,{success:!1,message:"Database not configured"};let emailService=getEmailService?.()??null;if(!emailService?.isAvailable())return ctx.set.status=503,{success:!1,message:"Email service not available"};let{email}=ctx.body,antiEnum=authConfig.authentication?.preventUserEnumeration===!0,uniformResend={success:!0,message:"If your email is registered and not yet verified, a verification email has been sent."},users=await db.select().from(usersTable).where(sql11`email = ${email}`).limit(1);if(users.length===0)return antiEnum?uniformResend:{success:!0,message:"If your email is registered, you will receive a verification email."};let user=users[0];if(user.verifiedAt){if(antiEnum)return uniformResend;return ctx.set.status=409,{success:!1,message:"Email is already verified"}}let maxAttempts=registerConfig.emailVerification?.maxResendAttempts||3,currentAttempts=user.emailVerificationAttempts||0;if(currentAttempts>=maxAttempts){if(logger2.warn("[AUTH] Resend verification failed - max attempts reached",{email,attempts:currentAttempts}),antiEnum)return uniformResend;return ctx.set.status=429,{success:!1,message:"Maximum resend attempts reached. Please contact support.",data:{maxAttemptsReached:!0,attemptsUsed:currentAttempts,maxAttempts}}}let resendCooldown=registerConfig.emailVerification?.resendCooldown||"60s",cooldownMs=parseTimeToMs(resendCooldown),lastSentAt=user.emailVerificationSentAt;if(lastSentAt){let timeSinceLastSent=Date.now()-new Date(lastSentAt).getTime();if(timeSinceLastSent<cooldownMs){let waitSeconds=Math.ceil((cooldownMs-timeSinceLastSent)/1000);if(antiEnum)return uniformResend;return ctx.set.status=429,{success:!1,message:`Please wait ${waitSeconds} seconds before requesting another verification email.`,data:{cooldownRemaining:waitSeconds,canResendAt:new Date(Date.now()+waitSeconds*1000).toISOString()}}}}let verificationToken=generateVerificationToken(),tokenExpiresIn=registerConfig.emailVerification?.tokenExpiresIn||"24h",tokenExpiresAt=new Date(Date.now()+parseTimeToMs(tokenExpiresIn));await db.update(usersTable).set({emailVerificationToken:hashVerificationToken(verificationToken),emailVerificationTokenExpiresAt:tokenExpiresAt,emailVerificationSentAt:new Date,emailVerificationAttempts:currentAttempts+1}).where(sql11`id = ${user.id}`);let verificationLink=buildEmailActionLink({request:ctx.request,configuredUrl:registerConfig.emailVerification?.redirectUrl,path:"/verify-email",query:{token:verificationToken},allowedOrigins:authConfig.authentication?.trustedAppOrigins}),result=await emailService.sendVerificationEmail(email,email.split("@")[0]||"User",verificationLink,appName||"Nucleus");if(result.success){logger2.info("[AUTH] Verification email resent",{email});let cooldownSeconds=Math.ceil(cooldownMs/1000);if(antiEnum)return uniformResend;return{success:!0,message:"Verification email sent. Please check your inbox.",data:{cooldownSeconds,canResendAt:new Date(Date.now()+cooldownMs).toISOString(),attemptsRemaining:maxAttempts-(currentAttempts+1)}}}if(logger2.error("[AUTH] Failed to resend verification email",{email,error:result.error}),antiEnum)return uniformResend;return{success:!1,message:"Failed to send verification email. Please try again later."}}});return collectRoutes([{prefix:verifyRoute,routes:[routeGetVerifyroute]},{prefix:resendRoute,routes:[routePostResendroute]}])}var init_emailVerification=__esm(()=>{init_esm3();init_routeCollector();init_utils6()});import crypto7 from"crypto";function generateMagicToken(){return crypto7.randomBytes(32).toString("hex")}function hashToken(token,purpose){let input=purpose?`${purpose}:${token}`:token;return crypto7.createHash("sha256").update(input).digest("hex")}function parseTimeToMs3(time2){let match=time2.match(/^(\d+)(s|m|h|d)$/);if(!match?.[1]||!match[2])return 900000;let value2=parseInt(match[1],10);switch(match[2]){case"s":return value2*1000;case"m":return value2*60*1000;case"h":return value2*60*60*1000;case"d":return value2*24*60*60*1000;default:return 900000}}var TOKEN_PURPOSE;var init_utils8=__esm(()=>{TOKEN_PURPOSE={magicLink:"magic-link",invite:"invite"}});import{eq as eq51}from"drizzle-orm";function createInviteRoute(config,inviteConfig,getEmailService,storeMagicToken,appName,getMagicToken){let{db,logger:logger2}=config,route=inviteConfig.route||"/auth/invite",expiresIn=inviteConfig.tokenExpiresIn||"7d",redirectUrl=inviteConfig.redirectUrl||"";if(!inviteConfig.enabled)return collectRoutes();let declared=[],routePostRoute=defineRoute({method:"POST",path:"/",body:InviteBodySchema,detail:{tags:["Authentication"],summary:"Invite User",description:"Invite a new user by sending them an email with a magic link to set their password"},handler:async(ctx)=>{let resolved=resolveAuthTablesForRequest(ctx.request,config),{usersTable}=resolved;if(!db||!usersTable)return ctx.set.status=500,{success:!1,message:"Database not configured"};let emailService=getEmailService?.();if(!emailService?.isAvailable())return logger2.error("[AUTH] Invite requested but email service not available"),ctx.set.status=503,{success:!1,message:"Email service not available"};let{email:rawEmail}=ctx.body,email=rawEmail.toLowerCase(),antiEnum=config.authentication?.preventUserEnumeration===!0,uniformInvite={success:!0,message:"If the email address is eligible, an invitation has been sent."},existingUsers=await db.select().from(usersTable).where(eq51(usersTable.email,email)).limit(1),userId;if(existingUsers.length>0){let existingUser=existingUsers[0];if(existingUser.verifiedAt||existingUser.password){if(logger2.warn("[AUTH] Invite failed - user already verified",{email}),antiEnum)return uniformInvite;return ctx.set.status=409,{success:!1,message:"User with this email is already verified"}}userId=existingUser.id,logger2.info("[AUTH] Resending invitation to existing unverified user",{userId,email})}else{let insertValues={email,password:null,emailVerified:!1,isLocked:!1,createdAt:new Date,updatedAt:new Date},newUser=(await db.insert(usersTable).values(insertValues).returning())[0];if(!newUser)return logger2.error("[AUTH] Failed to create invited user",{email}),ctx.set.status=500,{success:!1,message:"Failed to create user"};userId=newUser.id,logger2.info("[AUTH] Invited user created",{userId,email});let inviteDefaultRoles=resolveDefaultRoleNames(config.authentication);if(inviteDefaultRoles.length>0)await assignDefaultRoles({db,userId,roleNames:inviteDefaultRoles,rolesTable:resolved.rolesTable??config.rolesTable??null,userRolesTable:resolved.userRolesTable??config.userRolesTable??null,logger:logger2})}let token=generateMagicToken(),tokenHash=hashToken(token,TOKEN_PURPOSE.invite),expiresAt=new Date(Date.now()+parseTimeToMs3(expiresIn)),reqSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0;await storeMagicToken({userId,email,tokenHash,expiresAt},reqSchemaName);let inviteLink=redirectUrl?buildEmailActionLink({request:ctx.request,configuredUrl:redirectUrl,path:extractConfiguredPath(redirectUrl,"/set-password"),query:{token,invite:"true"},allowedOrigins:config.authentication?.trustedAppOrigins}):`/set-password?token=${token}&invite=true`,sendResult=await emailService.sendInvitationEmail(email,inviteLink,appName||"Nucleus");if(!sendResult.success){if(logger2.error("[AUTH] Failed to send invitation email",{email,error:sendResult.error}),antiEnum)return uniformInvite;return{success:!0,message:"User created but failed to send invitation email",data:{id:userId,email}}}if(logger2.info("[AUTH] Invitation email sent",{email,userId}),antiEnum)return uniformInvite;return{success:!0,message:"Invitation sent successfully",data:{id:userId,email}}}});if(declared.push(routePostRoute),getMagicToken){let routePostRouteVerify=defineRoute({method:"POST",path:"/verify",body:Type.Object({token:Type.String()}),detail:{tags:["Authentication"],summary:"Verify Invite Token",description:"Validate an invite token without consuming it. Returns user info if valid."},handler:async(ctx)=>{let{token}=ctx.body;if(!token)return ctx.set.status=400,{success:!1,message:"Token is required"};let tokenHash=hashToken(token,TOKEN_PURPOSE.invite),invSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0,storedToken=await getMagicToken(tokenHash,invSchemaName);if(!storedToken)return logger2.warn("[AUTH] Invalid invite verify token"),ctx.set.status=400,{success:!1,message:"Invalid or expired token"};if(new Date>storedToken.expiresAt)return logger2.warn("[AUTH] Expired invite verify token",{email:storedToken.email}),ctx.set.status=400,{success:!1,message:"Invalid or expired token"};return logger2.info("[AUTH] Invite token verified (not consumed)",{email:storedToken.email,userId:storedToken.userId}),{success:!0,data:{userId:storedToken.userId,email:storedToken.email}}}});declared.push(routePostRouteVerify)}return collectRoutes([{prefix:route,routes:declared}])}var InviteBodySchema;var init_invite=__esm(()=>{init_esm3();init_routeCollector();init_assignDefaultRole();init_utils8();InviteBodySchema=Type.Object({email:Type.String({format:"email"})})});var exports_DeviceTrust={};__export(exports_DeviceTrust,{selectUsableDeviceRow:()=>selectUsableDeviceRow,readDeviceTokenFromCookieHeader:()=>readDeviceTokenFromCookieHeader,mintDeviceToken:()=>mintDeviceToken,isDeviceRowUsable:()=>isDeviceRowUsable,isApprovalMethod:()=>isApprovalMethod,hashDeviceToken:()=>hashDeviceToken,deviceCookieName:()=>deviceCookieName,decideDeviceApproval:()=>decideDeviceApproval,buildDeviceTokenCookie:()=>buildDeviceTokenCookie,buildClearedDeviceTokenCookie:()=>buildClearedDeviceTokenCookie,DEVICE_TOKEN_BYTES:()=>DEVICE_TOKEN_BYTES});import{createHash as createHash5,randomBytes as randomBytes6}from"crypto";function hashDeviceToken(raw){return createHash5("sha256").update(raw).digest("hex")}function mintDeviceToken(){let raw=randomBytes6(DEVICE_TOKEN_BYTES).toString("hex");return{raw,hash:hashDeviceToken(raw)}}function isDeviceRowUsable(row,now){if(!row||row.status!=="trusted")return!1;if((row.revoked_at??row.revokedAt)!=null)return!1;let expRaw=row.expires_at??row.expiresAt;if(expRaw==null)return!1;let exp=expRaw instanceof Date?expRaw:new Date(expRaw);if(Number.isNaN(exp.getTime())||exp<=now)return!1;return!0}function selectUsableDeviceRow(rows,expectedUserId,now){if(!expectedUserId)return null;for(let row of rows??[]){if((row.user_id??row.userId)!==expectedUserId)continue;if(isDeviceRowUsable(row,now))return row}return null}function buildDeviceTokenCookie(raw,opts){let prefix=opts.cookiePrefix??"",name2=`${prefix}${opts.cookieName??"device_token"}`,sameSite=opts.sameSite??"Lax",secure=prefix==="__Secure-"||prefix==="__Host-"||opts.secure!==!1,allowDomain=prefix!=="__Host-",parts=[`${name2}=${raw}`,"Path=/","HttpOnly",`SameSite=${sameSite}`];if(secure)parts.push("Secure");if(allowDomain&&opts.domain)parts.push(`Domain=${opts.domain}`);return parts.push(`Max-Age=${Math.max(0,Math.floor(opts.maxAgeSeconds))}`),parts.join("; ")}function buildClearedDeviceTokenCookie(opts){return buildDeviceTokenCookie("",{...opts,maxAgeSeconds:0})}function decideDeviceApproval(params){let NO_APPROVAL={requiresApproval:!1,deny:!1,mintToken:!1,trustImmediately:!1},TRUST_NOW={requiresApproval:!1,deny:!1,mintToken:!0,trustImmediately:!0};if(params.knownDevice)return NO_APPROVAL;if(params.isImpersonation||!isApprovalMethod(params.loginMethod,params.approvalMethods))return NO_APPROVAL;if(!params.hasValidFingerprint&&params.onUnidentifiable==="deny")return{requiresApproval:!1,deny:!0,mintToken:!1,trustImmediately:!1};if(params.trustNewDevices!==!1||params.isEmailExempt||params.isFirstEverDevice)return TRUST_NOW;return{requiresApproval:!0,deny:!1,mintToken:!0,trustImmediately:!1}}function deviceCookieName(cfg){return`${cfg?.cookiePrefix??""}${cfg?.cookieName??"device_token"}`}function readDeviceTokenFromCookieHeader(cookieHeader,cfg){if(!cookieHeader)return;let name2=deviceCookieName(cfg);for(let part of cookieHeader.split(";")){let eq52=part.indexOf("=");if(eq52<0)continue;if(part.slice(0,eq52).trim()===name2)return part.slice(eq52+1).trim()}return}function isApprovalMethod(method,approvalMethods){if(!method)return!1;for(let m of approvalMethods){if(m===method)return!0;if(m.endsWith(":*")&&method.startsWith(m.slice(0,-1)))return!0}return!1}var DEVICE_TOKEN_BYTES=32;var init_DeviceTrust=()=>{};import{createHash as createHash6}from"crypto";function hashRefreshToken(token){return createHash6("sha256").update(token).digest("hex")}function classifyRefreshToken(input){let grace=input.graceSeconds??DEFAULT_REUSE_GRACE_SECONDS;if(!input.currentHash)return{action:"adopt"};if(input.presentedHash===input.currentHash)return{action:"rotate"};if(input.previousHash&&input.presentedHash===input.previousHash){let rotatedAt=input.previousRotatedAt?Date.parse(input.previousRotatedAt):Number.NaN;if(Number.isFinite(rotatedAt)&&input.nowMs-rotatedAt<=grace*1000)return{action:"grace"};return{action:"revoke",reason:"refresh token was superseded and is being replayed outside the grace window"}}return{action:"revoke",reason:"refresh token does not match this session, current or previous"}}var DEFAULT_REUSE_GRACE_SECONDS=30;var init_reuseDetection=()=>{};async function recordIssuedRefreshToken(sessionId,refreshToken,options){if(!sessionId||!refreshToken)return;try{let result=await updateSession({sessionId,updates:{refreshTokenHash:hashRefreshToken(refreshToken),...options?.previousHash?{previousRefreshTokenHash:options.previousHash,previousRefreshTokenAt:new Date().toISOString()}:{}}});if(!result.success)logger.warn("[AUTH] Could not record the issued refresh token; reuse detection is off",{sessionId,error:result.error})}catch(error3){logger.warn("[AUTH] Could not record the issued refresh token; reuse detection is off",{sessionId,error:error3 instanceof Error?error3.message:String(error3)})}}var init_recordIssued=__esm(()=>{init_Update();init_Logger2();init_reuseDetection()});async function resolveUserIdByIdentifier(input){let value2=input.value.trim();if(value2==="")return null;for(let source of input.sources){if(!source.table||!source.column||!source.userColumn)continue;let found=(await input.lookup(source,value2)).filter((id)=>String(id??"").trim()!=="");if(found.length>1)return null;if(found.length===1)return String(found[0])}return null}function looksLikeAlternateIdentifier(value2){let trimmed=value2.trim();return trimmed!==""&&!trimmed.includes("@")}var init_identifier=()=>{};import{eq as eq52}from"drizzle-orm";async function lookupIdentifier(db,schemaTables,source,value2){let table=pickByName(schemaTables,source.table);if(!table)return[];let column=pickByName(table,source.column),userColumn=pickByName(table,source.userColumn);if(!column||!userColumn)return[];return(await db.select({userId:userColumn}).from(table).where(eq52(column,value2)).limit(2)).map((row)=>String(row.userId??""))}var init_identifierQuery=__esm(()=>{init_identifier()});var LoginBodySchema,LoginResponseSchema;var init_types12=__esm(()=>{init_esm3();LoginBodySchema=Type.Object({email:Type.String({minLength:1}),password:Type.String({minLength:1}),rememberMe:Type.Optional(Type.Boolean()),captchaId:Type.Optional(Type.String()),captchaAnswer:Type.Optional(Type.String()),deviceHint:Type.Optional(Type.String())}),LoginResponseSchema=Type.Object({success:Type.Boolean(),message:Type.Optional(Type.String()),data:Type.Optional(Type.Object({user:Type.Object({id:Type.String(),email:Type.String()}),accessToken:Type.String(),refreshToken:Type.String()}))})});import{eq as eq53}from"drizzle-orm";function createLoginRoute(config,loginConfig,signAccessToken,signRefreshToken,createSession,saveSessionToDb,cookieConfig,tokenResponseConfig,captchaService){let{db,logger:logger2}=config,log4=logger2.scoped(AUTH_LOGIN),route=loginConfig.route||"/auth/login",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};if(!loginConfig.enabled)return collectRoutes();let identifierSources=loginConfig.identifierSources??[],rememberMeAllowed=loginConfig.rememberMe!==!1,routePostRoute=defineRoute({method:"POST",path:"/",body:LoginBodySchema,detail:{tags:["Authentication"],summary:"Login",description:"Authenticate user with email and password"},handler: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,rememberMe:rememberMeRequested,captchaId,captchaAnswer,deviceHint}=ctx.body,rememberMe=rememberMeAllowed?rememberMeRequested:!1,email=rawEmail.toLowerCase(),user=(await db.select().from(usersTable).where(eq53(usersTable.email,email)).limit(1))[0];if(!user&&identifierSources.length>0&&looksLikeAlternateIdentifier(rawEmail)){let resolvedId=await resolveUserIdByIdentifier({sources:identifierSources,value:rawEmail,lookup:(source,value2)=>lookupIdentifier(db,resolved.schemaTables,source,value2)});if(resolvedId)user=(await db.select().from(usersTable).where(eq53(usersTable.id,resolvedId)).limit(1))[0]}let captchaEnabledForLogin=loginConfig.captcha?.enabled!==!1,captchaThreshold=loginConfig.captcha?.requireAfterFailures,currentFailedAttempts=user?.failedLoginAttempts||0,captchaRequired=Boolean(captchaService?.isEnabled())&&captchaEnabledForLogin&&(captchaThreshold===void 0||currentFailedAttempts>=captchaThreshold);if(captchaRequired&&captchaService){if(!captchaId||!captchaAnswer)return ctx.set.status=400,{success:!1,message:"Captcha is required",requiresCaptcha:!0};let captchaResult=await captchaService.validate(captchaId,captchaAnswer);if(!captchaResult.valid)return ctx.set.status=400,{success:!1,message:captchaResult.message||"Invalid captcha",attemptsRemaining:captchaResult.attemptsRemaining,requiresCaptcha:!0}}let reqUrl=new URL(ctx.request.url),auditIp=ctx.request.headers.get("x-client-ip")?.trim()||"unknown",auditUa=ctx.request.headers.get("user-agent")||"unknown";if(!user)return await verifyPassword(password4,await getTimingEqualizerHash()).catch(()=>!1),log4.warn("Login failed - user not found",{email}),await logger2.audit({entityName:"users",operation:"LOGIN_FAILED",summary:`Login failed: user not found (${email})`,ipAddress:auditIp,userAgent:auditUa,path:reqUrl.pathname,query:reqUrl.search}),ctx.set.status=401,{success:!1,message:"Invalid email or password",requiresCaptcha:captchaRequired};if(user.isActive===!1)return log4.warn("Login failed - account inactive",{email,userId:user.id}),ctx.set.status=403,{success:!1,message:"Account is not active"};if(user.isLocked){let lockedUntil=user.lockedUntil;if(lockedUntil&&new Date(lockedUntil)<new Date)await db.update(usersTable).set({isLocked:!1,lockedUntil:null,failedLoginAttempts:0}).where(eq53(usersTable.id,user.id)),log4.info("Account lock expired, auto-unlocked",{userId:user.id,email});else return log4.warn("Login failed - account locked",{email,userId:user.id}),await logger2.audit({entityName:"users",entityId:user.id,operation:"LOGIN_FAILED",userId:user.id,summary:`Login failed: account locked (${email})`,ipAddress:auditIp,userAgent:auditUa,path:reqUrl.pathname,query:reqUrl.search}),ctx.set.status=403,{success:!1,message:"Account is locked"}}let userCohortId=user.cohortId;if(userCohortId&&db){let cohortsTable=resolved.schemaTables?.userCohorts??resolved.schemaTables?.user_cohorts;if(cohortsTable){let cohortExpiresAt=(await db.select().from(cohortsTable).where(eq53(cohortsTable.id,userCohortId)).limit(1))[0]?.expiresAt;if(cohortExpiresAt&&new Date(cohortExpiresAt)<new Date)return log4.warn("Login failed - cohort expired",{email,userId:user.id,cohortId:userCohortId,expiresAt:cohortExpiresAt}),await logger2.audit({entityName:"users",entityId:user.id,operation:"LOGIN_FAILED",userId:user.id,summary:`Login failed: cohort expired (${email})`,ipAddress:auditIp,userAgent:auditUa,path:reqUrl.pathname,query:reqUrl.search}),ctx.set.status=403,{success:!1,message:"Your access has expired. Please contact your administrator."}}}if(!await verifyPassword(password4,user.password)){let failedAttempts=(user.failedLoginAttempts||0)+1;await db.update(usersTable).set({failedLoginAttempts:failedAttempts,isLocked:failedAttempts>=5,lockedUntil:failedAttempts>=5?new Date(Date.now()+1800000):null}).where(eq53(usersTable.id,user.id)),log4.warn("Login failed - invalid password",{email,failedAttempts}),await logger2.audit({entityName:"users",entityId:user.id,operation:"LOGIN_FAILED",userId:user.id,summary:`Login failed: invalid password (${email}, attempt ${failedAttempts})`,ipAddress:auditIp,userAgent:auditUa,path:reqUrl.pathname,query:reqUrl.search});let captchaWillBeRequired=Boolean(captchaService?.isEnabled())&&captchaEnabledForLogin&&(captchaThreshold===void 0||failedAttempts>=captchaThreshold);return ctx.set.status=401,{success:!1,message:"Invalid email or password",requiresCaptcha:captchaWillBeRequired}}await db.update(usersTable).set({failedLoginAttempts:0,lastLoginAt:new Date,loginCount:(user.loginCount||0)+1}).where(eq53(usersTable.id,user.id));let ipAddress=ctx.request.headers.get("x-client-ip")?.trim()||"127.0.0.1",isPrivateOrLoopback=(ip)=>!ip||ip==="127.0.0.1"||ip==="::1"||ip==="localhost"||ip.startsWith("10.")||ip.startsWith("192.168.")||ip.startsWith("172."),userAgent=ctx.request.headers.get("user-agent")||"Unknown Browser";log4.info("Parsed device info",{ipAddress,userAgent});let deviceInfo=parseUserAgentForLogin(userAgent,ipAddress,deviceHint,deviceContextFromRequest(ctx.request).clientHints);if(!isPrivateOrLoopback(ipAddress)){let abortCtrl=new AbortController,geoTimeout=setTimeout(()=>abortCtrl.abort(),2000);fetch(`https://ip-api.com/json/${ipAddress}?fields=country,city`,{signal:abortCtrl.signal}).then(async(res)=>{if(clearTimeout(geoTimeout),!res.ok)return;let geo=await res.json();if(geo.country)deviceInfo.locationCountry=geo.country;if(geo.city)deviceInfo.locationCity=geo.city}).catch(()=>{clearTimeout(geoTimeout)})}let userRoles=[],userClaims=[],userClaimScopes={};if(db)try{let{fetchUserRolesAndClaims:fetchUserRolesAndClaims2}=await Promise.resolve().then(() => (init_fetchUserRolesAndClaims(),exports_fetchUserRolesAndClaims)),rc=await fetchUserRolesAndClaims2(db,user.id,resolved);userRoles=rc.roles,userClaims=rc.claims,userClaimScopes=rc.claimScopes}catch(roleError){log4.error("Failed to fetch user roles/claims for JWT",{userId:user.id,error:roleError instanceof Error?roleError.message:String(roleError),stack:roleError instanceof Error?roleError.stack:void 0})}let requestOrigin=ctx.request.headers.get("origin")||ctx.request.headers.get("referer")?.replace(/\/[^/]*$/,"")||void 0,sessionParams={userId:user.id,deviceInfo,loginMethod:"password",rememberMe,requestOrigin},sessionId=await createSession(sessionParams),accessToken=signAccessToken(user.id,userRoles.length>0?userRoles:void 0,userClaims.length>0?userClaims:void 0,ctx.request.headers.get("x-tenant-schema")??void 0,userClaimScopes,sessionId),refreshToken=signRefreshToken(user.id,rememberMe,sessionId);await recordIssuedRefreshToken(sessionId,refreshToken);let requiresApproval=!1,pendingSessionId=sessionId,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);let saveResult=await saveSessionToDb(sessionId,sessionParams,reqSchemaName);if(saveResult?.denied)return log4.warn("Login blocked by device-trust (unidentifiable device)",{userId:user.id}),ctx.set.status=403,{success:!1,message:"This device could not be recognized. Please contact support."};if(deviceTokenCookie=saveResult?.deviceTokenCookie,saveResult?.requiresApproval){if(requiresApproval=!0,saveResult.sessionId)pendingSessionId=saveResult.sessionId}}if(requiresApproval){ctx.set.status=202;let approvalHeaders={"Content-Type":"application/json"};if(deviceTokenCookie)approvalHeaders["Set-Cookie"]=deviceTokenCookie;return new Response(JSON.stringify({success:!1,requiresApproval:!0,sessionId:pendingSessionId,message:"Login requires approval. Please check your email to approve this device."}),{status:202,headers:approvalHeaders})}log4.info("Login successful",{userId:user.id,email,rememberMe}),await logger2.audit({entityName:"users",entityId:user.id,operation:"LOGIN",userId:user.id,summary:`${email} logged in successfully`,ipAddress,userAgent,path:reqUrl.pathname,query:reqUrl.search});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);ctx.set.headers["x-session-id"]=sessionId;let responseData={user:{id:user.id,email:user.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(withEnvelopeFlags({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 collectRoutes([{prefix:route,routes:[routePostRoute]}])}var dummyHashPromise=null,getTimingEqualizerHash=()=>{if(!dummyHashPromise)dummyHashPromise=hashPassword("nucleus-login-timing-equalizer");return dummyHashPromise};var init_login=__esm(()=>{init_routeCollector();init_response();init_DeviceTrust();init_scopes();init_recordIssued();init_utils6();init_identifier();init_identifierQuery();init_types12();init_deviceInfo();init_utils7();init_types12()});function clearAuthCookies(cookieConfig){let path7=cookieConfig?.path||"/",sameSite=cookieConfig?.sameSite||"Strict",securePart=cookieConfig?.secure!==!1?"; Secure":"",domainPart=cookieConfig?.domain?`; Domain=${cookieConfig.domain}`:"",accessName=cookieConfig?.accessTokenName||"access_token",refreshName=cookieConfig?.refreshTokenName||"refresh_token",sessionName=cookieConfig?.sessionTokenName||"session_token",clearOpts=`; Path=${path7}; HttpOnly; SameSite=${sameSite}${securePart}${domainPart}; Max-Age=0`;return[`${accessName}=${clearOpts}`,`${refreshName}=${clearOpts}`,`${sessionName}=${clearOpts}`,...IMPERSONATION_COOKIE_NAMES.map((name2)=>`${name2}=${clearOpts}`)]}var init_utils9=__esm(()=>{init_impersonationCookies()});var LogoutResponseSchema;var init_types13=__esm(()=>{init_esm3();LogoutResponseSchema=Type.Object({success:Type.Boolean(),message:Type.Optional(Type.String())})});function createLogoutRoute(config,logoutConfig,destroySession,revokeSessionInDb,cookieConfig){let{logger:logger2}=config,route=logoutConfig.route||"/auth/logout";if(!logoutConfig.enabled)return collectRoutes();let routePostRoute=defineRoute({method:"POST",path:"/",detail:{tags:["Authentication"],summary:"Logout",description:"Logout and invalidate session"},handler:async(ctx)=>{let sessionCookieName=cookieConfig?.sessionTokenName||"session_token",fromCookie=(ctx.request.headers.get("cookie")??"").split(";").map((one)=>one.trim()).find((one)=>one.startsWith(`${sessionCookieName}=`))?.slice(sessionCookieName.length+1),sessionId=ctx.request.headers.get("x-session-id")||fromCookie||null,userId=ctx.request.headers.get("x-user-id");if(sessionId){if(await destroySession(sessionId),revokeSessionInDb){let reqSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0;await revokeSessionInDb(sessionId,"user_logout",reqSchemaName)}}let clearCookies=clearAuthCookies(cookieConfig),responseHeaders=new Headers;responseHeaders.set("Content-Type","application/json");for(let cookie of clearCookies)responseHeaders.append("Set-Cookie",cookie);logger2.info("[AUTH] Logout successful",{userId,sessionId});let reqUrl=new URL(ctx.request.url);logger2.audit({entityName:"users",entityId:userId||void 0,operation:"LOGOUT",userId:userId||void 0,summary:`User logged out${sessionId?` (session: ${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});let jsonBody=JSON.stringify(withEnvelopeFlags({success:!0,message:"Logged out successfully"}));return new Response(jsonBody,{status:200,headers:responseHeaders})}});return collectRoutes([{prefix:route,routes:[routePostRoute]}])}var init_logout=__esm(()=>{init_response();init_routeCollector();init_utils9();init_types13()});var MagicLinkRequestSchema,MagicLinkVerifySchema,MagicLinkResponseSchema,MagicLinkVerifyResponseSchema;var init_types14=__esm(()=>{init_esm3();MagicLinkRequestSchema=Type.Object({email:Type.String({format:"email"})}),MagicLinkVerifySchema=Type.Object({token:Type.String()}),MagicLinkResponseSchema=Type.Object({success:Type.Boolean(),message:Type.Optional(Type.String())}),MagicLinkVerifyResponseSchema=Type.Object({success:Type.Boolean(),message:Type.Optional(Type.String()),data:Type.Optional(Type.Object({user:Type.Object({id:Type.String(),email:Type.String()}),accessToken:Type.String(),refreshToken:Type.String()}))})});import{eq as eq54}from"drizzle-orm";function createMagicLinkRoute(config,magicLinkConfig,getEmailService,signAccessToken,signRefreshToken,createSession,storeMagicToken,getMagicToken,deleteMagicToken,appName,cookieConfig,saveSessionToDb){let{db,logger:logger2}=config,requestRoute=magicLinkConfig.route||"/auth/magic-link",verifyRoute=magicLinkConfig.verifyRoute||"/auth/magic-link/verify",expiresIn=magicLinkConfig.expiresIn||"15m",redirectUrl=magicLinkConfig.redirectUrl||"";if(!magicLinkConfig.enabled)return collectRoutes();let routePostRequestroute=defineRoute({method:"POST",path:"/",body:MagicLinkRequestSchema,detail:{tags:["Authentication"],summary:"Request Magic Link",description:"Send a magic link to the user's email for passwordless login"},handler:async(ctx)=>{let{usersTable}=resolveAuthTablesForRequest(ctx.request,config);if(!db||!usersTable)return ctx.set.status=500,{success:!1,message:"Database not configured"};let emailService=getEmailService?.();if(!emailService?.isAvailable())return logger2.error("[AUTH] Magic link requested but email service not available"),ctx.set.status=503,{success:!1,message:"Email service not available"};let{email:rawEmail}=ctx.body,email=rawEmail.toLowerCase(),user=(await db.select().from(usersTable).where(eq54(usersTable.email,email)).limit(1))[0];if(!user)return logger2.info("[AUTH] Magic link requested for non-existent email",{email}),{success:!0,message:"If an account exists, a magic link has been sent"};if(user.isLocked)return logger2.warn("[AUTH] Magic link requested for locked account",{email}),{success:!0,message:"If an account exists, a magic link has been sent"};let token=generateMagicToken(),tokenHash=hashToken(token,TOKEN_PURPOSE.magicLink),expiresAt=new Date(Date.now()+parseTimeToMs3(expiresIn)),reqSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0;await storeMagicToken({userId:user.id,email,tokenHash,expiresAt},reqSchemaName);let magicLink=redirectUrl?buildEmailActionLink({request:ctx.request,configuredUrl:redirectUrl,path:extractConfiguredPath(redirectUrl,"/verify-magic-link"),query:{token},allowedOrigins:config.authentication?.trustedAppOrigins}):`${verifyRoute}?token=${token}`,result=await emailService.sendMagicLinkEmail(email,magicLink,appName);if(!result.success)return logger2.error("[AUTH] Failed to send magic link email",{email,error:result.error}),ctx.set.status=500,{success:!1,message:result.error?`The email could not be sent: ${result.error}`:"The email could not be sent"};return logger2.info("[AUTH] Magic link sent",{email,userId:user.id}),{success:!0,message:"If an account exists, a magic link has been sent"}}}),routeGetVerifyroute=defineRoute({method:"GET",path:"/",query:MagicLinkVerifySchema,detail:{tags:["Authentication"],summary:"Verify Magic Link",description:"Verify magic link token and login user"},handler:async(ctx)=>{let{usersTable}=resolveAuthTablesForRequest(ctx.request,config);if(!db||!usersTable)return ctx.set.status=500,{success:!1,message:"Database not configured"};let token=ctx.query.token;if(!token)return ctx.set.status=400,{success:!1,message:"Token is required"};let tokenHash=hashToken(token,TOKEN_PURPOSE.magicLink),reqSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0,storedToken=await getMagicToken(tokenHash,reqSchemaName);if(!storedToken)return logger2.warn("[AUTH] Invalid magic link token"),ctx.set.status=400,{success:!1,message:"Invalid or expired token"};if(new Date>storedToken.expiresAt)return await deleteMagicToken(tokenHash,reqSchemaName),logger2.warn("[AUTH] Expired magic link token",{email:storedToken.email}),ctx.set.status=400,{success:!1,message:"Invalid or expired token"};let user=(await db.select().from(usersTable).where(eq54(usersTable.id,storedToken.userId)).limit(1))[0];if(!user)return await deleteMagicToken(tokenHash,reqSchemaName),ctx.set.status=404,{success:!1,message:"User not found"};if(user.isActive===!1)return await deleteMagicToken(tokenHash,reqSchemaName),logger2.warn("[AUTH] Magic link denied: account inactive",{email:storedToken.email}),ctx.set.status=400,{success:!1,message:"Invalid or expired token"};if(user.isLocked){let lockedUntil=user.lockedUntil;if(!(lockedUntil&&new Date(lockedUntil)<new Date))return await deleteMagicToken(tokenHash,reqSchemaName),logger2.warn("[AUTH] Magic link denied: account locked",{email:storedToken.email}),ctx.set.status=400,{success:!1,message:"Invalid or expired token"}}await deleteMagicToken(tokenHash,reqSchemaName),await db.update(usersTable).set({lastLoginAt:new Date,loginCount:(user.loginCount||0)+1,emailVerified:!0}).where(eq54(usersTable.id,user.id));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")||"",mlUserRoles=[],mlUserClaims=[],mlUserClaimScopes={};if(db)try{let resolved=resolveAuthTablesForRequest(ctx.request,config),rc=await fetchUserRolesAndClaims(db,user.id,resolved);mlUserRoles=rc.roles,mlUserClaims=rc.claims,mlUserClaimScopes=rc.claimScopes}catch{}let sessionParams={userId:user.id,deviceInfo:ensureDeviceInfo({ipAddress,userAgent},deviceContextFromRequest(ctx.request).clientHints),loginMethod:"magic_link",rememberMe:!0},sessionId=await createSession(sessionParams),accessToken=signAccessToken(user.id,mlUserRoles.length>0?mlUserRoles:void 0,mlUserClaims.length>0?mlUserClaims:void 0,ctx.request.headers.get("x-tenant-schema")??void 0,mlUserClaimScopes,sessionId),refreshToken=signRefreshToken(user.id,void 0,sessionId),requiresApproval=!1,approvalSessionId=sessionId,deviceTokenCookie;if(saveSessionToDb){let reqSchemaName2=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,reqSchemaName2);if(saveResult?.denied)return ctx.set.status=403,{success:!1,message:"This device could not be recognized. Please contact support."};if(deviceTokenCookie=saveResult?.deviceTokenCookie,saveResult?.requiresApproval){if(requiresApproval=!0,saveResult.sessionId)approvalSessionId=saveResult.sessionId}}if(requiresApproval){if(deviceTokenCookie)ctx.set.headers["Set-Cookie"]=deviceTokenCookie;return logger2.info("[AUTH] Magic link login requires device approval",{userId:user.id,email:user.email,approvalSessionId}),ctx.set.status=202,{success:!1,requiresApproval:!0,sessionId:approvalSessionId,message:"Login requires approval. Please check your email to approve this device."}}logger2.info("[AUTH] Magic link login successful",{userId:user.id,email:user.email});let 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||2592000,secure:cookieConfig?.secure??!0,httpOnly:cookieConfig?.httpOnly??!0,sameSite:cookieConfig?.sameSite||"strict",path:cookieConfig?.path||"/",domain:cookieConfig?.domain},securePart=cookies.secure?"; Secure":"",domainPart=cookies.domain?`; Domain=${cookies.domain}`:"",cookieOptions=`; Path=${cookies.path}; HttpOnly; SameSite=${cookies.sameSite}${securePart}${domainPart}`,responseHeaders=new Headers;if(responseHeaders.set("Content-Type","application/json"),responseHeaders.set("x-session-id",sessionId),responseHeaders.append("Set-Cookie",`${cookies.accessTokenName}=${accessToken}${cookieOptions}; Max-Age=${cookies.accessTokenMaxAge}`),responseHeaders.append("Set-Cookie",`${cookies.refreshTokenName}=${refreshToken}${cookieOptions}; Max-Age=${cookies.refreshTokenMaxAge}`),responseHeaders.append("Set-Cookie",`${cookies.sessionTokenName}=${sessionId}${cookieOptions}; Max-Age=${cookies.sessionTokenMaxAge}`),deviceTokenCookie)responseHeaders.append("Set-Cookie",deviceTokenCookie);let jsonBody=JSON.stringify(withEnvelopeFlags({success:!0,data:{user:{id:user.id,email:user.email},accessToken,refreshToken,sessionId}}));return new Response(jsonBody,{status:200,headers:responseHeaders})}});return collectRoutes([{prefix:requestRoute,routes:[routePostRequestroute]},{prefix:verifyRoute,routes:[routeGetVerifyroute]}])}var init_magicLink=__esm(()=>{init_DeviceTrust();init_response();init_routeCollector();init_fetchUserRolesAndClaims();init_deviceInfo();init_types14();init_utils8();init_types14()});import{eq as eq55}from"drizzle-orm";function createMeRoute(config,meConfig,_schemaTables,_schemaRelations,_databaseUrl){let{db,logger:logger2}=config,route=meConfig.route||"/auth/me";if(!meConfig.enabled)return collectRoutes();let routeGetRoute=defineRoute({method:"GET",path:"/",detail:{tags:["Authentication"],summary:"Get current user",description:"Get the currently authenticated user with profile, addresses, phones and files"},handler:async(ctx)=>{let resolved=resolveAuthTablesForRequest(ctx.request,config),usersTable=resolved.usersTable,reqSchemaTables=resolved.schemaTables;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:"Unauthorized"};let user=(await db.select().from(usersTable).where(eq55(usersTable.id,userId)).limit(1))[0];if(!user)return ctx.set.status=404,{success:!1,message:"User not found"};let{password:rawPassword,...restUser}=user,hasPassword=typeof rawPassword==="string"&&rawPassword.length>0,safeUser={...stripUnclaimedSensitive(redactSensitiveOutput(restUser)),hasPassword},profile=null,addresses=[],phones=[],files=[],roles=[],claims=[];if(meConfig.includeProfile&&reqSchemaTables){let profilesTable=reqSchemaTables.profiles;if(profilesTable&&db)profile=(await db.select().from(profilesTable).where(eq55(profilesTable.userId,userId)).limit(1))[0]||null}if(meConfig.includeAddresses&&reqSchemaTables){let addressesTable=reqSchemaTables.addresses;if(addressesTable&&db)addresses=await db.select().from(addressesTable).where(eq55(addressesTable.ownerId,userId))}if(meConfig.includePhones&&reqSchemaTables){let phonesTable=reqSchemaTables.phones;if(phonesTable&&db)phones=await db.select().from(phonesTable).where(eq55(phonesTable.ownerId,userId))}if(meConfig.includeFiles&&reqSchemaTables){let filesTable=reqSchemaTables.files;if(filesTable&&db)files=await db.select().from(filesTable).where(eq55(filesTable.uploadedBy,userId))}if(meConfig.includeRoles&&reqSchemaTables){let{userRoles:userRolesTable,roles:rolesTable}=reqSchemaTables;if(userRolesTable&&rolesTable&&db){let roleIds=(await db.select().from(userRolesTable).where(eq55(userRolesTable.userId,userId))).map((ur)=>ur.roleId);if(roleIds.length>0){let{inArray:inArray19}=await import("drizzle-orm");roles=await db.select().from(rolesTable).where(inArray19(rolesTable.id,roleIds))}}}if(meConfig.includeClaims&&reqSchemaTables){let{userRoles:userRolesTable,roleClaims:roleClaimsTable,claims:claimsTable}=reqSchemaTables;if(userRolesTable&&roleClaimsTable&&claimsTable&&db){let urCols=userRolesTable,rcCols=roleClaimsTable,cCols=claimsTable,roleIds=(await db.select({roleId:urCols.roleId}).from(userRolesTable).where(eq55(urCols.userId,userId))).map((r)=>r.roleId);if(roleIds.length>0){let{inArray:inArray19}=await import("drizzle-orm"),claimRows=await db.select({action:cCols.action,scope:rcCols.scope}).from(roleClaimsTable).innerJoin(claimsTable,eq55(rcCols.claimId,cCols.id)).where(inArray19(rcCols.roleId,roleIds)),seen=new Set;for(let row of claimRows){let r=row,key2=`${r.action}|${r.scope??""}`;if(!seen.has(key2))seen.add(key2),claims.push({action:r.action,scope:r.scope??null})}}}}logger2.debug("[AUTH] Me endpoint accessed",{userId});let responseData={success:!0,data:{user:safeUser,profile:stripUnclaimedSensitive(profile),addresses:stripUnclaimedSensitive(addresses),phones:stripUnclaimedSensitive(phones),files:stripUnclaimedSensitive(files),roles:stripUnclaimedSensitive(roles),claims}};return JSON.parse(JSON.stringify(responseData,(_key,value2)=>typeof value2==="bigint"?Number(value2):value2))}});return collectRoutes([{prefix:route,routes:[routeGetRoute]}])}var init_me=__esm(()=>{init_routeCollector();init_utils5();init_sensitiveColumns()});var exports_accountState={};__export(exports_accountState,{evaluateAccountState:()=>evaluateAccountState});function evaluateAccountState(user,now=new Date){if(user.isActive===!1)return{allowed:!1,reason:"inactive",message:"Account is not active"};if(user.isLocked===!0){let raw=user.lockedUntil,lockedUntil=raw instanceof Date?raw:raw?new Date(raw):null;if(!lockedUntil||Number.isNaN(lockedUntil.getTime())||lockedUntil>now)return{allowed:!1,reason:"locked",message:"Account is locked"}}return{allowed:!0}}import{eq as eq56}from"drizzle-orm";async function applyProfileSync(args){let{usersTable,profilesTable,userId,rawProfile,config,isNewUser,logger:logger2}=args,db=args.db;try{if(config.enabled===!1)return;if(!isNewUser&&config.syncOnEveryLogin===!1)return;if(!config.mappings||config.mappings.length===0)return;let userIdCol=usersTable.id,currentUser=(await db.select().from(usersTable).where(eq56(userIdCol,userId)).limit(1))[0]??{},currentProfile={},profileExists=!1;if(profilesTable){let profileUserIdCol=profilesTable.userId,profileRows=await db.select().from(profilesTable).where(eq56(profileUserIdCol,userId)).limit(1);if(profileRows[0])currentProfile=profileRows[0],profileExists=!0}let{users,profiles,errors:errors2}=resolveProfileSyncUpdates({mappings:config.mappings,rawProfile,currentUser,currentProfile});for(let message of errors2)logger2.warn(`[OAUTH] profileSync: ${message}`);if(Object.keys(users).length>0)await db.update(usersTable).set({...users,updatedAt:new Date}).where(eq56(userIdCol,userId)),logger2.info("[OAUTH] profileSync updated users",{userId,columns:Object.keys(users)});if(profilesTable&&Object.keys(profiles).length>0){if(profileExists){let profileUserIdCol=profilesTable.userId;await db.update(profilesTable).set({...profiles,updatedAt:new Date}).where(eq56(profileUserIdCol,userId))}else if(config.createProfileIfMissing!==!1){let now=new Date;await db.insert(profilesTable).values({userId,firstName:rawProfile.givenName??"",lastName:rawProfile.surname??"",...profiles,createdAt:now,updatedAt:now})}else return;logger2.info("[OAUTH] profileSync updated profiles",{userId,columns:Object.keys(profiles),created:!profileExists})}}catch(err){logger2.warn("[OAUTH] profileSync failed \u2014 login continues",{userId,error:err instanceof Error?err.message:String(err)})}}var init_applyProfileSync=()=>{};import{and as and25,eq as eq57}from"drizzle-orm";function buildOAuthStateCookie(value2,opts){let securePart=opts.secure?"; Secure":"",domainPart=opts.domain?`; Domain=${opts.domain}`:"";return`${OAUTH_STATE_COOKIE}=${value2}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${opts.maxAge}${securePart}${domainPart}`}function createOAuthRoutes(config,oauthService,signAccessToken,signRefreshToken,createSession,saveSessionToDb,cookieConfig,tokenResponseConfig,getEmailService,storeMagicToken,appName){let{db,logger:logger2}=config,basePath=oauthService.basePath;for(let provider of oauthService.getEnabledProviders()){let p=oauthService.config.providers[provider];logger2.info("[OAUTH] Provider loaded",{provider,clientIdSuffix:p?.clientId?p.clientId.slice(-4):"none",tenantId:p?.tenantId??"common",redirectUri:p?.redirectUri,scopes:p?.scopes})}let 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||"lax",path:cookieConfig?.path||"/",domain:cookieConfig?.domain},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}},routeGetProvider=defineRoute({method:"GET",path:"/:provider",detail:{tags:["Authentication"],summary:"OAuth Redirect",description:"Redirects to the OAuth provider authorization page"},handler: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`};let redirectUrl=ctx.query.redirect_url,authUrl=await oauthService.buildAuthorizationUrl(provider,void 0,redirectUrl);logger2.debug("[OAUTH] Redirecting to provider",{provider,authUrl:authUrl.replace(/(client_id=)[^&]+/,"$1***")});let 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}}),routeGetProviderCallback=defineRoute({method:"GET",path:"/:provider/callback",detail:{tags:["Authentication"],summary:"OAuth Callback",description:"Handles the OAuth provider callback, creates session and sets cookies"},handler:async(ctx)=>{let provider=ctx.params.provider,query=ctx.query,{code,state,error:error3,error_description}=query,portalOrigin=resolveAppOrigin(ctx.request,oauthService.successRedirectUrl,config.authentication?.trustedAppOrigins),errorRedirect=absolutizeRedirect(oauthService.errorRedirectUrl,portalOrigin);if(error3)return logger2.warn("[OAUTH] Provider returned error",{provider,error:error3,error_description}),ctx.set.status=302,ctx.set.headers.Location=`${errorRedirect}?error=${encodeURIComponent(error_description||error3)}`,null;if(!code||!state)return ctx.set.status=302,ctx.set.headers.Location=`${errorRedirect}?error=missing_code_or_state`,null;let statePayload=await oauthService.consumeState(state);if(!statePayload)return logger2.warn("[OAUTH] Invalid or expired state",{provider,state}),ctx.set.status=302,ctx.set.headers.Location=`${errorRedirect}?error=invalid_state`,null;let cookieState=readCookie(ctx.request,OAUTH_STATE_COOKIE),clearStateCookie=buildOAuthStateCookie("",{maxAge:0,secure:cookies.secure,domain:cookies.domain});if(!cookieState||cookieState!==state)return logger2.warn("[OAUTH] State cookie missing or mismatched (possible CSRF)",{provider}),ctx.set.status=302,ctx.set.headers["Set-Cookie"]=clearStateCookie,ctx.set.headers.Location=`${errorRedirect}?error=invalid_state`,null;if(statePayload.provider!==provider)return logger2.warn("[OAUTH] State provider mismatch",{expected:statePayload.provider,got:provider}),ctx.set.status=302,ctx.set.headers.Location=`${errorRedirect}?error=provider_mismatch`,null;let resolved=resolveAuthTablesForRequest(ctx.request,config),usersTable=resolved.usersTable,oauthTable=resolved.oauthAccountsTable??config.oauthAccountsTable;if(!db||!usersTable)return ctx.set.status=302,ctx.set.headers.Location=`${errorRedirect}?error=server_error`,null;logger2.debug("[OAUTH] Exchanging code",{provider,codeLength:code.length,statePresent:!!state});let callbackResult;try{callbackResult=await oauthService.exchangeCode(provider,code)}catch(err){let e=err;return logger2.error("[OAUTH] Code exchange failed",err,{provider,errorMessage:e?.message,errorName:e?.name,errorCauseCode:e?.cause?.code,errorCauseMessage:e?.cause?.message}),ctx.set.status=302,ctx.set.headers.Location=`${errorRedirect}?error=token_exchange_failed`,null}let{profile,tokens}=callbackResult,userId=null;if(statePayload.linkUserId&&oauthService.allowAccountLinking){if(userId=statePayload.linkUserId,oauthTable){let existing=await db.select().from(oauthTable).where(and25(eq57(oauthTable.provider,provider),eq57(oauthTable.providerAccountId,profile.providerAccountId))).limit(1);if(existing.length>0){let existingRecord=existing[0];if(existingRecord.userId!==userId)return ctx.set.status=302,ctx.set.headers.Location=`${errorRedirect}?error=account_already_linked_to_another_user`,null;await db.update(oauthTable).set({accessToken:tokens.accessToken,refreshToken:tokens.refreshToken??null,tokenExpiresAt:tokens.expiresAt??null,scope:tokens.scope??null,rawProfile:profile.rawProfile,lastUsedAt:new Date}).where(eq57(oauthTable.id,existingRecord.id))}else await db.insert(oauthTable).values({userId,provider,providerAccountId:profile.providerAccountId,providerEmail:profile.email??null,providerName:profile.name??null,providerAvatarUrl:profile.avatarUrl??null,accessToken:tokens.accessToken,refreshToken:tokens.refreshToken??null,tokenExpiresAt:tokens.expiresAt??null,scope:tokens.scope??null,rawProfile:profile.rawProfile,isPrimary:!1,lastUsedAt:new Date})}let linkSyncConfig=oauthService.profileSync;if(linkSyncConfig){let linkSyncTables=resolveAuthTablesForRequest(ctx.request,config);await applyProfileSync({db,usersTable,profilesTable:linkSyncTables.schemaTables?.profiles,userId,rawProfile:profile.rawProfile,config:linkSyncConfig,isNewUser:!1,logger:logger2})}logger2.info("[OAUTH] Account linked successfully",{userId,provider});let successUrl2=absolutizeRedirect(resolveSafeOAuthRedirect(statePayload.redirectUrl,oauthService.successRedirectUrl,config.authentication?.trustedAppOrigins),portalOrigin);return ctx.set.status=302,ctx.set.headers.Location=`${successUrl2}?linked=true&provider=${provider}`,null}if(oauthTable){let existingOAuth=await db.select().from(oauthTable).where(and25(eq57(oauthTable.provider,provider),eq57(oauthTable.providerAccountId,profile.providerAccountId))).limit(1);if(existingOAuth.length>0){let oauthRecord=existingOAuth[0];userId=oauthRecord.userId,await db.update(oauthTable).set({accessToken:tokens.accessToken,refreshToken:tokens.refreshToken??null,tokenExpiresAt:tokens.expiresAt??null,scope:tokens.scope??null,rawProfile:profile.rawProfile,lastUsedAt:new Date}).where(eq57(oauthTable.id,oauthRecord.id))}}if(!userId&&profile.email){let normalizedOAuthEmail=profile.email.toLowerCase(),existingUsers=await db.select().from(usersTable).where(eq57(usersTable.email,normalizedOAuthEmail)).limit(1);if(existingUsers.length>0&&profile.emailVerified!==!0)return logger2.warn("[OAUTH] Refusing account merge: provider email not verified",{provider,email:profile.email}),ctx.set.status=302,ctx.set.headers.Location=`${errorRedirect}?error=email_not_verified`,null;if(existingUsers.length>0){if(userId=existingUsers[0].id,oauthTable)await db.insert(oauthTable).values({userId,provider,providerAccountId:profile.providerAccountId,providerEmail:profile.email??null,providerName:profile.name??null,providerAvatarUrl:profile.avatarUrl??null,accessToken:tokens.accessToken,refreshToken:tokens.refreshToken??null,tokenExpiresAt:tokens.expiresAt??null,scope:tokens.scope??null,rawProfile:profile.rawProfile,isPrimary:!1,lastUsedAt:new Date}).onConflictDoNothing();logger2.info("[OAUTH] Merged OAuth account with existing user by email",{userId,provider,email:profile.email})}}let createdNewUser=!1;if(!userId){if(!oauthService.autoCreateUser)return ctx.set.status=302,ctx.set.headers.Location=`${errorRedirect}?error=user_not_found`,null;let userEmail=(profile.email??`${provider}_${profile.providerAccountId}@oauth.local`).toLowerCase(),providerEmailVerified=profile.emailVerified===!0;if(userId=(await db.insert(usersTable).values({email:userEmail,password:null,emailVerified:providerEmailVerified,verifiedAt:providerEmailVerified?new Date:null,isActive:!0}).returning())[0].id,oauthTable)await db.insert(oauthTable).values({userId,provider,providerAccountId:profile.providerAccountId,providerEmail:profile.email??null,providerName:profile.name??null,providerAvatarUrl:profile.avatarUrl??null,accessToken:tokens.accessToken,refreshToken:tokens.refreshToken??null,tokenExpiresAt:tokens.expiresAt??null,scope:tokens.scope??null,rawProfile:profile.rawProfile,isPrimary:!0,lastUsedAt:new Date});if(createdNewUser=!0,logger2.info("[OAUTH] Created new user via OAuth",{userId,provider,email:profile.email}),oauthService.sendInviteOnCreate&&profile.email&&getEmailService?.()?.isAvailable()&&storeMagicToken)try{let rawToken=generateMagicToken(),tokenHash=hashToken(rawToken,TOKEN_PURPOSE.invite),expiresAt=new Date(Date.now()+parseTimeToMs3("7d")),oauthSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0;await storeMagicToken({userId,email:profile.email,tokenHash,expiresAt},oauthSchemaName);let inviteLink=buildEmailActionLink({request:ctx.request,configuredUrl:oauthService.successRedirectUrl,path:"/set-password",query:{token:rawToken},allowedOrigins:config.authentication?.trustedAppOrigins});await getEmailService?.()?.sendEmail({to:profile.email,subject:`Welcome to ${appName??"the platform"} \u2014 Set your password`,html:`
447
+ `),polyTables=(Array.isArray(polyResult)?polyResult:polyResult.rows||[]).map((r)=>String(r.table_name||""));logger2.info("[AUTH] Change User ID - discovered schema references",{fkConstraints:fkConstraints.map((f)=>`${f.table_name}.${f.column_name} (${f.constraint_name})`),userIdColumns:userIdColumns.map((u)=>`${u.table_name}.${u.column_name}`),auditColumns:auditColumns.map((a)=>`${a.table_name}.${a.column_name}`),polyTables});let allUpdates=new Map,addUpdate=(tableName,columnName)=>{if(!allUpdates.has(tableName))allUpdates.set(tableName,new Set);allUpdates.get(tableName)?.add(columnName)};for(let fk of fkConstraints)addUpdate(fk.table_name,fk.column_name);for(let col13 of userIdColumns)addUpdate(col13.table_name,col13.column_name);for(let col13 of auditColumns)addUpdate(col13.table_name,col13.column_name);logger2.info("[AUTH] Change User ID - all column updates to execute",{updates:Array.from(allUpdates.entries()).map(([t,cols])=>`${t}: [${Array.from(cols).join(", ")}]`)}),await db.transaction(async(tx)=>{for(let fk of fkConstraints)await tx.execute(sql9.raw(`ALTER TABLE "${effectiveSchema}"."${fk.table_name}" DROP CONSTRAINT "${fk.constraint_name}"`));await tx.execute(sql9.raw(`UPDATE "${effectiveSchema}"."users" SET "id" = '${newId}' WHERE "id" = '${currentId}'`));for(let[tableName,columns]of allUpdates.entries())for(let columnName of columns)await tx.execute(sql9.raw(`UPDATE "${effectiveSchema}"."${tableName}" SET "${columnName}" = '${newId}' WHERE "${columnName}" = '${currentId}'`));for(let tableName of polyTables)await tx.execute(sql9.raw(`UPDATE "${effectiveSchema}"."${tableName}" SET "owner_id" = '${newId}' WHERE "owner_id" = '${currentId}' AND "owner_type" IN ('user', 'users', 'User')`));for(let fk of fkConstraints){let onDelete=fk.delete_rule==="CASCADE"?"ON DELETE CASCADE":fk.delete_rule==="SET NULL"?"ON DELETE SET NULL":"";await tx.execute(sql9.raw(`ALTER TABLE "${effectiveSchema}"."${fk.table_name}" ADD CONSTRAINT "${fk.constraint_name}" FOREIGN KEY ("${fk.column_name}") REFERENCES "${effectiveSchema}"."users" ("id") ${onDelete}`))}});let allAffectedTables=[...new Set([...fkConstraints.map((f)=>f.table_name),...userIdColumns.map((u)=>u.table_name),...auditColumns.map((a)=>a.table_name),...polyTables])];return logger2.info("[AUTH] User ID changed successfully",{godminId:requestingUserId,currentId,newId,email:targetUser.email,affectedTables:allAffectedTables,fkConstraintsDroppedAndReadded:fkConstraints.length}),await logger2.audit({entityName:"users",entityId:newId,operation:"CHANGE_USER_ID",userId:requestingUserId,summary:`Godmin changed user ID: ${currentId} \u2192 ${newId} (${targetUser.email})`,oldValues:{id:currentId},newValues:{id:newId},ipAddress:ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||"unknown",userAgent:ctx.request.headers.get("user-agent")||"unknown",path:new URL(ctx.request.url).pathname,query:""}),{success:!0,data:{oldId:currentId,newId,email:targetUser.email,affectedTables:allAffectedTables}}}catch(error3){return logger2.error("[AUTH] Failed to change user ID",{error:error3 instanceof Error?error3.message:String(error3),stack:error3 instanceof Error?error3.stack:void 0,currentId,newId}),ctx.set.status=500,{success:!1,message:error3 instanceof Error?error3.message:"Failed to change user ID"}}}})];return collectRoutes([{routes:declared}])}var init_changeUserId=__esm(()=>{init_esm3();init_routeCollector();init_requireGodmin();init_resolveMutationSchema()});function decideCreateUserAuthz(params){let{callerIsGod,callerRoleNames,allowRoles,requestedRoleNames,godminRoleName}=params,callerIsGodmin=callerIsGod||callerRoleNames.includes(godminRoleName);if(!(callerIsGodmin||callerRoleNames.some((n)=>allowRoles.includes(n))))return{allowed:!1,status:403,reason:"not_privileged"};if(requestedRoleNames.includes(godminRoleName)&&!callerIsGodmin)return{allowed:!1,status:403,reason:"godmin_assignment_forbidden"};if(!callerIsGodmin){let heldByCaller=new Set(callerRoleNames);if(!requestedRoleNames.every((r)=>heldByCaller.has(r)))return{allowed:!1,status:403,reason:"role_above_caller"}}return{allowed:!0,callerIsGodmin}}import{eq as eq43,inArray as inArray17}from"drizzle-orm";function createCreateUserRoute(config,createUserConfig){let{db,logger:logger2}=config;if(createUserConfig?.enabled===!1)return collectRoutes();let route=createUserConfig?.route||"/auth/admin/create-user",allowRoles=createUserConfig?.allowRoles??[],declared=[defineRoute({method:"POST",path:"/",body:CreateUserBodySchema,detail:{tags:["Authentication","Admin"],summary:"Admin Create User",description:"Create a fully active user with a password, roles, and optional profile. Admin-only. No email is sent."},handler:async(ctx)=>{let resolved=resolveAuthTablesForRequest(ctx.request,config),{usersTable,rolesTable:resolvedRolesTable,userRolesTable:resolvedUserRolesTable,schemaTables}=resolved,profilesTable=schemaTables?.profiles,rolesTable=resolvedRolesTable??config.rolesTable??null,userRolesTable=resolvedUserRolesTable??config.userRolesTable??null;if(!db||!usersTable)return ctx.set.status=500,{success:!1,message:"Database not configured"};let requestingUserId=ctx.request.headers.get("x-user-id");if(!requestingUserId)return ctx.set.status=401,{success:!1,message:"Unauthorized"};let requestingUser=(await db.select().from(usersTable).where(eq43(usersTable.id,requestingUserId)).limit(1))[0];if(!requestingUser)return ctx.set.status=401,{success:!1,message:"Unauthorized"};let callerIsGod=Boolean(requestingUser.isGod),callerRoleNames=[];if(!callerIsGod&&rolesTable&&userRolesTable){let rolesCols=rolesTable,userRolesCols=userRolesTable;callerRoleNames=(await db.select({name:rolesCols.name}).from(userRolesTable).innerJoin(rolesTable,eq43(rolesCols.id,userRolesCols.roleId)).where(eq43(userRolesCols.userId,requestingUserId))).map((r)=>r.name)}let requestedRoleIds=ctx.body.roleIds??[],foundRoles=[];if(requestedRoleIds.length>0&&rolesTable){let rolesCols=rolesTable;foundRoles=(await db.select({id:rolesCols.id,name:rolesCols.name}).from(rolesTable).where(inArray17(rolesCols.id,requestedRoleIds))).map((r)=>({id:r.id,name:r.name}))}let decision=decideCreateUserAuthz({callerIsGod,callerRoleNames,allowRoles,requestedRoleNames:foundRoles.map((r)=>r.name),godminRoleName:GODMIN_ROLE_NAME});if(!decision.allowed)return logger2.warn("[AUTH] create-user denied",{callerId:requestingUserId,reason:decision.reason}),ctx.set.status=decision.status,{success:!1,message:decision.reason==="godmin_assignment_forbidden"?"Forbidden: assigning the godmin role requires godmin privileges":"Forbidden"};let{email:rawEmail,password:password4,profile}=ctx.body,email=rawEmail.toLowerCase().trim(),passwordValidation=validatePasswordStrength(password4,config.authentication?.passwordPolicy,email);if(!passwordValidation.valid)return ctx.set.status=400,{success:!1,message:"Password too weak",errors:passwordValidation.errors};if((await db.select().from(usersTable).where(eq43(usersTable.email,email)).limit(1)).length>0)return logger2.warn("[AUTH] create-user failed \u2014 email already exists",{email}),ctx.set.status=409,{success:!1,message:"Email already registered"};let now=new Date,hashedPassword=await hashPassword(password4),insertValues={email,password:hashedPassword,emailVerified:!0,verifiedAt:now,isLocked:!1,createdAt:now,updatedAt:now},newUser=(await db.insert(usersTable).values(insertValues).returning())[0];if(!newUser)return logger2.error("[AUTH] create-user insert returned nothing",{email}),ctx.set.status=500,{success:!1,message:"Failed to create user"};let newUserId=newUser.id;if(requestedRoleIds.length>0&&rolesTable&&userRolesTable)try{let validRoleIds=foundRoles.map((r)=>r.id);if(validRoleIds.length>0)await db.insert(userRolesTable).values(validRoleIds.map((roleId)=>({userId:newUserId,roleId})));let skipped=requestedRoleIds.filter((id)=>!validRoleIds.includes(id));if(skipped.length>0)logger2.warn("[AUTH] create-user: some roleIds were not found",{userId:newUserId,skipped});if(foundRoles.some((r)=>r.name===GODMIN_ROLE_NAME))await db.update(usersTable).set({isGod:!0}).where(eq43(usersTable.id,newUserId))}catch(err){logger2.warn("[AUTH] create-user: failed to assign roles \u2014 continuing",{userId:newUserId,error:err instanceof Error?err.message:String(err)})}else{let createDefaultRoles=resolveDefaultRoleNames(config.authentication);if(createDefaultRoles.length>0)await assignDefaultRoles({db,userId:newUserId,roleNames:createDefaultRoles,rolesTable,userRolesTable,logger:logger2})}if(profile&&profilesTable)try{await db.insert(profilesTable).values({userId:newUserId,firstName:profile.firstName??null,lastName:profile.lastName??null,phoneNumber:profile.phoneNumber??null,createdAt:now,updatedAt:now})}catch(err){logger2.warn("[AUTH] create-user: failed to create profile \u2014 continuing",{userId:newUserId,error:err instanceof Error?err.message:String(err)})}let 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";return logger2.audit({entityName:"users",entityId:newUserId,operation:"ADMIN_CREATE_USER",userId:requestingUserId,summary:`Admin ${requestingUserId} created user ${email}`,ipAddress:auditIp,userAgent:auditUa,path:reqUrl.pathname,query:reqUrl.search}),logger2.info("[AUTH] Admin created user",{newUserId,email,createdBy:requestingUserId}),{success:!0,message:"User created successfully",data:{id:newUserId,email}}}})];return collectRoutes([{prefix:route,routes:declared}])}var CreateUserBodySchema;var init_createUser=__esm(()=>{init_esm3();init_Authorization();init_routeCollector();init_assignDefaultRole();init_utils6();CreateUserBodySchema=Type.Object({email:Type.String({format:"email"}),password:Type.String({minLength:8}),roleIds:Type.Optional(Type.Array(Type.String())),profile:Type.Optional(Type.Object({firstName:Type.Optional(Type.String()),lastName:Type.Optional(Type.String()),phoneNumber:Type.Optional(Type.String())}))})});import{eq as eq44}from"drizzle-orm";function createHardDeleteUserRoute(config){let{db,logger:logger2}=config,declared=[defineRoute({method:"DELETE",path:"/auth/admin/hard-delete/:userId",params:HardDeleteUserParamsSchema,detail:{tags:["Authentication","Admin"],summary:"Hard Delete User",description:"Permanently delete a user and all related data (roles, sessions, profile, files, etc.). Godmin-only. Irreversible."},handler:async(ctx)=>{let resolved=resolveAuthTablesForRequest(ctx.request,config),{usersTable,userRolesTable,sessionsTable,oauthAccountsTable,apiKeysTable,schemaTables}=resolved;if(!db||!usersTable)return ctx.set.status=500,{success:!1,message:"Database not configured"};let requestingUserId=ctx.request.headers.get("x-user-id");if(!requestingUserId)return ctx.set.status=401,{success:!1,message:"Unauthorized"};let caller=(await db.select().from(usersTable).where(eq44(usersTable.id,requestingUserId)).limit(1))[0],callerIsGodmin=Boolean(caller?.isGod)||hasGodminRole(ctx.request)||await userHasGodminRoleInDb(db,config.schemaTables??{},requestingUserId,config.logger);if(!caller||!callerIsGodmin)return ctx.set.status=403,{success:!1,message:"Forbidden \u2014 only godmin can hard-delete users"};let{userId:targetUserId}=ctx.params;if(targetUserId===requestingUserId)return ctx.set.status=400,{success:!1,message:"Cannot hard-delete yourself"};let targetUser=(await db.select().from(usersTable).where(eq44(usersTable.id,targetUserId)).limit(1))[0];if(!targetUser)return ctx.set.status=404,{success:!1,message:"User not found"};if(targetUser.isGod)return ctx.set.status=403,{success:!1,message:"Cannot hard-delete a godmin user"};let deletedRelations=[],col13=(table,name2)=>table[name2],profilesTable=schemaTables?.profiles,addressesTable=schemaTables?.addresses,phonesTable=schemaTables?.phones,filesTable=schemaTables?.files,runCascade=async(exec)=>{deletedRelations.length=0;let del=async(table,columnName,label)=>{if(!table)return;await exec.delete(table).where(eq44(col13(table,columnName),targetUserId)),deletedRelations.push(label)};if(await del(userRolesTable,"userId","user_roles"),await del(sessionsTable,"userId","sessions"),await del(oauthAccountsTable,"userId","oauth_accounts"),await del(apiKeysTable,"userId","api_keys"),await del(profilesTable,"userId","profiles"),await del(addressesTable,"ownerId","addresses"),await del(phonesTable,"ownerId","phones"),filesTable)await exec.update(filesTable).set({uploadedBy:null}).where(eq44(col13(filesTable,"uploadedBy"),targetUserId)),deletedRelations.push("files:dissociated");await exec.delete(usersTable).where(eq44(usersTable.id,targetUserId))};try{let txDb=db;if(typeof txDb.transaction==="function")await txDb.transaction(async(tx)=>{await runCascade(tx)});else await runCascade(db)}catch(err){return logger2.error("[AUTH] hard-delete failed; transaction rolled back",{userId:targetUserId,error:err instanceof Error?err.message:String(err)}),ctx.set.status=500,{success:!1,message:"Hard delete failed and was rolled back"}}let reqUrl=new URL(ctx.request.url);return logger2.audit({entityName:"users",entityId:targetUserId,operation:"ADMIN_HARD_DELETE_USER",userId:requestingUserId,summary:`Godmin ${requestingUserId} hard-deleted user ${targetUser.email} (${targetUserId}). Cleaned: ${deletedRelations.join(", ")}`,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}),logger2.info("[AUTH] Hard-deleted user",{targetUserId,email:targetUser.email,deletedBy:requestingUserId,cleanedRelations:deletedRelations}),{success:!0,message:"User permanently deleted",data:{userId:targetUserId,email:targetUser.email,cleanedRelations:deletedRelations}}}})];return collectRoutes([{routes:declared}])}var HardDeleteUserParamsSchema;var init_hardDeleteUser=__esm(()=>{init_esm3();init_routeCollector();init_adminGuard();HardDeleteUserParamsSchema=Type.Object({userId:Type.String()})});var exports_fetchUserRolesAndClaims={};__export(exports_fetchUserRolesAndClaims,{fetchUserRolesAndClaims:()=>fetchUserRolesAndClaims});import{and as and23,eq as eq45,inArray as inArray18}from"drizzle-orm";var noopLogger,fetchUserRolesAndClaims=async(db,userId,resolved,logger2=noopLogger)=>{let{userRolesTable,rolesTable,roleClaimsTable,claimsTable,usersTable}=resolved,result={roles:[],claims:[],claimScopes:{}};if(!userRolesTable||!rolesTable)return result;let roleIds=(await db.select().from(userRolesTable).where(and23(eq45(userRolesTable.userId,userId),activeRowsPredicate(userRolesTable)))).map((r)=>r.roleId);if(roleIds.length===0)return result;let roleRows=await db.select().from(rolesTable).where(inArray18(rolesTable.id,roleIds));if(result.roles=roleRows.map((r)=>r.name),roleClaimsTable&&claimsTable){let rows=(await db.select({action:claimsTable.action,scope:roleClaimsTable.scope}).from(roleClaimsTable).innerJoin(claimsTable,eq45(roleClaimsTable.claimId,claimsTable.id)).where(and23(inArray18(roleClaimsTable.roleId,roleIds),activeRowsPredicate(roleClaimsTable)))).filter((r)=>typeof r.action==="string"&&r.action.length>0).map((r)=>({action:r.action,scope:r.scope??null})),userData;if(rows.some((r)=>!!r.scope)&&usersTable)try{userData=(await db.select().from(usersTable).where(eq45(usersTable.id,userId)))[0]}catch(err){logger2.warn("[Authorization] Failed to load user row for scope resolution",{userId,error:err instanceof Error?err.message:String(err)})}let{claimScopes,dropped}=resolveClaimScopes(rows,userData,logger2),droppedSet=new Set(dropped);result.claims=Array.from(new Set(rows.map((r)=>r.action))).filter((a)=>!droppedSet.has(a)),result.claimScopes=claimScopes}return result};var init_fetchUserRolesAndClaims=__esm(()=>{init_activeRows();init_Middleware();noopLogger={debug:()=>{},info:()=>{},warn:()=>{},error:()=>{}}});var IMPERSONATION_COOKIE_NAMES;var init_impersonationCookies=__esm(()=>{IMPERSONATION_COOKIE_NAMES=["nucleus_impersonating_as","nucleus_godmin_id","nucleus_impersonating_email","nucleus_imp_sig"]});import{eq as eq46}from"drizzle-orm";function createImpersonateRoute(config,signAccessToken,signRefreshToken,verifyRefreshToken,createSession,saveSessionToDb,cookieConfig){let{db,logger:logger2}=config,requireGodmin2=createRequireGodmin(config),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,sameSite:cookieConfig?.sameSite||"lax",path:cookieConfig?.path||"/",domain:cookieConfig?.domain},routePostAuthAdminImpersonate=defineRoute({method:"POST",path:"/auth/admin/impersonate",body:Type.Object({targetUserId:Type.String()}),detail:{tags:["Authentication"],summary:"Impersonate User",description:"Godmin: start a session as another user"},handler:async(ctx)=>{let{usersTable}=resolveAuthTablesForRequest(ctx.request,config);if(!db||!usersTable)return ctx.set.status=500,{success:!1,message:"Database not configured"};let{targetUserId}=ctx.body,gate=await requireGodmin2(ctx.request);if(!gate.ok)return ctx.set.status=gate.status,{success:!1,message:gate.message};let{callerId:requestingUserId,caller:requestingUser}=gate;if(requestingUserId===targetUserId)return ctx.set.status=400,{success:!1,message:"Cannot impersonate yourself"};let targetUser=(await db.select().from(usersTable).where(eq46(usersTable.id,targetUserId)).limit(1))[0];if(!targetUser)return ctx.set.status=404,{success:!1,message:"Target user not found"};let impRoles=[],impClaims=[],impClaimScopes={};if(config.db)try{let resolved=resolveAuthTablesForRequest(ctx.request,config),rc=await fetchUserRolesAndClaims(config.db,targetUserId,resolved);impRoles=rc.roles,impClaims=rc.claims,impClaimScopes=rc.claimScopes}catch{}let reqIp=ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||"unknown",sessionParams={userId:targetUserId,deviceInfo:{deviceName:"Impersonation Session",browserName:"Admin",osName:"Admin",ipAddress:reqIp},loginMethod:"impersonation"},sessionId=await createSession(sessionParams),accessToken=signAccessToken(targetUserId,impRoles.length>0?impRoles:void 0,impClaims.length>0?impClaims:void 0,ctx.request.headers.get("x-tenant-schema")??void 0,impClaimScopes,sessionId),refreshToken=signRefreshToken(targetUserId,void 0,sessionId);if(saveSessionToDb){let impSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0;await saveSessionToDb(sessionId,sessionParams,impSchemaName)}logger2.info("[AUTH] Impersonation started",{godminId:requestingUserId,targetUserId,targetEmail:targetUser.email}),await logger2.audit({entityName:"users",entityId:targetUserId,operation:"IMPERSONATE_START",userId:requestingUserId,summary:`Godmin ${requestingUser.email} started impersonating ${targetUser.email}`,ipAddress:ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||"unknown",userAgent:ctx.request.headers.get("user-agent")||"unknown",path:new URL(ctx.request.url).pathname,query:""});let securePart=cookies.secure?"; Secure":"",domainPart=cookies.domain?`; Domain=${cookies.domain}`:"",cookieOptions=`; Path=${cookies.path}; HttpOnly; SameSite=${cookies.sameSite}${securePart}${domainPart}`,cookiesToSet=[`${cookies.accessTokenName}=${accessToken}${cookieOptions}; Max-Age=${cookies.accessTokenMaxAge}`,`${cookies.refreshTokenName}=${refreshToken}${cookieOptions}; Max-Age=${cookies.refreshTokenMaxAge}`,`${cookies.sessionTokenName}=${sessionId}${cookieOptions}; Max-Age=${cookies.sessionTokenMaxAge}`,`nucleus_impersonating_as=${targetUserId}${cookieOptions}; Max-Age=${cookies.sessionTokenMaxAge}`,`nucleus_godmin_id=${requestingUserId}${cookieOptions}; Max-Age=${cookies.sessionTokenMaxAge}`,`nucleus_impersonating_email=${encodeURIComponent(String(targetUser.email))}${cookieOptions}; Max-Age=${cookies.sessionTokenMaxAge}`,`nucleus_imp_sig=${signRefreshToken(requestingUserId)}${cookieOptions}; Max-Age=${cookies.refreshTokenMaxAge}`],jsonBody=JSON.stringify(withEnvelopeFlags({success:!0,data:{targetUser:{id:targetUser.id,email:targetUser.email},godminId:requestingUserId,sessionId}})),headers=new Headers;headers.set("Content-Type","application/json");for(let cookie of cookiesToSet)headers.append("Set-Cookie",cookie);return new Response(jsonBody,{status:200,headers})}}),routePostAuthAdminImpersonateStop=defineRoute({method:"POST",path:"/auth/admin/impersonate/stop",detail:{tags:["Authentication"],summary:"Stop Impersonation",description:"Godmin: restore own session after impersonation"},handler:async(ctx)=>{let{usersTable}=resolveAuthTablesForRequest(ctx.request,config);if(!db||!usersTable)return ctx.set.status=500,{success:!1,message:"Database not configured"};let cookieHeader=ctx.request.headers.get("cookie")||"",impSig=Object.fromEntries(cookieHeader.split(";").map((c)=>{let[k,...v]=c.trim().split("=");return[k?.trim(),v.join("=")]})).nucleus_imp_sig,verified=impSig?verifyRefreshToken(impSig):{valid:!1};if(!verified.valid||!verified.payload?.sub)return ctx.set.status=401,{success:!1,message:"No valid impersonation session"};let godminId=verified.payload.sub,godminUser=(await db.select().from(usersTable).where(eq46(usersTable.id,godminId)).limit(1))[0];if(!godminUser)return ctx.set.status=404,{success:!1,message:"Godmin user not found"};if(godminUser.isGod!==!0)return ctx.set.status=403,{success:!1,message:"Forbidden: godmin privileges required"};let stateRefusal=accountStateRefusal(godminUser);if(stateRefusal)return ctx.set.status=401,{success:!1,message:stateRefusal};let godminRoles=[],godminClaims=[],godminClaimScopes={};if(config.db)try{let resolvedStop=resolveAuthTablesForRequest(ctx.request,config),rcStop=await fetchUserRolesAndClaims(config.db,godminId,resolvedStop);godminRoles=rcStop.roles,godminClaims=rcStop.claims,godminClaimScopes=rcStop.claimScopes}catch{}let stopReqIp=ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||"unknown",sessionParams={userId:godminId,deviceInfo:{deviceName:"Restored Admin Session",browserName:"Admin",osName:"Admin",ipAddress:stopReqIp},loginMethod:"impersonation_stop"},sessionId=await createSession(sessionParams),accessToken=signAccessToken(godminId,godminRoles.length>0?godminRoles:void 0,godminClaims.length>0?godminClaims:void 0,ctx.request.headers.get("x-tenant-schema")??void 0,godminClaimScopes,sessionId),refreshToken=signRefreshToken(godminId,void 0,sessionId);if(saveSessionToDb){let stopSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0;await saveSessionToDb(sessionId,sessionParams,stopSchemaName)}logger2.info("[AUTH] Impersonation stopped",{godminId}),await logger2.audit({entityName:"users",entityId:godminId,operation:"IMPERSONATE_STOP",userId:godminId,summary:`Godmin ${godminUser.email} stopped impersonation`,ipAddress:ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||"unknown",userAgent:ctx.request.headers.get("user-agent")||"unknown",path:new URL(ctx.request.url).pathname,query:""});let securePart=cookies.secure?"; Secure":"",domainPart=cookies.domain?`; Domain=${cookies.domain}`:"",cookieOptions=`; Path=${cookies.path}; HttpOnly; SameSite=${cookies.sameSite}${securePart}${domainPart}`,expiredOptions=`; Path=${cookies.path}; HttpOnly; SameSite=${cookies.sameSite}${securePart}${domainPart}; Max-Age=0`,cookiesToSet=[`${cookies.accessTokenName}=${accessToken}${cookieOptions}; Max-Age=${cookies.accessTokenMaxAge}`,`${cookies.refreshTokenName}=${refreshToken}${cookieOptions}; Max-Age=${cookies.refreshTokenMaxAge}`,`${cookies.sessionTokenName}=${sessionId}${cookieOptions}; Max-Age=${cookies.sessionTokenMaxAge}`,...IMPERSONATION_COOKIE_NAMES.map((name2)=>`${name2}=deleted${expiredOptions}`)],jsonBody=JSON.stringify(withEnvelopeFlags({success:!0,data:{godminUser:{id:godminUser.id,email:godminUser.email},sessionId}})),headers=new Headers;headers.set("Content-Type","application/json");for(let cookie of cookiesToSet)headers.append("Set-Cookie",cookie);return new Response(jsonBody,{status:200,headers})}});return collectRoutes([{routes:[routePostAuthAdminImpersonate,routePostAuthAdminImpersonateStop]}])}var init_impersonate=__esm(()=>{init_esm3();init_response();init_routeCollector();init_fetchUserRolesAndClaims();init_impersonationCookies();init_requireGodmin()});function clearedLockoutState(){return{failedLoginAttempts:0,isLocked:!1,lockedUntil:null}}function planPasswordRotation(input){let protectedRoles=new Set(input.protectedRoles.filter((name2)=>name2.trim()!=="")),plan={rotate:[],skipped:[]};for(let user of input.users){if(user.id===input.callerId){plan.skipped.push({id:user.id,reason:"caller"});continue}if(user.isGod){plan.skipped.push({id:user.id,reason:"godmin"});continue}if(user.roleNames.some((name2)=>protectedRoles.has(name2))){plan.skipped.push({id:user.id,reason:"protected_role"});continue}if(input.includeInactive!==!0&&user.isActive===!1){plan.skipped.push({id:user.id,reason:"inactive"});continue}plan.rotate.push(user.id)}return plan}function splitByRecoverability(users,rotate){let rotating=new Set(rotate),selfService=[],needsAdmin=[];for(let user of users){if(!rotating.has(user.id))continue;if(String(user.email??"").trim()==="")needsAdmin.push(user.id);else selfService.push(user.id)}return{selfService,needsAdmin}}import{randomBytes as randomBytes5}from"crypto";import{eq as eq47}from"drizzle-orm";function createPasswordAdminRoutes(config,options){let{db,logger:logger2}=config,log4=logger2.scoped(AUTH_LOGIN),passwordToolsEnabled=options?.passwordTools===!0,requireGodmin2=createRequireGodmin(config),routePostAuthAdminRotatePasswords=defineRoute({method:"POST",path:"/auth/admin/rotate-passwords",body:Type.Object({confirm:Type.Optional(Type.Boolean()),protectedRoles:Type.Optional(Type.Array(Type.String())),includeInactive:Type.Optional(Type.Boolean())}),detail:{tags:["Authentication"],summary:"Rotate every password except the ones named",description:"Without `confirm: true` this only reports what it would do. Confirmed, it gives every selected account a different random password, signs those sessions out, and tells nobody the new values \u2014 recovery is the reset link or an administrator."},handler:async(ctx)=>{if(!passwordToolsEnabled)return notEnabled(ctx);let gate=await requireGodmin2(ctx.request);if(!gate.ok)return ctx.set.status=gate.status,{success:!1,message:gate.message};let resolved=resolveAuthTablesForRequest(ctx.request,config),usersTable=resolved.usersTable,body=ctx.body,users=await db?.select().from(usersTable),roleNamesByUser=await loadRoleNames(resolved,users),candidates=users.map((row)=>({id:String(row.id),roleNames:roleNamesByUser.get(String(row.id))??[],isGod:row.isGod===!0,email:row.email??null,isActive:row.isActive})),plan=planPasswordRotation({users:candidates,callerId:gate.callerId,protectedRoles:body.protectedRoles??[],includeInactive:body.includeInactive===!0}),recovery=splitByRecoverability(candidates,plan.rotate);if(body.confirm!==!0)return{success:!0,data:{preview:!0,willRotate:plan.rotate.length,skipped:plan.skipped.length,skippedReasons:countReasons(plan.skipped),canResetThemselves:recovery.selfService.length,needAdministrator:recovery.needsAdmin.length}};if(plan.rotate.length===0)return{success:!0,data:{preview:!1,rotated:0,signedOut:0}};let rotated=0;for(let userId of plan.rotate){let hashed=await hashPassword(randomBytes5(24).toString("base64url"));await db?.update(usersTable).set({password:hashed,updatedAt:new Date}).where(eq47(usersTable.id,userId)),rotated+=1}let signedOut=await revokeSessions(resolved,plan.rotate);return log4.warn("Bulk password rotation",{by:gate.callerId,rotated,signedOut,skipped:plan.skipped.length}),{success:!0,data:{preview:!1,rotated,signedOut,canResetThemselves:recovery.selfService.length,needAdministrator:recovery.needsAdmin.length}}}}),routePostAuthAdminSetUserPassword=defineRoute({method:"POST",path:"/auth/admin/set-user-password",body:Type.Object({userId:Type.String(),password:Type.String()}),detail:{tags:["Authentication"],summary:"Set one account's password",description:"For the person who has no email address and cannot use the reset link. Applies the same strength policy as a self-service change and ends that account\u2019s existing sessions."},handler:async(ctx)=>{if(!passwordToolsEnabled)return notEnabled(ctx);let gate=await requireGodmin2(ctx.request);if(!gate.ok)return ctx.set.status=gate.status,{success:!1,message:gate.message};let resolved=resolveAuthTablesForRequest(ctx.request,config),usersTable=resolved.usersTable,{userId,password:password4}=ctx.body,strength=validatePasswordStrength(password4);if(!strength.valid)return ctx.set.status=400,{success:!1,message:strength.errors?.join(" ")??"Password is too weak"};let rows=await db?.select().from(usersTable).where(eq47(usersTable.id,userId)).limit(1);if(!rows||rows.length===0)return ctx.set.status=404,{success:!1,message:"User not found"};await db?.update(usersTable).set({password:await hashPassword(password4),updatedAt:new Date,...clearedLockoutState()}).where(eq47(usersTable.id,userId));let signedOut=await revokeSessions(resolved,[userId]);return log4.warn("Password set by administrator",{by:gate.callerId,userId,signedOut}),{success:!0,data:{signedOut,lockoutCleared:!0}}}});return collectRoutes([{routes:[routePostAuthAdminRotatePasswords,routePostAuthAdminSetUserPassword]}]);function notEnabled(ctx){return ctx.set.status=404,{success:!1,message:"Not found"}}async function loadRoleNames(resolved,users){let byUser=new Map,userRoles=resolved.userRolesTable,roles=resolved.rolesTable;if(!db||!userRoles||!roles||users.length===0)return byUser;let roleRows=await db.select().from(roles),roleNameById=new Map(roleRows.map((row)=>[String(row.id),String(row.name??"")])),links=await db.select().from(userRoles);for(let link of links){let userId=String(link.userId??""),name2=roleNameById.get(String(link.roleId??""));if(!userId||!name2)continue;byUser.set(userId,[...byUser.get(userId)??[],name2])}return byUser}async function revokeSessions(resolved,userIds){return revokeUserSessions({db,sessionsTable:resolved.sessionsTable,onError:(error3)=>log4.error("Sessions could not be revoked after a password change",{error:error3})},userIds)}}function countReasons(skipped){let counts={};for(let item of skipped)counts[item.reason]=(counts[item.reason]??0)+1;return counts}var init_passwordAdmin=__esm(()=>{init_esm3();init_scopes();init_routeCollector();init_utils6();init_requireGodmin();init_revokeUserSessions()});import{eq as eq48}from"drizzle-orm";function createUserStateRoutes(config){let{db,logger:logger2}=config,log4=logger2.scoped(AUTH_LOGIN),requireGodmin2=createRequireGodmin(config);async function loadTarget(request,userId){let resolved=resolveAuthTablesForRequest(request,config),usersTable=resolved.usersTable;if(!db||!usersTable)return{ok:!1,status:500,message:"Database not configured"};let user=(await db.select().from(usersTable).where(eq48(usersTable.id,userId)).limit(1))?.[0];if(!user)return{ok:!1,status:404,message:"User not found"};return{ok:!0,usersTable,sessionsTable:resolved.sessionsTable??null,user}}async function writeState(usersTable,userId,changes){await db?.update(usersTable).set({...changes,updatedAt:new Date}).where(eq48(usersTable.id,userId))}let routePostAuthAdminLockUser=defineRoute({method:"POST",path:"/auth/admin/lock-user",body:Type.Object({userId:Type.String(),until:Type.Optional(Type.String())}),detail:{tags:["Authentication"],summary:"Lock one account",description:"Refuses further login attempts. `until` is an ISO timestamp for a temporary lock; omit it to lock indefinitely. Cannot be used on the calling administrator."},handler:async(ctx)=>{let gate=await requireGodmin2(ctx.request);if(!gate.ok)return ctx.set.status=gate.status,{success:!1,message:gate.message};let{userId,until}=ctx.body,target2=await loadTarget(ctx.request,userId);if(!target2.ok)return ctx.set.status=target2.status,{success:!1,message:target2.message};if(userId===gate.callerId)return ctx.set.status=400,{success:!1,message:"An administrator cannot lock their own account"};let lockedUntil=until?new Date(until):null;if(until&&Number.isNaN(lockedUntil?.getTime()))return ctx.set.status=400,{success:!1,message:"`until` is not a valid date"};await writeState(target2.usersTable,userId,{isLocked:!0,lockedUntil});let signedOut=await revokeUserSessions({db,sessionsTable:target2.sessionsTable,onError:(error3)=>log4.error("Sessions could not be revoked after locking an account",{error:error3,userId})},[userId]);return log4.warn("Account locked by administrator",{by:gate.callerId,userId,until:lockedUntil,signedOut}),{success:!0,data:{isLocked:!0,lockedUntil,signedOut}}}}),routePostAuthAdminUnlockUser=defineRoute({method:"POST",path:"/auth/admin/unlock-user",body:Type.Object({userId:Type.String()}),detail:{tags:["Authentication"],summary:"Release one account from lockout",description:"Clears isLocked, lockedUntil AND failedLoginAttempts together, without touching the password. Clearing only isLocked is not a release: the login route sends any account at or above the captcha threshold through the captcha gate before it checks the password, and the counter only clears on a login that succeeds."},handler:async(ctx)=>{let gate=await requireGodmin2(ctx.request);if(!gate.ok)return ctx.set.status=gate.status,{success:!1,message:gate.message};let{userId}=ctx.body,target2=await loadTarget(ctx.request,userId);if(!target2.ok)return ctx.set.status=target2.status,{success:!1,message:target2.message};let wasLocked=target2.user.isLocked===!0,clearedFailedAttempts=Number(target2.user.failedLoginAttempts??0);return await writeState(target2.usersTable,userId,clearedLockoutState()),log4.warn("Account lockout cleared by administrator",{by:gate.callerId,userId,wasLocked,clearedFailedAttempts}),{success:!0,data:{wasLocked,clearedFailedAttempts}}}}),routePostAuthAdminMarkEmailVerified=defineRoute({method:"POST",path:"/auth/admin/mark-email-verified",body:Type.Object({userId:Type.String()}),detail:{tags:["Authentication"],summary:"Mark one account as email-verified",description:"For an installation with no working mail transport, where the verification message can never arrive. Idempotent: an account that is already verified keeps its original timestamp."},handler:async(ctx)=>{let gate=await requireGodmin2(ctx.request);if(!gate.ok)return ctx.set.status=gate.status,{success:!1,message:gate.message};let{userId}=ctx.body,target2=await loadTarget(ctx.request,userId);if(!target2.ok)return ctx.set.status=target2.status,{success:!1,message:target2.message};let alreadyVerified=Boolean(target2.user.verifiedAt),verifiedAt=alreadyVerified?target2.user.verifiedAt:new Date;if(!alreadyVerified)await writeState(target2.usersTable,userId,{verifiedAt,emailVerified:!0});return log4.warn("Email marked verified by administrator",{by:gate.callerId,userId,alreadyVerified}),{success:!0,data:{verifiedAt,alreadyVerified}}}});return collectRoutes([{routes:[routePostAuthAdminLockUser,routePostAuthAdminUnlockUser,routePostAuthAdminMarkEmailVerified]}])}var init_userState=__esm(()=>{init_esm3();init_scopes();init_routeCollector();init_requireGodmin();init_revokeUserSessions()});import{and as and24,count as count2,eq as eq49,sql as sql10}from"drizzle-orm";function createApiKeyRoutes(app,config,apiKeysConfig){let basePath=apiKeysConfig.route||"/auth/api-keys",apiKeysTable=config.apiKeysTable,{db,logger:logger2}=config;if(!apiKeysTable||!db){logger2.warn("[API_KEYS] api_keys table or database not available. Skipping API key routes.");return}let getResolvedConfig=(request)=>{let resolved=resolveAuthTablesForRequest(request,config),reqApiKeysTable=resolved.apiKeysTable??apiKeysTable;return{config:{...config,usersTable:resolved.usersTable??config.usersTable,userRolesTable:resolved.userRolesTable,rolesTable:resolved.rolesTable,roleClaimsTable:resolved.roleClaimsTable,claimsTable:resolved.claimsTable,apiKeysTable:reqApiKeysTable,schemaTables:resolved.schemaTables},apiKeysTable:reqApiKeysTable}},keyPrefix=apiKeysConfig.keyPrefix||"nk_live",maxKeysPerUser=apiKeysConfig.maxKeysPerUser||10,preventManagement=apiKeysConfig.preventApiKeyManagement??!1,routePostRoot=defineRoute({method:"POST",path:"/",handler:async({request,set:set2,body:rawBody})=>{let resolved=getResolvedConfig(request),reqApiKeysTable=resolved.apiKeysTable,reqConfig=resolved.config;if(preventManagement)return set2.status=403,{isSuccess:!1,message:"API key management is disabled via configuration",data:null};if(request.headers.get("x-auth-type")==="api_key")return set2.status=403,{isSuccess:!1,message:"API keys cannot be created using another API key",data:null};let userId=request.headers.get("x-user-id");if(!userId)return set2.status=401,{isSuccess:!1,message:"Unauthenticated",data:null};let body;try{body=rawBody??{}}catch{return set2.status=400,{isSuccess:!1,message:"Invalid request body",data:null}}if(!body.name||typeof body.name!=="string"||body.name.trim().length===0)return set2.status=400,{isSuccess:!1,message:"Name is required",data:null};if(body.ownerType==="application"){if(!(apiKeysConfig.allowApplicationKeys??!0))return set2.status=403,{isSuccess:!1,message:"Application API keys are not allowed",data:null};if(!body.applicationName||body.applicationName.trim().length===0)return set2.status=400,{isSuccess:!1,message:"Application name is required for application keys",data:null}}if(((await db.select({count:count2()}).from(reqApiKeysTable).where(and24(eq49(col13(reqApiKeysTable,"userId"),userId),eq49(col13(reqApiKeysTable,"isActive"),!0))))[0]?.count||0)>=maxKeysPerUser)return set2.status=400,{isSuccess:!1,message:`Maximum API key limit reached (${maxKeysPerUser})`,data:null};let userRoles=await getUserRoleNames(reqConfig,userId),userClaims=await getUserClaimActions(reqConfig,userId),isGodmin4=userRoles.some((r)=>isGodminRole(r)),requestedRoles=body.allowedRoles||[],requestedClaims=body.allowedClaims||[],requestedScopes=body.allowedScopes||[],stripGodmin=(roles)=>roles.filter((r)=>!isGodminRole(r)),finalRoles,finalClaims;if(isGodmin4)finalRoles=requestedRoles.length>0?stripGodmin(requestedRoles):[],finalClaims=requestedClaims.length>0?requestedClaims:userClaims;else{let validRoles=intersectPermissions(userRoles,requestedRoles),validClaims=intersectPermissions(userClaims,requestedClaims);if(requestedRoles.length>0&&validRoles.length!==requestedRoles.length){let disallowed=requestedRoles.filter((r)=>!userRoles.includes(r));return set2.status=403,{isSuccess:!1,message:`Cannot assign roles you do not have: ${disallowed.join(", ")}`,data:null}}if(requestedClaims.length>0&&validClaims.length!==requestedClaims.length){let disallowed=requestedClaims.filter((c)=>!userClaims.includes(c));return set2.status=403,{isSuccess:!1,message:`Cannot assign claims you do not have: ${disallowed.join(", ")}`,data:null}}if(requestedScopes.length>0){let userScopes=await getUserScopes(reqConfig,userId);if(intersectPermissions(userScopes,requestedScopes).length!==requestedScopes.length){let disallowed=requestedScopes.filter((s)=>!userScopes.includes(s));return set2.status=403,{isSuccess:!1,message:`Cannot assign scopes you do not have: ${disallowed.join(", ")}`,data:null}}}finalRoles=stripGodmin(requestedRoles.length>0?validRoles:userRoles),finalClaims=requestedClaims.length>0?validClaims:userClaims}let expiresAt=null,expiresInStr=body.expiresIn||apiKeysConfig.defaultExpiresIn;if(expiresInStr){let seconds=parseTimeToSeconds2(expiresInStr);expiresAt=new Date(Date.now()+seconds*1000)}let generated=generateApiKey(keyPrefix);try{let createdId=(await db.insert(reqApiKeysTable).values({userId,name:body.name.trim(),description:body.description?.trim()||null,keyHash:generated.keyHash,keyPreview:generated.keyPreview,ownerType:body.ownerType||"personal",applicationName:body.applicationName?.trim()||null,expiresAt,isActive:!0,usageCount:0,createdAt:new Date,updatedAt:new Date}).returning())[0]?.id;if(finalRoles.length>0||finalClaims.length>0||requestedScopes.length>0){let jsonbUpdate={};if(finalRoles.length>0)jsonbUpdate.allowedRoles=JSON.stringify(finalRoles);if(finalClaims.length>0)jsonbUpdate.allowedClaims=JSON.stringify(finalClaims);if(requestedScopes.length>0)jsonbUpdate.allowedScopes=JSON.stringify(requestedScopes);await db.update(reqApiKeysTable).set(jsonbUpdate).where(eq49(col13(reqApiKeysTable,"keyHash"),generated.keyHash))}return logger2.info("[API_KEYS] API key created",{keyId:createdId,userId,ownerType:body.ownerType||"personal",applicationName:body.applicationName,rolesCount:finalRoles.length,claimsCount:finalClaims.length,expiresAt:expiresAt?.toISOString()||"never"}),{isSuccess:!0,message:"API key created successfully. Save this key \u2014 it will not be shown again.",data:{id:createdId,key:generated.rawKey,keyPreview:generated.keyPreview,name:body.name.trim(),ownerType:body.ownerType||"personal",applicationName:body.applicationName||null,allowedRoles:finalRoles,allowedClaims:finalClaims,allowedScopes:requestedScopes,expiresAt:expiresAt?.toISOString()||null}}}catch(err){let errDetail;try{errDetail=JSON.stringify(err,Object.getOwnPropertyNames(err))}catch{errDetail=err instanceof Error?err.message:String(err)}return logger2.error("[API_KEYS] Failed to create API key",{error:errDetail,userId}),set2.status=500,{isSuccess:!1,message:"Failed to create API key",data:null}}}}),routeGetRoot=defineRoute({method:"GET",path:"/",detail:{tags:["API keys"],summary:"List the caller's API keys",description:"Every key the caller owns, revoked ones included \u2014 a revoked key is history, not "+"garbage, and it is the only place `revokedAt` / `revokedReason` can be read. Filter on `isActive` for the live set; that is also what `maxKeysPerUser` counts."},handler:async({request,set:set2})=>{let{apiKeysTable:reqApiKeysTable}=getResolvedConfig(request),userId=request.headers.get("x-user-id");if(!userId)return set2.status=401,{isSuccess:!1,message:"Unauthenticated",data:null};let items=(await db.select().from(reqApiKeysTable).where(eq49(col13(reqApiKeysTable,"userId"),userId)).orderBy(sql10`created_at DESC`)).map((row)=>sanitizeKeyForResponse(row));return{isSuccess:!0,data:{items,totalCount:items.length}}}}),routeGetId=defineRoute({method:"GET",path:"/:id",detail:{tags:["API keys"],summary:"Read one API key",description:"A REVOKED key still answers 200 here \u2014 revoking does not delete the row. Read "+"`isActive` / `revokedAt` rather than the status to decide whether the key is live. 404 means no such key, or a key belonging to somebody else."},handler:async({params,request,set:set2})=>{let{apiKeysTable:reqApiKeysTable}=getResolvedConfig(request),userId=request.headers.get("x-user-id");if(!userId)return set2.status=401,{isSuccess:!1,message:"Unauthenticated",data:null};let keyId=params.id,record3=(await db.select().from(reqApiKeysTable).where(and24(eq49(col13(reqApiKeysTable,"id"),keyId),eq49(col13(reqApiKeysTable,"userId"),userId))).limit(1))[0];if(!record3)return set2.status=404,{isSuccess:!1,message:"API key not found",data:null};return{isSuccess:!0,data:sanitizeKeyForResponse(record3)}}}),routePatchId=defineRoute({method:"PATCH",path:"/:id",handler:async({params,request,set:set2,body:rawBody})=>{let resolved=getResolvedConfig(request),reqApiKeysTable=resolved.apiKeysTable,reqConfig=resolved.config;if(preventManagement)return set2.status=403,{isSuccess:!1,message:"API key management is disabled via configuration",data:null};if(request.headers.get("x-auth-type")==="api_key")return set2.status=403,{isSuccess:!1,message:"API keys cannot be modified using another API key",data:null};let userId=request.headers.get("x-user-id");if(!userId)return set2.status=401,{isSuccess:!1,message:"Unauthenticated",data:null};let keyId=params.id,body;try{body=rawBody??{}}catch{return set2.status=400,{isSuccess:!1,message:"Invalid request body",data:null}}let existing=(await db.select().from(reqApiKeysTable).where(and24(eq49(col13(reqApiKeysTable,"id"),keyId),eq49(col13(reqApiKeysTable,"userId"),userId))).limit(1))[0];if(!existing)return set2.status=404,{isSuccess:!1,message:"API key not found",data:null};if(existing.revokedAt)return set2.status=400,{isSuccess:!1,message:"Cannot modify a revoked API key",data:null};let userRoles=await getUserRoleNames(reqConfig,userId),userClaims=await getUserClaimActions(reqConfig,userId),updatePayload={updatedAt:new Date};if(body.name!==void 0){if(typeof body.name!=="string"||body.name.trim().length===0)return set2.status=400,{isSuccess:!1,message:"Name cannot be empty",data:null};updatePayload.name=body.name.trim()}if(body.description!==void 0)updatePayload.description=body.description?.trim()||null;if(body.allowedRoles!==void 0){if(intersectPermissions(userRoles,body.allowedRoles).length!==body.allowedRoles.length){let disallowed=body.allowedRoles.filter((r)=>!userRoles.includes(r));return set2.status=403,{isSuccess:!1,message:`Cannot assign roles you do not have: ${disallowed.join(", ")}`,data:null}}updatePayload.allowedRoles=body.allowedRoles.filter((r)=>!isGodminRole(r))}if(body.allowedClaims!==void 0){if(intersectPermissions(userClaims,body.allowedClaims).length!==body.allowedClaims.length){let disallowed=body.allowedClaims.filter((c)=>!userClaims.includes(c));return set2.status=403,{isSuccess:!1,message:`Cannot assign claims you do not have: ${disallowed.join(", ")}`,data:null}}updatePayload.allowedClaims=body.allowedClaims}if(body.allowedScopes!==void 0){if(body.allowedScopes.length>0){let userScopes=await getUserScopes(reqConfig,userId);if(intersectPermissions(userScopes,body.allowedScopes).length!==body.allowedScopes.length){let disallowed=body.allowedScopes.filter((s)=>!userScopes.includes(s));return set2.status=403,{isSuccess:!1,message:`Cannot assign scopes you do not have: ${disallowed.join(", ")}`,data:null}}}updatePayload.allowedScopes=body.allowedScopes}try{await db.update(reqApiKeysTable).set(updatePayload).where(eq49(col13(reqApiKeysTable,"id"),keyId)),logger2.info("[API_KEYS] API key updated",{keyId,userId});let updated=await db.select().from(reqApiKeysTable).where(eq49(col13(reqApiKeysTable,"id"),keyId)).limit(1);return{isSuccess:!0,message:"API key updated successfully",data:sanitizeKeyForResponse(updated[0])}}catch(err){return logger2.error("[API_KEYS] Failed to update API key",{error:err,keyId,userId}),set2.status=500,{isSuccess:!1,message:"Failed to update API key",data:null}}}}),revokeApiKey=async({params,request,set:set2,body:rawBody})=>{let{apiKeysTable:reqApiKeysTable}=getResolvedConfig(request);if(request.headers.get("x-auth-type")==="api_key")return set2.status=403,{isSuccess:!1,message:"API keys cannot be revoked using another API key",data:null};let userId=request.headers.get("x-user-id");if(!userId)return set2.status=401,{isSuccess:!1,message:"Unauthenticated",data:null};let keyId=params.id,body={};try{if(rawBody)body=rawBody}catch{}let record3=(await db.select().from(reqApiKeysTable).where(and24(eq49(col13(reqApiKeysTable,"id"),keyId),eq49(col13(reqApiKeysTable,"userId"),userId))).limit(1))[0];if(!record3)return set2.status=404,{isSuccess:!1,message:"API key not found",data:null};if(record3.revokedAt)return set2.status=400,{isSuccess:!1,message:"API key is already revoked",data:null};let revokedAt=new Date,revokedReason=body.reason||"user_revoked";try{await db.update(reqApiKeysTable).set({isActive:!1,revokedAt,revokedReason,updatedAt:revokedAt}).where(eq49(col13(reqApiKeysTable,"id"),keyId)),logger2.info("[API_KEYS] API key revoked",{keyId,userId,reason:revokedReason});let revoked=(await db.select().from(reqApiKeysTable).where(eq49(col13(reqApiKeysTable,"id"),keyId)).limit(1))[0]??{...record3,isActive:!1,revokedAt,revokedReason,updatedAt:revokedAt};return{isSuccess:!0,message:"API key revoked. It can no longer authenticate. This is a revoke, not a delete \u2014 "+`the record is retained as history and still reads at GET ${basePath}/${keyId}.`,data:sanitizeKeyForResponse(revoked)}}catch(err){return logger2.error("[API_KEYS] Failed to revoke API key",{error:err,keyId,userId}),set2.status=500,{isSuccess:!1,message:"Failed to revoke API key",data:null}}},revokeDetail=(summary)=>({tags:["API keys"],summary,description:"Revokes the key: it stops authenticating immediately, on this request. It is NOT a "+"delete \u2014 the row is retained as the audit record (`revokedAt`, `revokedReason`, "+"`usageCount`, `lastUsedAt`), so `GET /:id` still answers 200 with `isActive:false` and the list still carries it. The answer carries the revoked record. Revoking an "+"already-revoked key answers 400, not 404 \u2014 the row is still there. Revoking is "+"deliberately NOT blocked by `preventApiKeyManagement`: being unable to kill a leaked key because management is off would be worse than the rule it enforces."}),routePostIdRevoke=defineRoute({method:"POST",path:"/:id/revoke",detail:revokeDetail("Revoke an API key"),handler:(ctx)=>revokeApiKey(ctx)}),routeDeleteId=defineRoute({method:"DELETE",path:"/:id",detail:revokeDetail("Revoke an API key (alias of POST /:id/revoke)"),handler:(ctx)=>revokeApiKey(ctx)});mountRoutes(app,[{prefix:basePath,routes:[routePostRoot,routeGetRoot,routeGetId,routePatchId,routePostIdRevoke,routeDeleteId]}]),logger2.info(`[API_KEYS] Routes registered at ${basePath}`)}var col13=(table,name2)=>table[name2],getUserRoleNames=async(config,userId)=>{if(!config.db||!config.userRolesTable||!config.rolesTable)return[];return(await config.db.select({name:col13(config.rolesTable,"name")}).from(config.userRolesTable).innerJoin(config.rolesTable,eq49(col13(config.rolesTable,"id"),col13(config.userRolesTable,"roleId"))).where(eq49(col13(config.userRolesTable,"userId"),userId))).map((r)=>r.name).filter((n)=>n!==void 0)},getUserClaimActions=async(config,userId)=>{if(!config.db||!config.userRolesTable||!config.roleClaimsTable||!config.claimsTable)return[];let rows=await config.db.select({action:col13(config.claimsTable,"action")}).from(config.userRolesTable).innerJoin(config.roleClaimsTable,eq49(col13(config.roleClaimsTable,"roleId"),col13(config.userRolesTable,"roleId"))).innerJoin(config.claimsTable,eq49(col13(config.claimsTable,"id"),col13(config.roleClaimsTable,"claimId"))).where(eq49(col13(config.userRolesTable,"userId"),userId)),uniqueActions=new Set(rows.map((r)=>r.action));return Array.from(uniqueActions).filter((a)=>a!==void 0)},getUserScopes=async(config,userId)=>{if(!config.db||!config.userRolesTable||!config.roleClaimsTable)return[];let rows=await config.db.select({scope:col13(config.roleClaimsTable,"scope")}).from(config.userRolesTable).innerJoin(config.roleClaimsTable,eq49(col13(config.roleClaimsTable,"roleId"),col13(config.userRolesTable,"roleId"))).where(eq49(col13(config.userRolesTable,"userId"),userId)),uniqueScopes=new Set(rows.map((r)=>r.scope).filter((s)=>s!==null&&s!==void 0&&s.length>0));return Array.from(uniqueScopes)},sanitizeKeyForResponse=(record3)=>{let{keyHash:_keyHash,...safe}=record3;return safe};var init_apiKeys=__esm(()=>{init_ApiKey();init_GodminSetup();init_routeMount();init_utils5()});function createCaptchaRoutes(config){let{captchaService,logger:logger2,basePath="/auth/captcha"}=config;if(!captchaService.isEnabled())return collectRoutes();let warnedUnattributable=!1,warnUnattributableOnce=()=>{if(warnedUnattributable)return;warnedUnattributable=!0,logger2.warn("[CAPTCHA] No client address on the request \u2014 the per-IP generate limit is OFF. "+"Every caller would otherwise share one bucket, which locks out an entire office behind one egress. Configure the proxy or ingress to forward X-Forwarded-For.")},routeGetBasepathGenerate=defineRoute({method:"GET",path:"/generate",query:GenerateCaptchaQuerySchema,detail:{tags:["Captcha"],summary:"Generate Captcha",description:"Generate a new captcha challenge"},handler:async(ctx)=>{let{type:type4,difficulty}=ctx.query,ipAddress=ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||ctx.request.headers.get("x-real-ip")?.trim()||ctx.request.headers.get("cf-connecting-ip")?.trim()||void 0;if(!ipAddress)warnUnattributableOnce();let result=await captchaService.generate(type4,difficulty,ipAddress);if(result.rateLimited)return ctx.set.status=429,{success:!1,message:result.message??"Too many requests. Please try again later."};return logger2.info("[CAPTCHA] Challenge generated via endpoint",{challengeId:result.challengeId,type:result.type,ipAddress}),{success:!0,data:{challengeId:result.challengeId,type:result.type,question:result.question,expiresAt:result.expiresAt,...result.imageData?{imageData:result.imageData}:{},...result.puzzleData?{puzzleData:{pieces:result.puzzleData.pieces}}:{}}}}}),routePostBasepathValidate=defineRoute({method:"POST",path:"/validate",body:ValidateCaptchaBodySchema,detail:{tags:["Captcha"],summary:"Validate Captcha",description:"Validate a captcha answer"},handler:async(ctx)=>{let{challengeId,answer}=ctx.body,result=await captchaService.validate(challengeId,answer);if(!result.valid)ctx.set.status=400;return result}});return collectRoutes([{prefix:basePath,routes:[routeGetBasepathGenerate,routePostBasepathValidate]}])}var GenerateCaptchaQuerySchema,ValidateCaptchaBodySchema;var init_captcha=__esm(()=>{init_esm3();init_routeCollector();GenerateCaptchaQuerySchema=Type.Object({type:Type.Optional(Type.Union([Type.Literal("math"),Type.Literal("image"),Type.Literal("puzzle"),Type.Literal("text")])),difficulty:Type.Optional(Type.Union([Type.Literal("easy"),Type.Literal("medium"),Type.Literal("hard")]))}),ValidateCaptchaBodySchema=Type.Object({challengeId:Type.String(),answer:Type.String()})});import{eq as eq50}from"drizzle-orm";function createCheckRoute(config,checkConfig){let{db,logger:logger2}=config,route=checkConfig.route||"/auth/check",routePostCheck=defineRoute({method:"POST",path:"/",detail:{tags:["Auth"],summary:"Check authorization for a given entity and method",description:"Allows consumer/resource servers to perform full claim checks (including scope) against the IDP's authorization system."},handler:async(ctx)=>{let{body,request,set:set2}=ctx,userId=request.headers.get("x-user-id");if(!userId)return set2.status=401,{isSuccess:!1,message:"Unauthenticated",data:null};if(!db)return set2.status=503,{isSuccess:!1,message:"Authorization service unavailable",data:null};let{schemaTables}=resolveAuthTablesForRequest(request,config);if(!schemaTables)return set2.status=503,{isSuccess:!1,message:"Authorization tables not available",data:null};let{entity,method,fields,relations}=body;if(!entity||!method)return set2.status=400,{isSuccess:!1,message:"entity and method are required",data:null};let result=await checkAuthorization({userId,method,entity,requestedFields:fields,requestedRelations:relations,db,schemaTables,logger:logger2,getUserData:async()=>{let usersTbl=schemaTables[AUTHORIZATION_TABLE_KEYS.users];if(!usersTbl||!db)return;let idCol=usersTbl.id;if(!idCol)return;return(await db.select().from(usersTbl).where(eq50(idCol,userId)).limit(1))[0]}});return{isSuccess:!0,message:result.authorized?"Authorized":"Forbidden",data:{authorized:result.authorized,reason:result.reason,allowedFields:result.allowedFields,allowedRelations:result.allowedRelations,scopeFilters:result.scopeFilters}}}});return collectRoutes([{prefix:route,routes:[routePostCheck]}])}var init_check=__esm(()=>{init_Authorization();init_routeCollector()});import{sql as sql11}from"drizzle-orm";function createEmailVerificationRoutes(config){let{authConfig,registerConfig,getEmailService,appName}=config,{db,logger:logger2}=authConfig;if(!registerConfig.enabled||!registerConfig.emailVerification?.enabled)return collectRoutes();let verifyRoute="/verify-email",resendRoute="/resend-verification",routeGetVerifyroute=defineRoute({method:"GET",path:"/",query:Type.Object({token:Type.String()}),detail:{tags:["Authentication"],summary:"Verify Email",description:"Verify user email address using the verification token"},handler:async(ctx)=>{let{usersTable}=resolveAuthTablesForRequest(ctx.request,authConfig);if(!db||!usersTable)return ctx.set.status=500,{success:!1,message:"Database not configured"};let token=ctx.query.token;if(!token)return ctx.set.status=400,{success:!1,message:"Verification token is required"};let users=await db.select().from(usersTable).where(sql11`email_verification_token = ${hashVerificationToken(token)}`).limit(1);if(users.length===0)return logger2.warn("[AUTH] Email verification failed - invalid token"),ctx.set.status=400,{success:!1,message:"Invalid or expired verification token"};let user=users[0],tokenExpiresAt=user.emailVerificationTokenExpiresAt;if(tokenExpiresAt&&new Date>new Date(tokenExpiresAt))return logger2.warn("[AUTH] Email verification failed - token expired",{userId:user.id,email:user.email}),ctx.set.status=400,{success:!1,message:"Verification token has expired. Please request a new one."};await db.update(usersTable).set({verifiedAt:new Date,emailVerificationToken:null,emailVerificationTokenExpiresAt:null}).where(sql11`id = ${user.id}`),logger2.info("[AUTH] Email verified successfully",{userId:user.id,email:user.email});let emailService=getEmailService?.()??null;if(registerConfig.emailVerification?.templates?.welcome?.enabled&&emailService?.isAvailable())emailService.sendWelcomeEmail(user.email,user.email.split("@")[0]||"User",appName||"Nucleus").catch((err)=>{logger2.error("[AUTH] Failed to send welcome email after verification",{email:user.email,error:err})});return{success:!0,message:"Email verified successfully. You can now log in.",data:{redirectUrl:registerConfig.emailVerification?.redirectUrl||"/login",verified:!0}}}}),routePostResendroute=defineRoute({method:"POST",path:"/",body:Type.Object({email:Type.String({format:"email"})}),detail:{tags:["Authentication"],summary:"Resend Verification Email",description:"Resend the email verification link to the user"},handler:async(ctx)=>{let{usersTable}=resolveAuthTablesForRequest(ctx.request,authConfig);if(!db||!usersTable)return ctx.set.status=500,{success:!1,message:"Database not configured"};let emailService=getEmailService?.()??null;if(!emailService?.isAvailable())return ctx.set.status=503,{success:!1,message:"Email service not available"};let{email}=ctx.body,antiEnum=authConfig.authentication?.preventUserEnumeration===!0,uniformResend={success:!0,message:"If your email is registered and not yet verified, a verification email has been sent."},users=await db.select().from(usersTable).where(sql11`email = ${email}`).limit(1);if(users.length===0)return antiEnum?uniformResend:{success:!0,message:"If your email is registered, you will receive a verification email."};let user=users[0];if(user.verifiedAt){if(antiEnum)return uniformResend;return ctx.set.status=409,{success:!1,message:"Email is already verified"}}let maxAttempts=registerConfig.emailVerification?.maxResendAttempts||3,currentAttempts=user.emailVerificationAttempts||0;if(currentAttempts>=maxAttempts){if(logger2.warn("[AUTH] Resend verification failed - max attempts reached",{email,attempts:currentAttempts}),antiEnum)return uniformResend;return ctx.set.status=429,{success:!1,message:"Maximum resend attempts reached. Please contact support.",data:{maxAttemptsReached:!0,attemptsUsed:currentAttempts,maxAttempts}}}let resendCooldown=registerConfig.emailVerification?.resendCooldown||"60s",cooldownMs=parseTimeToMs(resendCooldown),lastSentAt=user.emailVerificationSentAt;if(lastSentAt){let timeSinceLastSent=Date.now()-new Date(lastSentAt).getTime();if(timeSinceLastSent<cooldownMs){let waitSeconds=Math.ceil((cooldownMs-timeSinceLastSent)/1000);if(antiEnum)return uniformResend;return ctx.set.status=429,{success:!1,message:`Please wait ${waitSeconds} seconds before requesting another verification email.`,data:{cooldownRemaining:waitSeconds,canResendAt:new Date(Date.now()+waitSeconds*1000).toISOString()}}}}let verificationToken=generateVerificationToken(),tokenExpiresIn=registerConfig.emailVerification?.tokenExpiresIn||"24h",tokenExpiresAt=new Date(Date.now()+parseTimeToMs(tokenExpiresIn));await db.update(usersTable).set({emailVerificationToken:hashVerificationToken(verificationToken),emailVerificationTokenExpiresAt:tokenExpiresAt,emailVerificationSentAt:new Date,emailVerificationAttempts:currentAttempts+1}).where(sql11`id = ${user.id}`);let verificationLink=buildEmailActionLink({request:ctx.request,configuredUrl:registerConfig.emailVerification?.redirectUrl,path:"/verify-email",query:{token:verificationToken},allowedOrigins:authConfig.authentication?.trustedAppOrigins}),result=await emailService.sendVerificationEmail(email,email.split("@")[0]||"User",verificationLink,appName||"Nucleus");if(result.success){logger2.info("[AUTH] Verification email resent",{email});let cooldownSeconds=Math.ceil(cooldownMs/1000);if(antiEnum)return uniformResend;return{success:!0,message:"Verification email sent. Please check your inbox.",data:{cooldownSeconds,canResendAt:new Date(Date.now()+cooldownMs).toISOString(),attemptsRemaining:maxAttempts-(currentAttempts+1)}}}if(logger2.error("[AUTH] Failed to resend verification email",{email,error:result.error}),antiEnum)return uniformResend;return{success:!1,message:"Failed to send verification email. Please try again later."}}});return collectRoutes([{prefix:verifyRoute,routes:[routeGetVerifyroute]},{prefix:resendRoute,routes:[routePostResendroute]}])}var init_emailVerification=__esm(()=>{init_esm3();init_routeCollector();init_utils6()});import crypto7 from"crypto";function generateMagicToken(){return crypto7.randomBytes(32).toString("hex")}function hashToken(token,purpose){let input=purpose?`${purpose}:${token}`:token;return crypto7.createHash("sha256").update(input).digest("hex")}function parseTimeToMs3(time2){let match=time2.match(/^(\d+)(s|m|h|d)$/);if(!match?.[1]||!match[2])return 900000;let value2=parseInt(match[1],10);switch(match[2]){case"s":return value2*1000;case"m":return value2*60*1000;case"h":return value2*60*60*1000;case"d":return value2*24*60*60*1000;default:return 900000}}var TOKEN_PURPOSE;var init_utils8=__esm(()=>{TOKEN_PURPOSE={magicLink:"magic-link",invite:"invite"}});import{eq as eq51}from"drizzle-orm";function createInviteRoute(config,inviteConfig,getEmailService,storeMagicToken,appName,getMagicToken){let{db,logger:logger2}=config,route=inviteConfig.route||"/auth/invite",expiresIn=inviteConfig.tokenExpiresIn||"7d",redirectUrl=inviteConfig.redirectUrl||"";if(!inviteConfig.enabled)return collectRoutes();let declared=[],routePostRoute=defineRoute({method:"POST",path:"/",body:InviteBodySchema,detail:{tags:["Authentication"],summary:"Invite User",description:"Invite a new user by sending them an email with a magic link to set their password"},handler:async(ctx)=>{let resolved=resolveAuthTablesForRequest(ctx.request,config),{usersTable}=resolved;if(!db||!usersTable)return ctx.set.status=500,{success:!1,message:"Database not configured"};let emailService=getEmailService?.();if(!emailService?.isAvailable())return logger2.error("[AUTH] Invite requested but email service not available"),ctx.set.status=503,{success:!1,message:"Email service not available"};let{email:rawEmail}=ctx.body,email=rawEmail.toLowerCase(),antiEnum=config.authentication?.preventUserEnumeration===!0,uniformInvite={success:!0,message:"If the email address is eligible, an invitation has been sent."},existingUsers=await db.select().from(usersTable).where(eq51(usersTable.email,email)).limit(1),userId;if(existingUsers.length>0){let existingUser=existingUsers[0];if(existingUser.verifiedAt||existingUser.password){if(logger2.warn("[AUTH] Invite failed - user already verified",{email}),antiEnum)return uniformInvite;return ctx.set.status=409,{success:!1,message:"User with this email is already verified"}}userId=existingUser.id,logger2.info("[AUTH] Resending invitation to existing unverified user",{userId,email})}else{let insertValues={email,password:null,emailVerified:!1,isLocked:!1,createdAt:new Date,updatedAt:new Date},newUser=(await db.insert(usersTable).values(insertValues).returning())[0];if(!newUser)return logger2.error("[AUTH] Failed to create invited user",{email}),ctx.set.status=500,{success:!1,message:"Failed to create user"};userId=newUser.id,logger2.info("[AUTH] Invited user created",{userId,email});let inviteDefaultRoles=resolveDefaultRoleNames(config.authentication);if(inviteDefaultRoles.length>0)await assignDefaultRoles({db,userId,roleNames:inviteDefaultRoles,rolesTable:resolved.rolesTable??config.rolesTable??null,userRolesTable:resolved.userRolesTable??config.userRolesTable??null,logger:logger2})}let token=generateMagicToken(),tokenHash=hashToken(token,TOKEN_PURPOSE.invite),expiresAt=new Date(Date.now()+parseTimeToMs3(expiresIn)),reqSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0;await storeMagicToken({userId,email,tokenHash,expiresAt},reqSchemaName);let inviteLink=redirectUrl?buildEmailActionLink({request:ctx.request,configuredUrl:redirectUrl,path:extractConfiguredPath(redirectUrl,"/set-password"),query:{token,invite:"true"},allowedOrigins:config.authentication?.trustedAppOrigins}):`/set-password?token=${token}&invite=true`,sendResult=await emailService.sendInvitationEmail(email,inviteLink,appName||"Nucleus");if(!sendResult.success){if(logger2.error("[AUTH] Failed to send invitation email",{email,error:sendResult.error}),antiEnum)return uniformInvite;return{success:!0,message:"User created but failed to send invitation email",data:{id:userId,email}}}if(logger2.info("[AUTH] Invitation email sent",{email,userId}),antiEnum)return uniformInvite;return{success:!0,message:"Invitation sent successfully",data:{id:userId,email}}}});if(declared.push(routePostRoute),getMagicToken){let routePostRouteVerify=defineRoute({method:"POST",path:"/verify",body:Type.Object({token:Type.String()}),detail:{tags:["Authentication"],summary:"Verify Invite Token",description:"Validate an invite token without consuming it. Returns user info if valid."},handler:async(ctx)=>{let{token}=ctx.body;if(!token)return ctx.set.status=400,{success:!1,message:"Token is required"};let tokenHash=hashToken(token,TOKEN_PURPOSE.invite),invSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0,storedToken=await getMagicToken(tokenHash,invSchemaName);if(!storedToken)return logger2.warn("[AUTH] Invalid invite verify token"),ctx.set.status=400,{success:!1,message:"Invalid or expired token"};if(new Date>storedToken.expiresAt)return logger2.warn("[AUTH] Expired invite verify token",{email:storedToken.email}),ctx.set.status=400,{success:!1,message:"Invalid or expired token"};return logger2.info("[AUTH] Invite token verified (not consumed)",{email:storedToken.email,userId:storedToken.userId}),{success:!0,data:{userId:storedToken.userId,email:storedToken.email}}}});declared.push(routePostRouteVerify)}return collectRoutes([{prefix:route,routes:declared}])}var InviteBodySchema;var init_invite=__esm(()=>{init_esm3();init_routeCollector();init_assignDefaultRole();init_utils8();InviteBodySchema=Type.Object({email:Type.String({format:"email"})})});var exports_DeviceTrust={};__export(exports_DeviceTrust,{selectUsableDeviceRow:()=>selectUsableDeviceRow,readDeviceTokenFromCookieHeader:()=>readDeviceTokenFromCookieHeader,mintDeviceToken:()=>mintDeviceToken,isDeviceRowUsable:()=>isDeviceRowUsable,isApprovalMethod:()=>isApprovalMethod,hashDeviceToken:()=>hashDeviceToken,deviceCookieName:()=>deviceCookieName,decideDeviceApproval:()=>decideDeviceApproval,buildDeviceTokenCookie:()=>buildDeviceTokenCookie,buildClearedDeviceTokenCookie:()=>buildClearedDeviceTokenCookie,DEVICE_TOKEN_BYTES:()=>DEVICE_TOKEN_BYTES});import{createHash as createHash5,randomBytes as randomBytes6}from"crypto";function hashDeviceToken(raw){return createHash5("sha256").update(raw).digest("hex")}function mintDeviceToken(){let raw=randomBytes6(DEVICE_TOKEN_BYTES).toString("hex");return{raw,hash:hashDeviceToken(raw)}}function isDeviceRowUsable(row,now){if(!row||row.status!=="trusted")return!1;if((row.revoked_at??row.revokedAt)!=null)return!1;let expRaw=row.expires_at??row.expiresAt;if(expRaw==null)return!1;let exp=expRaw instanceof Date?expRaw:new Date(expRaw);if(Number.isNaN(exp.getTime())||exp<=now)return!1;return!0}function selectUsableDeviceRow(rows,expectedUserId,now){if(!expectedUserId)return null;for(let row of rows??[]){if((row.user_id??row.userId)!==expectedUserId)continue;if(isDeviceRowUsable(row,now))return row}return null}function buildDeviceTokenCookie(raw,opts){let prefix=opts.cookiePrefix??"",name2=`${prefix}${opts.cookieName??"device_token"}`,sameSite=opts.sameSite??"Lax",secure=prefix==="__Secure-"||prefix==="__Host-"||opts.secure!==!1,allowDomain=prefix!=="__Host-",parts=[`${name2}=${raw}`,"Path=/","HttpOnly",`SameSite=${sameSite}`];if(secure)parts.push("Secure");if(allowDomain&&opts.domain)parts.push(`Domain=${opts.domain}`);return parts.push(`Max-Age=${Math.max(0,Math.floor(opts.maxAgeSeconds))}`),parts.join("; ")}function buildClearedDeviceTokenCookie(opts){return buildDeviceTokenCookie("",{...opts,maxAgeSeconds:0})}function decideDeviceApproval(params){let NO_APPROVAL={requiresApproval:!1,deny:!1,mintToken:!1,trustImmediately:!1},TRUST_NOW={requiresApproval:!1,deny:!1,mintToken:!0,trustImmediately:!0};if(params.knownDevice)return NO_APPROVAL;if(params.isImpersonation||!isApprovalMethod(params.loginMethod,params.approvalMethods))return NO_APPROVAL;if(!params.hasValidFingerprint&&params.onUnidentifiable==="deny")return{requiresApproval:!1,deny:!0,mintToken:!1,trustImmediately:!1};if(params.trustNewDevices!==!1||params.isEmailExempt||params.isFirstEverDevice)return TRUST_NOW;return{requiresApproval:!0,deny:!1,mintToken:!0,trustImmediately:!1}}function deviceCookieName(cfg){return`${cfg?.cookiePrefix??""}${cfg?.cookieName??"device_token"}`}function readDeviceTokenFromCookieHeader(cookieHeader,cfg){if(!cookieHeader)return;let name2=deviceCookieName(cfg);for(let part of cookieHeader.split(";")){let eq52=part.indexOf("=");if(eq52<0)continue;if(part.slice(0,eq52).trim()===name2)return part.slice(eq52+1).trim()}return}function isApprovalMethod(method,approvalMethods){if(!method)return!1;for(let m of approvalMethods){if(m===method)return!0;if(m.endsWith(":*")&&method.startsWith(m.slice(0,-1)))return!0}return!1}var DEVICE_TOKEN_BYTES=32;var init_DeviceTrust=()=>{};import{createHash as createHash6}from"crypto";function hashRefreshToken(token){return createHash6("sha256").update(token).digest("hex")}function classifyRefreshToken(input){let grace=input.graceSeconds??DEFAULT_REUSE_GRACE_SECONDS;if(!input.currentHash)return{action:"adopt"};if(input.presentedHash===input.currentHash)return{action:"rotate"};if(input.previousHash&&input.presentedHash===input.previousHash){let rotatedAt=input.previousRotatedAt?Date.parse(input.previousRotatedAt):Number.NaN;if(Number.isFinite(rotatedAt)&&input.nowMs-rotatedAt<=grace*1000)return{action:"grace"};return{action:"revoke",reason:"refresh token was superseded and is being replayed outside the grace window"}}return{action:"revoke",reason:"refresh token does not match this session, current or previous"}}var DEFAULT_REUSE_GRACE_SECONDS=30;var init_reuseDetection=()=>{};async function recordIssuedRefreshToken(sessionId,refreshToken,options){if(!sessionId||!refreshToken)return;try{let result=await updateSession({sessionId,updates:{refreshTokenHash:hashRefreshToken(refreshToken),...options?.previousHash?{previousRefreshTokenHash:options.previousHash,previousRefreshTokenAt:new Date().toISOString()}:{}}});if(!result.success)logger.warn("[AUTH] Could not record the issued refresh token; reuse detection is off",{sessionId,error:result.error})}catch(error3){logger.warn("[AUTH] Could not record the issued refresh token; reuse detection is off",{sessionId,error:error3 instanceof Error?error3.message:String(error3)})}}var init_recordIssued=__esm(()=>{init_Update();init_Logger2();init_reuseDetection()});async function resolveUserIdByIdentifier(input){let value2=input.value.trim();if(value2==="")return null;for(let source of input.sources){if(!source.table||!source.column||!source.userColumn)continue;let found=(await input.lookup(source,value2)).filter((id)=>String(id??"").trim()!=="");if(found.length>1)return null;if(found.length===1)return String(found[0])}return null}function looksLikeAlternateIdentifier(value2){let trimmed=value2.trim();return trimmed!==""&&!trimmed.includes("@")}var init_identifier=()=>{};import{eq as eq52}from"drizzle-orm";async function lookupIdentifier(db,schemaTables,source,value2){let table=pickByName(schemaTables,source.table);if(!table)return[];let column=pickByName(table,source.column),userColumn=pickByName(table,source.userColumn);if(!column||!userColumn)return[];return(await db.select({userId:userColumn}).from(table).where(eq52(column,value2)).limit(2)).map((row)=>String(row.userId??""))}var init_identifierQuery=__esm(()=>{init_identifier()});var LoginBodySchema,LoginResponseSchema;var init_types12=__esm(()=>{init_esm3();LoginBodySchema=Type.Object({email:Type.String({minLength:1}),password:Type.String({minLength:1}),rememberMe:Type.Optional(Type.Boolean()),captchaId:Type.Optional(Type.String()),captchaAnswer:Type.Optional(Type.String()),deviceHint:Type.Optional(Type.String())}),LoginResponseSchema=Type.Object({success:Type.Boolean(),message:Type.Optional(Type.String()),data:Type.Optional(Type.Object({user:Type.Object({id:Type.String(),email:Type.String()}),accessToken:Type.String(),refreshToken:Type.String()}))})});import{eq as eq53}from"drizzle-orm";function createLoginRoute(config,loginConfig,signAccessToken,signRefreshToken,createSession,saveSessionToDb,cookieConfig,tokenResponseConfig,captchaService){let{db,logger:logger2}=config,log4=logger2.scoped(AUTH_LOGIN),route=loginConfig.route||"/auth/login",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};if(!loginConfig.enabled)return collectRoutes();let identifierSources=loginConfig.identifierSources??[],rememberMeAllowed=loginConfig.rememberMe!==!1,routePostRoute=defineRoute({method:"POST",path:"/",body:LoginBodySchema,detail:{tags:["Authentication"],summary:"Login",description:"Authenticate user with email and password"},handler: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,rememberMe:rememberMeRequested,captchaId,captchaAnswer,deviceHint}=ctx.body,rememberMe=rememberMeAllowed?rememberMeRequested:!1,email=rawEmail.toLowerCase(),user=(await db.select().from(usersTable).where(eq53(usersTable.email,email)).limit(1))[0];if(!user&&identifierSources.length>0&&looksLikeAlternateIdentifier(rawEmail)){let resolvedId=await resolveUserIdByIdentifier({sources:identifierSources,value:rawEmail,lookup:(source,value2)=>lookupIdentifier(db,resolved.schemaTables,source,value2)});if(resolvedId)user=(await db.select().from(usersTable).where(eq53(usersTable.id,resolvedId)).limit(1))[0]}let captchaEnabledForLogin=loginConfig.captcha?.enabled!==!1,captchaThreshold=loginConfig.captcha?.requireAfterFailures,currentFailedAttempts=user?.failedLoginAttempts||0,captchaRequired=Boolean(captchaService?.isEnabled())&&captchaEnabledForLogin&&(captchaThreshold===void 0||currentFailedAttempts>=captchaThreshold);if(captchaRequired&&captchaService){if(!captchaId||!captchaAnswer)return ctx.set.status=400,{success:!1,message:"Captcha is required",requiresCaptcha:!0};let captchaResult=await captchaService.validate(captchaId,captchaAnswer);if(!captchaResult.valid)return ctx.set.status=400,{success:!1,message:captchaResult.message||"Invalid captcha",attemptsRemaining:captchaResult.attemptsRemaining,requiresCaptcha:!0}}let reqUrl=new URL(ctx.request.url),auditIp=ctx.request.headers.get("x-client-ip")?.trim()||"unknown",auditUa=ctx.request.headers.get("user-agent")||"unknown";if(!user)return await verifyPassword(password4,await getTimingEqualizerHash()).catch(()=>!1),log4.warn("Login failed - user not found",{email}),await logger2.audit({entityName:"users",operation:"LOGIN_FAILED",summary:`Login failed: user not found (${email})`,ipAddress:auditIp,userAgent:auditUa,path:reqUrl.pathname,query:reqUrl.search}),ctx.set.status=401,{success:!1,message:"Invalid email or password",requiresCaptcha:captchaRequired};if(user.isActive===!1)return log4.warn("Login failed - account inactive",{email,userId:user.id}),ctx.set.status=403,{success:!1,message:"Account is not active"};if(user.isLocked){let lockedUntil=user.lockedUntil;if(lockedUntil&&new Date(lockedUntil)<new Date)await db.update(usersTable).set({isLocked:!1,lockedUntil:null,failedLoginAttempts:0}).where(eq53(usersTable.id,user.id)),log4.info("Account lock expired, auto-unlocked",{userId:user.id,email});else return log4.warn("Login failed - account locked",{email,userId:user.id}),await logger2.audit({entityName:"users",entityId:user.id,operation:"LOGIN_FAILED",userId:user.id,summary:`Login failed: account locked (${email})`,ipAddress:auditIp,userAgent:auditUa,path:reqUrl.pathname,query:reqUrl.search}),ctx.set.status=403,{success:!1,message:"Account is locked"}}let userCohortId=user.cohortId;if(userCohortId&&db){let cohortsTable=resolved.schemaTables?.userCohorts??resolved.schemaTables?.user_cohorts;if(cohortsTable){let cohortExpiresAt=(await db.select().from(cohortsTable).where(eq53(cohortsTable.id,userCohortId)).limit(1))[0]?.expiresAt;if(cohortExpiresAt&&new Date(cohortExpiresAt)<new Date)return log4.warn("Login failed - cohort expired",{email,userId:user.id,cohortId:userCohortId,expiresAt:cohortExpiresAt}),await logger2.audit({entityName:"users",entityId:user.id,operation:"LOGIN_FAILED",userId:user.id,summary:`Login failed: cohort expired (${email})`,ipAddress:auditIp,userAgent:auditUa,path:reqUrl.pathname,query:reqUrl.search}),ctx.set.status=403,{success:!1,message:"Your access has expired. Please contact your administrator."}}}if(!await verifyPassword(password4,user.password)){let failedAttempts=(user.failedLoginAttempts||0)+1;await db.update(usersTable).set({failedLoginAttempts:failedAttempts,isLocked:failedAttempts>=5,lockedUntil:failedAttempts>=5?new Date(Date.now()+1800000):null}).where(eq53(usersTable.id,user.id)),log4.warn("Login failed - invalid password",{email,failedAttempts}),await logger2.audit({entityName:"users",entityId:user.id,operation:"LOGIN_FAILED",userId:user.id,summary:`Login failed: invalid password (${email}, attempt ${failedAttempts})`,ipAddress:auditIp,userAgent:auditUa,path:reqUrl.pathname,query:reqUrl.search});let captchaWillBeRequired=Boolean(captchaService?.isEnabled())&&captchaEnabledForLogin&&(captchaThreshold===void 0||failedAttempts>=captchaThreshold);return ctx.set.status=401,{success:!1,message:"Invalid email or password",requiresCaptcha:captchaWillBeRequired}}await db.update(usersTable).set({failedLoginAttempts:0,lastLoginAt:new Date,loginCount:(user.loginCount||0)+1}).where(eq53(usersTable.id,user.id));let ipAddress=ctx.request.headers.get("x-client-ip")?.trim()||"127.0.0.1",isPrivateOrLoopback=(ip)=>!ip||ip==="127.0.0.1"||ip==="::1"||ip==="localhost"||ip.startsWith("10.")||ip.startsWith("192.168.")||ip.startsWith("172."),userAgent=ctx.request.headers.get("user-agent")||"Unknown Browser";log4.info("Parsed device info",{ipAddress,userAgent});let deviceInfo=parseUserAgentForLogin(userAgent,ipAddress,deviceHint,deviceContextFromRequest(ctx.request).clientHints);if(!isPrivateOrLoopback(ipAddress)){let abortCtrl=new AbortController,geoTimeout=setTimeout(()=>abortCtrl.abort(),2000);fetch(`https://ip-api.com/json/${ipAddress}?fields=country,city`,{signal:abortCtrl.signal}).then(async(res)=>{if(clearTimeout(geoTimeout),!res.ok)return;let geo=await res.json();if(geo.country)deviceInfo.locationCountry=geo.country;if(geo.city)deviceInfo.locationCity=geo.city}).catch(()=>{clearTimeout(geoTimeout)})}let userRoles=[],userClaims=[],userClaimScopes={};if(db)try{let{fetchUserRolesAndClaims:fetchUserRolesAndClaims2}=await Promise.resolve().then(() => (init_fetchUserRolesAndClaims(),exports_fetchUserRolesAndClaims)),rc=await fetchUserRolesAndClaims2(db,user.id,resolved);userRoles=rc.roles,userClaims=rc.claims,userClaimScopes=rc.claimScopes}catch(roleError){log4.error("Failed to fetch user roles/claims for JWT",{userId:user.id,error:roleError instanceof Error?roleError.message:String(roleError),stack:roleError instanceof Error?roleError.stack:void 0})}let requestOrigin=ctx.request.headers.get("origin")||ctx.request.headers.get("referer")?.replace(/\/[^/]*$/,"")||void 0,sessionParams={userId:user.id,deviceInfo,loginMethod:"password",rememberMe,requestOrigin},sessionId=await createSession(sessionParams),accessToken=signAccessToken(user.id,userRoles.length>0?userRoles:void 0,userClaims.length>0?userClaims:void 0,ctx.request.headers.get("x-tenant-schema")??void 0,userClaimScopes,sessionId),refreshToken=signRefreshToken(user.id,rememberMe,sessionId);await recordIssuedRefreshToken(sessionId,refreshToken);let requiresApproval=!1,pendingSessionId=sessionId,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);let saveResult=await saveSessionToDb(sessionId,sessionParams,reqSchemaName);if(saveResult?.denied)return log4.warn("Login blocked by device-trust (unidentifiable device)",{userId:user.id}),ctx.set.status=403,{success:!1,message:"This device could not be recognized. Please contact support."};if(deviceTokenCookie=saveResult?.deviceTokenCookie,saveResult?.requiresApproval){if(requiresApproval=!0,saveResult.sessionId)pendingSessionId=saveResult.sessionId}}if(requiresApproval){ctx.set.status=202;let approvalHeaders={"Content-Type":"application/json"};if(deviceTokenCookie)approvalHeaders["Set-Cookie"]=deviceTokenCookie;return new Response(JSON.stringify({success:!1,requiresApproval:!0,sessionId:pendingSessionId,message:"Login requires approval. Please check your email to approve this device."}),{status:202,headers:approvalHeaders})}log4.info("Login successful",{userId:user.id,email,rememberMe}),await logger2.audit({entityName:"users",entityId:user.id,operation:"LOGIN",userId:user.id,summary:`${email} logged in successfully`,ipAddress,userAgent,path:reqUrl.pathname,query:reqUrl.search});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);ctx.set.headers["x-session-id"]=sessionId;let responseData={user:{id:user.id,email:user.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(withEnvelopeFlags({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 collectRoutes([{prefix:route,routes:[routePostRoute]}])}var dummyHashPromise=null,getTimingEqualizerHash=()=>{if(!dummyHashPromise)dummyHashPromise=hashPassword("nucleus-login-timing-equalizer");return dummyHashPromise};var init_login=__esm(()=>{init_routeCollector();init_response();init_DeviceTrust();init_scopes();init_recordIssued();init_utils6();init_identifier();init_identifierQuery();init_types12();init_deviceInfo();init_utils7();init_types12()});function clearAuthCookies(cookieConfig){let path7=cookieConfig?.path||"/",sameSite=cookieConfig?.sameSite||"Strict",securePart=cookieConfig?.secure!==!1?"; Secure":"",domainPart=cookieConfig?.domain?`; Domain=${cookieConfig.domain}`:"",accessName=cookieConfig?.accessTokenName||"access_token",refreshName=cookieConfig?.refreshTokenName||"refresh_token",sessionName=cookieConfig?.sessionTokenName||"session_token",clearOpts=`; Path=${path7}; HttpOnly; SameSite=${sameSite}${securePart}${domainPart}; Max-Age=0`;return[`${accessName}=${clearOpts}`,`${refreshName}=${clearOpts}`,`${sessionName}=${clearOpts}`,...IMPERSONATION_COOKIE_NAMES.map((name2)=>`${name2}=${clearOpts}`)]}var init_utils9=__esm(()=>{init_impersonationCookies()});var LogoutResponseSchema;var init_types13=__esm(()=>{init_esm3();LogoutResponseSchema=Type.Object({success:Type.Boolean(),message:Type.Optional(Type.String())})});function createLogoutRoute(config,logoutConfig,destroySession,revokeSessionInDb,cookieConfig){let{logger:logger2}=config,route=logoutConfig.route||"/auth/logout";if(!logoutConfig.enabled)return collectRoutes();let routePostRoute=defineRoute({method:"POST",path:"/",detail:{tags:["Authentication"],summary:"Logout",description:"Logout and invalidate session"},handler:async(ctx)=>{let sessionCookieName=cookieConfig?.sessionTokenName||"session_token",fromCookie=(ctx.request.headers.get("cookie")??"").split(";").map((one)=>one.trim()).find((one)=>one.startsWith(`${sessionCookieName}=`))?.slice(sessionCookieName.length+1),sessionId=ctx.request.headers.get("x-session-id")||fromCookie||null,userId=ctx.request.headers.get("x-user-id");if(sessionId){if(await destroySession(sessionId),revokeSessionInDb){let reqSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0;await revokeSessionInDb(sessionId,"user_logout",reqSchemaName)}}let clearCookies=clearAuthCookies(cookieConfig),responseHeaders=new Headers;responseHeaders.set("Content-Type","application/json");for(let cookie of clearCookies)responseHeaders.append("Set-Cookie",cookie);logger2.info("[AUTH] Logout successful",{userId,sessionId});let reqUrl=new URL(ctx.request.url);logger2.audit({entityName:"users",entityId:userId||void 0,operation:"LOGOUT",userId:userId||void 0,summary:`User logged out${sessionId?` (session: ${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});let jsonBody=JSON.stringify(withEnvelopeFlags({success:!0,message:"Logged out successfully"}));return new Response(jsonBody,{status:200,headers:responseHeaders})}});return collectRoutes([{prefix:route,routes:[routePostRoute]}])}var init_logout=__esm(()=>{init_response();init_routeCollector();init_utils9();init_types13()});var MagicLinkRequestSchema,MagicLinkVerifySchema,MagicLinkResponseSchema,MagicLinkVerifyResponseSchema;var init_types14=__esm(()=>{init_esm3();MagicLinkRequestSchema=Type.Object({email:Type.String({format:"email"})}),MagicLinkVerifySchema=Type.Object({token:Type.String()}),MagicLinkResponseSchema=Type.Object({success:Type.Boolean(),message:Type.Optional(Type.String())}),MagicLinkVerifyResponseSchema=Type.Object({success:Type.Boolean(),message:Type.Optional(Type.String()),data:Type.Optional(Type.Object({user:Type.Object({id:Type.String(),email:Type.String()}),accessToken:Type.String(),refreshToken:Type.String()}))})});import{eq as eq54}from"drizzle-orm";function createMagicLinkRoute(config,magicLinkConfig,getEmailService,signAccessToken,signRefreshToken,createSession,storeMagicToken,getMagicToken,deleteMagicToken,appName,cookieConfig,saveSessionToDb){let{db,logger:logger2}=config,requestRoute=magicLinkConfig.route||"/auth/magic-link",verifyRoute=magicLinkConfig.verifyRoute||"/auth/magic-link/verify",expiresIn=magicLinkConfig.expiresIn||"15m",redirectUrl=magicLinkConfig.redirectUrl||"";if(!magicLinkConfig.enabled)return collectRoutes();let routePostRequestroute=defineRoute({method:"POST",path:"/",body:MagicLinkRequestSchema,detail:{tags:["Authentication"],summary:"Request Magic Link",description:"Send a magic link to the user's email for passwordless login"},handler:async(ctx)=>{let{usersTable}=resolveAuthTablesForRequest(ctx.request,config);if(!db||!usersTable)return ctx.set.status=500,{success:!1,message:"Database not configured"};let emailService=getEmailService?.();if(!emailService?.isAvailable())return logger2.error("[AUTH] Magic link requested but email service not available"),ctx.set.status=503,{success:!1,message:"Email service not available"};let{email:rawEmail}=ctx.body,email=rawEmail.toLowerCase(),user=(await db.select().from(usersTable).where(eq54(usersTable.email,email)).limit(1))[0];if(!user)return logger2.info("[AUTH] Magic link requested for non-existent email",{email}),{success:!0,message:"If an account exists, a magic link has been sent"};if(user.isLocked)return logger2.warn("[AUTH] Magic link requested for locked account",{email}),{success:!0,message:"If an account exists, a magic link has been sent"};let token=generateMagicToken(),tokenHash=hashToken(token,TOKEN_PURPOSE.magicLink),expiresAt=new Date(Date.now()+parseTimeToMs3(expiresIn)),reqSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0;await storeMagicToken({userId:user.id,email,tokenHash,expiresAt},reqSchemaName);let magicLink=redirectUrl?buildEmailActionLink({request:ctx.request,configuredUrl:redirectUrl,path:extractConfiguredPath(redirectUrl,"/verify-magic-link"),query:{token},allowedOrigins:config.authentication?.trustedAppOrigins}):`${verifyRoute}?token=${token}`,result=await emailService.sendMagicLinkEmail(email,magicLink,appName);if(!result.success)return logger2.error("[AUTH] Failed to send magic link email",{email,error:result.error}),ctx.set.status=500,{success:!1,message:result.error?`The email could not be sent: ${result.error}`:"The email could not be sent"};return logger2.info("[AUTH] Magic link sent",{email,userId:user.id}),{success:!0,message:"If an account exists, a magic link has been sent"}}}),routeGetVerifyroute=defineRoute({method:"GET",path:"/",query:MagicLinkVerifySchema,detail:{tags:["Authentication"],summary:"Verify Magic Link",description:"Verify magic link token and login user"},handler:async(ctx)=>{let{usersTable}=resolveAuthTablesForRequest(ctx.request,config);if(!db||!usersTable)return ctx.set.status=500,{success:!1,message:"Database not configured"};let token=ctx.query.token;if(!token)return ctx.set.status=400,{success:!1,message:"Token is required"};let tokenHash=hashToken(token,TOKEN_PURPOSE.magicLink),reqSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0,storedToken=await getMagicToken(tokenHash,reqSchemaName);if(!storedToken)return logger2.warn("[AUTH] Invalid magic link token"),ctx.set.status=400,{success:!1,message:"Invalid or expired token"};if(new Date>storedToken.expiresAt)return await deleteMagicToken(tokenHash,reqSchemaName),logger2.warn("[AUTH] Expired magic link token",{email:storedToken.email}),ctx.set.status=400,{success:!1,message:"Invalid or expired token"};let user=(await db.select().from(usersTable).where(eq54(usersTable.id,storedToken.userId)).limit(1))[0];if(!user)return await deleteMagicToken(tokenHash,reqSchemaName),ctx.set.status=404,{success:!1,message:"User not found"};if(user.isActive===!1)return await deleteMagicToken(tokenHash,reqSchemaName),logger2.warn("[AUTH] Magic link denied: account inactive",{email:storedToken.email}),ctx.set.status=400,{success:!1,message:"Invalid or expired token"};if(user.isLocked){let lockedUntil=user.lockedUntil;if(!(lockedUntil&&new Date(lockedUntil)<new Date))return await deleteMagicToken(tokenHash,reqSchemaName),logger2.warn("[AUTH] Magic link denied: account locked",{email:storedToken.email}),ctx.set.status=400,{success:!1,message:"Invalid or expired token"}}await deleteMagicToken(tokenHash,reqSchemaName),await db.update(usersTable).set({lastLoginAt:new Date,loginCount:(user.loginCount||0)+1,emailVerified:!0}).where(eq54(usersTable.id,user.id));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")||"",mlUserRoles=[],mlUserClaims=[],mlUserClaimScopes={};if(db)try{let resolved=resolveAuthTablesForRequest(ctx.request,config),rc=await fetchUserRolesAndClaims(db,user.id,resolved);mlUserRoles=rc.roles,mlUserClaims=rc.claims,mlUserClaimScopes=rc.claimScopes}catch{}let sessionParams={userId:user.id,deviceInfo:ensureDeviceInfo({ipAddress,userAgent},deviceContextFromRequest(ctx.request).clientHints),loginMethod:"magic_link",rememberMe:!0},sessionId=await createSession(sessionParams),accessToken=signAccessToken(user.id,mlUserRoles.length>0?mlUserRoles:void 0,mlUserClaims.length>0?mlUserClaims:void 0,ctx.request.headers.get("x-tenant-schema")??void 0,mlUserClaimScopes,sessionId),refreshToken=signRefreshToken(user.id,void 0,sessionId),requiresApproval=!1,approvalSessionId=sessionId,deviceTokenCookie;if(saveSessionToDb){let reqSchemaName2=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,reqSchemaName2);if(saveResult?.denied)return ctx.set.status=403,{success:!1,message:"This device could not be recognized. Please contact support."};if(deviceTokenCookie=saveResult?.deviceTokenCookie,saveResult?.requiresApproval){if(requiresApproval=!0,saveResult.sessionId)approvalSessionId=saveResult.sessionId}}if(requiresApproval){if(deviceTokenCookie)ctx.set.headers["Set-Cookie"]=deviceTokenCookie;return logger2.info("[AUTH] Magic link login requires device approval",{userId:user.id,email:user.email,approvalSessionId}),ctx.set.status=202,{success:!1,requiresApproval:!0,sessionId:approvalSessionId,message:"Login requires approval. Please check your email to approve this device."}}logger2.info("[AUTH] Magic link login successful",{userId:user.id,email:user.email});let 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||2592000,secure:cookieConfig?.secure??!0,httpOnly:cookieConfig?.httpOnly??!0,sameSite:cookieConfig?.sameSite||"strict",path:cookieConfig?.path||"/",domain:cookieConfig?.domain},securePart=cookies.secure?"; Secure":"",domainPart=cookies.domain?`; Domain=${cookies.domain}`:"",cookieOptions=`; Path=${cookies.path}; HttpOnly; SameSite=${cookies.sameSite}${securePart}${domainPart}`,responseHeaders=new Headers;if(responseHeaders.set("Content-Type","application/json"),responseHeaders.set("x-session-id",sessionId),responseHeaders.append("Set-Cookie",`${cookies.accessTokenName}=${accessToken}${cookieOptions}; Max-Age=${cookies.accessTokenMaxAge}`),responseHeaders.append("Set-Cookie",`${cookies.refreshTokenName}=${refreshToken}${cookieOptions}; Max-Age=${cookies.refreshTokenMaxAge}`),responseHeaders.append("Set-Cookie",`${cookies.sessionTokenName}=${sessionId}${cookieOptions}; Max-Age=${cookies.sessionTokenMaxAge}`),deviceTokenCookie)responseHeaders.append("Set-Cookie",deviceTokenCookie);let jsonBody=JSON.stringify(withEnvelopeFlags({success:!0,data:{user:{id:user.id,email:user.email},accessToken,refreshToken,sessionId}}));return new Response(jsonBody,{status:200,headers:responseHeaders})}});return collectRoutes([{prefix:requestRoute,routes:[routePostRequestroute]},{prefix:verifyRoute,routes:[routeGetVerifyroute]}])}var init_magicLink=__esm(()=>{init_DeviceTrust();init_response();init_routeCollector();init_fetchUserRolesAndClaims();init_deviceInfo();init_types14();init_utils8();init_types14()});import{eq as eq55}from"drizzle-orm";function createMeRoute(config,meConfig,_schemaTables,_schemaRelations,_databaseUrl){let{db,logger:logger2}=config,route=meConfig.route||"/auth/me";if(!meConfig.enabled)return collectRoutes();let routeGetRoute=defineRoute({method:"GET",path:"/",detail:{tags:["Authentication"],summary:"Get current user",description:"Get the currently authenticated user with profile, addresses, phones and files"},handler:async(ctx)=>{let resolved=resolveAuthTablesForRequest(ctx.request,config),usersTable=resolved.usersTable,reqSchemaTables=resolved.schemaTables;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:"Unauthorized"};let user=(await db.select().from(usersTable).where(eq55(usersTable.id,userId)).limit(1))[0];if(!user)return ctx.set.status=404,{success:!1,message:"User not found"};let{password:rawPassword,...restUser}=user,hasPassword=typeof rawPassword==="string"&&rawPassword.length>0,safeUser={...stripUnclaimedSensitive(redactSensitiveOutput(restUser)),hasPassword},profile=null,addresses=[],phones=[],files=[],roles=[],claims=[];if(meConfig.includeProfile&&reqSchemaTables){let profilesTable=reqSchemaTables.profiles;if(profilesTable&&db)profile=(await db.select().from(profilesTable).where(eq55(profilesTable.userId,userId)).limit(1))[0]||null}if(meConfig.includeAddresses&&reqSchemaTables){let addressesTable=reqSchemaTables.addresses;if(addressesTable&&db)addresses=await db.select().from(addressesTable).where(eq55(addressesTable.ownerId,userId))}if(meConfig.includePhones&&reqSchemaTables){let phonesTable=reqSchemaTables.phones;if(phonesTable&&db)phones=await db.select().from(phonesTable).where(eq55(phonesTable.ownerId,userId))}if(meConfig.includeFiles&&reqSchemaTables){let filesTable=reqSchemaTables.files;if(filesTable&&db)files=await db.select().from(filesTable).where(eq55(filesTable.uploadedBy,userId))}if(meConfig.includeRoles&&reqSchemaTables){let{userRoles:userRolesTable,roles:rolesTable}=reqSchemaTables;if(userRolesTable&&rolesTable&&db){let roleIds=(await db.select().from(userRolesTable).where(eq55(userRolesTable.userId,userId))).map((ur)=>ur.roleId);if(roleIds.length>0){let{inArray:inArray19}=await import("drizzle-orm");roles=await db.select().from(rolesTable).where(inArray19(rolesTable.id,roleIds))}}}if(meConfig.includeClaims&&reqSchemaTables){let{userRoles:userRolesTable,roleClaims:roleClaimsTable,claims:claimsTable}=reqSchemaTables;if(userRolesTable&&roleClaimsTable&&claimsTable&&db){let urCols=userRolesTable,rcCols=roleClaimsTable,cCols=claimsTable,roleIds=(await db.select({roleId:urCols.roleId}).from(userRolesTable).where(eq55(urCols.userId,userId))).map((r)=>r.roleId);if(roleIds.length>0){let{inArray:inArray19}=await import("drizzle-orm"),claimRows=await db.select({action:cCols.action,scope:rcCols.scope}).from(roleClaimsTable).innerJoin(claimsTable,eq55(rcCols.claimId,cCols.id)).where(inArray19(rcCols.roleId,roleIds)),seen=new Set;for(let row of claimRows){let r=row,key2=`${r.action}|${r.scope??""}`;if(!seen.has(key2))seen.add(key2),claims.push({action:r.action,scope:r.scope??null})}}}}logger2.debug("[AUTH] Me endpoint accessed",{userId});let responseData={success:!0,data:{user:safeUser,profile:stripUnclaimedSensitive(profile),addresses:stripUnclaimedSensitive(addresses),phones:stripUnclaimedSensitive(phones),files:stripUnclaimedSensitive(files),roles:stripUnclaimedSensitive(roles),claims}};return JSON.parse(JSON.stringify(responseData,(_key,value2)=>typeof value2==="bigint"?Number(value2):value2))}});return collectRoutes([{prefix:route,routes:[routeGetRoute]}])}var init_me=__esm(()=>{init_routeCollector();init_utils5();init_sensitiveColumns()});var exports_accountState={};__export(exports_accountState,{evaluateAccountState:()=>evaluateAccountState});function evaluateAccountState(user,now=new Date){if(user.isActive===!1)return{allowed:!1,reason:"inactive",message:"Account is not active"};if(user.isLocked===!0){let raw=user.lockedUntil,lockedUntil=raw instanceof Date?raw:raw?new Date(raw):null;if(!lockedUntil||Number.isNaN(lockedUntil.getTime())||lockedUntil>now)return{allowed:!1,reason:"locked",message:"Account is locked"}}return{allowed:!0}}import{eq as eq56}from"drizzle-orm";async function applyProfileSync(args){let{usersTable,profilesTable,userId,rawProfile,config,isNewUser,logger:logger2}=args,db=args.db;try{if(config.enabled===!1)return;if(!isNewUser&&config.syncOnEveryLogin===!1)return;if(!config.mappings||config.mappings.length===0)return;let userIdCol=usersTable.id,currentUser=(await db.select().from(usersTable).where(eq56(userIdCol,userId)).limit(1))[0]??{},currentProfile={},profileExists=!1;if(profilesTable){let profileUserIdCol=profilesTable.userId,profileRows=await db.select().from(profilesTable).where(eq56(profileUserIdCol,userId)).limit(1);if(profileRows[0])currentProfile=profileRows[0],profileExists=!0}let{users,profiles,errors:errors2}=resolveProfileSyncUpdates({mappings:config.mappings,rawProfile,currentUser,currentProfile});for(let message of errors2)logger2.warn(`[OAUTH] profileSync: ${message}`);if(Object.keys(users).length>0)await db.update(usersTable).set({...users,updatedAt:new Date}).where(eq56(userIdCol,userId)),logger2.info("[OAUTH] profileSync updated users",{userId,columns:Object.keys(users)});if(profilesTable&&Object.keys(profiles).length>0){if(profileExists){let profileUserIdCol=profilesTable.userId;await db.update(profilesTable).set({...profiles,updatedAt:new Date}).where(eq56(profileUserIdCol,userId))}else if(config.createProfileIfMissing!==!1){let now=new Date;await db.insert(profilesTable).values({userId,firstName:rawProfile.givenName??"",lastName:rawProfile.surname??"",...profiles,createdAt:now,updatedAt:now})}else return;logger2.info("[OAUTH] profileSync updated profiles",{userId,columns:Object.keys(profiles),created:!profileExists})}}catch(err){logger2.warn("[OAUTH] profileSync failed \u2014 login continues",{userId,error:err instanceof Error?err.message:String(err)})}}var init_applyProfileSync=()=>{};import{and as and25,eq as eq57}from"drizzle-orm";function buildOAuthStateCookie(value2,opts){let securePart=opts.secure?"; Secure":"",domainPart=opts.domain?`; Domain=${opts.domain}`:"";return`${OAUTH_STATE_COOKIE}=${value2}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${opts.maxAge}${securePart}${domainPart}`}function createOAuthRoutes(config,oauthService,signAccessToken,signRefreshToken,createSession,saveSessionToDb,cookieConfig,tokenResponseConfig,getEmailService,storeMagicToken,appName){let{db,logger:logger2}=config,basePath=oauthService.basePath;for(let provider of oauthService.getEnabledProviders()){let p=oauthService.config.providers[provider];logger2.info("[OAUTH] Provider loaded",{provider,clientIdSuffix:p?.clientId?p.clientId.slice(-4):"none",tenantId:p?.tenantId??"common",redirectUri:p?.redirectUri,scopes:p?.scopes})}let 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||"lax",path:cookieConfig?.path||"/",domain:cookieConfig?.domain},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}},routeGetProvider=defineRoute({method:"GET",path:"/:provider",detail:{tags:["Authentication"],summary:"OAuth Redirect",description:"Redirects to the OAuth provider authorization page"},handler: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`};let redirectUrl=ctx.query.redirect_url,authUrl=await oauthService.buildAuthorizationUrl(provider,void 0,redirectUrl);logger2.debug("[OAUTH] Redirecting to provider",{provider,authUrl:authUrl.replace(/(client_id=)[^&]+/,"$1***")});let 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}}),routeGetProviderCallback=defineRoute({method:"GET",path:"/:provider/callback",detail:{tags:["Authentication"],summary:"OAuth Callback",description:"Handles the OAuth provider callback, creates session and sets cookies"},handler:async(ctx)=>{let provider=ctx.params.provider,query=ctx.query,{code,state,error:error3,error_description}=query,portalOrigin=resolveAppOrigin(ctx.request,oauthService.successRedirectUrl,config.authentication?.trustedAppOrigins),errorRedirect=absolutizeRedirect(oauthService.errorRedirectUrl,portalOrigin);if(error3)return logger2.warn("[OAUTH] Provider returned error",{provider,error:error3,error_description}),ctx.set.status=302,ctx.set.headers.Location=`${errorRedirect}?error=${encodeURIComponent(error_description||error3)}`,null;if(!code||!state)return ctx.set.status=302,ctx.set.headers.Location=`${errorRedirect}?error=missing_code_or_state`,null;let statePayload=await oauthService.consumeState(state);if(!statePayload)return logger2.warn("[OAUTH] Invalid or expired state",{provider,state}),ctx.set.status=302,ctx.set.headers.Location=`${errorRedirect}?error=invalid_state`,null;let cookieState=readCookie(ctx.request,OAUTH_STATE_COOKIE),clearStateCookie=buildOAuthStateCookie("",{maxAge:0,secure:cookies.secure,domain:cookies.domain});if(!cookieState||cookieState!==state)return logger2.warn("[OAUTH] State cookie missing or mismatched (possible CSRF)",{provider}),ctx.set.status=302,ctx.set.headers["Set-Cookie"]=clearStateCookie,ctx.set.headers.Location=`${errorRedirect}?error=invalid_state`,null;if(statePayload.provider!==provider)return logger2.warn("[OAUTH] State provider mismatch",{expected:statePayload.provider,got:provider}),ctx.set.status=302,ctx.set.headers.Location=`${errorRedirect}?error=provider_mismatch`,null;let resolved=resolveAuthTablesForRequest(ctx.request,config),usersTable=resolved.usersTable,oauthTable=resolved.oauthAccountsTable??config.oauthAccountsTable;if(!db||!usersTable)return ctx.set.status=302,ctx.set.headers.Location=`${errorRedirect}?error=server_error`,null;logger2.debug("[OAUTH] Exchanging code",{provider,codeLength:code.length,statePresent:!!state});let callbackResult;try{callbackResult=await oauthService.exchangeCode(provider,code)}catch(err){let e=err;return logger2.error("[OAUTH] Code exchange failed",err,{provider,errorMessage:e?.message,errorName:e?.name,errorCauseCode:e?.cause?.code,errorCauseMessage:e?.cause?.message}),ctx.set.status=302,ctx.set.headers.Location=`${errorRedirect}?error=token_exchange_failed`,null}let{profile,tokens}=callbackResult,userId=null;if(statePayload.linkUserId&&oauthService.allowAccountLinking){if(userId=statePayload.linkUserId,oauthTable){let existing=await db.select().from(oauthTable).where(and25(eq57(oauthTable.provider,provider),eq57(oauthTable.providerAccountId,profile.providerAccountId))).limit(1);if(existing.length>0){let existingRecord=existing[0];if(existingRecord.userId!==userId)return ctx.set.status=302,ctx.set.headers.Location=`${errorRedirect}?error=account_already_linked_to_another_user`,null;await db.update(oauthTable).set({accessToken:tokens.accessToken,refreshToken:tokens.refreshToken??null,tokenExpiresAt:tokens.expiresAt??null,scope:tokens.scope??null,rawProfile:profile.rawProfile,lastUsedAt:new Date}).where(eq57(oauthTable.id,existingRecord.id))}else await db.insert(oauthTable).values({userId,provider,providerAccountId:profile.providerAccountId,providerEmail:profile.email??null,providerName:profile.name??null,providerAvatarUrl:profile.avatarUrl??null,accessToken:tokens.accessToken,refreshToken:tokens.refreshToken??null,tokenExpiresAt:tokens.expiresAt??null,scope:tokens.scope??null,rawProfile:profile.rawProfile,isPrimary:!1,lastUsedAt:new Date})}let linkSyncConfig=oauthService.profileSync;if(linkSyncConfig){let linkSyncTables=resolveAuthTablesForRequest(ctx.request,config);await applyProfileSync({db,usersTable,profilesTable:linkSyncTables.schemaTables?.profiles,userId,rawProfile:profile.rawProfile,config:linkSyncConfig,isNewUser:!1,logger:logger2})}logger2.info("[OAUTH] Account linked successfully",{userId,provider});let successUrl2=absolutizeRedirect(resolveSafeOAuthRedirect(statePayload.redirectUrl,oauthService.successRedirectUrl,config.authentication?.trustedAppOrigins),portalOrigin);return ctx.set.status=302,ctx.set.headers.Location=`${successUrl2}?linked=true&provider=${provider}`,null}if(oauthTable){let existingOAuth=await db.select().from(oauthTable).where(and25(eq57(oauthTable.provider,provider),eq57(oauthTable.providerAccountId,profile.providerAccountId))).limit(1);if(existingOAuth.length>0){let oauthRecord=existingOAuth[0];userId=oauthRecord.userId,await db.update(oauthTable).set({accessToken:tokens.accessToken,refreshToken:tokens.refreshToken??null,tokenExpiresAt:tokens.expiresAt??null,scope:tokens.scope??null,rawProfile:profile.rawProfile,lastUsedAt:new Date}).where(eq57(oauthTable.id,oauthRecord.id))}}if(!userId&&profile.email){let normalizedOAuthEmail=profile.email.toLowerCase(),existingUsers=await db.select().from(usersTable).where(eq57(usersTable.email,normalizedOAuthEmail)).limit(1);if(existingUsers.length>0&&profile.emailVerified!==!0)return logger2.warn("[OAUTH] Refusing account merge: provider email not verified",{provider,email:profile.email}),ctx.set.status=302,ctx.set.headers.Location=`${errorRedirect}?error=email_not_verified`,null;if(existingUsers.length>0){if(userId=existingUsers[0].id,oauthTable)await db.insert(oauthTable).values({userId,provider,providerAccountId:profile.providerAccountId,providerEmail:profile.email??null,providerName:profile.name??null,providerAvatarUrl:profile.avatarUrl??null,accessToken:tokens.accessToken,refreshToken:tokens.refreshToken??null,tokenExpiresAt:tokens.expiresAt??null,scope:tokens.scope??null,rawProfile:profile.rawProfile,isPrimary:!1,lastUsedAt:new Date}).onConflictDoNothing();logger2.info("[OAUTH] Merged OAuth account with existing user by email",{userId,provider,email:profile.email})}}let createdNewUser=!1;if(!userId){if(!oauthService.autoCreateUser)return ctx.set.status=302,ctx.set.headers.Location=`${errorRedirect}?error=user_not_found`,null;let userEmail=(profile.email??`${provider}_${profile.providerAccountId}@oauth.local`).toLowerCase(),providerEmailVerified=profile.emailVerified===!0;if(userId=(await db.insert(usersTable).values({email:userEmail,password:null,emailVerified:providerEmailVerified,verifiedAt:providerEmailVerified?new Date:null,isActive:!0}).returning())[0].id,oauthTable)await db.insert(oauthTable).values({userId,provider,providerAccountId:profile.providerAccountId,providerEmail:profile.email??null,providerName:profile.name??null,providerAvatarUrl:profile.avatarUrl??null,accessToken:tokens.accessToken,refreshToken:tokens.refreshToken??null,tokenExpiresAt:tokens.expiresAt??null,scope:tokens.scope??null,rawProfile:profile.rawProfile,isPrimary:!0,lastUsedAt:new Date});if(createdNewUser=!0,logger2.info("[OAUTH] Created new user via OAuth",{userId,provider,email:profile.email}),oauthService.sendInviteOnCreate&&profile.email&&getEmailService?.()?.isAvailable()&&storeMagicToken)try{let rawToken=generateMagicToken(),tokenHash=hashToken(rawToken,TOKEN_PURPOSE.invite),expiresAt=new Date(Date.now()+parseTimeToMs3("7d")),oauthSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0;await storeMagicToken({userId,email:profile.email,tokenHash,expiresAt},oauthSchemaName);let inviteLink=buildEmailActionLink({request:ctx.request,configuredUrl:oauthService.successRedirectUrl,path:"/set-password",query:{token:rawToken},allowedOrigins:config.authentication?.trustedAppOrigins});await getEmailService?.()?.sendEmail({to:profile.email,subject:`Welcome to ${appName??"the platform"} \u2014 Set your password`,html:`
448
448
  <p>Hi${profile.name?` ${profile.name}`:""},</p>
449
449
  <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>
450
450
  <p><a href="${inviteLink}">Set your password</a></p>
package/dist/server.js CHANGED
@@ -444,7 +444,7 @@ ${strMDSAlgs}`)}if(attestationStatementAlg!==void 0&&authenticatorGetInfo?.algor
444
444
  AND c2.column_name = 'owner_type'
445
445
  AND c1.table_schema = ${effectiveSchema}
446
446
  AND c1.data_type = 'uuid'
447
- `),polyTables=(Array.isArray(polyResult)?polyResult:polyResult.rows||[]).map((r)=>String(r.table_name||""));logger2.info("[AUTH] Change User ID - discovered schema references",{fkConstraints:fkConstraints.map((f)=>`${f.table_name}.${f.column_name} (${f.constraint_name})`),userIdColumns:userIdColumns.map((u)=>`${u.table_name}.${u.column_name}`),auditColumns:auditColumns.map((a)=>`${a.table_name}.${a.column_name}`),polyTables});let allUpdates=new Map,addUpdate=(tableName,columnName)=>{if(!allUpdates.has(tableName))allUpdates.set(tableName,new Set);allUpdates.get(tableName)?.add(columnName)};for(let fk of fkConstraints)addUpdate(fk.table_name,fk.column_name);for(let col13 of userIdColumns)addUpdate(col13.table_name,col13.column_name);for(let col13 of auditColumns)addUpdate(col13.table_name,col13.column_name);logger2.info("[AUTH] Change User ID - all column updates to execute",{updates:Array.from(allUpdates.entries()).map(([t,cols])=>`${t}: [${Array.from(cols).join(", ")}]`)}),await db.transaction(async(tx)=>{for(let fk of fkConstraints)await tx.execute(sql9.raw(`ALTER TABLE "${effectiveSchema}"."${fk.table_name}" DROP CONSTRAINT "${fk.constraint_name}"`));await tx.execute(sql9.raw(`UPDATE "${effectiveSchema}"."users" SET "id" = '${newId}' WHERE "id" = '${currentId}'`));for(let[tableName,columns]of allUpdates.entries())for(let columnName of columns)await tx.execute(sql9.raw(`UPDATE "${effectiveSchema}"."${tableName}" SET "${columnName}" = '${newId}' WHERE "${columnName}" = '${currentId}'`));for(let tableName of polyTables)await tx.execute(sql9.raw(`UPDATE "${effectiveSchema}"."${tableName}" SET "owner_id" = '${newId}' WHERE "owner_id" = '${currentId}' AND "owner_type" IN ('user', 'users', 'User')`));for(let fk of fkConstraints){let onDelete=fk.delete_rule==="CASCADE"?"ON DELETE CASCADE":fk.delete_rule==="SET NULL"?"ON DELETE SET NULL":"";await tx.execute(sql9.raw(`ALTER TABLE "${effectiveSchema}"."${fk.table_name}" ADD CONSTRAINT "${fk.constraint_name}" FOREIGN KEY ("${fk.column_name}") REFERENCES "${effectiveSchema}"."users" ("id") ${onDelete}`))}});let allAffectedTables=[...new Set([...fkConstraints.map((f)=>f.table_name),...userIdColumns.map((u)=>u.table_name),...auditColumns.map((a)=>a.table_name),...polyTables])];return logger2.info("[AUTH] User ID changed successfully",{godminId:requestingUserId,currentId,newId,email:targetUser.email,affectedTables:allAffectedTables,fkConstraintsDroppedAndReadded:fkConstraints.length}),await logger2.audit({entityName:"users",entityId:newId,operation:"CHANGE_USER_ID",userId:requestingUserId,summary:`Godmin changed user ID: ${currentId} \u2192 ${newId} (${targetUser.email})`,oldValues:{id:currentId},newValues:{id:newId},ipAddress:ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||"unknown",userAgent:ctx.request.headers.get("user-agent")||"unknown",path:new URL(ctx.request.url).pathname,query:""}),{success:!0,data:{oldId:currentId,newId,email:targetUser.email,affectedTables:allAffectedTables}}}catch(error3){return logger2.error("[AUTH] Failed to change user ID",{error:error3 instanceof Error?error3.message:String(error3),stack:error3 instanceof Error?error3.stack:void 0,currentId,newId}),ctx.set.status=500,{success:!1,message:error3 instanceof Error?error3.message:"Failed to change user ID"}}}})];return collectRoutes([{routes:declared}])}var init_changeUserId=__esm(()=>{init_esm3();init_routeCollector();init_requireGodmin();init_resolveMutationSchema()});function decideCreateUserAuthz(params){let{callerIsGod,callerRoleNames,allowRoles,requestedRoleNames,godminRoleName}=params,callerIsGodmin=callerIsGod||callerRoleNames.includes(godminRoleName);if(!(callerIsGodmin||callerRoleNames.some((n)=>allowRoles.includes(n))))return{allowed:!1,status:403,reason:"not_privileged"};if(requestedRoleNames.includes(godminRoleName)&&!callerIsGodmin)return{allowed:!1,status:403,reason:"godmin_assignment_forbidden"};if(!callerIsGodmin){let heldByCaller=new Set(callerRoleNames);if(!requestedRoleNames.every((r)=>heldByCaller.has(r)))return{allowed:!1,status:403,reason:"role_above_caller"}}return{allowed:!0,callerIsGodmin}}import{eq as eq43,inArray as inArray17}from"drizzle-orm";function createCreateUserRoute(config,createUserConfig){let{db,logger:logger2}=config;if(createUserConfig?.enabled===!1)return collectRoutes();let route=createUserConfig?.route||"/auth/admin/create-user",allowRoles=createUserConfig?.allowRoles??[],declared=[defineRoute({method:"POST",path:"/",body:CreateUserBodySchema,detail:{tags:["Authentication","Admin"],summary:"Admin Create User",description:"Create a fully active user with a password, roles, and optional profile. Admin-only. No email is sent."},handler:async(ctx)=>{let resolved=resolveAuthTablesForRequest(ctx.request,config),{usersTable,rolesTable:resolvedRolesTable,userRolesTable:resolvedUserRolesTable,schemaTables}=resolved,profilesTable=schemaTables?.profiles,rolesTable=resolvedRolesTable??config.rolesTable??null,userRolesTable=resolvedUserRolesTable??config.userRolesTable??null;if(!db||!usersTable)return ctx.set.status=500,{success:!1,message:"Database not configured"};let requestingUserId=ctx.request.headers.get("x-user-id");if(!requestingUserId)return ctx.set.status=401,{success:!1,message:"Unauthorized"};let requestingUser=(await db.select().from(usersTable).where(eq43(usersTable.id,requestingUserId)).limit(1))[0];if(!requestingUser)return ctx.set.status=401,{success:!1,message:"Unauthorized"};let callerIsGod=Boolean(requestingUser.isGod),callerRoleNames=[];if(!callerIsGod&&rolesTable&&userRolesTable){let rolesCols=rolesTable,userRolesCols=userRolesTable;callerRoleNames=(await db.select({name:rolesCols.name}).from(userRolesTable).innerJoin(rolesTable,eq43(rolesCols.id,userRolesCols.roleId)).where(eq43(userRolesCols.userId,requestingUserId))).map((r)=>r.name)}let requestedRoleIds=ctx.body.roleIds??[],foundRoles=[];if(requestedRoleIds.length>0&&rolesTable){let rolesCols=rolesTable;foundRoles=(await db.select({id:rolesCols.id,name:rolesCols.name}).from(rolesTable).where(inArray17(rolesCols.id,requestedRoleIds))).map((r)=>({id:r.id,name:r.name}))}let decision=decideCreateUserAuthz({callerIsGod,callerRoleNames,allowRoles,requestedRoleNames:foundRoles.map((r)=>r.name),godminRoleName:GODMIN_ROLE_NAME});if(!decision.allowed)return logger2.warn("[AUTH] create-user denied",{callerId:requestingUserId,reason:decision.reason}),ctx.set.status=decision.status,{success:!1,message:decision.reason==="godmin_assignment_forbidden"?"Forbidden: assigning the godmin role requires godmin privileges":"Forbidden"};let{email:rawEmail,password:password4,profile}=ctx.body,email=rawEmail.toLowerCase().trim(),passwordValidation=validatePasswordStrength(password4,config.authentication?.passwordPolicy,email);if(!passwordValidation.valid)return ctx.set.status=400,{success:!1,message:"Password too weak",errors:passwordValidation.errors};if((await db.select().from(usersTable).where(eq43(usersTable.email,email)).limit(1)).length>0)return logger2.warn("[AUTH] create-user failed \u2014 email already exists",{email}),ctx.set.status=409,{success:!1,message:"Email already registered"};let now=new Date,hashedPassword=await hashPassword(password4),insertValues={email,password:hashedPassword,emailVerified:!0,verifiedAt:now,isLocked:!1,createdAt:now,updatedAt:now},newUser=(await db.insert(usersTable).values(insertValues).returning())[0];if(!newUser)return logger2.error("[AUTH] create-user insert returned nothing",{email}),ctx.set.status=500,{success:!1,message:"Failed to create user"};let newUserId=newUser.id;if(requestedRoleIds.length>0&&rolesTable&&userRolesTable)try{let validRoleIds=foundRoles.map((r)=>r.id);if(validRoleIds.length>0)await db.insert(userRolesTable).values(validRoleIds.map((roleId)=>({userId:newUserId,roleId})));let skipped=requestedRoleIds.filter((id)=>!validRoleIds.includes(id));if(skipped.length>0)logger2.warn("[AUTH] create-user: some roleIds were not found",{userId:newUserId,skipped});if(foundRoles.some((r)=>r.name===GODMIN_ROLE_NAME))await db.update(usersTable).set({isGod:!0}).where(eq43(usersTable.id,newUserId))}catch(err){logger2.warn("[AUTH] create-user: failed to assign roles \u2014 continuing",{userId:newUserId,error:err instanceof Error?err.message:String(err)})}else{let createDefaultRoles=resolveDefaultRoleNames(config.authentication);if(createDefaultRoles.length>0)await assignDefaultRoles({db,userId:newUserId,roleNames:createDefaultRoles,rolesTable,userRolesTable,logger:logger2})}if(profile&&profilesTable)try{await db.insert(profilesTable).values({userId:newUserId,firstName:profile.firstName??null,lastName:profile.lastName??null,phoneNumber:profile.phoneNumber??null,createdAt:now,updatedAt:now})}catch(err){logger2.warn("[AUTH] create-user: failed to create profile \u2014 continuing",{userId:newUserId,error:err instanceof Error?err.message:String(err)})}let 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";return logger2.audit({entityName:"users",entityId:newUserId,operation:"ADMIN_CREATE_USER",userId:requestingUserId,summary:`Admin ${requestingUserId} created user ${email}`,ipAddress:auditIp,userAgent:auditUa,path:reqUrl.pathname,query:reqUrl.search}),logger2.info("[AUTH] Admin created user",{newUserId,email,createdBy:requestingUserId}),{success:!0,message:"User created successfully",data:{id:newUserId,email}}}})];return collectRoutes([{prefix:route,routes:declared}])}var CreateUserBodySchema;var init_createUser=__esm(()=>{init_esm3();init_Authorization();init_routeCollector();init_assignDefaultRole();init_utils6();CreateUserBodySchema=Type.Object({email:Type.String({format:"email"}),password:Type.String({minLength:8}),roleIds:Type.Optional(Type.Array(Type.String())),profile:Type.Optional(Type.Object({firstName:Type.Optional(Type.String()),lastName:Type.Optional(Type.String()),phoneNumber:Type.Optional(Type.String())}))})});import{eq as eq44}from"drizzle-orm";function createHardDeleteUserRoute(config){let{db,logger:logger2}=config,declared=[defineRoute({method:"DELETE",path:"/auth/admin/hard-delete/:userId",params:HardDeleteUserParamsSchema,detail:{tags:["Authentication","Admin"],summary:"Hard Delete User",description:"Permanently delete a user and all related data (roles, sessions, profile, files, etc.). Godmin-only. Irreversible."},handler:async(ctx)=>{let resolved=resolveAuthTablesForRequest(ctx.request,config),{usersTable,userRolesTable,sessionsTable,oauthAccountsTable,apiKeysTable,schemaTables}=resolved;if(!db||!usersTable)return ctx.set.status=500,{success:!1,message:"Database not configured"};let requestingUserId=ctx.request.headers.get("x-user-id");if(!requestingUserId)return ctx.set.status=401,{success:!1,message:"Unauthorized"};let caller=(await db.select().from(usersTable).where(eq44(usersTable.id,requestingUserId)).limit(1))[0],callerIsGodmin=Boolean(caller?.isGod)||hasGodminRole(ctx.request)||await userHasGodminRoleInDb(db,config.schemaTables??{},requestingUserId,config.logger);if(!caller||!callerIsGodmin)return ctx.set.status=403,{success:!1,message:"Forbidden \u2014 only godmin can hard-delete users"};let{userId:targetUserId}=ctx.params;if(targetUserId===requestingUserId)return ctx.set.status=400,{success:!1,message:"Cannot hard-delete yourself"};let targetUser=(await db.select().from(usersTable).where(eq44(usersTable.id,targetUserId)).limit(1))[0];if(!targetUser)return ctx.set.status=404,{success:!1,message:"User not found"};if(targetUser.isGod)return ctx.set.status=403,{success:!1,message:"Cannot hard-delete a godmin user"};let deletedRelations=[],col13=(table,name2)=>table[name2],profilesTable=schemaTables?.profiles,addressesTable=schemaTables?.addresses,phonesTable=schemaTables?.phones,filesTable=schemaTables?.files,runCascade=async(exec)=>{deletedRelations.length=0;let del=async(table,columnName,label)=>{if(!table)return;await exec.delete(table).where(eq44(col13(table,columnName),targetUserId)),deletedRelations.push(label)};await del(userRolesTable,"userId","user_roles"),await del(sessionsTable,"userId","sessions"),await del(oauthAccountsTable,"userId","oauth_accounts"),await del(apiKeysTable,"userId","api_keys"),await del(profilesTable,"userId","profiles"),await del(addressesTable,"ownerId","addresses"),await del(phonesTable,"ownerId","phones"),await del(filesTable,"uploadedBy","files"),await exec.delete(usersTable).where(eq44(usersTable.id,targetUserId))};try{let txDb=db;if(typeof txDb.transaction==="function")await txDb.transaction(async(tx)=>{await runCascade(tx)});else await runCascade(db)}catch(err){return logger2.error("[AUTH] hard-delete failed; transaction rolled back",{userId:targetUserId,error:err instanceof Error?err.message:String(err)}),ctx.set.status=500,{success:!1,message:"Hard delete failed and was rolled back"}}let reqUrl=new URL(ctx.request.url);return logger2.audit({entityName:"users",entityId:targetUserId,operation:"ADMIN_HARD_DELETE_USER",userId:requestingUserId,summary:`Godmin ${requestingUserId} hard-deleted user ${targetUser.email} (${targetUserId}). Cleaned: ${deletedRelations.join(", ")}`,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}),logger2.info("[AUTH] Hard-deleted user",{targetUserId,email:targetUser.email,deletedBy:requestingUserId,cleanedRelations:deletedRelations}),{success:!0,message:"User permanently deleted",data:{userId:targetUserId,email:targetUser.email,cleanedRelations:deletedRelations}}}})];return collectRoutes([{routes:declared}])}var HardDeleteUserParamsSchema;var init_hardDeleteUser=__esm(()=>{init_esm3();init_routeCollector();init_adminGuard();HardDeleteUserParamsSchema=Type.Object({userId:Type.String()})});var exports_fetchUserRolesAndClaims={};__export(exports_fetchUserRolesAndClaims,{fetchUserRolesAndClaims:()=>fetchUserRolesAndClaims});import{and as and23,eq as eq45,inArray as inArray18}from"drizzle-orm";var noopLogger,fetchUserRolesAndClaims=async(db,userId,resolved,logger2=noopLogger)=>{let{userRolesTable,rolesTable,roleClaimsTable,claimsTable,usersTable}=resolved,result={roles:[],claims:[],claimScopes:{}};if(!userRolesTable||!rolesTable)return result;let roleIds=(await db.select().from(userRolesTable).where(and23(eq45(userRolesTable.userId,userId),activeRowsPredicate(userRolesTable)))).map((r)=>r.roleId);if(roleIds.length===0)return result;let roleRows=await db.select().from(rolesTable).where(inArray18(rolesTable.id,roleIds));if(result.roles=roleRows.map((r)=>r.name),roleClaimsTable&&claimsTable){let rows=(await db.select({action:claimsTable.action,scope:roleClaimsTable.scope}).from(roleClaimsTable).innerJoin(claimsTable,eq45(roleClaimsTable.claimId,claimsTable.id)).where(and23(inArray18(roleClaimsTable.roleId,roleIds),activeRowsPredicate(roleClaimsTable)))).filter((r)=>typeof r.action==="string"&&r.action.length>0).map((r)=>({action:r.action,scope:r.scope??null})),userData;if(rows.some((r)=>!!r.scope)&&usersTable)try{userData=(await db.select().from(usersTable).where(eq45(usersTable.id,userId)))[0]}catch(err){logger2.warn("[Authorization] Failed to load user row for scope resolution",{userId,error:err instanceof Error?err.message:String(err)})}let{claimScopes,dropped}=resolveClaimScopes(rows,userData,logger2),droppedSet=new Set(dropped);result.claims=Array.from(new Set(rows.map((r)=>r.action))).filter((a)=>!droppedSet.has(a)),result.claimScopes=claimScopes}return result};var init_fetchUserRolesAndClaims=__esm(()=>{init_activeRows();init_Middleware();noopLogger={debug:()=>{},info:()=>{},warn:()=>{},error:()=>{}}});var IMPERSONATION_COOKIE_NAMES;var init_impersonationCookies=__esm(()=>{IMPERSONATION_COOKIE_NAMES=["nucleus_impersonating_as","nucleus_godmin_id","nucleus_impersonating_email","nucleus_imp_sig"]});import{eq as eq46}from"drizzle-orm";function createImpersonateRoute(config,signAccessToken,signRefreshToken,verifyRefreshToken,createSession,saveSessionToDb,cookieConfig){let{db,logger:logger2}=config,requireGodmin2=createRequireGodmin(config),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,sameSite:cookieConfig?.sameSite||"lax",path:cookieConfig?.path||"/",domain:cookieConfig?.domain},routePostAuthAdminImpersonate=defineRoute({method:"POST",path:"/auth/admin/impersonate",body:Type.Object({targetUserId:Type.String()}),detail:{tags:["Authentication"],summary:"Impersonate User",description:"Godmin: start a session as another user"},handler:async(ctx)=>{let{usersTable}=resolveAuthTablesForRequest(ctx.request,config);if(!db||!usersTable)return ctx.set.status=500,{success:!1,message:"Database not configured"};let{targetUserId}=ctx.body,gate=await requireGodmin2(ctx.request);if(!gate.ok)return ctx.set.status=gate.status,{success:!1,message:gate.message};let{callerId:requestingUserId,caller:requestingUser}=gate;if(requestingUserId===targetUserId)return ctx.set.status=400,{success:!1,message:"Cannot impersonate yourself"};let targetUser=(await db.select().from(usersTable).where(eq46(usersTable.id,targetUserId)).limit(1))[0];if(!targetUser)return ctx.set.status=404,{success:!1,message:"Target user not found"};let impRoles=[],impClaims=[],impClaimScopes={};if(config.db)try{let resolved=resolveAuthTablesForRequest(ctx.request,config),rc=await fetchUserRolesAndClaims(config.db,targetUserId,resolved);impRoles=rc.roles,impClaims=rc.claims,impClaimScopes=rc.claimScopes}catch{}let reqIp=ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||"unknown",sessionParams={userId:targetUserId,deviceInfo:{deviceName:"Impersonation Session",browserName:"Admin",osName:"Admin",ipAddress:reqIp},loginMethod:"impersonation"},sessionId=await createSession(sessionParams),accessToken=signAccessToken(targetUserId,impRoles.length>0?impRoles:void 0,impClaims.length>0?impClaims:void 0,ctx.request.headers.get("x-tenant-schema")??void 0,impClaimScopes,sessionId),refreshToken=signRefreshToken(targetUserId,void 0,sessionId);if(saveSessionToDb){let impSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0;await saveSessionToDb(sessionId,sessionParams,impSchemaName)}logger2.info("[AUTH] Impersonation started",{godminId:requestingUserId,targetUserId,targetEmail:targetUser.email}),await logger2.audit({entityName:"users",entityId:targetUserId,operation:"IMPERSONATE_START",userId:requestingUserId,summary:`Godmin ${requestingUser.email} started impersonating ${targetUser.email}`,ipAddress:ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||"unknown",userAgent:ctx.request.headers.get("user-agent")||"unknown",path:new URL(ctx.request.url).pathname,query:""});let securePart=cookies.secure?"; Secure":"",domainPart=cookies.domain?`; Domain=${cookies.domain}`:"",cookieOptions=`; Path=${cookies.path}; HttpOnly; SameSite=${cookies.sameSite}${securePart}${domainPart}`,cookiesToSet=[`${cookies.accessTokenName}=${accessToken}${cookieOptions}; Max-Age=${cookies.accessTokenMaxAge}`,`${cookies.refreshTokenName}=${refreshToken}${cookieOptions}; Max-Age=${cookies.refreshTokenMaxAge}`,`${cookies.sessionTokenName}=${sessionId}${cookieOptions}; Max-Age=${cookies.sessionTokenMaxAge}`,`nucleus_impersonating_as=${targetUserId}${cookieOptions}; Max-Age=${cookies.sessionTokenMaxAge}`,`nucleus_godmin_id=${requestingUserId}${cookieOptions}; Max-Age=${cookies.sessionTokenMaxAge}`,`nucleus_impersonating_email=${encodeURIComponent(String(targetUser.email))}${cookieOptions}; Max-Age=${cookies.sessionTokenMaxAge}`,`nucleus_imp_sig=${signRefreshToken(requestingUserId)}${cookieOptions}; Max-Age=${cookies.refreshTokenMaxAge}`],jsonBody=JSON.stringify(withEnvelopeFlags({success:!0,data:{targetUser:{id:targetUser.id,email:targetUser.email},godminId:requestingUserId,sessionId}})),headers=new Headers;headers.set("Content-Type","application/json");for(let cookie of cookiesToSet)headers.append("Set-Cookie",cookie);return new Response(jsonBody,{status:200,headers})}}),routePostAuthAdminImpersonateStop=defineRoute({method:"POST",path:"/auth/admin/impersonate/stop",detail:{tags:["Authentication"],summary:"Stop Impersonation",description:"Godmin: restore own session after impersonation"},handler:async(ctx)=>{let{usersTable}=resolveAuthTablesForRequest(ctx.request,config);if(!db||!usersTable)return ctx.set.status=500,{success:!1,message:"Database not configured"};let cookieHeader=ctx.request.headers.get("cookie")||"",impSig=Object.fromEntries(cookieHeader.split(";").map((c)=>{let[k,...v]=c.trim().split("=");return[k?.trim(),v.join("=")]})).nucleus_imp_sig,verified=impSig?verifyRefreshToken(impSig):{valid:!1};if(!verified.valid||!verified.payload?.sub)return ctx.set.status=401,{success:!1,message:"No valid impersonation session"};let godminId=verified.payload.sub,godminUser=(await db.select().from(usersTable).where(eq46(usersTable.id,godminId)).limit(1))[0];if(!godminUser)return ctx.set.status=404,{success:!1,message:"Godmin user not found"};if(godminUser.isGod!==!0)return ctx.set.status=403,{success:!1,message:"Forbidden: godmin privileges required"};let stateRefusal=accountStateRefusal(godminUser);if(stateRefusal)return ctx.set.status=401,{success:!1,message:stateRefusal};let godminRoles=[],godminClaims=[],godminClaimScopes={};if(config.db)try{let resolvedStop=resolveAuthTablesForRequest(ctx.request,config),rcStop=await fetchUserRolesAndClaims(config.db,godminId,resolvedStop);godminRoles=rcStop.roles,godminClaims=rcStop.claims,godminClaimScopes=rcStop.claimScopes}catch{}let stopReqIp=ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||"unknown",sessionParams={userId:godminId,deviceInfo:{deviceName:"Restored Admin Session",browserName:"Admin",osName:"Admin",ipAddress:stopReqIp},loginMethod:"impersonation_stop"},sessionId=await createSession(sessionParams),accessToken=signAccessToken(godminId,godminRoles.length>0?godminRoles:void 0,godminClaims.length>0?godminClaims:void 0,ctx.request.headers.get("x-tenant-schema")??void 0,godminClaimScopes,sessionId),refreshToken=signRefreshToken(godminId,void 0,sessionId);if(saveSessionToDb){let stopSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0;await saveSessionToDb(sessionId,sessionParams,stopSchemaName)}logger2.info("[AUTH] Impersonation stopped",{godminId}),await logger2.audit({entityName:"users",entityId:godminId,operation:"IMPERSONATE_STOP",userId:godminId,summary:`Godmin ${godminUser.email} stopped impersonation`,ipAddress:ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||"unknown",userAgent:ctx.request.headers.get("user-agent")||"unknown",path:new URL(ctx.request.url).pathname,query:""});let securePart=cookies.secure?"; Secure":"",domainPart=cookies.domain?`; Domain=${cookies.domain}`:"",cookieOptions=`; Path=${cookies.path}; HttpOnly; SameSite=${cookies.sameSite}${securePart}${domainPart}`,expiredOptions=`; Path=${cookies.path}; HttpOnly; SameSite=${cookies.sameSite}${securePart}${domainPart}; Max-Age=0`,cookiesToSet=[`${cookies.accessTokenName}=${accessToken}${cookieOptions}; Max-Age=${cookies.accessTokenMaxAge}`,`${cookies.refreshTokenName}=${refreshToken}${cookieOptions}; Max-Age=${cookies.refreshTokenMaxAge}`,`${cookies.sessionTokenName}=${sessionId}${cookieOptions}; Max-Age=${cookies.sessionTokenMaxAge}`,...IMPERSONATION_COOKIE_NAMES.map((name2)=>`${name2}=deleted${expiredOptions}`)],jsonBody=JSON.stringify(withEnvelopeFlags({success:!0,data:{godminUser:{id:godminUser.id,email:godminUser.email},sessionId}})),headers=new Headers;headers.set("Content-Type","application/json");for(let cookie of cookiesToSet)headers.append("Set-Cookie",cookie);return new Response(jsonBody,{status:200,headers})}});return collectRoutes([{routes:[routePostAuthAdminImpersonate,routePostAuthAdminImpersonateStop]}])}var init_impersonate=__esm(()=>{init_esm3();init_response();init_routeCollector();init_fetchUserRolesAndClaims();init_impersonationCookies();init_requireGodmin()});function clearedLockoutState(){return{failedLoginAttempts:0,isLocked:!1,lockedUntil:null}}function planPasswordRotation(input){let protectedRoles=new Set(input.protectedRoles.filter((name2)=>name2.trim()!=="")),plan={rotate:[],skipped:[]};for(let user of input.users){if(user.id===input.callerId){plan.skipped.push({id:user.id,reason:"caller"});continue}if(user.isGod){plan.skipped.push({id:user.id,reason:"godmin"});continue}if(user.roleNames.some((name2)=>protectedRoles.has(name2))){plan.skipped.push({id:user.id,reason:"protected_role"});continue}if(input.includeInactive!==!0&&user.isActive===!1){plan.skipped.push({id:user.id,reason:"inactive"});continue}plan.rotate.push(user.id)}return plan}function splitByRecoverability(users,rotate){let rotating=new Set(rotate),selfService=[],needsAdmin=[];for(let user of users){if(!rotating.has(user.id))continue;if(String(user.email??"").trim()==="")needsAdmin.push(user.id);else selfService.push(user.id)}return{selfService,needsAdmin}}import{randomBytes as randomBytes5}from"crypto";import{eq as eq47}from"drizzle-orm";function createPasswordAdminRoutes(config,options){let{db,logger:logger2}=config,log3=logger2.scoped(AUTH_LOGIN),passwordToolsEnabled=options?.passwordTools===!0,requireGodmin2=createRequireGodmin(config),routePostAuthAdminRotatePasswords=defineRoute({method:"POST",path:"/auth/admin/rotate-passwords",body:Type.Object({confirm:Type.Optional(Type.Boolean()),protectedRoles:Type.Optional(Type.Array(Type.String())),includeInactive:Type.Optional(Type.Boolean())}),detail:{tags:["Authentication"],summary:"Rotate every password except the ones named",description:"Without `confirm: true` this only reports what it would do. Confirmed, it gives every selected account a different random password, signs those sessions out, and tells nobody the new values \u2014 recovery is the reset link or an administrator."},handler:async(ctx)=>{if(!passwordToolsEnabled)return notEnabled(ctx);let gate=await requireGodmin2(ctx.request);if(!gate.ok)return ctx.set.status=gate.status,{success:!1,message:gate.message};let resolved=resolveAuthTablesForRequest(ctx.request,config),usersTable=resolved.usersTable,body=ctx.body,users=await db?.select().from(usersTable),roleNamesByUser=await loadRoleNames(resolved,users),candidates=users.map((row)=>({id:String(row.id),roleNames:roleNamesByUser.get(String(row.id))??[],isGod:row.isGod===!0,email:row.email??null,isActive:row.isActive})),plan=planPasswordRotation({users:candidates,callerId:gate.callerId,protectedRoles:body.protectedRoles??[],includeInactive:body.includeInactive===!0}),recovery=splitByRecoverability(candidates,plan.rotate);if(body.confirm!==!0)return{success:!0,data:{preview:!0,willRotate:plan.rotate.length,skipped:plan.skipped.length,skippedReasons:countReasons(plan.skipped),canResetThemselves:recovery.selfService.length,needAdministrator:recovery.needsAdmin.length}};if(plan.rotate.length===0)return{success:!0,data:{preview:!1,rotated:0,signedOut:0}};let rotated=0;for(let userId of plan.rotate){let hashed=await hashPassword(randomBytes5(24).toString("base64url"));await db?.update(usersTable).set({password:hashed,updatedAt:new Date}).where(eq47(usersTable.id,userId)),rotated+=1}let signedOut=await revokeSessions(resolved,plan.rotate);return log3.warn("Bulk password rotation",{by:gate.callerId,rotated,signedOut,skipped:plan.skipped.length}),{success:!0,data:{preview:!1,rotated,signedOut,canResetThemselves:recovery.selfService.length,needAdministrator:recovery.needsAdmin.length}}}}),routePostAuthAdminSetUserPassword=defineRoute({method:"POST",path:"/auth/admin/set-user-password",body:Type.Object({userId:Type.String(),password:Type.String()}),detail:{tags:["Authentication"],summary:"Set one account's password",description:"For the person who has no email address and cannot use the reset link. Applies the same strength policy as a self-service change and ends that account\u2019s existing sessions."},handler:async(ctx)=>{if(!passwordToolsEnabled)return notEnabled(ctx);let gate=await requireGodmin2(ctx.request);if(!gate.ok)return ctx.set.status=gate.status,{success:!1,message:gate.message};let resolved=resolveAuthTablesForRequest(ctx.request,config),usersTable=resolved.usersTable,{userId,password:password4}=ctx.body,strength=validatePasswordStrength(password4);if(!strength.valid)return ctx.set.status=400,{success:!1,message:strength.errors?.join(" ")??"Password is too weak"};let rows=await db?.select().from(usersTable).where(eq47(usersTable.id,userId)).limit(1);if(!rows||rows.length===0)return ctx.set.status=404,{success:!1,message:"User not found"};await db?.update(usersTable).set({password:await hashPassword(password4),updatedAt:new Date,...clearedLockoutState()}).where(eq47(usersTable.id,userId));let signedOut=await revokeSessions(resolved,[userId]);return log3.warn("Password set by administrator",{by:gate.callerId,userId,signedOut}),{success:!0,data:{signedOut,lockoutCleared:!0}}}});return collectRoutes([{routes:[routePostAuthAdminRotatePasswords,routePostAuthAdminSetUserPassword]}]);function notEnabled(ctx){return ctx.set.status=404,{success:!1,message:"Not found"}}async function loadRoleNames(resolved,users){let byUser=new Map,userRoles=resolved.userRolesTable,roles=resolved.rolesTable;if(!db||!userRoles||!roles||users.length===0)return byUser;let roleRows=await db.select().from(roles),roleNameById=new Map(roleRows.map((row)=>[String(row.id),String(row.name??"")])),links=await db.select().from(userRoles);for(let link of links){let userId=String(link.userId??""),name2=roleNameById.get(String(link.roleId??""));if(!userId||!name2)continue;byUser.set(userId,[...byUser.get(userId)??[],name2])}return byUser}async function revokeSessions(resolved,userIds){return revokeUserSessions({db,sessionsTable:resolved.sessionsTable,onError:(error3)=>log3.error("Sessions could not be revoked after a password change",{error:error3})},userIds)}}function countReasons(skipped){let counts={};for(let item of skipped)counts[item.reason]=(counts[item.reason]??0)+1;return counts}var init_passwordAdmin=__esm(()=>{init_esm3();init_scopes();init_routeCollector();init_utils6();init_requireGodmin();init_revokeUserSessions()});import{eq as eq48}from"drizzle-orm";function createUserStateRoutes(config){let{db,logger:logger2}=config,log3=logger2.scoped(AUTH_LOGIN),requireGodmin2=createRequireGodmin(config);async function loadTarget(request,userId){let resolved=resolveAuthTablesForRequest(request,config),usersTable=resolved.usersTable;if(!db||!usersTable)return{ok:!1,status:500,message:"Database not configured"};let user=(await db.select().from(usersTable).where(eq48(usersTable.id,userId)).limit(1))?.[0];if(!user)return{ok:!1,status:404,message:"User not found"};return{ok:!0,usersTable,sessionsTable:resolved.sessionsTable??null,user}}async function writeState(usersTable,userId,changes){await db?.update(usersTable).set({...changes,updatedAt:new Date}).where(eq48(usersTable.id,userId))}let routePostAuthAdminLockUser=defineRoute({method:"POST",path:"/auth/admin/lock-user",body:Type.Object({userId:Type.String(),until:Type.Optional(Type.String())}),detail:{tags:["Authentication"],summary:"Lock one account",description:"Refuses further login attempts. `until` is an ISO timestamp for a temporary lock; omit it to lock indefinitely. Cannot be used on the calling administrator."},handler:async(ctx)=>{let gate=await requireGodmin2(ctx.request);if(!gate.ok)return ctx.set.status=gate.status,{success:!1,message:gate.message};let{userId,until}=ctx.body,target2=await loadTarget(ctx.request,userId);if(!target2.ok)return ctx.set.status=target2.status,{success:!1,message:target2.message};if(userId===gate.callerId)return ctx.set.status=400,{success:!1,message:"An administrator cannot lock their own account"};let lockedUntil=until?new Date(until):null;if(until&&Number.isNaN(lockedUntil?.getTime()))return ctx.set.status=400,{success:!1,message:"`until` is not a valid date"};await writeState(target2.usersTable,userId,{isLocked:!0,lockedUntil});let signedOut=await revokeUserSessions({db,sessionsTable:target2.sessionsTable,onError:(error3)=>log3.error("Sessions could not be revoked after locking an account",{error:error3,userId})},[userId]);return log3.warn("Account locked by administrator",{by:gate.callerId,userId,until:lockedUntil,signedOut}),{success:!0,data:{isLocked:!0,lockedUntil,signedOut}}}}),routePostAuthAdminUnlockUser=defineRoute({method:"POST",path:"/auth/admin/unlock-user",body:Type.Object({userId:Type.String()}),detail:{tags:["Authentication"],summary:"Release one account from lockout",description:"Clears isLocked, lockedUntil AND failedLoginAttempts together, without touching the password. Clearing only isLocked is not a release: the login route sends any account at or above the captcha threshold through the captcha gate before it checks the password, and the counter only clears on a login that succeeds."},handler:async(ctx)=>{let gate=await requireGodmin2(ctx.request);if(!gate.ok)return ctx.set.status=gate.status,{success:!1,message:gate.message};let{userId}=ctx.body,target2=await loadTarget(ctx.request,userId);if(!target2.ok)return ctx.set.status=target2.status,{success:!1,message:target2.message};let wasLocked=target2.user.isLocked===!0,clearedFailedAttempts=Number(target2.user.failedLoginAttempts??0);return await writeState(target2.usersTable,userId,clearedLockoutState()),log3.warn("Account lockout cleared by administrator",{by:gate.callerId,userId,wasLocked,clearedFailedAttempts}),{success:!0,data:{wasLocked,clearedFailedAttempts}}}}),routePostAuthAdminMarkEmailVerified=defineRoute({method:"POST",path:"/auth/admin/mark-email-verified",body:Type.Object({userId:Type.String()}),detail:{tags:["Authentication"],summary:"Mark one account as email-verified",description:"For an installation with no working mail transport, where the verification message can never arrive. Idempotent: an account that is already verified keeps its original timestamp."},handler:async(ctx)=>{let gate=await requireGodmin2(ctx.request);if(!gate.ok)return ctx.set.status=gate.status,{success:!1,message:gate.message};let{userId}=ctx.body,target2=await loadTarget(ctx.request,userId);if(!target2.ok)return ctx.set.status=target2.status,{success:!1,message:target2.message};let alreadyVerified=Boolean(target2.user.verifiedAt),verifiedAt=alreadyVerified?target2.user.verifiedAt:new Date;if(!alreadyVerified)await writeState(target2.usersTable,userId,{verifiedAt,emailVerified:!0});return log3.warn("Email marked verified by administrator",{by:gate.callerId,userId,alreadyVerified}),{success:!0,data:{verifiedAt,alreadyVerified}}}});return collectRoutes([{routes:[routePostAuthAdminLockUser,routePostAuthAdminUnlockUser,routePostAuthAdminMarkEmailVerified]}])}var init_userState=__esm(()=>{init_esm3();init_scopes();init_routeCollector();init_requireGodmin();init_revokeUserSessions()});import{and as and24,count as count2,eq as eq49,sql as sql10}from"drizzle-orm";function createApiKeyRoutes(app,config,apiKeysConfig){let basePath=apiKeysConfig.route||"/auth/api-keys",apiKeysTable=config.apiKeysTable,{db,logger:logger2}=config;if(!apiKeysTable||!db){logger2.warn("[API_KEYS] api_keys table or database not available. Skipping API key routes.");return}let getResolvedConfig=(request)=>{let resolved=resolveAuthTablesForRequest(request,config),reqApiKeysTable=resolved.apiKeysTable??apiKeysTable;return{config:{...config,usersTable:resolved.usersTable??config.usersTable,userRolesTable:resolved.userRolesTable,rolesTable:resolved.rolesTable,roleClaimsTable:resolved.roleClaimsTable,claimsTable:resolved.claimsTable,apiKeysTable:reqApiKeysTable,schemaTables:resolved.schemaTables},apiKeysTable:reqApiKeysTable}},keyPrefix=apiKeysConfig.keyPrefix||"nk_live",maxKeysPerUser=apiKeysConfig.maxKeysPerUser||10,preventManagement=apiKeysConfig.preventApiKeyManagement??!1,routePostRoot=defineRoute({method:"POST",path:"/",handler:async({request,set:set2,body:rawBody})=>{let resolved=getResolvedConfig(request),reqApiKeysTable=resolved.apiKeysTable,reqConfig=resolved.config;if(preventManagement)return set2.status=403,{isSuccess:!1,message:"API key management is disabled via configuration",data:null};if(request.headers.get("x-auth-type")==="api_key")return set2.status=403,{isSuccess:!1,message:"API keys cannot be created using another API key",data:null};let userId=request.headers.get("x-user-id");if(!userId)return set2.status=401,{isSuccess:!1,message:"Unauthenticated",data:null};let body;try{body=rawBody??{}}catch{return set2.status=400,{isSuccess:!1,message:"Invalid request body",data:null}}if(!body.name||typeof body.name!=="string"||body.name.trim().length===0)return set2.status=400,{isSuccess:!1,message:"Name is required",data:null};if(body.ownerType==="application"){if(!(apiKeysConfig.allowApplicationKeys??!0))return set2.status=403,{isSuccess:!1,message:"Application API keys are not allowed",data:null};if(!body.applicationName||body.applicationName.trim().length===0)return set2.status=400,{isSuccess:!1,message:"Application name is required for application keys",data:null}}if(((await db.select({count:count2()}).from(reqApiKeysTable).where(and24(eq49(col13(reqApiKeysTable,"userId"),userId),eq49(col13(reqApiKeysTable,"isActive"),!0))))[0]?.count||0)>=maxKeysPerUser)return set2.status=400,{isSuccess:!1,message:`Maximum API key limit reached (${maxKeysPerUser})`,data:null};let userRoles=await getUserRoleNames(reqConfig,userId),userClaims=await getUserClaimActions(reqConfig,userId),isGodmin4=userRoles.some((r)=>isGodminRole(r)),requestedRoles=body.allowedRoles||[],requestedClaims=body.allowedClaims||[],requestedScopes=body.allowedScopes||[],stripGodmin=(roles)=>roles.filter((r)=>!isGodminRole(r)),finalRoles,finalClaims;if(isGodmin4)finalRoles=requestedRoles.length>0?stripGodmin(requestedRoles):[],finalClaims=requestedClaims.length>0?requestedClaims:userClaims;else{let validRoles=intersectPermissions(userRoles,requestedRoles),validClaims=intersectPermissions(userClaims,requestedClaims);if(requestedRoles.length>0&&validRoles.length!==requestedRoles.length){let disallowed=requestedRoles.filter((r)=>!userRoles.includes(r));return set2.status=403,{isSuccess:!1,message:`Cannot assign roles you do not have: ${disallowed.join(", ")}`,data:null}}if(requestedClaims.length>0&&validClaims.length!==requestedClaims.length){let disallowed=requestedClaims.filter((c)=>!userClaims.includes(c));return set2.status=403,{isSuccess:!1,message:`Cannot assign claims you do not have: ${disallowed.join(", ")}`,data:null}}if(requestedScopes.length>0){let userScopes=await getUserScopes(reqConfig,userId);if(intersectPermissions(userScopes,requestedScopes).length!==requestedScopes.length){let disallowed=requestedScopes.filter((s)=>!userScopes.includes(s));return set2.status=403,{isSuccess:!1,message:`Cannot assign scopes you do not have: ${disallowed.join(", ")}`,data:null}}}finalRoles=stripGodmin(requestedRoles.length>0?validRoles:userRoles),finalClaims=requestedClaims.length>0?validClaims:userClaims}let expiresAt=null,expiresInStr=body.expiresIn||apiKeysConfig.defaultExpiresIn;if(expiresInStr){let seconds=parseTimeToSeconds2(expiresInStr);expiresAt=new Date(Date.now()+seconds*1000)}let generated=generateApiKey(keyPrefix);try{let createdId=(await db.insert(reqApiKeysTable).values({userId,name:body.name.trim(),description:body.description?.trim()||null,keyHash:generated.keyHash,keyPreview:generated.keyPreview,ownerType:body.ownerType||"personal",applicationName:body.applicationName?.trim()||null,expiresAt,isActive:!0,usageCount:0,createdAt:new Date,updatedAt:new Date}).returning())[0]?.id;if(finalRoles.length>0||finalClaims.length>0||requestedScopes.length>0){let jsonbUpdate={};if(finalRoles.length>0)jsonbUpdate.allowedRoles=JSON.stringify(finalRoles);if(finalClaims.length>0)jsonbUpdate.allowedClaims=JSON.stringify(finalClaims);if(requestedScopes.length>0)jsonbUpdate.allowedScopes=JSON.stringify(requestedScopes);await db.update(reqApiKeysTable).set(jsonbUpdate).where(eq49(col13(reqApiKeysTable,"keyHash"),generated.keyHash))}return logger2.info("[API_KEYS] API key created",{keyId:createdId,userId,ownerType:body.ownerType||"personal",applicationName:body.applicationName,rolesCount:finalRoles.length,claimsCount:finalClaims.length,expiresAt:expiresAt?.toISOString()||"never"}),{isSuccess:!0,message:"API key created successfully. Save this key \u2014 it will not be shown again.",data:{id:createdId,key:generated.rawKey,keyPreview:generated.keyPreview,name:body.name.trim(),ownerType:body.ownerType||"personal",applicationName:body.applicationName||null,allowedRoles:finalRoles,allowedClaims:finalClaims,allowedScopes:requestedScopes,expiresAt:expiresAt?.toISOString()||null}}}catch(err){let errDetail;try{errDetail=JSON.stringify(err,Object.getOwnPropertyNames(err))}catch{errDetail=err instanceof Error?err.message:String(err)}return logger2.error("[API_KEYS] Failed to create API key",{error:errDetail,userId}),set2.status=500,{isSuccess:!1,message:"Failed to create API key",data:null}}}}),routeGetRoot=defineRoute({method:"GET",path:"/",detail:{tags:["API keys"],summary:"List the caller's API keys",description:"Every key the caller owns, revoked ones included \u2014 a revoked key is history, not "+"garbage, and it is the only place `revokedAt` / `revokedReason` can be read. Filter on `isActive` for the live set; that is also what `maxKeysPerUser` counts."},handler:async({request,set:set2})=>{let{apiKeysTable:reqApiKeysTable}=getResolvedConfig(request),userId=request.headers.get("x-user-id");if(!userId)return set2.status=401,{isSuccess:!1,message:"Unauthenticated",data:null};let items=(await db.select().from(reqApiKeysTable).where(eq49(col13(reqApiKeysTable,"userId"),userId)).orderBy(sql10`created_at DESC`)).map((row)=>sanitizeKeyForResponse(row));return{isSuccess:!0,data:{items,totalCount:items.length}}}}),routeGetId=defineRoute({method:"GET",path:"/:id",detail:{tags:["API keys"],summary:"Read one API key",description:"A REVOKED key still answers 200 here \u2014 revoking does not delete the row. Read "+"`isActive` / `revokedAt` rather than the status to decide whether the key is live. 404 means no such key, or a key belonging to somebody else."},handler:async({params,request,set:set2})=>{let{apiKeysTable:reqApiKeysTable}=getResolvedConfig(request),userId=request.headers.get("x-user-id");if(!userId)return set2.status=401,{isSuccess:!1,message:"Unauthenticated",data:null};let keyId=params.id,record3=(await db.select().from(reqApiKeysTable).where(and24(eq49(col13(reqApiKeysTable,"id"),keyId),eq49(col13(reqApiKeysTable,"userId"),userId))).limit(1))[0];if(!record3)return set2.status=404,{isSuccess:!1,message:"API key not found",data:null};return{isSuccess:!0,data:sanitizeKeyForResponse(record3)}}}),routePatchId=defineRoute({method:"PATCH",path:"/:id",handler:async({params,request,set:set2,body:rawBody})=>{let resolved=getResolvedConfig(request),reqApiKeysTable=resolved.apiKeysTable,reqConfig=resolved.config;if(preventManagement)return set2.status=403,{isSuccess:!1,message:"API key management is disabled via configuration",data:null};if(request.headers.get("x-auth-type")==="api_key")return set2.status=403,{isSuccess:!1,message:"API keys cannot be modified using another API key",data:null};let userId=request.headers.get("x-user-id");if(!userId)return set2.status=401,{isSuccess:!1,message:"Unauthenticated",data:null};let keyId=params.id,body;try{body=rawBody??{}}catch{return set2.status=400,{isSuccess:!1,message:"Invalid request body",data:null}}let existing=(await db.select().from(reqApiKeysTable).where(and24(eq49(col13(reqApiKeysTable,"id"),keyId),eq49(col13(reqApiKeysTable,"userId"),userId))).limit(1))[0];if(!existing)return set2.status=404,{isSuccess:!1,message:"API key not found",data:null};if(existing.revokedAt)return set2.status=400,{isSuccess:!1,message:"Cannot modify a revoked API key",data:null};let userRoles=await getUserRoleNames(reqConfig,userId),userClaims=await getUserClaimActions(reqConfig,userId),updatePayload={updatedAt:new Date};if(body.name!==void 0){if(typeof body.name!=="string"||body.name.trim().length===0)return set2.status=400,{isSuccess:!1,message:"Name cannot be empty",data:null};updatePayload.name=body.name.trim()}if(body.description!==void 0)updatePayload.description=body.description?.trim()||null;if(body.allowedRoles!==void 0){if(intersectPermissions(userRoles,body.allowedRoles).length!==body.allowedRoles.length){let disallowed=body.allowedRoles.filter((r)=>!userRoles.includes(r));return set2.status=403,{isSuccess:!1,message:`Cannot assign roles you do not have: ${disallowed.join(", ")}`,data:null}}updatePayload.allowedRoles=body.allowedRoles.filter((r)=>!isGodminRole(r))}if(body.allowedClaims!==void 0){if(intersectPermissions(userClaims,body.allowedClaims).length!==body.allowedClaims.length){let disallowed=body.allowedClaims.filter((c)=>!userClaims.includes(c));return set2.status=403,{isSuccess:!1,message:`Cannot assign claims you do not have: ${disallowed.join(", ")}`,data:null}}updatePayload.allowedClaims=body.allowedClaims}if(body.allowedScopes!==void 0){if(body.allowedScopes.length>0){let userScopes=await getUserScopes(reqConfig,userId);if(intersectPermissions(userScopes,body.allowedScopes).length!==body.allowedScopes.length){let disallowed=body.allowedScopes.filter((s)=>!userScopes.includes(s));return set2.status=403,{isSuccess:!1,message:`Cannot assign scopes you do not have: ${disallowed.join(", ")}`,data:null}}}updatePayload.allowedScopes=body.allowedScopes}try{await db.update(reqApiKeysTable).set(updatePayload).where(eq49(col13(reqApiKeysTable,"id"),keyId)),logger2.info("[API_KEYS] API key updated",{keyId,userId});let updated=await db.select().from(reqApiKeysTable).where(eq49(col13(reqApiKeysTable,"id"),keyId)).limit(1);return{isSuccess:!0,message:"API key updated successfully",data:sanitizeKeyForResponse(updated[0])}}catch(err){return logger2.error("[API_KEYS] Failed to update API key",{error:err,keyId,userId}),set2.status=500,{isSuccess:!1,message:"Failed to update API key",data:null}}}}),revokeApiKey=async({params,request,set:set2,body:rawBody})=>{let{apiKeysTable:reqApiKeysTable}=getResolvedConfig(request);if(request.headers.get("x-auth-type")==="api_key")return set2.status=403,{isSuccess:!1,message:"API keys cannot be revoked using another API key",data:null};let userId=request.headers.get("x-user-id");if(!userId)return set2.status=401,{isSuccess:!1,message:"Unauthenticated",data:null};let keyId=params.id,body={};try{if(rawBody)body=rawBody}catch{}let record3=(await db.select().from(reqApiKeysTable).where(and24(eq49(col13(reqApiKeysTable,"id"),keyId),eq49(col13(reqApiKeysTable,"userId"),userId))).limit(1))[0];if(!record3)return set2.status=404,{isSuccess:!1,message:"API key not found",data:null};if(record3.revokedAt)return set2.status=400,{isSuccess:!1,message:"API key is already revoked",data:null};let revokedAt=new Date,revokedReason=body.reason||"user_revoked";try{await db.update(reqApiKeysTable).set({isActive:!1,revokedAt,revokedReason,updatedAt:revokedAt}).where(eq49(col13(reqApiKeysTable,"id"),keyId)),logger2.info("[API_KEYS] API key revoked",{keyId,userId,reason:revokedReason});let revoked=(await db.select().from(reqApiKeysTable).where(eq49(col13(reqApiKeysTable,"id"),keyId)).limit(1))[0]??{...record3,isActive:!1,revokedAt,revokedReason,updatedAt:revokedAt};return{isSuccess:!0,message:"API key revoked. It can no longer authenticate. This is a revoke, not a delete \u2014 "+`the record is retained as history and still reads at GET ${basePath}/${keyId}.`,data:sanitizeKeyForResponse(revoked)}}catch(err){return logger2.error("[API_KEYS] Failed to revoke API key",{error:err,keyId,userId}),set2.status=500,{isSuccess:!1,message:"Failed to revoke API key",data:null}}},revokeDetail=(summary)=>({tags:["API keys"],summary,description:"Revokes the key: it stops authenticating immediately, on this request. It is NOT a "+"delete \u2014 the row is retained as the audit record (`revokedAt`, `revokedReason`, "+"`usageCount`, `lastUsedAt`), so `GET /:id` still answers 200 with `isActive:false` and the list still carries it. The answer carries the revoked record. Revoking an "+"already-revoked key answers 400, not 404 \u2014 the row is still there. Revoking is "+"deliberately NOT blocked by `preventApiKeyManagement`: being unable to kill a leaked key because management is off would be worse than the rule it enforces."}),routePostIdRevoke=defineRoute({method:"POST",path:"/:id/revoke",detail:revokeDetail("Revoke an API key"),handler:(ctx)=>revokeApiKey(ctx)}),routeDeleteId=defineRoute({method:"DELETE",path:"/:id",detail:revokeDetail("Revoke an API key (alias of POST /:id/revoke)"),handler:(ctx)=>revokeApiKey(ctx)});mountRoutes(app,[{prefix:basePath,routes:[routePostRoot,routeGetRoot,routeGetId,routePatchId,routePostIdRevoke,routeDeleteId]}]),logger2.info(`[API_KEYS] Routes registered at ${basePath}`)}var col13=(table,name2)=>table[name2],getUserRoleNames=async(config,userId)=>{if(!config.db||!config.userRolesTable||!config.rolesTable)return[];return(await config.db.select({name:col13(config.rolesTable,"name")}).from(config.userRolesTable).innerJoin(config.rolesTable,eq49(col13(config.rolesTable,"id"),col13(config.userRolesTable,"roleId"))).where(eq49(col13(config.userRolesTable,"userId"),userId))).map((r)=>r.name).filter((n)=>n!==void 0)},getUserClaimActions=async(config,userId)=>{if(!config.db||!config.userRolesTable||!config.roleClaimsTable||!config.claimsTable)return[];let rows=await config.db.select({action:col13(config.claimsTable,"action")}).from(config.userRolesTable).innerJoin(config.roleClaimsTable,eq49(col13(config.roleClaimsTable,"roleId"),col13(config.userRolesTable,"roleId"))).innerJoin(config.claimsTable,eq49(col13(config.claimsTable,"id"),col13(config.roleClaimsTable,"claimId"))).where(eq49(col13(config.userRolesTable,"userId"),userId)),uniqueActions=new Set(rows.map((r)=>r.action));return Array.from(uniqueActions).filter((a)=>a!==void 0)},getUserScopes=async(config,userId)=>{if(!config.db||!config.userRolesTable||!config.roleClaimsTable)return[];let rows=await config.db.select({scope:col13(config.roleClaimsTable,"scope")}).from(config.userRolesTable).innerJoin(config.roleClaimsTable,eq49(col13(config.roleClaimsTable,"roleId"),col13(config.userRolesTable,"roleId"))).where(eq49(col13(config.userRolesTable,"userId"),userId)),uniqueScopes=new Set(rows.map((r)=>r.scope).filter((s)=>s!==null&&s!==void 0&&s.length>0));return Array.from(uniqueScopes)},sanitizeKeyForResponse=(record3)=>{let{keyHash:_keyHash,...safe}=record3;return safe};var init_apiKeys=__esm(()=>{init_ApiKey();init_GodminSetup();init_routeMount();init_utils5()});function createCaptchaRoutes(config){let{captchaService,logger:logger2,basePath="/auth/captcha"}=config;if(!captchaService.isEnabled())return collectRoutes();let warnedUnattributable=!1,warnUnattributableOnce=()=>{if(warnedUnattributable)return;warnedUnattributable=!0,logger2.warn("[CAPTCHA] No client address on the request \u2014 the per-IP generate limit is OFF. "+"Every caller would otherwise share one bucket, which locks out an entire office behind one egress. Configure the proxy or ingress to forward X-Forwarded-For.")},routeGetBasepathGenerate=defineRoute({method:"GET",path:"/generate",query:GenerateCaptchaQuerySchema,detail:{tags:["Captcha"],summary:"Generate Captcha",description:"Generate a new captcha challenge"},handler:async(ctx)=>{let{type:type4,difficulty}=ctx.query,ipAddress=ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||ctx.request.headers.get("x-real-ip")?.trim()||ctx.request.headers.get("cf-connecting-ip")?.trim()||void 0;if(!ipAddress)warnUnattributableOnce();let result=await captchaService.generate(type4,difficulty,ipAddress);if(result.rateLimited)return ctx.set.status=429,{success:!1,message:result.message??"Too many requests. Please try again later."};return logger2.info("[CAPTCHA] Challenge generated via endpoint",{challengeId:result.challengeId,type:result.type,ipAddress}),{success:!0,data:{challengeId:result.challengeId,type:result.type,question:result.question,expiresAt:result.expiresAt,...result.imageData?{imageData:result.imageData}:{},...result.puzzleData?{puzzleData:{pieces:result.puzzleData.pieces}}:{}}}}}),routePostBasepathValidate=defineRoute({method:"POST",path:"/validate",body:ValidateCaptchaBodySchema,detail:{tags:["Captcha"],summary:"Validate Captcha",description:"Validate a captcha answer"},handler:async(ctx)=>{let{challengeId,answer}=ctx.body,result=await captchaService.validate(challengeId,answer);if(!result.valid)ctx.set.status=400;return result}});return collectRoutes([{prefix:basePath,routes:[routeGetBasepathGenerate,routePostBasepathValidate]}])}var GenerateCaptchaQuerySchema,ValidateCaptchaBodySchema;var init_captcha=__esm(()=>{init_esm3();init_routeCollector();GenerateCaptchaQuerySchema=Type.Object({type:Type.Optional(Type.Union([Type.Literal("math"),Type.Literal("image"),Type.Literal("puzzle"),Type.Literal("text")])),difficulty:Type.Optional(Type.Union([Type.Literal("easy"),Type.Literal("medium"),Type.Literal("hard")]))}),ValidateCaptchaBodySchema=Type.Object({challengeId:Type.String(),answer:Type.String()})});import{eq as eq50}from"drizzle-orm";function createCheckRoute(config,checkConfig){let{db,logger:logger2}=config,route=checkConfig.route||"/auth/check",routePostCheck=defineRoute({method:"POST",path:"/",detail:{tags:["Auth"],summary:"Check authorization for a given entity and method",description:"Allows consumer/resource servers to perform full claim checks (including scope) against the IDP's authorization system."},handler:async(ctx)=>{let{body,request,set:set2}=ctx,userId=request.headers.get("x-user-id");if(!userId)return set2.status=401,{isSuccess:!1,message:"Unauthenticated",data:null};if(!db)return set2.status=503,{isSuccess:!1,message:"Authorization service unavailable",data:null};let{schemaTables}=resolveAuthTablesForRequest(request,config);if(!schemaTables)return set2.status=503,{isSuccess:!1,message:"Authorization tables not available",data:null};let{entity,method,fields,relations}=body;if(!entity||!method)return set2.status=400,{isSuccess:!1,message:"entity and method are required",data:null};let result=await checkAuthorization({userId,method,entity,requestedFields:fields,requestedRelations:relations,db,schemaTables,logger:logger2,getUserData:async()=>{let usersTbl=schemaTables[AUTHORIZATION_TABLE_KEYS.users];if(!usersTbl||!db)return;let idCol=usersTbl.id;if(!idCol)return;return(await db.select().from(usersTbl).where(eq50(idCol,userId)).limit(1))[0]}});return{isSuccess:!0,message:result.authorized?"Authorized":"Forbidden",data:{authorized:result.authorized,reason:result.reason,allowedFields:result.allowedFields,allowedRelations:result.allowedRelations,scopeFilters:result.scopeFilters}}}});return collectRoutes([{prefix:route,routes:[routePostCheck]}])}var init_check=__esm(()=>{init_Authorization();init_routeCollector()});import{sql as sql11}from"drizzle-orm";function createEmailVerificationRoutes(config){let{authConfig,registerConfig,getEmailService,appName}=config,{db,logger:logger2}=authConfig;if(!registerConfig.enabled||!registerConfig.emailVerification?.enabled)return collectRoutes();let verifyRoute="/verify-email",resendRoute="/resend-verification",routeGetVerifyroute=defineRoute({method:"GET",path:"/",query:Type.Object({token:Type.String()}),detail:{tags:["Authentication"],summary:"Verify Email",description:"Verify user email address using the verification token"},handler:async(ctx)=>{let{usersTable}=resolveAuthTablesForRequest(ctx.request,authConfig);if(!db||!usersTable)return ctx.set.status=500,{success:!1,message:"Database not configured"};let token=ctx.query.token;if(!token)return ctx.set.status=400,{success:!1,message:"Verification token is required"};let users=await db.select().from(usersTable).where(sql11`email_verification_token = ${hashVerificationToken(token)}`).limit(1);if(users.length===0)return logger2.warn("[AUTH] Email verification failed - invalid token"),ctx.set.status=400,{success:!1,message:"Invalid or expired verification token"};let user=users[0],tokenExpiresAt=user.emailVerificationTokenExpiresAt;if(tokenExpiresAt&&new Date>new Date(tokenExpiresAt))return logger2.warn("[AUTH] Email verification failed - token expired",{userId:user.id,email:user.email}),ctx.set.status=400,{success:!1,message:"Verification token has expired. Please request a new one."};await db.update(usersTable).set({verifiedAt:new Date,emailVerificationToken:null,emailVerificationTokenExpiresAt:null}).where(sql11`id = ${user.id}`),logger2.info("[AUTH] Email verified successfully",{userId:user.id,email:user.email});let emailService=getEmailService?.()??null;if(registerConfig.emailVerification?.templates?.welcome?.enabled&&emailService?.isAvailable())emailService.sendWelcomeEmail(user.email,user.email.split("@")[0]||"User",appName||"Nucleus").catch((err)=>{logger2.error("[AUTH] Failed to send welcome email after verification",{email:user.email,error:err})});return{success:!0,message:"Email verified successfully. You can now log in.",data:{redirectUrl:registerConfig.emailVerification?.redirectUrl||"/login",verified:!0}}}}),routePostResendroute=defineRoute({method:"POST",path:"/",body:Type.Object({email:Type.String({format:"email"})}),detail:{tags:["Authentication"],summary:"Resend Verification Email",description:"Resend the email verification link to the user"},handler:async(ctx)=>{let{usersTable}=resolveAuthTablesForRequest(ctx.request,authConfig);if(!db||!usersTable)return ctx.set.status=500,{success:!1,message:"Database not configured"};let emailService=getEmailService?.()??null;if(!emailService?.isAvailable())return ctx.set.status=503,{success:!1,message:"Email service not available"};let{email}=ctx.body,antiEnum=authConfig.authentication?.preventUserEnumeration===!0,uniformResend={success:!0,message:"If your email is registered and not yet verified, a verification email has been sent."},users=await db.select().from(usersTable).where(sql11`email = ${email}`).limit(1);if(users.length===0)return antiEnum?uniformResend:{success:!0,message:"If your email is registered, you will receive a verification email."};let user=users[0];if(user.verifiedAt){if(antiEnum)return uniformResend;return ctx.set.status=409,{success:!1,message:"Email is already verified"}}let maxAttempts=registerConfig.emailVerification?.maxResendAttempts||3,currentAttempts=user.emailVerificationAttempts||0;if(currentAttempts>=maxAttempts){if(logger2.warn("[AUTH] Resend verification failed - max attempts reached",{email,attempts:currentAttempts}),antiEnum)return uniformResend;return ctx.set.status=429,{success:!1,message:"Maximum resend attempts reached. Please contact support.",data:{maxAttemptsReached:!0,attemptsUsed:currentAttempts,maxAttempts}}}let resendCooldown=registerConfig.emailVerification?.resendCooldown||"60s",cooldownMs=parseTimeToMs(resendCooldown),lastSentAt=user.emailVerificationSentAt;if(lastSentAt){let timeSinceLastSent=Date.now()-new Date(lastSentAt).getTime();if(timeSinceLastSent<cooldownMs){let waitSeconds=Math.ceil((cooldownMs-timeSinceLastSent)/1000);if(antiEnum)return uniformResend;return ctx.set.status=429,{success:!1,message:`Please wait ${waitSeconds} seconds before requesting another verification email.`,data:{cooldownRemaining:waitSeconds,canResendAt:new Date(Date.now()+waitSeconds*1000).toISOString()}}}}let verificationToken=generateVerificationToken(),tokenExpiresIn=registerConfig.emailVerification?.tokenExpiresIn||"24h",tokenExpiresAt=new Date(Date.now()+parseTimeToMs(tokenExpiresIn));await db.update(usersTable).set({emailVerificationToken:hashVerificationToken(verificationToken),emailVerificationTokenExpiresAt:tokenExpiresAt,emailVerificationSentAt:new Date,emailVerificationAttempts:currentAttempts+1}).where(sql11`id = ${user.id}`);let verificationLink=buildEmailActionLink({request:ctx.request,configuredUrl:registerConfig.emailVerification?.redirectUrl,path:"/verify-email",query:{token:verificationToken},allowedOrigins:authConfig.authentication?.trustedAppOrigins}),result=await emailService.sendVerificationEmail(email,email.split("@")[0]||"User",verificationLink,appName||"Nucleus");if(result.success){logger2.info("[AUTH] Verification email resent",{email});let cooldownSeconds=Math.ceil(cooldownMs/1000);if(antiEnum)return uniformResend;return{success:!0,message:"Verification email sent. Please check your inbox.",data:{cooldownSeconds,canResendAt:new Date(Date.now()+cooldownMs).toISOString(),attemptsRemaining:maxAttempts-(currentAttempts+1)}}}if(logger2.error("[AUTH] Failed to resend verification email",{email,error:result.error}),antiEnum)return uniformResend;return{success:!1,message:"Failed to send verification email. Please try again later."}}});return collectRoutes([{prefix:verifyRoute,routes:[routeGetVerifyroute]},{prefix:resendRoute,routes:[routePostResendroute]}])}var init_emailVerification=__esm(()=>{init_esm3();init_routeCollector();init_utils6()});import crypto7 from"crypto";function generateMagicToken(){return crypto7.randomBytes(32).toString("hex")}function hashToken(token,purpose){let input=purpose?`${purpose}:${token}`:token;return crypto7.createHash("sha256").update(input).digest("hex")}function parseTimeToMs3(time2){let match=time2.match(/^(\d+)(s|m|h|d)$/);if(!match?.[1]||!match[2])return 900000;let value2=parseInt(match[1],10);switch(match[2]){case"s":return value2*1000;case"m":return value2*60*1000;case"h":return value2*60*60*1000;case"d":return value2*24*60*60*1000;default:return 900000}}var TOKEN_PURPOSE;var init_utils8=__esm(()=>{TOKEN_PURPOSE={magicLink:"magic-link",invite:"invite"}});import{eq as eq51}from"drizzle-orm";function createInviteRoute(config,inviteConfig,getEmailService,storeMagicToken,appName,getMagicToken){let{db,logger:logger2}=config,route=inviteConfig.route||"/auth/invite",expiresIn=inviteConfig.tokenExpiresIn||"7d",redirectUrl=inviteConfig.redirectUrl||"";if(!inviteConfig.enabled)return collectRoutes();let declared=[],routePostRoute=defineRoute({method:"POST",path:"/",body:InviteBodySchema,detail:{tags:["Authentication"],summary:"Invite User",description:"Invite a new user by sending them an email with a magic link to set their password"},handler:async(ctx)=>{let resolved=resolveAuthTablesForRequest(ctx.request,config),{usersTable}=resolved;if(!db||!usersTable)return ctx.set.status=500,{success:!1,message:"Database not configured"};let emailService=getEmailService?.();if(!emailService?.isAvailable())return logger2.error("[AUTH] Invite requested but email service not available"),ctx.set.status=503,{success:!1,message:"Email service not available"};let{email:rawEmail}=ctx.body,email=rawEmail.toLowerCase(),antiEnum=config.authentication?.preventUserEnumeration===!0,uniformInvite={success:!0,message:"If the email address is eligible, an invitation has been sent."},existingUsers=await db.select().from(usersTable).where(eq51(usersTable.email,email)).limit(1),userId;if(existingUsers.length>0){let existingUser=existingUsers[0];if(existingUser.verifiedAt||existingUser.password){if(logger2.warn("[AUTH] Invite failed - user already verified",{email}),antiEnum)return uniformInvite;return ctx.set.status=409,{success:!1,message:"User with this email is already verified"}}userId=existingUser.id,logger2.info("[AUTH] Resending invitation to existing unverified user",{userId,email})}else{let insertValues={email,password:null,emailVerified:!1,isLocked:!1,createdAt:new Date,updatedAt:new Date},newUser=(await db.insert(usersTable).values(insertValues).returning())[0];if(!newUser)return logger2.error("[AUTH] Failed to create invited user",{email}),ctx.set.status=500,{success:!1,message:"Failed to create user"};userId=newUser.id,logger2.info("[AUTH] Invited user created",{userId,email});let inviteDefaultRoles=resolveDefaultRoleNames(config.authentication);if(inviteDefaultRoles.length>0)await assignDefaultRoles({db,userId,roleNames:inviteDefaultRoles,rolesTable:resolved.rolesTable??config.rolesTable??null,userRolesTable:resolved.userRolesTable??config.userRolesTable??null,logger:logger2})}let token=generateMagicToken(),tokenHash=hashToken(token,TOKEN_PURPOSE.invite),expiresAt=new Date(Date.now()+parseTimeToMs3(expiresIn)),reqSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0;await storeMagicToken({userId,email,tokenHash,expiresAt},reqSchemaName);let inviteLink=redirectUrl?buildEmailActionLink({request:ctx.request,configuredUrl:redirectUrl,path:extractConfiguredPath(redirectUrl,"/set-password"),query:{token,invite:"true"},allowedOrigins:config.authentication?.trustedAppOrigins}):`/set-password?token=${token}&invite=true`,sendResult=await emailService.sendInvitationEmail(email,inviteLink,appName||"Nucleus");if(!sendResult.success){if(logger2.error("[AUTH] Failed to send invitation email",{email,error:sendResult.error}),antiEnum)return uniformInvite;return{success:!0,message:"User created but failed to send invitation email",data:{id:userId,email}}}if(logger2.info("[AUTH] Invitation email sent",{email,userId}),antiEnum)return uniformInvite;return{success:!0,message:"Invitation sent successfully",data:{id:userId,email}}}});if(declared.push(routePostRoute),getMagicToken){let routePostRouteVerify=defineRoute({method:"POST",path:"/verify",body:Type.Object({token:Type.String()}),detail:{tags:["Authentication"],summary:"Verify Invite Token",description:"Validate an invite token without consuming it. Returns user info if valid."},handler:async(ctx)=>{let{token}=ctx.body;if(!token)return ctx.set.status=400,{success:!1,message:"Token is required"};let tokenHash=hashToken(token,TOKEN_PURPOSE.invite),invSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0,storedToken=await getMagicToken(tokenHash,invSchemaName);if(!storedToken)return logger2.warn("[AUTH] Invalid invite verify token"),ctx.set.status=400,{success:!1,message:"Invalid or expired token"};if(new Date>storedToken.expiresAt)return logger2.warn("[AUTH] Expired invite verify token",{email:storedToken.email}),ctx.set.status=400,{success:!1,message:"Invalid or expired token"};return logger2.info("[AUTH] Invite token verified (not consumed)",{email:storedToken.email,userId:storedToken.userId}),{success:!0,data:{userId:storedToken.userId,email:storedToken.email}}}});declared.push(routePostRouteVerify)}return collectRoutes([{prefix:route,routes:declared}])}var InviteBodySchema;var init_invite=__esm(()=>{init_esm3();init_routeCollector();init_assignDefaultRole();init_utils8();InviteBodySchema=Type.Object({email:Type.String({format:"email"})})});var exports_DeviceTrust={};__export(exports_DeviceTrust,{selectUsableDeviceRow:()=>selectUsableDeviceRow,readDeviceTokenFromCookieHeader:()=>readDeviceTokenFromCookieHeader,mintDeviceToken:()=>mintDeviceToken,isDeviceRowUsable:()=>isDeviceRowUsable,isApprovalMethod:()=>isApprovalMethod,hashDeviceToken:()=>hashDeviceToken,deviceCookieName:()=>deviceCookieName,decideDeviceApproval:()=>decideDeviceApproval,buildDeviceTokenCookie:()=>buildDeviceTokenCookie,buildClearedDeviceTokenCookie:()=>buildClearedDeviceTokenCookie,DEVICE_TOKEN_BYTES:()=>DEVICE_TOKEN_BYTES});import{createHash as createHash5,randomBytes as randomBytes6}from"crypto";function hashDeviceToken(raw){return createHash5("sha256").update(raw).digest("hex")}function mintDeviceToken(){let raw=randomBytes6(DEVICE_TOKEN_BYTES).toString("hex");return{raw,hash:hashDeviceToken(raw)}}function isDeviceRowUsable(row,now){if(!row||row.status!=="trusted")return!1;if((row.revoked_at??row.revokedAt)!=null)return!1;let expRaw=row.expires_at??row.expiresAt;if(expRaw==null)return!1;let exp=expRaw instanceof Date?expRaw:new Date(expRaw);if(Number.isNaN(exp.getTime())||exp<=now)return!1;return!0}function selectUsableDeviceRow(rows,expectedUserId,now){if(!expectedUserId)return null;for(let row of rows??[]){if((row.user_id??row.userId)!==expectedUserId)continue;if(isDeviceRowUsable(row,now))return row}return null}function buildDeviceTokenCookie(raw,opts){let prefix=opts.cookiePrefix??"",name2=`${prefix}${opts.cookieName??"device_token"}`,sameSite=opts.sameSite??"Lax",secure=prefix==="__Secure-"||prefix==="__Host-"||opts.secure!==!1,allowDomain=prefix!=="__Host-",parts=[`${name2}=${raw}`,"Path=/","HttpOnly",`SameSite=${sameSite}`];if(secure)parts.push("Secure");if(allowDomain&&opts.domain)parts.push(`Domain=${opts.domain}`);return parts.push(`Max-Age=${Math.max(0,Math.floor(opts.maxAgeSeconds))}`),parts.join("; ")}function buildClearedDeviceTokenCookie(opts){return buildDeviceTokenCookie("",{...opts,maxAgeSeconds:0})}function decideDeviceApproval(params){let NO_APPROVAL={requiresApproval:!1,deny:!1,mintToken:!1,trustImmediately:!1},TRUST_NOW={requiresApproval:!1,deny:!1,mintToken:!0,trustImmediately:!0};if(params.knownDevice)return NO_APPROVAL;if(params.isImpersonation||!isApprovalMethod(params.loginMethod,params.approvalMethods))return NO_APPROVAL;if(!params.hasValidFingerprint&&params.onUnidentifiable==="deny")return{requiresApproval:!1,deny:!0,mintToken:!1,trustImmediately:!1};if(params.trustNewDevices!==!1||params.isEmailExempt||params.isFirstEverDevice)return TRUST_NOW;return{requiresApproval:!0,deny:!1,mintToken:!0,trustImmediately:!1}}function deviceCookieName(cfg){return`${cfg?.cookiePrefix??""}${cfg?.cookieName??"device_token"}`}function readDeviceTokenFromCookieHeader(cookieHeader,cfg){if(!cookieHeader)return;let name2=deviceCookieName(cfg);for(let part of cookieHeader.split(";")){let eq52=part.indexOf("=");if(eq52<0)continue;if(part.slice(0,eq52).trim()===name2)return part.slice(eq52+1).trim()}return}function isApprovalMethod(method,approvalMethods){if(!method)return!1;for(let m of approvalMethods){if(m===method)return!0;if(m.endsWith(":*")&&method.startsWith(m.slice(0,-1)))return!0}return!1}var DEVICE_TOKEN_BYTES=32;var init_DeviceTrust=()=>{};import{createHash as createHash6}from"crypto";function hashRefreshToken(token){return createHash6("sha256").update(token).digest("hex")}function classifyRefreshToken(input){let grace=input.graceSeconds??DEFAULT_REUSE_GRACE_SECONDS;if(!input.currentHash)return{action:"adopt"};if(input.presentedHash===input.currentHash)return{action:"rotate"};if(input.previousHash&&input.presentedHash===input.previousHash){let rotatedAt=input.previousRotatedAt?Date.parse(input.previousRotatedAt):Number.NaN;if(Number.isFinite(rotatedAt)&&input.nowMs-rotatedAt<=grace*1000)return{action:"grace"};return{action:"revoke",reason:"refresh token was superseded and is being replayed outside the grace window"}}return{action:"revoke",reason:"refresh token does not match this session, current or previous"}}var DEFAULT_REUSE_GRACE_SECONDS=30;var init_reuseDetection=()=>{};async function recordIssuedRefreshToken(sessionId,refreshToken,options){if(!sessionId||!refreshToken)return;try{let result=await updateSession({sessionId,updates:{refreshTokenHash:hashRefreshToken(refreshToken),...options?.previousHash?{previousRefreshTokenHash:options.previousHash,previousRefreshTokenAt:new Date().toISOString()}:{}}});if(!result.success)logger.warn("[AUTH] Could not record the issued refresh token; reuse detection is off",{sessionId,error:result.error})}catch(error3){logger.warn("[AUTH] Could not record the issued refresh token; reuse detection is off",{sessionId,error:error3 instanceof Error?error3.message:String(error3)})}}var init_recordIssued=__esm(()=>{init_Update();init_Logger2();init_reuseDetection()});async function resolveUserIdByIdentifier(input){let value2=input.value.trim();if(value2==="")return null;for(let source of input.sources){if(!source.table||!source.column||!source.userColumn)continue;let found=(await input.lookup(source,value2)).filter((id)=>String(id??"").trim()!=="");if(found.length>1)return null;if(found.length===1)return String(found[0])}return null}function looksLikeAlternateIdentifier(value2){let trimmed=value2.trim();return trimmed!==""&&!trimmed.includes("@")}var init_identifier=()=>{};import{eq as eq52}from"drizzle-orm";async function lookupIdentifier(db,schemaTables,source,value2){let table=pickByName(schemaTables,source.table);if(!table)return[];let column=pickByName(table,source.column),userColumn=pickByName(table,source.userColumn);if(!column||!userColumn)return[];return(await db.select({userId:userColumn}).from(table).where(eq52(column,value2)).limit(2)).map((row)=>String(row.userId??""))}var init_identifierQuery=__esm(()=>{init_identifier()});var LoginBodySchema,LoginResponseSchema;var init_types12=__esm(()=>{init_esm3();LoginBodySchema=Type.Object({email:Type.String({minLength:1}),password:Type.String({minLength:1}),rememberMe:Type.Optional(Type.Boolean()),captchaId:Type.Optional(Type.String()),captchaAnswer:Type.Optional(Type.String()),deviceHint:Type.Optional(Type.String())}),LoginResponseSchema=Type.Object({success:Type.Boolean(),message:Type.Optional(Type.String()),data:Type.Optional(Type.Object({user:Type.Object({id:Type.String(),email:Type.String()}),accessToken:Type.String(),refreshToken:Type.String()}))})});import{eq as eq53}from"drizzle-orm";function createLoginRoute(config,loginConfig,signAccessToken,signRefreshToken,createSession,saveSessionToDb,cookieConfig,tokenResponseConfig,captchaService){let{db,logger:logger2}=config,log3=logger2.scoped(AUTH_LOGIN),route=loginConfig.route||"/auth/login",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};if(!loginConfig.enabled)return collectRoutes();let identifierSources=loginConfig.identifierSources??[],rememberMeAllowed=loginConfig.rememberMe!==!1,routePostRoute=defineRoute({method:"POST",path:"/",body:LoginBodySchema,detail:{tags:["Authentication"],summary:"Login",description:"Authenticate user with email and password"},handler: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,rememberMe:rememberMeRequested,captchaId,captchaAnswer,deviceHint}=ctx.body,rememberMe=rememberMeAllowed?rememberMeRequested:!1,email=rawEmail.toLowerCase(),user=(await db.select().from(usersTable).where(eq53(usersTable.email,email)).limit(1))[0];if(!user&&identifierSources.length>0&&looksLikeAlternateIdentifier(rawEmail)){let resolvedId=await resolveUserIdByIdentifier({sources:identifierSources,value:rawEmail,lookup:(source,value2)=>lookupIdentifier(db,resolved.schemaTables,source,value2)});if(resolvedId)user=(await db.select().from(usersTable).where(eq53(usersTable.id,resolvedId)).limit(1))[0]}let captchaEnabledForLogin=loginConfig.captcha?.enabled!==!1,captchaThreshold=loginConfig.captcha?.requireAfterFailures,currentFailedAttempts=user?.failedLoginAttempts||0,captchaRequired=Boolean(captchaService?.isEnabled())&&captchaEnabledForLogin&&(captchaThreshold===void 0||currentFailedAttempts>=captchaThreshold);if(captchaRequired&&captchaService){if(!captchaId||!captchaAnswer)return ctx.set.status=400,{success:!1,message:"Captcha is required",requiresCaptcha:!0};let captchaResult=await captchaService.validate(captchaId,captchaAnswer);if(!captchaResult.valid)return ctx.set.status=400,{success:!1,message:captchaResult.message||"Invalid captcha",attemptsRemaining:captchaResult.attemptsRemaining,requiresCaptcha:!0}}let reqUrl=new URL(ctx.request.url),auditIp=ctx.request.headers.get("x-client-ip")?.trim()||"unknown",auditUa=ctx.request.headers.get("user-agent")||"unknown";if(!user)return await verifyPassword(password4,await getTimingEqualizerHash()).catch(()=>!1),log3.warn("Login failed - user not found",{email}),await logger2.audit({entityName:"users",operation:"LOGIN_FAILED",summary:`Login failed: user not found (${email})`,ipAddress:auditIp,userAgent:auditUa,path:reqUrl.pathname,query:reqUrl.search}),ctx.set.status=401,{success:!1,message:"Invalid email or password",requiresCaptcha:captchaRequired};if(user.isActive===!1)return log3.warn("Login failed - account inactive",{email,userId:user.id}),ctx.set.status=403,{success:!1,message:"Account is not active"};if(user.isLocked){let lockedUntil=user.lockedUntil;if(lockedUntil&&new Date(lockedUntil)<new Date)await db.update(usersTable).set({isLocked:!1,lockedUntil:null,failedLoginAttempts:0}).where(eq53(usersTable.id,user.id)),log3.info("Account lock expired, auto-unlocked",{userId:user.id,email});else return log3.warn("Login failed - account locked",{email,userId:user.id}),await logger2.audit({entityName:"users",entityId:user.id,operation:"LOGIN_FAILED",userId:user.id,summary:`Login failed: account locked (${email})`,ipAddress:auditIp,userAgent:auditUa,path:reqUrl.pathname,query:reqUrl.search}),ctx.set.status=403,{success:!1,message:"Account is locked"}}let userCohortId=user.cohortId;if(userCohortId&&db){let cohortsTable=resolved.schemaTables?.userCohorts??resolved.schemaTables?.user_cohorts;if(cohortsTable){let cohortExpiresAt=(await db.select().from(cohortsTable).where(eq53(cohortsTable.id,userCohortId)).limit(1))[0]?.expiresAt;if(cohortExpiresAt&&new Date(cohortExpiresAt)<new Date)return log3.warn("Login failed - cohort expired",{email,userId:user.id,cohortId:userCohortId,expiresAt:cohortExpiresAt}),await logger2.audit({entityName:"users",entityId:user.id,operation:"LOGIN_FAILED",userId:user.id,summary:`Login failed: cohort expired (${email})`,ipAddress:auditIp,userAgent:auditUa,path:reqUrl.pathname,query:reqUrl.search}),ctx.set.status=403,{success:!1,message:"Your access has expired. Please contact your administrator."}}}if(!await verifyPassword(password4,user.password)){let failedAttempts=(user.failedLoginAttempts||0)+1;await db.update(usersTable).set({failedLoginAttempts:failedAttempts,isLocked:failedAttempts>=5,lockedUntil:failedAttempts>=5?new Date(Date.now()+1800000):null}).where(eq53(usersTable.id,user.id)),log3.warn("Login failed - invalid password",{email,failedAttempts}),await logger2.audit({entityName:"users",entityId:user.id,operation:"LOGIN_FAILED",userId:user.id,summary:`Login failed: invalid password (${email}, attempt ${failedAttempts})`,ipAddress:auditIp,userAgent:auditUa,path:reqUrl.pathname,query:reqUrl.search});let captchaWillBeRequired=Boolean(captchaService?.isEnabled())&&captchaEnabledForLogin&&(captchaThreshold===void 0||failedAttempts>=captchaThreshold);return ctx.set.status=401,{success:!1,message:"Invalid email or password",requiresCaptcha:captchaWillBeRequired}}await db.update(usersTable).set({failedLoginAttempts:0,lastLoginAt:new Date,loginCount:(user.loginCount||0)+1}).where(eq53(usersTable.id,user.id));let ipAddress=ctx.request.headers.get("x-client-ip")?.trim()||"127.0.0.1",isPrivateOrLoopback=(ip)=>!ip||ip==="127.0.0.1"||ip==="::1"||ip==="localhost"||ip.startsWith("10.")||ip.startsWith("192.168.")||ip.startsWith("172."),userAgent=ctx.request.headers.get("user-agent")||"Unknown Browser";log3.info("Parsed device info",{ipAddress,userAgent});let deviceInfo=parseUserAgentForLogin(userAgent,ipAddress,deviceHint,deviceContextFromRequest(ctx.request).clientHints);if(!isPrivateOrLoopback(ipAddress)){let abortCtrl=new AbortController,geoTimeout=setTimeout(()=>abortCtrl.abort(),2000);fetch(`https://ip-api.com/json/${ipAddress}?fields=country,city`,{signal:abortCtrl.signal}).then(async(res)=>{if(clearTimeout(geoTimeout),!res.ok)return;let geo=await res.json();if(geo.country)deviceInfo.locationCountry=geo.country;if(geo.city)deviceInfo.locationCity=geo.city}).catch(()=>{clearTimeout(geoTimeout)})}let userRoles=[],userClaims=[],userClaimScopes={};if(db)try{let{fetchUserRolesAndClaims:fetchUserRolesAndClaims2}=await Promise.resolve().then(() => (init_fetchUserRolesAndClaims(),exports_fetchUserRolesAndClaims)),rc=await fetchUserRolesAndClaims2(db,user.id,resolved);userRoles=rc.roles,userClaims=rc.claims,userClaimScopes=rc.claimScopes}catch(roleError){log3.error("Failed to fetch user roles/claims for JWT",{userId:user.id,error:roleError instanceof Error?roleError.message:String(roleError),stack:roleError instanceof Error?roleError.stack:void 0})}let requestOrigin=ctx.request.headers.get("origin")||ctx.request.headers.get("referer")?.replace(/\/[^/]*$/,"")||void 0,sessionParams={userId:user.id,deviceInfo,loginMethod:"password",rememberMe,requestOrigin},sessionId=await createSession(sessionParams),accessToken=signAccessToken(user.id,userRoles.length>0?userRoles:void 0,userClaims.length>0?userClaims:void 0,ctx.request.headers.get("x-tenant-schema")??void 0,userClaimScopes,sessionId),refreshToken=signRefreshToken(user.id,rememberMe,sessionId);await recordIssuedRefreshToken(sessionId,refreshToken);let requiresApproval=!1,pendingSessionId=sessionId,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);let saveResult=await saveSessionToDb(sessionId,sessionParams,reqSchemaName);if(saveResult?.denied)return log3.warn("Login blocked by device-trust (unidentifiable device)",{userId:user.id}),ctx.set.status=403,{success:!1,message:"This device could not be recognized. Please contact support."};if(deviceTokenCookie=saveResult?.deviceTokenCookie,saveResult?.requiresApproval){if(requiresApproval=!0,saveResult.sessionId)pendingSessionId=saveResult.sessionId}}if(requiresApproval){ctx.set.status=202;let approvalHeaders={"Content-Type":"application/json"};if(deviceTokenCookie)approvalHeaders["Set-Cookie"]=deviceTokenCookie;return new Response(JSON.stringify({success:!1,requiresApproval:!0,sessionId:pendingSessionId,message:"Login requires approval. Please check your email to approve this device."}),{status:202,headers:approvalHeaders})}log3.info("Login successful",{userId:user.id,email,rememberMe}),await logger2.audit({entityName:"users",entityId:user.id,operation:"LOGIN",userId:user.id,summary:`${email} logged in successfully`,ipAddress,userAgent,path:reqUrl.pathname,query:reqUrl.search});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);ctx.set.headers["x-session-id"]=sessionId;let responseData={user:{id:user.id,email:user.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(withEnvelopeFlags({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 collectRoutes([{prefix:route,routes:[routePostRoute]}])}var dummyHashPromise=null,getTimingEqualizerHash=()=>{if(!dummyHashPromise)dummyHashPromise=hashPassword("nucleus-login-timing-equalizer");return dummyHashPromise};var init_login=__esm(()=>{init_routeCollector();init_response();init_DeviceTrust();init_scopes();init_recordIssued();init_utils6();init_identifier();init_identifierQuery();init_types12();init_deviceInfo();init_utils7();init_types12()});function clearAuthCookies(cookieConfig){let path7=cookieConfig?.path||"/",sameSite=cookieConfig?.sameSite||"Strict",securePart=cookieConfig?.secure!==!1?"; Secure":"",domainPart=cookieConfig?.domain?`; Domain=${cookieConfig.domain}`:"",accessName=cookieConfig?.accessTokenName||"access_token",refreshName=cookieConfig?.refreshTokenName||"refresh_token",sessionName=cookieConfig?.sessionTokenName||"session_token",clearOpts=`; Path=${path7}; HttpOnly; SameSite=${sameSite}${securePart}${domainPart}; Max-Age=0`;return[`${accessName}=${clearOpts}`,`${refreshName}=${clearOpts}`,`${sessionName}=${clearOpts}`,...IMPERSONATION_COOKIE_NAMES.map((name2)=>`${name2}=${clearOpts}`)]}var init_utils9=__esm(()=>{init_impersonationCookies()});var LogoutResponseSchema;var init_types13=__esm(()=>{init_esm3();LogoutResponseSchema=Type.Object({success:Type.Boolean(),message:Type.Optional(Type.String())})});function createLogoutRoute(config,logoutConfig,destroySession,revokeSessionInDb,cookieConfig){let{logger:logger2}=config,route=logoutConfig.route||"/auth/logout";if(!logoutConfig.enabled)return collectRoutes();let routePostRoute=defineRoute({method:"POST",path:"/",detail:{tags:["Authentication"],summary:"Logout",description:"Logout and invalidate session"},handler:async(ctx)=>{let sessionCookieName=cookieConfig?.sessionTokenName||"session_token",fromCookie=(ctx.request.headers.get("cookie")??"").split(";").map((one)=>one.trim()).find((one)=>one.startsWith(`${sessionCookieName}=`))?.slice(sessionCookieName.length+1),sessionId=ctx.request.headers.get("x-session-id")||fromCookie||null,userId=ctx.request.headers.get("x-user-id");if(sessionId){if(await destroySession(sessionId),revokeSessionInDb){let reqSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0;await revokeSessionInDb(sessionId,"user_logout",reqSchemaName)}}let clearCookies=clearAuthCookies(cookieConfig),responseHeaders=new Headers;responseHeaders.set("Content-Type","application/json");for(let cookie of clearCookies)responseHeaders.append("Set-Cookie",cookie);logger2.info("[AUTH] Logout successful",{userId,sessionId});let reqUrl=new URL(ctx.request.url);logger2.audit({entityName:"users",entityId:userId||void 0,operation:"LOGOUT",userId:userId||void 0,summary:`User logged out${sessionId?` (session: ${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});let jsonBody=JSON.stringify(withEnvelopeFlags({success:!0,message:"Logged out successfully"}));return new Response(jsonBody,{status:200,headers:responseHeaders})}});return collectRoutes([{prefix:route,routes:[routePostRoute]}])}var init_logout=__esm(()=>{init_response();init_routeCollector();init_utils9();init_types13()});var MagicLinkRequestSchema,MagicLinkVerifySchema,MagicLinkResponseSchema,MagicLinkVerifyResponseSchema;var init_types14=__esm(()=>{init_esm3();MagicLinkRequestSchema=Type.Object({email:Type.String({format:"email"})}),MagicLinkVerifySchema=Type.Object({token:Type.String()}),MagicLinkResponseSchema=Type.Object({success:Type.Boolean(),message:Type.Optional(Type.String())}),MagicLinkVerifyResponseSchema=Type.Object({success:Type.Boolean(),message:Type.Optional(Type.String()),data:Type.Optional(Type.Object({user:Type.Object({id:Type.String(),email:Type.String()}),accessToken:Type.String(),refreshToken:Type.String()}))})});import{eq as eq54}from"drizzle-orm";function createMagicLinkRoute(config,magicLinkConfig,getEmailService,signAccessToken,signRefreshToken,createSession,storeMagicToken,getMagicToken,deleteMagicToken,appName,cookieConfig,saveSessionToDb){let{db,logger:logger2}=config,requestRoute=magicLinkConfig.route||"/auth/magic-link",verifyRoute=magicLinkConfig.verifyRoute||"/auth/magic-link/verify",expiresIn=magicLinkConfig.expiresIn||"15m",redirectUrl=magicLinkConfig.redirectUrl||"";if(!magicLinkConfig.enabled)return collectRoutes();let routePostRequestroute=defineRoute({method:"POST",path:"/",body:MagicLinkRequestSchema,detail:{tags:["Authentication"],summary:"Request Magic Link",description:"Send a magic link to the user's email for passwordless login"},handler:async(ctx)=>{let{usersTable}=resolveAuthTablesForRequest(ctx.request,config);if(!db||!usersTable)return ctx.set.status=500,{success:!1,message:"Database not configured"};let emailService=getEmailService?.();if(!emailService?.isAvailable())return logger2.error("[AUTH] Magic link requested but email service not available"),ctx.set.status=503,{success:!1,message:"Email service not available"};let{email:rawEmail}=ctx.body,email=rawEmail.toLowerCase(),user=(await db.select().from(usersTable).where(eq54(usersTable.email,email)).limit(1))[0];if(!user)return logger2.info("[AUTH] Magic link requested for non-existent email",{email}),{success:!0,message:"If an account exists, a magic link has been sent"};if(user.isLocked)return logger2.warn("[AUTH] Magic link requested for locked account",{email}),{success:!0,message:"If an account exists, a magic link has been sent"};let token=generateMagicToken(),tokenHash=hashToken(token,TOKEN_PURPOSE.magicLink),expiresAt=new Date(Date.now()+parseTimeToMs3(expiresIn)),reqSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0;await storeMagicToken({userId:user.id,email,tokenHash,expiresAt},reqSchemaName);let magicLink=redirectUrl?buildEmailActionLink({request:ctx.request,configuredUrl:redirectUrl,path:extractConfiguredPath(redirectUrl,"/verify-magic-link"),query:{token},allowedOrigins:config.authentication?.trustedAppOrigins}):`${verifyRoute}?token=${token}`,result=await emailService.sendMagicLinkEmail(email,magicLink,appName);if(!result.success)return logger2.error("[AUTH] Failed to send magic link email",{email,error:result.error}),ctx.set.status=500,{success:!1,message:result.error?`The email could not be sent: ${result.error}`:"The email could not be sent"};return logger2.info("[AUTH] Magic link sent",{email,userId:user.id}),{success:!0,message:"If an account exists, a magic link has been sent"}}}),routeGetVerifyroute=defineRoute({method:"GET",path:"/",query:MagicLinkVerifySchema,detail:{tags:["Authentication"],summary:"Verify Magic Link",description:"Verify magic link token and login user"},handler:async(ctx)=>{let{usersTable}=resolveAuthTablesForRequest(ctx.request,config);if(!db||!usersTable)return ctx.set.status=500,{success:!1,message:"Database not configured"};let token=ctx.query.token;if(!token)return ctx.set.status=400,{success:!1,message:"Token is required"};let tokenHash=hashToken(token,TOKEN_PURPOSE.magicLink),reqSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0,storedToken=await getMagicToken(tokenHash,reqSchemaName);if(!storedToken)return logger2.warn("[AUTH] Invalid magic link token"),ctx.set.status=400,{success:!1,message:"Invalid or expired token"};if(new Date>storedToken.expiresAt)return await deleteMagicToken(tokenHash,reqSchemaName),logger2.warn("[AUTH] Expired magic link token",{email:storedToken.email}),ctx.set.status=400,{success:!1,message:"Invalid or expired token"};let user=(await db.select().from(usersTable).where(eq54(usersTable.id,storedToken.userId)).limit(1))[0];if(!user)return await deleteMagicToken(tokenHash,reqSchemaName),ctx.set.status=404,{success:!1,message:"User not found"};if(user.isActive===!1)return await deleteMagicToken(tokenHash,reqSchemaName),logger2.warn("[AUTH] Magic link denied: account inactive",{email:storedToken.email}),ctx.set.status=400,{success:!1,message:"Invalid or expired token"};if(user.isLocked){let lockedUntil=user.lockedUntil;if(!(lockedUntil&&new Date(lockedUntil)<new Date))return await deleteMagicToken(tokenHash,reqSchemaName),logger2.warn("[AUTH] Magic link denied: account locked",{email:storedToken.email}),ctx.set.status=400,{success:!1,message:"Invalid or expired token"}}await deleteMagicToken(tokenHash,reqSchemaName),await db.update(usersTable).set({lastLoginAt:new Date,loginCount:(user.loginCount||0)+1,emailVerified:!0}).where(eq54(usersTable.id,user.id));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")||"",mlUserRoles=[],mlUserClaims=[],mlUserClaimScopes={};if(db)try{let resolved=resolveAuthTablesForRequest(ctx.request,config),rc=await fetchUserRolesAndClaims(db,user.id,resolved);mlUserRoles=rc.roles,mlUserClaims=rc.claims,mlUserClaimScopes=rc.claimScopes}catch{}let sessionParams={userId:user.id,deviceInfo:ensureDeviceInfo({ipAddress,userAgent},deviceContextFromRequest(ctx.request).clientHints),loginMethod:"magic_link",rememberMe:!0},sessionId=await createSession(sessionParams),accessToken=signAccessToken(user.id,mlUserRoles.length>0?mlUserRoles:void 0,mlUserClaims.length>0?mlUserClaims:void 0,ctx.request.headers.get("x-tenant-schema")??void 0,mlUserClaimScopes,sessionId),refreshToken=signRefreshToken(user.id,void 0,sessionId),requiresApproval=!1,approvalSessionId=sessionId,deviceTokenCookie;if(saveSessionToDb){let reqSchemaName2=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,reqSchemaName2);if(saveResult?.denied)return ctx.set.status=403,{success:!1,message:"This device could not be recognized. Please contact support."};if(deviceTokenCookie=saveResult?.deviceTokenCookie,saveResult?.requiresApproval){if(requiresApproval=!0,saveResult.sessionId)approvalSessionId=saveResult.sessionId}}if(requiresApproval){if(deviceTokenCookie)ctx.set.headers["Set-Cookie"]=deviceTokenCookie;return logger2.info("[AUTH] Magic link login requires device approval",{userId:user.id,email:user.email,approvalSessionId}),ctx.set.status=202,{success:!1,requiresApproval:!0,sessionId:approvalSessionId,message:"Login requires approval. Please check your email to approve this device."}}logger2.info("[AUTH] Magic link login successful",{userId:user.id,email:user.email});let 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||2592000,secure:cookieConfig?.secure??!0,httpOnly:cookieConfig?.httpOnly??!0,sameSite:cookieConfig?.sameSite||"strict",path:cookieConfig?.path||"/",domain:cookieConfig?.domain},securePart=cookies.secure?"; Secure":"",domainPart=cookies.domain?`; Domain=${cookies.domain}`:"",cookieOptions=`; Path=${cookies.path}; HttpOnly; SameSite=${cookies.sameSite}${securePart}${domainPart}`,responseHeaders=new Headers;if(responseHeaders.set("Content-Type","application/json"),responseHeaders.set("x-session-id",sessionId),responseHeaders.append("Set-Cookie",`${cookies.accessTokenName}=${accessToken}${cookieOptions}; Max-Age=${cookies.accessTokenMaxAge}`),responseHeaders.append("Set-Cookie",`${cookies.refreshTokenName}=${refreshToken}${cookieOptions}; Max-Age=${cookies.refreshTokenMaxAge}`),responseHeaders.append("Set-Cookie",`${cookies.sessionTokenName}=${sessionId}${cookieOptions}; Max-Age=${cookies.sessionTokenMaxAge}`),deviceTokenCookie)responseHeaders.append("Set-Cookie",deviceTokenCookie);let jsonBody=JSON.stringify(withEnvelopeFlags({success:!0,data:{user:{id:user.id,email:user.email},accessToken,refreshToken,sessionId}}));return new Response(jsonBody,{status:200,headers:responseHeaders})}});return collectRoutes([{prefix:requestRoute,routes:[routePostRequestroute]},{prefix:verifyRoute,routes:[routeGetVerifyroute]}])}var init_magicLink=__esm(()=>{init_DeviceTrust();init_response();init_routeCollector();init_fetchUserRolesAndClaims();init_deviceInfo();init_types14();init_utils8();init_types14()});import{eq as eq55}from"drizzle-orm";function createMeRoute(config,meConfig,_schemaTables,_schemaRelations,_databaseUrl){let{db,logger:logger2}=config,route=meConfig.route||"/auth/me";if(!meConfig.enabled)return collectRoutes();let routeGetRoute=defineRoute({method:"GET",path:"/",detail:{tags:["Authentication"],summary:"Get current user",description:"Get the currently authenticated user with profile, addresses, phones and files"},handler:async(ctx)=>{let resolved=resolveAuthTablesForRequest(ctx.request,config),usersTable=resolved.usersTable,reqSchemaTables=resolved.schemaTables;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:"Unauthorized"};let user=(await db.select().from(usersTable).where(eq55(usersTable.id,userId)).limit(1))[0];if(!user)return ctx.set.status=404,{success:!1,message:"User not found"};let{password:rawPassword,...restUser}=user,hasPassword=typeof rawPassword==="string"&&rawPassword.length>0,safeUser={...stripUnclaimedSensitive(redactSensitiveOutput(restUser)),hasPassword},profile=null,addresses=[],phones=[],files=[],roles=[],claims=[];if(meConfig.includeProfile&&reqSchemaTables){let profilesTable=reqSchemaTables.profiles;if(profilesTable&&db)profile=(await db.select().from(profilesTable).where(eq55(profilesTable.userId,userId)).limit(1))[0]||null}if(meConfig.includeAddresses&&reqSchemaTables){let addressesTable=reqSchemaTables.addresses;if(addressesTable&&db)addresses=await db.select().from(addressesTable).where(eq55(addressesTable.ownerId,userId))}if(meConfig.includePhones&&reqSchemaTables){let phonesTable=reqSchemaTables.phones;if(phonesTable&&db)phones=await db.select().from(phonesTable).where(eq55(phonesTable.ownerId,userId))}if(meConfig.includeFiles&&reqSchemaTables){let filesTable=reqSchemaTables.files;if(filesTable&&db)files=await db.select().from(filesTable).where(eq55(filesTable.uploadedBy,userId))}if(meConfig.includeRoles&&reqSchemaTables){let{userRoles:userRolesTable,roles:rolesTable}=reqSchemaTables;if(userRolesTable&&rolesTable&&db){let roleIds=(await db.select().from(userRolesTable).where(eq55(userRolesTable.userId,userId))).map((ur)=>ur.roleId);if(roleIds.length>0){let{inArray:inArray19}=await import("drizzle-orm");roles=await db.select().from(rolesTable).where(inArray19(rolesTable.id,roleIds))}}}if(meConfig.includeClaims&&reqSchemaTables){let{userRoles:userRolesTable,roleClaims:roleClaimsTable,claims:claimsTable}=reqSchemaTables;if(userRolesTable&&roleClaimsTable&&claimsTable&&db){let urCols=userRolesTable,rcCols=roleClaimsTable,cCols=claimsTable,roleIds=(await db.select({roleId:urCols.roleId}).from(userRolesTable).where(eq55(urCols.userId,userId))).map((r)=>r.roleId);if(roleIds.length>0){let{inArray:inArray19}=await import("drizzle-orm"),claimRows=await db.select({action:cCols.action,scope:rcCols.scope}).from(roleClaimsTable).innerJoin(claimsTable,eq55(rcCols.claimId,cCols.id)).where(inArray19(rcCols.roleId,roleIds)),seen=new Set;for(let row of claimRows){let r=row,key2=`${r.action}|${r.scope??""}`;if(!seen.has(key2))seen.add(key2),claims.push({action:r.action,scope:r.scope??null})}}}}logger2.debug("[AUTH] Me endpoint accessed",{userId});let responseData={success:!0,data:{user:safeUser,profile:stripUnclaimedSensitive(profile),addresses:stripUnclaimedSensitive(addresses),phones:stripUnclaimedSensitive(phones),files:stripUnclaimedSensitive(files),roles:stripUnclaimedSensitive(roles),claims}};return JSON.parse(JSON.stringify(responseData,(_key,value2)=>typeof value2==="bigint"?Number(value2):value2))}});return collectRoutes([{prefix:route,routes:[routeGetRoute]}])}var init_me=__esm(()=>{init_routeCollector();init_utils5();init_sensitiveColumns()});var exports_accountState={};__export(exports_accountState,{evaluateAccountState:()=>evaluateAccountState});function evaluateAccountState(user,now=new Date){if(user.isActive===!1)return{allowed:!1,reason:"inactive",message:"Account is not active"};if(user.isLocked===!0){let raw=user.lockedUntil,lockedUntil=raw instanceof Date?raw:raw?new Date(raw):null;if(!lockedUntil||Number.isNaN(lockedUntil.getTime())||lockedUntil>now)return{allowed:!1,reason:"locked",message:"Account is locked"}}return{allowed:!0}}import{eq as eq56}from"drizzle-orm";async function applyProfileSync(args){let{usersTable,profilesTable,userId,rawProfile,config,isNewUser,logger:logger2}=args,db=args.db;try{if(config.enabled===!1)return;if(!isNewUser&&config.syncOnEveryLogin===!1)return;if(!config.mappings||config.mappings.length===0)return;let userIdCol=usersTable.id,currentUser=(await db.select().from(usersTable).where(eq56(userIdCol,userId)).limit(1))[0]??{},currentProfile={},profileExists=!1;if(profilesTable){let profileUserIdCol=profilesTable.userId,profileRows=await db.select().from(profilesTable).where(eq56(profileUserIdCol,userId)).limit(1);if(profileRows[0])currentProfile=profileRows[0],profileExists=!0}let{users,profiles,errors:errors2}=resolveProfileSyncUpdates({mappings:config.mappings,rawProfile,currentUser,currentProfile});for(let message of errors2)logger2.warn(`[OAUTH] profileSync: ${message}`);if(Object.keys(users).length>0)await db.update(usersTable).set({...users,updatedAt:new Date}).where(eq56(userIdCol,userId)),logger2.info("[OAUTH] profileSync updated users",{userId,columns:Object.keys(users)});if(profilesTable&&Object.keys(profiles).length>0){if(profileExists){let profileUserIdCol=profilesTable.userId;await db.update(profilesTable).set({...profiles,updatedAt:new Date}).where(eq56(profileUserIdCol,userId))}else if(config.createProfileIfMissing!==!1){let now=new Date;await db.insert(profilesTable).values({userId,firstName:rawProfile.givenName??"",lastName:rawProfile.surname??"",...profiles,createdAt:now,updatedAt:now})}else return;logger2.info("[OAUTH] profileSync updated profiles",{userId,columns:Object.keys(profiles),created:!profileExists})}}catch(err){logger2.warn("[OAUTH] profileSync failed \u2014 login continues",{userId,error:err instanceof Error?err.message:String(err)})}}var init_applyProfileSync=()=>{};import{and as and25,eq as eq57}from"drizzle-orm";function buildOAuthStateCookie(value2,opts){let securePart=opts.secure?"; Secure":"",domainPart=opts.domain?`; Domain=${opts.domain}`:"";return`${OAUTH_STATE_COOKIE}=${value2}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${opts.maxAge}${securePart}${domainPart}`}function createOAuthRoutes(config,oauthService,signAccessToken,signRefreshToken,createSession,saveSessionToDb,cookieConfig,tokenResponseConfig,getEmailService,storeMagicToken,appName){let{db,logger:logger2}=config,basePath=oauthService.basePath;for(let provider of oauthService.getEnabledProviders()){let p=oauthService.config.providers[provider];logger2.info("[OAUTH] Provider loaded",{provider,clientIdSuffix:p?.clientId?p.clientId.slice(-4):"none",tenantId:p?.tenantId??"common",redirectUri:p?.redirectUri,scopes:p?.scopes})}let 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||"lax",path:cookieConfig?.path||"/",domain:cookieConfig?.domain},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}},routeGetProvider=defineRoute({method:"GET",path:"/:provider",detail:{tags:["Authentication"],summary:"OAuth Redirect",description:"Redirects to the OAuth provider authorization page"},handler: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`};let redirectUrl=ctx.query.redirect_url,authUrl=await oauthService.buildAuthorizationUrl(provider,void 0,redirectUrl);logger2.debug("[OAUTH] Redirecting to provider",{provider,authUrl:authUrl.replace(/(client_id=)[^&]+/,"$1***")});let 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}}),routeGetProviderCallback=defineRoute({method:"GET",path:"/:provider/callback",detail:{tags:["Authentication"],summary:"OAuth Callback",description:"Handles the OAuth provider callback, creates session and sets cookies"},handler:async(ctx)=>{let provider=ctx.params.provider,query=ctx.query,{code,state,error:error3,error_description}=query,portalOrigin=resolveAppOrigin(ctx.request,oauthService.successRedirectUrl,config.authentication?.trustedAppOrigins),errorRedirect=absolutizeRedirect(oauthService.errorRedirectUrl,portalOrigin);if(error3)return logger2.warn("[OAUTH] Provider returned error",{provider,error:error3,error_description}),ctx.set.status=302,ctx.set.headers.Location=`${errorRedirect}?error=${encodeURIComponent(error_description||error3)}`,null;if(!code||!state)return ctx.set.status=302,ctx.set.headers.Location=`${errorRedirect}?error=missing_code_or_state`,null;let statePayload=await oauthService.consumeState(state);if(!statePayload)return logger2.warn("[OAUTH] Invalid or expired state",{provider,state}),ctx.set.status=302,ctx.set.headers.Location=`${errorRedirect}?error=invalid_state`,null;let cookieState=readCookie(ctx.request,OAUTH_STATE_COOKIE),clearStateCookie=buildOAuthStateCookie("",{maxAge:0,secure:cookies.secure,domain:cookies.domain});if(!cookieState||cookieState!==state)return logger2.warn("[OAUTH] State cookie missing or mismatched (possible CSRF)",{provider}),ctx.set.status=302,ctx.set.headers["Set-Cookie"]=clearStateCookie,ctx.set.headers.Location=`${errorRedirect}?error=invalid_state`,null;if(statePayload.provider!==provider)return logger2.warn("[OAUTH] State provider mismatch",{expected:statePayload.provider,got:provider}),ctx.set.status=302,ctx.set.headers.Location=`${errorRedirect}?error=provider_mismatch`,null;let resolved=resolveAuthTablesForRequest(ctx.request,config),usersTable=resolved.usersTable,oauthTable=resolved.oauthAccountsTable??config.oauthAccountsTable;if(!db||!usersTable)return ctx.set.status=302,ctx.set.headers.Location=`${errorRedirect}?error=server_error`,null;logger2.debug("[OAUTH] Exchanging code",{provider,codeLength:code.length,statePresent:!!state});let callbackResult;try{callbackResult=await oauthService.exchangeCode(provider,code)}catch(err){let e=err;return logger2.error("[OAUTH] Code exchange failed",err,{provider,errorMessage:e?.message,errorName:e?.name,errorCauseCode:e?.cause?.code,errorCauseMessage:e?.cause?.message}),ctx.set.status=302,ctx.set.headers.Location=`${errorRedirect}?error=token_exchange_failed`,null}let{profile,tokens}=callbackResult,userId=null;if(statePayload.linkUserId&&oauthService.allowAccountLinking){if(userId=statePayload.linkUserId,oauthTable){let existing=await db.select().from(oauthTable).where(and25(eq57(oauthTable.provider,provider),eq57(oauthTable.providerAccountId,profile.providerAccountId))).limit(1);if(existing.length>0){let existingRecord=existing[0];if(existingRecord.userId!==userId)return ctx.set.status=302,ctx.set.headers.Location=`${errorRedirect}?error=account_already_linked_to_another_user`,null;await db.update(oauthTable).set({accessToken:tokens.accessToken,refreshToken:tokens.refreshToken??null,tokenExpiresAt:tokens.expiresAt??null,scope:tokens.scope??null,rawProfile:profile.rawProfile,lastUsedAt:new Date}).where(eq57(oauthTable.id,existingRecord.id))}else await db.insert(oauthTable).values({userId,provider,providerAccountId:profile.providerAccountId,providerEmail:profile.email??null,providerName:profile.name??null,providerAvatarUrl:profile.avatarUrl??null,accessToken:tokens.accessToken,refreshToken:tokens.refreshToken??null,tokenExpiresAt:tokens.expiresAt??null,scope:tokens.scope??null,rawProfile:profile.rawProfile,isPrimary:!1,lastUsedAt:new Date})}let linkSyncConfig=oauthService.profileSync;if(linkSyncConfig){let linkSyncTables=resolveAuthTablesForRequest(ctx.request,config);await applyProfileSync({db,usersTable,profilesTable:linkSyncTables.schemaTables?.profiles,userId,rawProfile:profile.rawProfile,config:linkSyncConfig,isNewUser:!1,logger:logger2})}logger2.info("[OAUTH] Account linked successfully",{userId,provider});let successUrl2=absolutizeRedirect(resolveSafeOAuthRedirect(statePayload.redirectUrl,oauthService.successRedirectUrl,config.authentication?.trustedAppOrigins),portalOrigin);return ctx.set.status=302,ctx.set.headers.Location=`${successUrl2}?linked=true&provider=${provider}`,null}if(oauthTable){let existingOAuth=await db.select().from(oauthTable).where(and25(eq57(oauthTable.provider,provider),eq57(oauthTable.providerAccountId,profile.providerAccountId))).limit(1);if(existingOAuth.length>0){let oauthRecord=existingOAuth[0];userId=oauthRecord.userId,await db.update(oauthTable).set({accessToken:tokens.accessToken,refreshToken:tokens.refreshToken??null,tokenExpiresAt:tokens.expiresAt??null,scope:tokens.scope??null,rawProfile:profile.rawProfile,lastUsedAt:new Date}).where(eq57(oauthTable.id,oauthRecord.id))}}if(!userId&&profile.email){let normalizedOAuthEmail=profile.email.toLowerCase(),existingUsers=await db.select().from(usersTable).where(eq57(usersTable.email,normalizedOAuthEmail)).limit(1);if(existingUsers.length>0&&profile.emailVerified!==!0)return logger2.warn("[OAUTH] Refusing account merge: provider email not verified",{provider,email:profile.email}),ctx.set.status=302,ctx.set.headers.Location=`${errorRedirect}?error=email_not_verified`,null;if(existingUsers.length>0){if(userId=existingUsers[0].id,oauthTable)await db.insert(oauthTable).values({userId,provider,providerAccountId:profile.providerAccountId,providerEmail:profile.email??null,providerName:profile.name??null,providerAvatarUrl:profile.avatarUrl??null,accessToken:tokens.accessToken,refreshToken:tokens.refreshToken??null,tokenExpiresAt:tokens.expiresAt??null,scope:tokens.scope??null,rawProfile:profile.rawProfile,isPrimary:!1,lastUsedAt:new Date}).onConflictDoNothing();logger2.info("[OAUTH] Merged OAuth account with existing user by email",{userId,provider,email:profile.email})}}let createdNewUser=!1;if(!userId){if(!oauthService.autoCreateUser)return ctx.set.status=302,ctx.set.headers.Location=`${errorRedirect}?error=user_not_found`,null;let userEmail=(profile.email??`${provider}_${profile.providerAccountId}@oauth.local`).toLowerCase(),providerEmailVerified=profile.emailVerified===!0;if(userId=(await db.insert(usersTable).values({email:userEmail,password:null,emailVerified:providerEmailVerified,verifiedAt:providerEmailVerified?new Date:null,isActive:!0}).returning())[0].id,oauthTable)await db.insert(oauthTable).values({userId,provider,providerAccountId:profile.providerAccountId,providerEmail:profile.email??null,providerName:profile.name??null,providerAvatarUrl:profile.avatarUrl??null,accessToken:tokens.accessToken,refreshToken:tokens.refreshToken??null,tokenExpiresAt:tokens.expiresAt??null,scope:tokens.scope??null,rawProfile:profile.rawProfile,isPrimary:!0,lastUsedAt:new Date});if(createdNewUser=!0,logger2.info("[OAUTH] Created new user via OAuth",{userId,provider,email:profile.email}),oauthService.sendInviteOnCreate&&profile.email&&getEmailService?.()?.isAvailable()&&storeMagicToken)try{let rawToken=generateMagicToken(),tokenHash=hashToken(rawToken,TOKEN_PURPOSE.invite),expiresAt=new Date(Date.now()+parseTimeToMs3("7d")),oauthSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0;await storeMagicToken({userId,email:profile.email,tokenHash,expiresAt},oauthSchemaName);let inviteLink=buildEmailActionLink({request:ctx.request,configuredUrl:oauthService.successRedirectUrl,path:"/set-password",query:{token:rawToken},allowedOrigins:config.authentication?.trustedAppOrigins});await getEmailService?.()?.sendEmail({to:profile.email,subject:`Welcome to ${appName??"the platform"} \u2014 Set your password`,html:`
447
+ `),polyTables=(Array.isArray(polyResult)?polyResult:polyResult.rows||[]).map((r)=>String(r.table_name||""));logger2.info("[AUTH] Change User ID - discovered schema references",{fkConstraints:fkConstraints.map((f)=>`${f.table_name}.${f.column_name} (${f.constraint_name})`),userIdColumns:userIdColumns.map((u)=>`${u.table_name}.${u.column_name}`),auditColumns:auditColumns.map((a)=>`${a.table_name}.${a.column_name}`),polyTables});let allUpdates=new Map,addUpdate=(tableName,columnName)=>{if(!allUpdates.has(tableName))allUpdates.set(tableName,new Set);allUpdates.get(tableName)?.add(columnName)};for(let fk of fkConstraints)addUpdate(fk.table_name,fk.column_name);for(let col13 of userIdColumns)addUpdate(col13.table_name,col13.column_name);for(let col13 of auditColumns)addUpdate(col13.table_name,col13.column_name);logger2.info("[AUTH] Change User ID - all column updates to execute",{updates:Array.from(allUpdates.entries()).map(([t,cols])=>`${t}: [${Array.from(cols).join(", ")}]`)}),await db.transaction(async(tx)=>{for(let fk of fkConstraints)await tx.execute(sql9.raw(`ALTER TABLE "${effectiveSchema}"."${fk.table_name}" DROP CONSTRAINT "${fk.constraint_name}"`));await tx.execute(sql9.raw(`UPDATE "${effectiveSchema}"."users" SET "id" = '${newId}' WHERE "id" = '${currentId}'`));for(let[tableName,columns]of allUpdates.entries())for(let columnName of columns)await tx.execute(sql9.raw(`UPDATE "${effectiveSchema}"."${tableName}" SET "${columnName}" = '${newId}' WHERE "${columnName}" = '${currentId}'`));for(let tableName of polyTables)await tx.execute(sql9.raw(`UPDATE "${effectiveSchema}"."${tableName}" SET "owner_id" = '${newId}' WHERE "owner_id" = '${currentId}' AND "owner_type" IN ('user', 'users', 'User')`));for(let fk of fkConstraints){let onDelete=fk.delete_rule==="CASCADE"?"ON DELETE CASCADE":fk.delete_rule==="SET NULL"?"ON DELETE SET NULL":"";await tx.execute(sql9.raw(`ALTER TABLE "${effectiveSchema}"."${fk.table_name}" ADD CONSTRAINT "${fk.constraint_name}" FOREIGN KEY ("${fk.column_name}") REFERENCES "${effectiveSchema}"."users" ("id") ${onDelete}`))}});let allAffectedTables=[...new Set([...fkConstraints.map((f)=>f.table_name),...userIdColumns.map((u)=>u.table_name),...auditColumns.map((a)=>a.table_name),...polyTables])];return logger2.info("[AUTH] User ID changed successfully",{godminId:requestingUserId,currentId,newId,email:targetUser.email,affectedTables:allAffectedTables,fkConstraintsDroppedAndReadded:fkConstraints.length}),await logger2.audit({entityName:"users",entityId:newId,operation:"CHANGE_USER_ID",userId:requestingUserId,summary:`Godmin changed user ID: ${currentId} \u2192 ${newId} (${targetUser.email})`,oldValues:{id:currentId},newValues:{id:newId},ipAddress:ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||"unknown",userAgent:ctx.request.headers.get("user-agent")||"unknown",path:new URL(ctx.request.url).pathname,query:""}),{success:!0,data:{oldId:currentId,newId,email:targetUser.email,affectedTables:allAffectedTables}}}catch(error3){return logger2.error("[AUTH] Failed to change user ID",{error:error3 instanceof Error?error3.message:String(error3),stack:error3 instanceof Error?error3.stack:void 0,currentId,newId}),ctx.set.status=500,{success:!1,message:error3 instanceof Error?error3.message:"Failed to change user ID"}}}})];return collectRoutes([{routes:declared}])}var init_changeUserId=__esm(()=>{init_esm3();init_routeCollector();init_requireGodmin();init_resolveMutationSchema()});function decideCreateUserAuthz(params){let{callerIsGod,callerRoleNames,allowRoles,requestedRoleNames,godminRoleName}=params,callerIsGodmin=callerIsGod||callerRoleNames.includes(godminRoleName);if(!(callerIsGodmin||callerRoleNames.some((n)=>allowRoles.includes(n))))return{allowed:!1,status:403,reason:"not_privileged"};if(requestedRoleNames.includes(godminRoleName)&&!callerIsGodmin)return{allowed:!1,status:403,reason:"godmin_assignment_forbidden"};if(!callerIsGodmin){let heldByCaller=new Set(callerRoleNames);if(!requestedRoleNames.every((r)=>heldByCaller.has(r)))return{allowed:!1,status:403,reason:"role_above_caller"}}return{allowed:!0,callerIsGodmin}}import{eq as eq43,inArray as inArray17}from"drizzle-orm";function createCreateUserRoute(config,createUserConfig){let{db,logger:logger2}=config;if(createUserConfig?.enabled===!1)return collectRoutes();let route=createUserConfig?.route||"/auth/admin/create-user",allowRoles=createUserConfig?.allowRoles??[],declared=[defineRoute({method:"POST",path:"/",body:CreateUserBodySchema,detail:{tags:["Authentication","Admin"],summary:"Admin Create User",description:"Create a fully active user with a password, roles, and optional profile. Admin-only. No email is sent."},handler:async(ctx)=>{let resolved=resolveAuthTablesForRequest(ctx.request,config),{usersTable,rolesTable:resolvedRolesTable,userRolesTable:resolvedUserRolesTable,schemaTables}=resolved,profilesTable=schemaTables?.profiles,rolesTable=resolvedRolesTable??config.rolesTable??null,userRolesTable=resolvedUserRolesTable??config.userRolesTable??null;if(!db||!usersTable)return ctx.set.status=500,{success:!1,message:"Database not configured"};let requestingUserId=ctx.request.headers.get("x-user-id");if(!requestingUserId)return ctx.set.status=401,{success:!1,message:"Unauthorized"};let requestingUser=(await db.select().from(usersTable).where(eq43(usersTable.id,requestingUserId)).limit(1))[0];if(!requestingUser)return ctx.set.status=401,{success:!1,message:"Unauthorized"};let callerIsGod=Boolean(requestingUser.isGod),callerRoleNames=[];if(!callerIsGod&&rolesTable&&userRolesTable){let rolesCols=rolesTable,userRolesCols=userRolesTable;callerRoleNames=(await db.select({name:rolesCols.name}).from(userRolesTable).innerJoin(rolesTable,eq43(rolesCols.id,userRolesCols.roleId)).where(eq43(userRolesCols.userId,requestingUserId))).map((r)=>r.name)}let requestedRoleIds=ctx.body.roleIds??[],foundRoles=[];if(requestedRoleIds.length>0&&rolesTable){let rolesCols=rolesTable;foundRoles=(await db.select({id:rolesCols.id,name:rolesCols.name}).from(rolesTable).where(inArray17(rolesCols.id,requestedRoleIds))).map((r)=>({id:r.id,name:r.name}))}let decision=decideCreateUserAuthz({callerIsGod,callerRoleNames,allowRoles,requestedRoleNames:foundRoles.map((r)=>r.name),godminRoleName:GODMIN_ROLE_NAME});if(!decision.allowed)return logger2.warn("[AUTH] create-user denied",{callerId:requestingUserId,reason:decision.reason}),ctx.set.status=decision.status,{success:!1,message:decision.reason==="godmin_assignment_forbidden"?"Forbidden: assigning the godmin role requires godmin privileges":"Forbidden"};let{email:rawEmail,password:password4,profile}=ctx.body,email=rawEmail.toLowerCase().trim(),passwordValidation=validatePasswordStrength(password4,config.authentication?.passwordPolicy,email);if(!passwordValidation.valid)return ctx.set.status=400,{success:!1,message:"Password too weak",errors:passwordValidation.errors};if((await db.select().from(usersTable).where(eq43(usersTable.email,email)).limit(1)).length>0)return logger2.warn("[AUTH] create-user failed \u2014 email already exists",{email}),ctx.set.status=409,{success:!1,message:"Email already registered"};let now=new Date,hashedPassword=await hashPassword(password4),insertValues={email,password:hashedPassword,emailVerified:!0,verifiedAt:now,isLocked:!1,createdAt:now,updatedAt:now},newUser=(await db.insert(usersTable).values(insertValues).returning())[0];if(!newUser)return logger2.error("[AUTH] create-user insert returned nothing",{email}),ctx.set.status=500,{success:!1,message:"Failed to create user"};let newUserId=newUser.id;if(requestedRoleIds.length>0&&rolesTable&&userRolesTable)try{let validRoleIds=foundRoles.map((r)=>r.id);if(validRoleIds.length>0)await db.insert(userRolesTable).values(validRoleIds.map((roleId)=>({userId:newUserId,roleId})));let skipped=requestedRoleIds.filter((id)=>!validRoleIds.includes(id));if(skipped.length>0)logger2.warn("[AUTH] create-user: some roleIds were not found",{userId:newUserId,skipped});if(foundRoles.some((r)=>r.name===GODMIN_ROLE_NAME))await db.update(usersTable).set({isGod:!0}).where(eq43(usersTable.id,newUserId))}catch(err){logger2.warn("[AUTH] create-user: failed to assign roles \u2014 continuing",{userId:newUserId,error:err instanceof Error?err.message:String(err)})}else{let createDefaultRoles=resolveDefaultRoleNames(config.authentication);if(createDefaultRoles.length>0)await assignDefaultRoles({db,userId:newUserId,roleNames:createDefaultRoles,rolesTable,userRolesTable,logger:logger2})}if(profile&&profilesTable)try{await db.insert(profilesTable).values({userId:newUserId,firstName:profile.firstName??null,lastName:profile.lastName??null,phoneNumber:profile.phoneNumber??null,createdAt:now,updatedAt:now})}catch(err){logger2.warn("[AUTH] create-user: failed to create profile \u2014 continuing",{userId:newUserId,error:err instanceof Error?err.message:String(err)})}let 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";return logger2.audit({entityName:"users",entityId:newUserId,operation:"ADMIN_CREATE_USER",userId:requestingUserId,summary:`Admin ${requestingUserId} created user ${email}`,ipAddress:auditIp,userAgent:auditUa,path:reqUrl.pathname,query:reqUrl.search}),logger2.info("[AUTH] Admin created user",{newUserId,email,createdBy:requestingUserId}),{success:!0,message:"User created successfully",data:{id:newUserId,email}}}})];return collectRoutes([{prefix:route,routes:declared}])}var CreateUserBodySchema;var init_createUser=__esm(()=>{init_esm3();init_Authorization();init_routeCollector();init_assignDefaultRole();init_utils6();CreateUserBodySchema=Type.Object({email:Type.String({format:"email"}),password:Type.String({minLength:8}),roleIds:Type.Optional(Type.Array(Type.String())),profile:Type.Optional(Type.Object({firstName:Type.Optional(Type.String()),lastName:Type.Optional(Type.String()),phoneNumber:Type.Optional(Type.String())}))})});import{eq as eq44}from"drizzle-orm";function createHardDeleteUserRoute(config){let{db,logger:logger2}=config,declared=[defineRoute({method:"DELETE",path:"/auth/admin/hard-delete/:userId",params:HardDeleteUserParamsSchema,detail:{tags:["Authentication","Admin"],summary:"Hard Delete User",description:"Permanently delete a user and all related data (roles, sessions, profile, files, etc.). Godmin-only. Irreversible."},handler:async(ctx)=>{let resolved=resolveAuthTablesForRequest(ctx.request,config),{usersTable,userRolesTable,sessionsTable,oauthAccountsTable,apiKeysTable,schemaTables}=resolved;if(!db||!usersTable)return ctx.set.status=500,{success:!1,message:"Database not configured"};let requestingUserId=ctx.request.headers.get("x-user-id");if(!requestingUserId)return ctx.set.status=401,{success:!1,message:"Unauthorized"};let caller=(await db.select().from(usersTable).where(eq44(usersTable.id,requestingUserId)).limit(1))[0],callerIsGodmin=Boolean(caller?.isGod)||hasGodminRole(ctx.request)||await userHasGodminRoleInDb(db,config.schemaTables??{},requestingUserId,config.logger);if(!caller||!callerIsGodmin)return ctx.set.status=403,{success:!1,message:"Forbidden \u2014 only godmin can hard-delete users"};let{userId:targetUserId}=ctx.params;if(targetUserId===requestingUserId)return ctx.set.status=400,{success:!1,message:"Cannot hard-delete yourself"};let targetUser=(await db.select().from(usersTable).where(eq44(usersTable.id,targetUserId)).limit(1))[0];if(!targetUser)return ctx.set.status=404,{success:!1,message:"User not found"};if(targetUser.isGod)return ctx.set.status=403,{success:!1,message:"Cannot hard-delete a godmin user"};let deletedRelations=[],col13=(table,name2)=>table[name2],profilesTable=schemaTables?.profiles,addressesTable=schemaTables?.addresses,phonesTable=schemaTables?.phones,filesTable=schemaTables?.files,runCascade=async(exec)=>{deletedRelations.length=0;let del=async(table,columnName,label)=>{if(!table)return;await exec.delete(table).where(eq44(col13(table,columnName),targetUserId)),deletedRelations.push(label)};if(await del(userRolesTable,"userId","user_roles"),await del(sessionsTable,"userId","sessions"),await del(oauthAccountsTable,"userId","oauth_accounts"),await del(apiKeysTable,"userId","api_keys"),await del(profilesTable,"userId","profiles"),await del(addressesTable,"ownerId","addresses"),await del(phonesTable,"ownerId","phones"),filesTable)await exec.update(filesTable).set({uploadedBy:null}).where(eq44(col13(filesTable,"uploadedBy"),targetUserId)),deletedRelations.push("files:dissociated");await exec.delete(usersTable).where(eq44(usersTable.id,targetUserId))};try{let txDb=db;if(typeof txDb.transaction==="function")await txDb.transaction(async(tx)=>{await runCascade(tx)});else await runCascade(db)}catch(err){return logger2.error("[AUTH] hard-delete failed; transaction rolled back",{userId:targetUserId,error:err instanceof Error?err.message:String(err)}),ctx.set.status=500,{success:!1,message:"Hard delete failed and was rolled back"}}let reqUrl=new URL(ctx.request.url);return logger2.audit({entityName:"users",entityId:targetUserId,operation:"ADMIN_HARD_DELETE_USER",userId:requestingUserId,summary:`Godmin ${requestingUserId} hard-deleted user ${targetUser.email} (${targetUserId}). Cleaned: ${deletedRelations.join(", ")}`,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}),logger2.info("[AUTH] Hard-deleted user",{targetUserId,email:targetUser.email,deletedBy:requestingUserId,cleanedRelations:deletedRelations}),{success:!0,message:"User permanently deleted",data:{userId:targetUserId,email:targetUser.email,cleanedRelations:deletedRelations}}}})];return collectRoutes([{routes:declared}])}var HardDeleteUserParamsSchema;var init_hardDeleteUser=__esm(()=>{init_esm3();init_routeCollector();init_adminGuard();HardDeleteUserParamsSchema=Type.Object({userId:Type.String()})});var exports_fetchUserRolesAndClaims={};__export(exports_fetchUserRolesAndClaims,{fetchUserRolesAndClaims:()=>fetchUserRolesAndClaims});import{and as and23,eq as eq45,inArray as inArray18}from"drizzle-orm";var noopLogger,fetchUserRolesAndClaims=async(db,userId,resolved,logger2=noopLogger)=>{let{userRolesTable,rolesTable,roleClaimsTable,claimsTable,usersTable}=resolved,result={roles:[],claims:[],claimScopes:{}};if(!userRolesTable||!rolesTable)return result;let roleIds=(await db.select().from(userRolesTable).where(and23(eq45(userRolesTable.userId,userId),activeRowsPredicate(userRolesTable)))).map((r)=>r.roleId);if(roleIds.length===0)return result;let roleRows=await db.select().from(rolesTable).where(inArray18(rolesTable.id,roleIds));if(result.roles=roleRows.map((r)=>r.name),roleClaimsTable&&claimsTable){let rows=(await db.select({action:claimsTable.action,scope:roleClaimsTable.scope}).from(roleClaimsTable).innerJoin(claimsTable,eq45(roleClaimsTable.claimId,claimsTable.id)).where(and23(inArray18(roleClaimsTable.roleId,roleIds),activeRowsPredicate(roleClaimsTable)))).filter((r)=>typeof r.action==="string"&&r.action.length>0).map((r)=>({action:r.action,scope:r.scope??null})),userData;if(rows.some((r)=>!!r.scope)&&usersTable)try{userData=(await db.select().from(usersTable).where(eq45(usersTable.id,userId)))[0]}catch(err){logger2.warn("[Authorization] Failed to load user row for scope resolution",{userId,error:err instanceof Error?err.message:String(err)})}let{claimScopes,dropped}=resolveClaimScopes(rows,userData,logger2),droppedSet=new Set(dropped);result.claims=Array.from(new Set(rows.map((r)=>r.action))).filter((a)=>!droppedSet.has(a)),result.claimScopes=claimScopes}return result};var init_fetchUserRolesAndClaims=__esm(()=>{init_activeRows();init_Middleware();noopLogger={debug:()=>{},info:()=>{},warn:()=>{},error:()=>{}}});var IMPERSONATION_COOKIE_NAMES;var init_impersonationCookies=__esm(()=>{IMPERSONATION_COOKIE_NAMES=["nucleus_impersonating_as","nucleus_godmin_id","nucleus_impersonating_email","nucleus_imp_sig"]});import{eq as eq46}from"drizzle-orm";function createImpersonateRoute(config,signAccessToken,signRefreshToken,verifyRefreshToken,createSession,saveSessionToDb,cookieConfig){let{db,logger:logger2}=config,requireGodmin2=createRequireGodmin(config),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,sameSite:cookieConfig?.sameSite||"lax",path:cookieConfig?.path||"/",domain:cookieConfig?.domain},routePostAuthAdminImpersonate=defineRoute({method:"POST",path:"/auth/admin/impersonate",body:Type.Object({targetUserId:Type.String()}),detail:{tags:["Authentication"],summary:"Impersonate User",description:"Godmin: start a session as another user"},handler:async(ctx)=>{let{usersTable}=resolveAuthTablesForRequest(ctx.request,config);if(!db||!usersTable)return ctx.set.status=500,{success:!1,message:"Database not configured"};let{targetUserId}=ctx.body,gate=await requireGodmin2(ctx.request);if(!gate.ok)return ctx.set.status=gate.status,{success:!1,message:gate.message};let{callerId:requestingUserId,caller:requestingUser}=gate;if(requestingUserId===targetUserId)return ctx.set.status=400,{success:!1,message:"Cannot impersonate yourself"};let targetUser=(await db.select().from(usersTable).where(eq46(usersTable.id,targetUserId)).limit(1))[0];if(!targetUser)return ctx.set.status=404,{success:!1,message:"Target user not found"};let impRoles=[],impClaims=[],impClaimScopes={};if(config.db)try{let resolved=resolveAuthTablesForRequest(ctx.request,config),rc=await fetchUserRolesAndClaims(config.db,targetUserId,resolved);impRoles=rc.roles,impClaims=rc.claims,impClaimScopes=rc.claimScopes}catch{}let reqIp=ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||"unknown",sessionParams={userId:targetUserId,deviceInfo:{deviceName:"Impersonation Session",browserName:"Admin",osName:"Admin",ipAddress:reqIp},loginMethod:"impersonation"},sessionId=await createSession(sessionParams),accessToken=signAccessToken(targetUserId,impRoles.length>0?impRoles:void 0,impClaims.length>0?impClaims:void 0,ctx.request.headers.get("x-tenant-schema")??void 0,impClaimScopes,sessionId),refreshToken=signRefreshToken(targetUserId,void 0,sessionId);if(saveSessionToDb){let impSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0;await saveSessionToDb(sessionId,sessionParams,impSchemaName)}logger2.info("[AUTH] Impersonation started",{godminId:requestingUserId,targetUserId,targetEmail:targetUser.email}),await logger2.audit({entityName:"users",entityId:targetUserId,operation:"IMPERSONATE_START",userId:requestingUserId,summary:`Godmin ${requestingUser.email} started impersonating ${targetUser.email}`,ipAddress:ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||"unknown",userAgent:ctx.request.headers.get("user-agent")||"unknown",path:new URL(ctx.request.url).pathname,query:""});let securePart=cookies.secure?"; Secure":"",domainPart=cookies.domain?`; Domain=${cookies.domain}`:"",cookieOptions=`; Path=${cookies.path}; HttpOnly; SameSite=${cookies.sameSite}${securePart}${domainPart}`,cookiesToSet=[`${cookies.accessTokenName}=${accessToken}${cookieOptions}; Max-Age=${cookies.accessTokenMaxAge}`,`${cookies.refreshTokenName}=${refreshToken}${cookieOptions}; Max-Age=${cookies.refreshTokenMaxAge}`,`${cookies.sessionTokenName}=${sessionId}${cookieOptions}; Max-Age=${cookies.sessionTokenMaxAge}`,`nucleus_impersonating_as=${targetUserId}${cookieOptions}; Max-Age=${cookies.sessionTokenMaxAge}`,`nucleus_godmin_id=${requestingUserId}${cookieOptions}; Max-Age=${cookies.sessionTokenMaxAge}`,`nucleus_impersonating_email=${encodeURIComponent(String(targetUser.email))}${cookieOptions}; Max-Age=${cookies.sessionTokenMaxAge}`,`nucleus_imp_sig=${signRefreshToken(requestingUserId)}${cookieOptions}; Max-Age=${cookies.refreshTokenMaxAge}`],jsonBody=JSON.stringify(withEnvelopeFlags({success:!0,data:{targetUser:{id:targetUser.id,email:targetUser.email},godminId:requestingUserId,sessionId}})),headers=new Headers;headers.set("Content-Type","application/json");for(let cookie of cookiesToSet)headers.append("Set-Cookie",cookie);return new Response(jsonBody,{status:200,headers})}}),routePostAuthAdminImpersonateStop=defineRoute({method:"POST",path:"/auth/admin/impersonate/stop",detail:{tags:["Authentication"],summary:"Stop Impersonation",description:"Godmin: restore own session after impersonation"},handler:async(ctx)=>{let{usersTable}=resolveAuthTablesForRequest(ctx.request,config);if(!db||!usersTable)return ctx.set.status=500,{success:!1,message:"Database not configured"};let cookieHeader=ctx.request.headers.get("cookie")||"",impSig=Object.fromEntries(cookieHeader.split(";").map((c)=>{let[k,...v]=c.trim().split("=");return[k?.trim(),v.join("=")]})).nucleus_imp_sig,verified=impSig?verifyRefreshToken(impSig):{valid:!1};if(!verified.valid||!verified.payload?.sub)return ctx.set.status=401,{success:!1,message:"No valid impersonation session"};let godminId=verified.payload.sub,godminUser=(await db.select().from(usersTable).where(eq46(usersTable.id,godminId)).limit(1))[0];if(!godminUser)return ctx.set.status=404,{success:!1,message:"Godmin user not found"};if(godminUser.isGod!==!0)return ctx.set.status=403,{success:!1,message:"Forbidden: godmin privileges required"};let stateRefusal=accountStateRefusal(godminUser);if(stateRefusal)return ctx.set.status=401,{success:!1,message:stateRefusal};let godminRoles=[],godminClaims=[],godminClaimScopes={};if(config.db)try{let resolvedStop=resolveAuthTablesForRequest(ctx.request,config),rcStop=await fetchUserRolesAndClaims(config.db,godminId,resolvedStop);godminRoles=rcStop.roles,godminClaims=rcStop.claims,godminClaimScopes=rcStop.claimScopes}catch{}let stopReqIp=ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||"unknown",sessionParams={userId:godminId,deviceInfo:{deviceName:"Restored Admin Session",browserName:"Admin",osName:"Admin",ipAddress:stopReqIp},loginMethod:"impersonation_stop"},sessionId=await createSession(sessionParams),accessToken=signAccessToken(godminId,godminRoles.length>0?godminRoles:void 0,godminClaims.length>0?godminClaims:void 0,ctx.request.headers.get("x-tenant-schema")??void 0,godminClaimScopes,sessionId),refreshToken=signRefreshToken(godminId,void 0,sessionId);if(saveSessionToDb){let stopSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0;await saveSessionToDb(sessionId,sessionParams,stopSchemaName)}logger2.info("[AUTH] Impersonation stopped",{godminId}),await logger2.audit({entityName:"users",entityId:godminId,operation:"IMPERSONATE_STOP",userId:godminId,summary:`Godmin ${godminUser.email} stopped impersonation`,ipAddress:ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||"unknown",userAgent:ctx.request.headers.get("user-agent")||"unknown",path:new URL(ctx.request.url).pathname,query:""});let securePart=cookies.secure?"; Secure":"",domainPart=cookies.domain?`; Domain=${cookies.domain}`:"",cookieOptions=`; Path=${cookies.path}; HttpOnly; SameSite=${cookies.sameSite}${securePart}${domainPart}`,expiredOptions=`; Path=${cookies.path}; HttpOnly; SameSite=${cookies.sameSite}${securePart}${domainPart}; Max-Age=0`,cookiesToSet=[`${cookies.accessTokenName}=${accessToken}${cookieOptions}; Max-Age=${cookies.accessTokenMaxAge}`,`${cookies.refreshTokenName}=${refreshToken}${cookieOptions}; Max-Age=${cookies.refreshTokenMaxAge}`,`${cookies.sessionTokenName}=${sessionId}${cookieOptions}; Max-Age=${cookies.sessionTokenMaxAge}`,...IMPERSONATION_COOKIE_NAMES.map((name2)=>`${name2}=deleted${expiredOptions}`)],jsonBody=JSON.stringify(withEnvelopeFlags({success:!0,data:{godminUser:{id:godminUser.id,email:godminUser.email},sessionId}})),headers=new Headers;headers.set("Content-Type","application/json");for(let cookie of cookiesToSet)headers.append("Set-Cookie",cookie);return new Response(jsonBody,{status:200,headers})}});return collectRoutes([{routes:[routePostAuthAdminImpersonate,routePostAuthAdminImpersonateStop]}])}var init_impersonate=__esm(()=>{init_esm3();init_response();init_routeCollector();init_fetchUserRolesAndClaims();init_impersonationCookies();init_requireGodmin()});function clearedLockoutState(){return{failedLoginAttempts:0,isLocked:!1,lockedUntil:null}}function planPasswordRotation(input){let protectedRoles=new Set(input.protectedRoles.filter((name2)=>name2.trim()!=="")),plan={rotate:[],skipped:[]};for(let user of input.users){if(user.id===input.callerId){plan.skipped.push({id:user.id,reason:"caller"});continue}if(user.isGod){plan.skipped.push({id:user.id,reason:"godmin"});continue}if(user.roleNames.some((name2)=>protectedRoles.has(name2))){plan.skipped.push({id:user.id,reason:"protected_role"});continue}if(input.includeInactive!==!0&&user.isActive===!1){plan.skipped.push({id:user.id,reason:"inactive"});continue}plan.rotate.push(user.id)}return plan}function splitByRecoverability(users,rotate){let rotating=new Set(rotate),selfService=[],needsAdmin=[];for(let user of users){if(!rotating.has(user.id))continue;if(String(user.email??"").trim()==="")needsAdmin.push(user.id);else selfService.push(user.id)}return{selfService,needsAdmin}}import{randomBytes as randomBytes5}from"crypto";import{eq as eq47}from"drizzle-orm";function createPasswordAdminRoutes(config,options){let{db,logger:logger2}=config,log3=logger2.scoped(AUTH_LOGIN),passwordToolsEnabled=options?.passwordTools===!0,requireGodmin2=createRequireGodmin(config),routePostAuthAdminRotatePasswords=defineRoute({method:"POST",path:"/auth/admin/rotate-passwords",body:Type.Object({confirm:Type.Optional(Type.Boolean()),protectedRoles:Type.Optional(Type.Array(Type.String())),includeInactive:Type.Optional(Type.Boolean())}),detail:{tags:["Authentication"],summary:"Rotate every password except the ones named",description:"Without `confirm: true` this only reports what it would do. Confirmed, it gives every selected account a different random password, signs those sessions out, and tells nobody the new values \u2014 recovery is the reset link or an administrator."},handler:async(ctx)=>{if(!passwordToolsEnabled)return notEnabled(ctx);let gate=await requireGodmin2(ctx.request);if(!gate.ok)return ctx.set.status=gate.status,{success:!1,message:gate.message};let resolved=resolveAuthTablesForRequest(ctx.request,config),usersTable=resolved.usersTable,body=ctx.body,users=await db?.select().from(usersTable),roleNamesByUser=await loadRoleNames(resolved,users),candidates=users.map((row)=>({id:String(row.id),roleNames:roleNamesByUser.get(String(row.id))??[],isGod:row.isGod===!0,email:row.email??null,isActive:row.isActive})),plan=planPasswordRotation({users:candidates,callerId:gate.callerId,protectedRoles:body.protectedRoles??[],includeInactive:body.includeInactive===!0}),recovery=splitByRecoverability(candidates,plan.rotate);if(body.confirm!==!0)return{success:!0,data:{preview:!0,willRotate:plan.rotate.length,skipped:plan.skipped.length,skippedReasons:countReasons(plan.skipped),canResetThemselves:recovery.selfService.length,needAdministrator:recovery.needsAdmin.length}};if(plan.rotate.length===0)return{success:!0,data:{preview:!1,rotated:0,signedOut:0}};let rotated=0;for(let userId of plan.rotate){let hashed=await hashPassword(randomBytes5(24).toString("base64url"));await db?.update(usersTable).set({password:hashed,updatedAt:new Date}).where(eq47(usersTable.id,userId)),rotated+=1}let signedOut=await revokeSessions(resolved,plan.rotate);return log3.warn("Bulk password rotation",{by:gate.callerId,rotated,signedOut,skipped:plan.skipped.length}),{success:!0,data:{preview:!1,rotated,signedOut,canResetThemselves:recovery.selfService.length,needAdministrator:recovery.needsAdmin.length}}}}),routePostAuthAdminSetUserPassword=defineRoute({method:"POST",path:"/auth/admin/set-user-password",body:Type.Object({userId:Type.String(),password:Type.String()}),detail:{tags:["Authentication"],summary:"Set one account's password",description:"For the person who has no email address and cannot use the reset link. Applies the same strength policy as a self-service change and ends that account\u2019s existing sessions."},handler:async(ctx)=>{if(!passwordToolsEnabled)return notEnabled(ctx);let gate=await requireGodmin2(ctx.request);if(!gate.ok)return ctx.set.status=gate.status,{success:!1,message:gate.message};let resolved=resolveAuthTablesForRequest(ctx.request,config),usersTable=resolved.usersTable,{userId,password:password4}=ctx.body,strength=validatePasswordStrength(password4);if(!strength.valid)return ctx.set.status=400,{success:!1,message:strength.errors?.join(" ")??"Password is too weak"};let rows=await db?.select().from(usersTable).where(eq47(usersTable.id,userId)).limit(1);if(!rows||rows.length===0)return ctx.set.status=404,{success:!1,message:"User not found"};await db?.update(usersTable).set({password:await hashPassword(password4),updatedAt:new Date,...clearedLockoutState()}).where(eq47(usersTable.id,userId));let signedOut=await revokeSessions(resolved,[userId]);return log3.warn("Password set by administrator",{by:gate.callerId,userId,signedOut}),{success:!0,data:{signedOut,lockoutCleared:!0}}}});return collectRoutes([{routes:[routePostAuthAdminRotatePasswords,routePostAuthAdminSetUserPassword]}]);function notEnabled(ctx){return ctx.set.status=404,{success:!1,message:"Not found"}}async function loadRoleNames(resolved,users){let byUser=new Map,userRoles=resolved.userRolesTable,roles=resolved.rolesTable;if(!db||!userRoles||!roles||users.length===0)return byUser;let roleRows=await db.select().from(roles),roleNameById=new Map(roleRows.map((row)=>[String(row.id),String(row.name??"")])),links=await db.select().from(userRoles);for(let link of links){let userId=String(link.userId??""),name2=roleNameById.get(String(link.roleId??""));if(!userId||!name2)continue;byUser.set(userId,[...byUser.get(userId)??[],name2])}return byUser}async function revokeSessions(resolved,userIds){return revokeUserSessions({db,sessionsTable:resolved.sessionsTable,onError:(error3)=>log3.error("Sessions could not be revoked after a password change",{error:error3})},userIds)}}function countReasons(skipped){let counts={};for(let item of skipped)counts[item.reason]=(counts[item.reason]??0)+1;return counts}var init_passwordAdmin=__esm(()=>{init_esm3();init_scopes();init_routeCollector();init_utils6();init_requireGodmin();init_revokeUserSessions()});import{eq as eq48}from"drizzle-orm";function createUserStateRoutes(config){let{db,logger:logger2}=config,log3=logger2.scoped(AUTH_LOGIN),requireGodmin2=createRequireGodmin(config);async function loadTarget(request,userId){let resolved=resolveAuthTablesForRequest(request,config),usersTable=resolved.usersTable;if(!db||!usersTable)return{ok:!1,status:500,message:"Database not configured"};let user=(await db.select().from(usersTable).where(eq48(usersTable.id,userId)).limit(1))?.[0];if(!user)return{ok:!1,status:404,message:"User not found"};return{ok:!0,usersTable,sessionsTable:resolved.sessionsTable??null,user}}async function writeState(usersTable,userId,changes){await db?.update(usersTable).set({...changes,updatedAt:new Date}).where(eq48(usersTable.id,userId))}let routePostAuthAdminLockUser=defineRoute({method:"POST",path:"/auth/admin/lock-user",body:Type.Object({userId:Type.String(),until:Type.Optional(Type.String())}),detail:{tags:["Authentication"],summary:"Lock one account",description:"Refuses further login attempts. `until` is an ISO timestamp for a temporary lock; omit it to lock indefinitely. Cannot be used on the calling administrator."},handler:async(ctx)=>{let gate=await requireGodmin2(ctx.request);if(!gate.ok)return ctx.set.status=gate.status,{success:!1,message:gate.message};let{userId,until}=ctx.body,target2=await loadTarget(ctx.request,userId);if(!target2.ok)return ctx.set.status=target2.status,{success:!1,message:target2.message};if(userId===gate.callerId)return ctx.set.status=400,{success:!1,message:"An administrator cannot lock their own account"};let lockedUntil=until?new Date(until):null;if(until&&Number.isNaN(lockedUntil?.getTime()))return ctx.set.status=400,{success:!1,message:"`until` is not a valid date"};await writeState(target2.usersTable,userId,{isLocked:!0,lockedUntil});let signedOut=await revokeUserSessions({db,sessionsTable:target2.sessionsTable,onError:(error3)=>log3.error("Sessions could not be revoked after locking an account",{error:error3,userId})},[userId]);return log3.warn("Account locked by administrator",{by:gate.callerId,userId,until:lockedUntil,signedOut}),{success:!0,data:{isLocked:!0,lockedUntil,signedOut}}}}),routePostAuthAdminUnlockUser=defineRoute({method:"POST",path:"/auth/admin/unlock-user",body:Type.Object({userId:Type.String()}),detail:{tags:["Authentication"],summary:"Release one account from lockout",description:"Clears isLocked, lockedUntil AND failedLoginAttempts together, without touching the password. Clearing only isLocked is not a release: the login route sends any account at or above the captcha threshold through the captcha gate before it checks the password, and the counter only clears on a login that succeeds."},handler:async(ctx)=>{let gate=await requireGodmin2(ctx.request);if(!gate.ok)return ctx.set.status=gate.status,{success:!1,message:gate.message};let{userId}=ctx.body,target2=await loadTarget(ctx.request,userId);if(!target2.ok)return ctx.set.status=target2.status,{success:!1,message:target2.message};let wasLocked=target2.user.isLocked===!0,clearedFailedAttempts=Number(target2.user.failedLoginAttempts??0);return await writeState(target2.usersTable,userId,clearedLockoutState()),log3.warn("Account lockout cleared by administrator",{by:gate.callerId,userId,wasLocked,clearedFailedAttempts}),{success:!0,data:{wasLocked,clearedFailedAttempts}}}}),routePostAuthAdminMarkEmailVerified=defineRoute({method:"POST",path:"/auth/admin/mark-email-verified",body:Type.Object({userId:Type.String()}),detail:{tags:["Authentication"],summary:"Mark one account as email-verified",description:"For an installation with no working mail transport, where the verification message can never arrive. Idempotent: an account that is already verified keeps its original timestamp."},handler:async(ctx)=>{let gate=await requireGodmin2(ctx.request);if(!gate.ok)return ctx.set.status=gate.status,{success:!1,message:gate.message};let{userId}=ctx.body,target2=await loadTarget(ctx.request,userId);if(!target2.ok)return ctx.set.status=target2.status,{success:!1,message:target2.message};let alreadyVerified=Boolean(target2.user.verifiedAt),verifiedAt=alreadyVerified?target2.user.verifiedAt:new Date;if(!alreadyVerified)await writeState(target2.usersTable,userId,{verifiedAt,emailVerified:!0});return log3.warn("Email marked verified by administrator",{by:gate.callerId,userId,alreadyVerified}),{success:!0,data:{verifiedAt,alreadyVerified}}}});return collectRoutes([{routes:[routePostAuthAdminLockUser,routePostAuthAdminUnlockUser,routePostAuthAdminMarkEmailVerified]}])}var init_userState=__esm(()=>{init_esm3();init_scopes();init_routeCollector();init_requireGodmin();init_revokeUserSessions()});import{and as and24,count as count2,eq as eq49,sql as sql10}from"drizzle-orm";function createApiKeyRoutes(app,config,apiKeysConfig){let basePath=apiKeysConfig.route||"/auth/api-keys",apiKeysTable=config.apiKeysTable,{db,logger:logger2}=config;if(!apiKeysTable||!db){logger2.warn("[API_KEYS] api_keys table or database not available. Skipping API key routes.");return}let getResolvedConfig=(request)=>{let resolved=resolveAuthTablesForRequest(request,config),reqApiKeysTable=resolved.apiKeysTable??apiKeysTable;return{config:{...config,usersTable:resolved.usersTable??config.usersTable,userRolesTable:resolved.userRolesTable,rolesTable:resolved.rolesTable,roleClaimsTable:resolved.roleClaimsTable,claimsTable:resolved.claimsTable,apiKeysTable:reqApiKeysTable,schemaTables:resolved.schemaTables},apiKeysTable:reqApiKeysTable}},keyPrefix=apiKeysConfig.keyPrefix||"nk_live",maxKeysPerUser=apiKeysConfig.maxKeysPerUser||10,preventManagement=apiKeysConfig.preventApiKeyManagement??!1,routePostRoot=defineRoute({method:"POST",path:"/",handler:async({request,set:set2,body:rawBody})=>{let resolved=getResolvedConfig(request),reqApiKeysTable=resolved.apiKeysTable,reqConfig=resolved.config;if(preventManagement)return set2.status=403,{isSuccess:!1,message:"API key management is disabled via configuration",data:null};if(request.headers.get("x-auth-type")==="api_key")return set2.status=403,{isSuccess:!1,message:"API keys cannot be created using another API key",data:null};let userId=request.headers.get("x-user-id");if(!userId)return set2.status=401,{isSuccess:!1,message:"Unauthenticated",data:null};let body;try{body=rawBody??{}}catch{return set2.status=400,{isSuccess:!1,message:"Invalid request body",data:null}}if(!body.name||typeof body.name!=="string"||body.name.trim().length===0)return set2.status=400,{isSuccess:!1,message:"Name is required",data:null};if(body.ownerType==="application"){if(!(apiKeysConfig.allowApplicationKeys??!0))return set2.status=403,{isSuccess:!1,message:"Application API keys are not allowed",data:null};if(!body.applicationName||body.applicationName.trim().length===0)return set2.status=400,{isSuccess:!1,message:"Application name is required for application keys",data:null}}if(((await db.select({count:count2()}).from(reqApiKeysTable).where(and24(eq49(col13(reqApiKeysTable,"userId"),userId),eq49(col13(reqApiKeysTable,"isActive"),!0))))[0]?.count||0)>=maxKeysPerUser)return set2.status=400,{isSuccess:!1,message:`Maximum API key limit reached (${maxKeysPerUser})`,data:null};let userRoles=await getUserRoleNames(reqConfig,userId),userClaims=await getUserClaimActions(reqConfig,userId),isGodmin4=userRoles.some((r)=>isGodminRole(r)),requestedRoles=body.allowedRoles||[],requestedClaims=body.allowedClaims||[],requestedScopes=body.allowedScopes||[],stripGodmin=(roles)=>roles.filter((r)=>!isGodminRole(r)),finalRoles,finalClaims;if(isGodmin4)finalRoles=requestedRoles.length>0?stripGodmin(requestedRoles):[],finalClaims=requestedClaims.length>0?requestedClaims:userClaims;else{let validRoles=intersectPermissions(userRoles,requestedRoles),validClaims=intersectPermissions(userClaims,requestedClaims);if(requestedRoles.length>0&&validRoles.length!==requestedRoles.length){let disallowed=requestedRoles.filter((r)=>!userRoles.includes(r));return set2.status=403,{isSuccess:!1,message:`Cannot assign roles you do not have: ${disallowed.join(", ")}`,data:null}}if(requestedClaims.length>0&&validClaims.length!==requestedClaims.length){let disallowed=requestedClaims.filter((c)=>!userClaims.includes(c));return set2.status=403,{isSuccess:!1,message:`Cannot assign claims you do not have: ${disallowed.join(", ")}`,data:null}}if(requestedScopes.length>0){let userScopes=await getUserScopes(reqConfig,userId);if(intersectPermissions(userScopes,requestedScopes).length!==requestedScopes.length){let disallowed=requestedScopes.filter((s)=>!userScopes.includes(s));return set2.status=403,{isSuccess:!1,message:`Cannot assign scopes you do not have: ${disallowed.join(", ")}`,data:null}}}finalRoles=stripGodmin(requestedRoles.length>0?validRoles:userRoles),finalClaims=requestedClaims.length>0?validClaims:userClaims}let expiresAt=null,expiresInStr=body.expiresIn||apiKeysConfig.defaultExpiresIn;if(expiresInStr){let seconds=parseTimeToSeconds2(expiresInStr);expiresAt=new Date(Date.now()+seconds*1000)}let generated=generateApiKey(keyPrefix);try{let createdId=(await db.insert(reqApiKeysTable).values({userId,name:body.name.trim(),description:body.description?.trim()||null,keyHash:generated.keyHash,keyPreview:generated.keyPreview,ownerType:body.ownerType||"personal",applicationName:body.applicationName?.trim()||null,expiresAt,isActive:!0,usageCount:0,createdAt:new Date,updatedAt:new Date}).returning())[0]?.id;if(finalRoles.length>0||finalClaims.length>0||requestedScopes.length>0){let jsonbUpdate={};if(finalRoles.length>0)jsonbUpdate.allowedRoles=JSON.stringify(finalRoles);if(finalClaims.length>0)jsonbUpdate.allowedClaims=JSON.stringify(finalClaims);if(requestedScopes.length>0)jsonbUpdate.allowedScopes=JSON.stringify(requestedScopes);await db.update(reqApiKeysTable).set(jsonbUpdate).where(eq49(col13(reqApiKeysTable,"keyHash"),generated.keyHash))}return logger2.info("[API_KEYS] API key created",{keyId:createdId,userId,ownerType:body.ownerType||"personal",applicationName:body.applicationName,rolesCount:finalRoles.length,claimsCount:finalClaims.length,expiresAt:expiresAt?.toISOString()||"never"}),{isSuccess:!0,message:"API key created successfully. Save this key \u2014 it will not be shown again.",data:{id:createdId,key:generated.rawKey,keyPreview:generated.keyPreview,name:body.name.trim(),ownerType:body.ownerType||"personal",applicationName:body.applicationName||null,allowedRoles:finalRoles,allowedClaims:finalClaims,allowedScopes:requestedScopes,expiresAt:expiresAt?.toISOString()||null}}}catch(err){let errDetail;try{errDetail=JSON.stringify(err,Object.getOwnPropertyNames(err))}catch{errDetail=err instanceof Error?err.message:String(err)}return logger2.error("[API_KEYS] Failed to create API key",{error:errDetail,userId}),set2.status=500,{isSuccess:!1,message:"Failed to create API key",data:null}}}}),routeGetRoot=defineRoute({method:"GET",path:"/",detail:{tags:["API keys"],summary:"List the caller's API keys",description:"Every key the caller owns, revoked ones included \u2014 a revoked key is history, not "+"garbage, and it is the only place `revokedAt` / `revokedReason` can be read. Filter on `isActive` for the live set; that is also what `maxKeysPerUser` counts."},handler:async({request,set:set2})=>{let{apiKeysTable:reqApiKeysTable}=getResolvedConfig(request),userId=request.headers.get("x-user-id");if(!userId)return set2.status=401,{isSuccess:!1,message:"Unauthenticated",data:null};let items=(await db.select().from(reqApiKeysTable).where(eq49(col13(reqApiKeysTable,"userId"),userId)).orderBy(sql10`created_at DESC`)).map((row)=>sanitizeKeyForResponse(row));return{isSuccess:!0,data:{items,totalCount:items.length}}}}),routeGetId=defineRoute({method:"GET",path:"/:id",detail:{tags:["API keys"],summary:"Read one API key",description:"A REVOKED key still answers 200 here \u2014 revoking does not delete the row. Read "+"`isActive` / `revokedAt` rather than the status to decide whether the key is live. 404 means no such key, or a key belonging to somebody else."},handler:async({params,request,set:set2})=>{let{apiKeysTable:reqApiKeysTable}=getResolvedConfig(request),userId=request.headers.get("x-user-id");if(!userId)return set2.status=401,{isSuccess:!1,message:"Unauthenticated",data:null};let keyId=params.id,record3=(await db.select().from(reqApiKeysTable).where(and24(eq49(col13(reqApiKeysTable,"id"),keyId),eq49(col13(reqApiKeysTable,"userId"),userId))).limit(1))[0];if(!record3)return set2.status=404,{isSuccess:!1,message:"API key not found",data:null};return{isSuccess:!0,data:sanitizeKeyForResponse(record3)}}}),routePatchId=defineRoute({method:"PATCH",path:"/:id",handler:async({params,request,set:set2,body:rawBody})=>{let resolved=getResolvedConfig(request),reqApiKeysTable=resolved.apiKeysTable,reqConfig=resolved.config;if(preventManagement)return set2.status=403,{isSuccess:!1,message:"API key management is disabled via configuration",data:null};if(request.headers.get("x-auth-type")==="api_key")return set2.status=403,{isSuccess:!1,message:"API keys cannot be modified using another API key",data:null};let userId=request.headers.get("x-user-id");if(!userId)return set2.status=401,{isSuccess:!1,message:"Unauthenticated",data:null};let keyId=params.id,body;try{body=rawBody??{}}catch{return set2.status=400,{isSuccess:!1,message:"Invalid request body",data:null}}let existing=(await db.select().from(reqApiKeysTable).where(and24(eq49(col13(reqApiKeysTable,"id"),keyId),eq49(col13(reqApiKeysTable,"userId"),userId))).limit(1))[0];if(!existing)return set2.status=404,{isSuccess:!1,message:"API key not found",data:null};if(existing.revokedAt)return set2.status=400,{isSuccess:!1,message:"Cannot modify a revoked API key",data:null};let userRoles=await getUserRoleNames(reqConfig,userId),userClaims=await getUserClaimActions(reqConfig,userId),updatePayload={updatedAt:new Date};if(body.name!==void 0){if(typeof body.name!=="string"||body.name.trim().length===0)return set2.status=400,{isSuccess:!1,message:"Name cannot be empty",data:null};updatePayload.name=body.name.trim()}if(body.description!==void 0)updatePayload.description=body.description?.trim()||null;if(body.allowedRoles!==void 0){if(intersectPermissions(userRoles,body.allowedRoles).length!==body.allowedRoles.length){let disallowed=body.allowedRoles.filter((r)=>!userRoles.includes(r));return set2.status=403,{isSuccess:!1,message:`Cannot assign roles you do not have: ${disallowed.join(", ")}`,data:null}}updatePayload.allowedRoles=body.allowedRoles.filter((r)=>!isGodminRole(r))}if(body.allowedClaims!==void 0){if(intersectPermissions(userClaims,body.allowedClaims).length!==body.allowedClaims.length){let disallowed=body.allowedClaims.filter((c)=>!userClaims.includes(c));return set2.status=403,{isSuccess:!1,message:`Cannot assign claims you do not have: ${disallowed.join(", ")}`,data:null}}updatePayload.allowedClaims=body.allowedClaims}if(body.allowedScopes!==void 0){if(body.allowedScopes.length>0){let userScopes=await getUserScopes(reqConfig,userId);if(intersectPermissions(userScopes,body.allowedScopes).length!==body.allowedScopes.length){let disallowed=body.allowedScopes.filter((s)=>!userScopes.includes(s));return set2.status=403,{isSuccess:!1,message:`Cannot assign scopes you do not have: ${disallowed.join(", ")}`,data:null}}}updatePayload.allowedScopes=body.allowedScopes}try{await db.update(reqApiKeysTable).set(updatePayload).where(eq49(col13(reqApiKeysTable,"id"),keyId)),logger2.info("[API_KEYS] API key updated",{keyId,userId});let updated=await db.select().from(reqApiKeysTable).where(eq49(col13(reqApiKeysTable,"id"),keyId)).limit(1);return{isSuccess:!0,message:"API key updated successfully",data:sanitizeKeyForResponse(updated[0])}}catch(err){return logger2.error("[API_KEYS] Failed to update API key",{error:err,keyId,userId}),set2.status=500,{isSuccess:!1,message:"Failed to update API key",data:null}}}}),revokeApiKey=async({params,request,set:set2,body:rawBody})=>{let{apiKeysTable:reqApiKeysTable}=getResolvedConfig(request);if(request.headers.get("x-auth-type")==="api_key")return set2.status=403,{isSuccess:!1,message:"API keys cannot be revoked using another API key",data:null};let userId=request.headers.get("x-user-id");if(!userId)return set2.status=401,{isSuccess:!1,message:"Unauthenticated",data:null};let keyId=params.id,body={};try{if(rawBody)body=rawBody}catch{}let record3=(await db.select().from(reqApiKeysTable).where(and24(eq49(col13(reqApiKeysTable,"id"),keyId),eq49(col13(reqApiKeysTable,"userId"),userId))).limit(1))[0];if(!record3)return set2.status=404,{isSuccess:!1,message:"API key not found",data:null};if(record3.revokedAt)return set2.status=400,{isSuccess:!1,message:"API key is already revoked",data:null};let revokedAt=new Date,revokedReason=body.reason||"user_revoked";try{await db.update(reqApiKeysTable).set({isActive:!1,revokedAt,revokedReason,updatedAt:revokedAt}).where(eq49(col13(reqApiKeysTable,"id"),keyId)),logger2.info("[API_KEYS] API key revoked",{keyId,userId,reason:revokedReason});let revoked=(await db.select().from(reqApiKeysTable).where(eq49(col13(reqApiKeysTable,"id"),keyId)).limit(1))[0]??{...record3,isActive:!1,revokedAt,revokedReason,updatedAt:revokedAt};return{isSuccess:!0,message:"API key revoked. It can no longer authenticate. This is a revoke, not a delete \u2014 "+`the record is retained as history and still reads at GET ${basePath}/${keyId}.`,data:sanitizeKeyForResponse(revoked)}}catch(err){return logger2.error("[API_KEYS] Failed to revoke API key",{error:err,keyId,userId}),set2.status=500,{isSuccess:!1,message:"Failed to revoke API key",data:null}}},revokeDetail=(summary)=>({tags:["API keys"],summary,description:"Revokes the key: it stops authenticating immediately, on this request. It is NOT a "+"delete \u2014 the row is retained as the audit record (`revokedAt`, `revokedReason`, "+"`usageCount`, `lastUsedAt`), so `GET /:id` still answers 200 with `isActive:false` and the list still carries it. The answer carries the revoked record. Revoking an "+"already-revoked key answers 400, not 404 \u2014 the row is still there. Revoking is "+"deliberately NOT blocked by `preventApiKeyManagement`: being unable to kill a leaked key because management is off would be worse than the rule it enforces."}),routePostIdRevoke=defineRoute({method:"POST",path:"/:id/revoke",detail:revokeDetail("Revoke an API key"),handler:(ctx)=>revokeApiKey(ctx)}),routeDeleteId=defineRoute({method:"DELETE",path:"/:id",detail:revokeDetail("Revoke an API key (alias of POST /:id/revoke)"),handler:(ctx)=>revokeApiKey(ctx)});mountRoutes(app,[{prefix:basePath,routes:[routePostRoot,routeGetRoot,routeGetId,routePatchId,routePostIdRevoke,routeDeleteId]}]),logger2.info(`[API_KEYS] Routes registered at ${basePath}`)}var col13=(table,name2)=>table[name2],getUserRoleNames=async(config,userId)=>{if(!config.db||!config.userRolesTable||!config.rolesTable)return[];return(await config.db.select({name:col13(config.rolesTable,"name")}).from(config.userRolesTable).innerJoin(config.rolesTable,eq49(col13(config.rolesTable,"id"),col13(config.userRolesTable,"roleId"))).where(eq49(col13(config.userRolesTable,"userId"),userId))).map((r)=>r.name).filter((n)=>n!==void 0)},getUserClaimActions=async(config,userId)=>{if(!config.db||!config.userRolesTable||!config.roleClaimsTable||!config.claimsTable)return[];let rows=await config.db.select({action:col13(config.claimsTable,"action")}).from(config.userRolesTable).innerJoin(config.roleClaimsTable,eq49(col13(config.roleClaimsTable,"roleId"),col13(config.userRolesTable,"roleId"))).innerJoin(config.claimsTable,eq49(col13(config.claimsTable,"id"),col13(config.roleClaimsTable,"claimId"))).where(eq49(col13(config.userRolesTable,"userId"),userId)),uniqueActions=new Set(rows.map((r)=>r.action));return Array.from(uniqueActions).filter((a)=>a!==void 0)},getUserScopes=async(config,userId)=>{if(!config.db||!config.userRolesTable||!config.roleClaimsTable)return[];let rows=await config.db.select({scope:col13(config.roleClaimsTable,"scope")}).from(config.userRolesTable).innerJoin(config.roleClaimsTable,eq49(col13(config.roleClaimsTable,"roleId"),col13(config.userRolesTable,"roleId"))).where(eq49(col13(config.userRolesTable,"userId"),userId)),uniqueScopes=new Set(rows.map((r)=>r.scope).filter((s)=>s!==null&&s!==void 0&&s.length>0));return Array.from(uniqueScopes)},sanitizeKeyForResponse=(record3)=>{let{keyHash:_keyHash,...safe}=record3;return safe};var init_apiKeys=__esm(()=>{init_ApiKey();init_GodminSetup();init_routeMount();init_utils5()});function createCaptchaRoutes(config){let{captchaService,logger:logger2,basePath="/auth/captcha"}=config;if(!captchaService.isEnabled())return collectRoutes();let warnedUnattributable=!1,warnUnattributableOnce=()=>{if(warnedUnattributable)return;warnedUnattributable=!0,logger2.warn("[CAPTCHA] No client address on the request \u2014 the per-IP generate limit is OFF. "+"Every caller would otherwise share one bucket, which locks out an entire office behind one egress. Configure the proxy or ingress to forward X-Forwarded-For.")},routeGetBasepathGenerate=defineRoute({method:"GET",path:"/generate",query:GenerateCaptchaQuerySchema,detail:{tags:["Captcha"],summary:"Generate Captcha",description:"Generate a new captcha challenge"},handler:async(ctx)=>{let{type:type4,difficulty}=ctx.query,ipAddress=ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||ctx.request.headers.get("x-real-ip")?.trim()||ctx.request.headers.get("cf-connecting-ip")?.trim()||void 0;if(!ipAddress)warnUnattributableOnce();let result=await captchaService.generate(type4,difficulty,ipAddress);if(result.rateLimited)return ctx.set.status=429,{success:!1,message:result.message??"Too many requests. Please try again later."};return logger2.info("[CAPTCHA] Challenge generated via endpoint",{challengeId:result.challengeId,type:result.type,ipAddress}),{success:!0,data:{challengeId:result.challengeId,type:result.type,question:result.question,expiresAt:result.expiresAt,...result.imageData?{imageData:result.imageData}:{},...result.puzzleData?{puzzleData:{pieces:result.puzzleData.pieces}}:{}}}}}),routePostBasepathValidate=defineRoute({method:"POST",path:"/validate",body:ValidateCaptchaBodySchema,detail:{tags:["Captcha"],summary:"Validate Captcha",description:"Validate a captcha answer"},handler:async(ctx)=>{let{challengeId,answer}=ctx.body,result=await captchaService.validate(challengeId,answer);if(!result.valid)ctx.set.status=400;return result}});return collectRoutes([{prefix:basePath,routes:[routeGetBasepathGenerate,routePostBasepathValidate]}])}var GenerateCaptchaQuerySchema,ValidateCaptchaBodySchema;var init_captcha=__esm(()=>{init_esm3();init_routeCollector();GenerateCaptchaQuerySchema=Type.Object({type:Type.Optional(Type.Union([Type.Literal("math"),Type.Literal("image"),Type.Literal("puzzle"),Type.Literal("text")])),difficulty:Type.Optional(Type.Union([Type.Literal("easy"),Type.Literal("medium"),Type.Literal("hard")]))}),ValidateCaptchaBodySchema=Type.Object({challengeId:Type.String(),answer:Type.String()})});import{eq as eq50}from"drizzle-orm";function createCheckRoute(config,checkConfig){let{db,logger:logger2}=config,route=checkConfig.route||"/auth/check",routePostCheck=defineRoute({method:"POST",path:"/",detail:{tags:["Auth"],summary:"Check authorization for a given entity and method",description:"Allows consumer/resource servers to perform full claim checks (including scope) against the IDP's authorization system."},handler:async(ctx)=>{let{body,request,set:set2}=ctx,userId=request.headers.get("x-user-id");if(!userId)return set2.status=401,{isSuccess:!1,message:"Unauthenticated",data:null};if(!db)return set2.status=503,{isSuccess:!1,message:"Authorization service unavailable",data:null};let{schemaTables}=resolveAuthTablesForRequest(request,config);if(!schemaTables)return set2.status=503,{isSuccess:!1,message:"Authorization tables not available",data:null};let{entity,method,fields,relations}=body;if(!entity||!method)return set2.status=400,{isSuccess:!1,message:"entity and method are required",data:null};let result=await checkAuthorization({userId,method,entity,requestedFields:fields,requestedRelations:relations,db,schemaTables,logger:logger2,getUserData:async()=>{let usersTbl=schemaTables[AUTHORIZATION_TABLE_KEYS.users];if(!usersTbl||!db)return;let idCol=usersTbl.id;if(!idCol)return;return(await db.select().from(usersTbl).where(eq50(idCol,userId)).limit(1))[0]}});return{isSuccess:!0,message:result.authorized?"Authorized":"Forbidden",data:{authorized:result.authorized,reason:result.reason,allowedFields:result.allowedFields,allowedRelations:result.allowedRelations,scopeFilters:result.scopeFilters}}}});return collectRoutes([{prefix:route,routes:[routePostCheck]}])}var init_check=__esm(()=>{init_Authorization();init_routeCollector()});import{sql as sql11}from"drizzle-orm";function createEmailVerificationRoutes(config){let{authConfig,registerConfig,getEmailService,appName}=config,{db,logger:logger2}=authConfig;if(!registerConfig.enabled||!registerConfig.emailVerification?.enabled)return collectRoutes();let verifyRoute="/verify-email",resendRoute="/resend-verification",routeGetVerifyroute=defineRoute({method:"GET",path:"/",query:Type.Object({token:Type.String()}),detail:{tags:["Authentication"],summary:"Verify Email",description:"Verify user email address using the verification token"},handler:async(ctx)=>{let{usersTable}=resolveAuthTablesForRequest(ctx.request,authConfig);if(!db||!usersTable)return ctx.set.status=500,{success:!1,message:"Database not configured"};let token=ctx.query.token;if(!token)return ctx.set.status=400,{success:!1,message:"Verification token is required"};let users=await db.select().from(usersTable).where(sql11`email_verification_token = ${hashVerificationToken(token)}`).limit(1);if(users.length===0)return logger2.warn("[AUTH] Email verification failed - invalid token"),ctx.set.status=400,{success:!1,message:"Invalid or expired verification token"};let user=users[0],tokenExpiresAt=user.emailVerificationTokenExpiresAt;if(tokenExpiresAt&&new Date>new Date(tokenExpiresAt))return logger2.warn("[AUTH] Email verification failed - token expired",{userId:user.id,email:user.email}),ctx.set.status=400,{success:!1,message:"Verification token has expired. Please request a new one."};await db.update(usersTable).set({verifiedAt:new Date,emailVerificationToken:null,emailVerificationTokenExpiresAt:null}).where(sql11`id = ${user.id}`),logger2.info("[AUTH] Email verified successfully",{userId:user.id,email:user.email});let emailService=getEmailService?.()??null;if(registerConfig.emailVerification?.templates?.welcome?.enabled&&emailService?.isAvailable())emailService.sendWelcomeEmail(user.email,user.email.split("@")[0]||"User",appName||"Nucleus").catch((err)=>{logger2.error("[AUTH] Failed to send welcome email after verification",{email:user.email,error:err})});return{success:!0,message:"Email verified successfully. You can now log in.",data:{redirectUrl:registerConfig.emailVerification?.redirectUrl||"/login",verified:!0}}}}),routePostResendroute=defineRoute({method:"POST",path:"/",body:Type.Object({email:Type.String({format:"email"})}),detail:{tags:["Authentication"],summary:"Resend Verification Email",description:"Resend the email verification link to the user"},handler:async(ctx)=>{let{usersTable}=resolveAuthTablesForRequest(ctx.request,authConfig);if(!db||!usersTable)return ctx.set.status=500,{success:!1,message:"Database not configured"};let emailService=getEmailService?.()??null;if(!emailService?.isAvailable())return ctx.set.status=503,{success:!1,message:"Email service not available"};let{email}=ctx.body,antiEnum=authConfig.authentication?.preventUserEnumeration===!0,uniformResend={success:!0,message:"If your email is registered and not yet verified, a verification email has been sent."},users=await db.select().from(usersTable).where(sql11`email = ${email}`).limit(1);if(users.length===0)return antiEnum?uniformResend:{success:!0,message:"If your email is registered, you will receive a verification email."};let user=users[0];if(user.verifiedAt){if(antiEnum)return uniformResend;return ctx.set.status=409,{success:!1,message:"Email is already verified"}}let maxAttempts=registerConfig.emailVerification?.maxResendAttempts||3,currentAttempts=user.emailVerificationAttempts||0;if(currentAttempts>=maxAttempts){if(logger2.warn("[AUTH] Resend verification failed - max attempts reached",{email,attempts:currentAttempts}),antiEnum)return uniformResend;return ctx.set.status=429,{success:!1,message:"Maximum resend attempts reached. Please contact support.",data:{maxAttemptsReached:!0,attemptsUsed:currentAttempts,maxAttempts}}}let resendCooldown=registerConfig.emailVerification?.resendCooldown||"60s",cooldownMs=parseTimeToMs(resendCooldown),lastSentAt=user.emailVerificationSentAt;if(lastSentAt){let timeSinceLastSent=Date.now()-new Date(lastSentAt).getTime();if(timeSinceLastSent<cooldownMs){let waitSeconds=Math.ceil((cooldownMs-timeSinceLastSent)/1000);if(antiEnum)return uniformResend;return ctx.set.status=429,{success:!1,message:`Please wait ${waitSeconds} seconds before requesting another verification email.`,data:{cooldownRemaining:waitSeconds,canResendAt:new Date(Date.now()+waitSeconds*1000).toISOString()}}}}let verificationToken=generateVerificationToken(),tokenExpiresIn=registerConfig.emailVerification?.tokenExpiresIn||"24h",tokenExpiresAt=new Date(Date.now()+parseTimeToMs(tokenExpiresIn));await db.update(usersTable).set({emailVerificationToken:hashVerificationToken(verificationToken),emailVerificationTokenExpiresAt:tokenExpiresAt,emailVerificationSentAt:new Date,emailVerificationAttempts:currentAttempts+1}).where(sql11`id = ${user.id}`);let verificationLink=buildEmailActionLink({request:ctx.request,configuredUrl:registerConfig.emailVerification?.redirectUrl,path:"/verify-email",query:{token:verificationToken},allowedOrigins:authConfig.authentication?.trustedAppOrigins}),result=await emailService.sendVerificationEmail(email,email.split("@")[0]||"User",verificationLink,appName||"Nucleus");if(result.success){logger2.info("[AUTH] Verification email resent",{email});let cooldownSeconds=Math.ceil(cooldownMs/1000);if(antiEnum)return uniformResend;return{success:!0,message:"Verification email sent. Please check your inbox.",data:{cooldownSeconds,canResendAt:new Date(Date.now()+cooldownMs).toISOString(),attemptsRemaining:maxAttempts-(currentAttempts+1)}}}if(logger2.error("[AUTH] Failed to resend verification email",{email,error:result.error}),antiEnum)return uniformResend;return{success:!1,message:"Failed to send verification email. Please try again later."}}});return collectRoutes([{prefix:verifyRoute,routes:[routeGetVerifyroute]},{prefix:resendRoute,routes:[routePostResendroute]}])}var init_emailVerification=__esm(()=>{init_esm3();init_routeCollector();init_utils6()});import crypto7 from"crypto";function generateMagicToken(){return crypto7.randomBytes(32).toString("hex")}function hashToken(token,purpose){let input=purpose?`${purpose}:${token}`:token;return crypto7.createHash("sha256").update(input).digest("hex")}function parseTimeToMs3(time2){let match=time2.match(/^(\d+)(s|m|h|d)$/);if(!match?.[1]||!match[2])return 900000;let value2=parseInt(match[1],10);switch(match[2]){case"s":return value2*1000;case"m":return value2*60*1000;case"h":return value2*60*60*1000;case"d":return value2*24*60*60*1000;default:return 900000}}var TOKEN_PURPOSE;var init_utils8=__esm(()=>{TOKEN_PURPOSE={magicLink:"magic-link",invite:"invite"}});import{eq as eq51}from"drizzle-orm";function createInviteRoute(config,inviteConfig,getEmailService,storeMagicToken,appName,getMagicToken){let{db,logger:logger2}=config,route=inviteConfig.route||"/auth/invite",expiresIn=inviteConfig.tokenExpiresIn||"7d",redirectUrl=inviteConfig.redirectUrl||"";if(!inviteConfig.enabled)return collectRoutes();let declared=[],routePostRoute=defineRoute({method:"POST",path:"/",body:InviteBodySchema,detail:{tags:["Authentication"],summary:"Invite User",description:"Invite a new user by sending them an email with a magic link to set their password"},handler:async(ctx)=>{let resolved=resolveAuthTablesForRequest(ctx.request,config),{usersTable}=resolved;if(!db||!usersTable)return ctx.set.status=500,{success:!1,message:"Database not configured"};let emailService=getEmailService?.();if(!emailService?.isAvailable())return logger2.error("[AUTH] Invite requested but email service not available"),ctx.set.status=503,{success:!1,message:"Email service not available"};let{email:rawEmail}=ctx.body,email=rawEmail.toLowerCase(),antiEnum=config.authentication?.preventUserEnumeration===!0,uniformInvite={success:!0,message:"If the email address is eligible, an invitation has been sent."},existingUsers=await db.select().from(usersTable).where(eq51(usersTable.email,email)).limit(1),userId;if(existingUsers.length>0){let existingUser=existingUsers[0];if(existingUser.verifiedAt||existingUser.password){if(logger2.warn("[AUTH] Invite failed - user already verified",{email}),antiEnum)return uniformInvite;return ctx.set.status=409,{success:!1,message:"User with this email is already verified"}}userId=existingUser.id,logger2.info("[AUTH] Resending invitation to existing unverified user",{userId,email})}else{let insertValues={email,password:null,emailVerified:!1,isLocked:!1,createdAt:new Date,updatedAt:new Date},newUser=(await db.insert(usersTable).values(insertValues).returning())[0];if(!newUser)return logger2.error("[AUTH] Failed to create invited user",{email}),ctx.set.status=500,{success:!1,message:"Failed to create user"};userId=newUser.id,logger2.info("[AUTH] Invited user created",{userId,email});let inviteDefaultRoles=resolveDefaultRoleNames(config.authentication);if(inviteDefaultRoles.length>0)await assignDefaultRoles({db,userId,roleNames:inviteDefaultRoles,rolesTable:resolved.rolesTable??config.rolesTable??null,userRolesTable:resolved.userRolesTable??config.userRolesTable??null,logger:logger2})}let token=generateMagicToken(),tokenHash=hashToken(token,TOKEN_PURPOSE.invite),expiresAt=new Date(Date.now()+parseTimeToMs3(expiresIn)),reqSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0;await storeMagicToken({userId,email,tokenHash,expiresAt},reqSchemaName);let inviteLink=redirectUrl?buildEmailActionLink({request:ctx.request,configuredUrl:redirectUrl,path:extractConfiguredPath(redirectUrl,"/set-password"),query:{token,invite:"true"},allowedOrigins:config.authentication?.trustedAppOrigins}):`/set-password?token=${token}&invite=true`,sendResult=await emailService.sendInvitationEmail(email,inviteLink,appName||"Nucleus");if(!sendResult.success){if(logger2.error("[AUTH] Failed to send invitation email",{email,error:sendResult.error}),antiEnum)return uniformInvite;return{success:!0,message:"User created but failed to send invitation email",data:{id:userId,email}}}if(logger2.info("[AUTH] Invitation email sent",{email,userId}),antiEnum)return uniformInvite;return{success:!0,message:"Invitation sent successfully",data:{id:userId,email}}}});if(declared.push(routePostRoute),getMagicToken){let routePostRouteVerify=defineRoute({method:"POST",path:"/verify",body:Type.Object({token:Type.String()}),detail:{tags:["Authentication"],summary:"Verify Invite Token",description:"Validate an invite token without consuming it. Returns user info if valid."},handler:async(ctx)=>{let{token}=ctx.body;if(!token)return ctx.set.status=400,{success:!1,message:"Token is required"};let tokenHash=hashToken(token,TOKEN_PURPOSE.invite),invSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0,storedToken=await getMagicToken(tokenHash,invSchemaName);if(!storedToken)return logger2.warn("[AUTH] Invalid invite verify token"),ctx.set.status=400,{success:!1,message:"Invalid or expired token"};if(new Date>storedToken.expiresAt)return logger2.warn("[AUTH] Expired invite verify token",{email:storedToken.email}),ctx.set.status=400,{success:!1,message:"Invalid or expired token"};return logger2.info("[AUTH] Invite token verified (not consumed)",{email:storedToken.email,userId:storedToken.userId}),{success:!0,data:{userId:storedToken.userId,email:storedToken.email}}}});declared.push(routePostRouteVerify)}return collectRoutes([{prefix:route,routes:declared}])}var InviteBodySchema;var init_invite=__esm(()=>{init_esm3();init_routeCollector();init_assignDefaultRole();init_utils8();InviteBodySchema=Type.Object({email:Type.String({format:"email"})})});var exports_DeviceTrust={};__export(exports_DeviceTrust,{selectUsableDeviceRow:()=>selectUsableDeviceRow,readDeviceTokenFromCookieHeader:()=>readDeviceTokenFromCookieHeader,mintDeviceToken:()=>mintDeviceToken,isDeviceRowUsable:()=>isDeviceRowUsable,isApprovalMethod:()=>isApprovalMethod,hashDeviceToken:()=>hashDeviceToken,deviceCookieName:()=>deviceCookieName,decideDeviceApproval:()=>decideDeviceApproval,buildDeviceTokenCookie:()=>buildDeviceTokenCookie,buildClearedDeviceTokenCookie:()=>buildClearedDeviceTokenCookie,DEVICE_TOKEN_BYTES:()=>DEVICE_TOKEN_BYTES});import{createHash as createHash5,randomBytes as randomBytes6}from"crypto";function hashDeviceToken(raw){return createHash5("sha256").update(raw).digest("hex")}function mintDeviceToken(){let raw=randomBytes6(DEVICE_TOKEN_BYTES).toString("hex");return{raw,hash:hashDeviceToken(raw)}}function isDeviceRowUsable(row,now){if(!row||row.status!=="trusted")return!1;if((row.revoked_at??row.revokedAt)!=null)return!1;let expRaw=row.expires_at??row.expiresAt;if(expRaw==null)return!1;let exp=expRaw instanceof Date?expRaw:new Date(expRaw);if(Number.isNaN(exp.getTime())||exp<=now)return!1;return!0}function selectUsableDeviceRow(rows,expectedUserId,now){if(!expectedUserId)return null;for(let row of rows??[]){if((row.user_id??row.userId)!==expectedUserId)continue;if(isDeviceRowUsable(row,now))return row}return null}function buildDeviceTokenCookie(raw,opts){let prefix=opts.cookiePrefix??"",name2=`${prefix}${opts.cookieName??"device_token"}`,sameSite=opts.sameSite??"Lax",secure=prefix==="__Secure-"||prefix==="__Host-"||opts.secure!==!1,allowDomain=prefix!=="__Host-",parts=[`${name2}=${raw}`,"Path=/","HttpOnly",`SameSite=${sameSite}`];if(secure)parts.push("Secure");if(allowDomain&&opts.domain)parts.push(`Domain=${opts.domain}`);return parts.push(`Max-Age=${Math.max(0,Math.floor(opts.maxAgeSeconds))}`),parts.join("; ")}function buildClearedDeviceTokenCookie(opts){return buildDeviceTokenCookie("",{...opts,maxAgeSeconds:0})}function decideDeviceApproval(params){let NO_APPROVAL={requiresApproval:!1,deny:!1,mintToken:!1,trustImmediately:!1},TRUST_NOW={requiresApproval:!1,deny:!1,mintToken:!0,trustImmediately:!0};if(params.knownDevice)return NO_APPROVAL;if(params.isImpersonation||!isApprovalMethod(params.loginMethod,params.approvalMethods))return NO_APPROVAL;if(!params.hasValidFingerprint&&params.onUnidentifiable==="deny")return{requiresApproval:!1,deny:!0,mintToken:!1,trustImmediately:!1};if(params.trustNewDevices!==!1||params.isEmailExempt||params.isFirstEverDevice)return TRUST_NOW;return{requiresApproval:!0,deny:!1,mintToken:!0,trustImmediately:!1}}function deviceCookieName(cfg){return`${cfg?.cookiePrefix??""}${cfg?.cookieName??"device_token"}`}function readDeviceTokenFromCookieHeader(cookieHeader,cfg){if(!cookieHeader)return;let name2=deviceCookieName(cfg);for(let part of cookieHeader.split(";")){let eq52=part.indexOf("=");if(eq52<0)continue;if(part.slice(0,eq52).trim()===name2)return part.slice(eq52+1).trim()}return}function isApprovalMethod(method,approvalMethods){if(!method)return!1;for(let m of approvalMethods){if(m===method)return!0;if(m.endsWith(":*")&&method.startsWith(m.slice(0,-1)))return!0}return!1}var DEVICE_TOKEN_BYTES=32;var init_DeviceTrust=()=>{};import{createHash as createHash6}from"crypto";function hashRefreshToken(token){return createHash6("sha256").update(token).digest("hex")}function classifyRefreshToken(input){let grace=input.graceSeconds??DEFAULT_REUSE_GRACE_SECONDS;if(!input.currentHash)return{action:"adopt"};if(input.presentedHash===input.currentHash)return{action:"rotate"};if(input.previousHash&&input.presentedHash===input.previousHash){let rotatedAt=input.previousRotatedAt?Date.parse(input.previousRotatedAt):Number.NaN;if(Number.isFinite(rotatedAt)&&input.nowMs-rotatedAt<=grace*1000)return{action:"grace"};return{action:"revoke",reason:"refresh token was superseded and is being replayed outside the grace window"}}return{action:"revoke",reason:"refresh token does not match this session, current or previous"}}var DEFAULT_REUSE_GRACE_SECONDS=30;var init_reuseDetection=()=>{};async function recordIssuedRefreshToken(sessionId,refreshToken,options){if(!sessionId||!refreshToken)return;try{let result=await updateSession({sessionId,updates:{refreshTokenHash:hashRefreshToken(refreshToken),...options?.previousHash?{previousRefreshTokenHash:options.previousHash,previousRefreshTokenAt:new Date().toISOString()}:{}}});if(!result.success)logger.warn("[AUTH] Could not record the issued refresh token; reuse detection is off",{sessionId,error:result.error})}catch(error3){logger.warn("[AUTH] Could not record the issued refresh token; reuse detection is off",{sessionId,error:error3 instanceof Error?error3.message:String(error3)})}}var init_recordIssued=__esm(()=>{init_Update();init_Logger2();init_reuseDetection()});async function resolveUserIdByIdentifier(input){let value2=input.value.trim();if(value2==="")return null;for(let source of input.sources){if(!source.table||!source.column||!source.userColumn)continue;let found=(await input.lookup(source,value2)).filter((id)=>String(id??"").trim()!=="");if(found.length>1)return null;if(found.length===1)return String(found[0])}return null}function looksLikeAlternateIdentifier(value2){let trimmed=value2.trim();return trimmed!==""&&!trimmed.includes("@")}var init_identifier=()=>{};import{eq as eq52}from"drizzle-orm";async function lookupIdentifier(db,schemaTables,source,value2){let table=pickByName(schemaTables,source.table);if(!table)return[];let column=pickByName(table,source.column),userColumn=pickByName(table,source.userColumn);if(!column||!userColumn)return[];return(await db.select({userId:userColumn}).from(table).where(eq52(column,value2)).limit(2)).map((row)=>String(row.userId??""))}var init_identifierQuery=__esm(()=>{init_identifier()});var LoginBodySchema,LoginResponseSchema;var init_types12=__esm(()=>{init_esm3();LoginBodySchema=Type.Object({email:Type.String({minLength:1}),password:Type.String({minLength:1}),rememberMe:Type.Optional(Type.Boolean()),captchaId:Type.Optional(Type.String()),captchaAnswer:Type.Optional(Type.String()),deviceHint:Type.Optional(Type.String())}),LoginResponseSchema=Type.Object({success:Type.Boolean(),message:Type.Optional(Type.String()),data:Type.Optional(Type.Object({user:Type.Object({id:Type.String(),email:Type.String()}),accessToken:Type.String(),refreshToken:Type.String()}))})});import{eq as eq53}from"drizzle-orm";function createLoginRoute(config,loginConfig,signAccessToken,signRefreshToken,createSession,saveSessionToDb,cookieConfig,tokenResponseConfig,captchaService){let{db,logger:logger2}=config,log3=logger2.scoped(AUTH_LOGIN),route=loginConfig.route||"/auth/login",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};if(!loginConfig.enabled)return collectRoutes();let identifierSources=loginConfig.identifierSources??[],rememberMeAllowed=loginConfig.rememberMe!==!1,routePostRoute=defineRoute({method:"POST",path:"/",body:LoginBodySchema,detail:{tags:["Authentication"],summary:"Login",description:"Authenticate user with email and password"},handler: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,rememberMe:rememberMeRequested,captchaId,captchaAnswer,deviceHint}=ctx.body,rememberMe=rememberMeAllowed?rememberMeRequested:!1,email=rawEmail.toLowerCase(),user=(await db.select().from(usersTable).where(eq53(usersTable.email,email)).limit(1))[0];if(!user&&identifierSources.length>0&&looksLikeAlternateIdentifier(rawEmail)){let resolvedId=await resolveUserIdByIdentifier({sources:identifierSources,value:rawEmail,lookup:(source,value2)=>lookupIdentifier(db,resolved.schemaTables,source,value2)});if(resolvedId)user=(await db.select().from(usersTable).where(eq53(usersTable.id,resolvedId)).limit(1))[0]}let captchaEnabledForLogin=loginConfig.captcha?.enabled!==!1,captchaThreshold=loginConfig.captcha?.requireAfterFailures,currentFailedAttempts=user?.failedLoginAttempts||0,captchaRequired=Boolean(captchaService?.isEnabled())&&captchaEnabledForLogin&&(captchaThreshold===void 0||currentFailedAttempts>=captchaThreshold);if(captchaRequired&&captchaService){if(!captchaId||!captchaAnswer)return ctx.set.status=400,{success:!1,message:"Captcha is required",requiresCaptcha:!0};let captchaResult=await captchaService.validate(captchaId,captchaAnswer);if(!captchaResult.valid)return ctx.set.status=400,{success:!1,message:captchaResult.message||"Invalid captcha",attemptsRemaining:captchaResult.attemptsRemaining,requiresCaptcha:!0}}let reqUrl=new URL(ctx.request.url),auditIp=ctx.request.headers.get("x-client-ip")?.trim()||"unknown",auditUa=ctx.request.headers.get("user-agent")||"unknown";if(!user)return await verifyPassword(password4,await getTimingEqualizerHash()).catch(()=>!1),log3.warn("Login failed - user not found",{email}),await logger2.audit({entityName:"users",operation:"LOGIN_FAILED",summary:`Login failed: user not found (${email})`,ipAddress:auditIp,userAgent:auditUa,path:reqUrl.pathname,query:reqUrl.search}),ctx.set.status=401,{success:!1,message:"Invalid email or password",requiresCaptcha:captchaRequired};if(user.isActive===!1)return log3.warn("Login failed - account inactive",{email,userId:user.id}),ctx.set.status=403,{success:!1,message:"Account is not active"};if(user.isLocked){let lockedUntil=user.lockedUntil;if(lockedUntil&&new Date(lockedUntil)<new Date)await db.update(usersTable).set({isLocked:!1,lockedUntil:null,failedLoginAttempts:0}).where(eq53(usersTable.id,user.id)),log3.info("Account lock expired, auto-unlocked",{userId:user.id,email});else return log3.warn("Login failed - account locked",{email,userId:user.id}),await logger2.audit({entityName:"users",entityId:user.id,operation:"LOGIN_FAILED",userId:user.id,summary:`Login failed: account locked (${email})`,ipAddress:auditIp,userAgent:auditUa,path:reqUrl.pathname,query:reqUrl.search}),ctx.set.status=403,{success:!1,message:"Account is locked"}}let userCohortId=user.cohortId;if(userCohortId&&db){let cohortsTable=resolved.schemaTables?.userCohorts??resolved.schemaTables?.user_cohorts;if(cohortsTable){let cohortExpiresAt=(await db.select().from(cohortsTable).where(eq53(cohortsTable.id,userCohortId)).limit(1))[0]?.expiresAt;if(cohortExpiresAt&&new Date(cohortExpiresAt)<new Date)return log3.warn("Login failed - cohort expired",{email,userId:user.id,cohortId:userCohortId,expiresAt:cohortExpiresAt}),await logger2.audit({entityName:"users",entityId:user.id,operation:"LOGIN_FAILED",userId:user.id,summary:`Login failed: cohort expired (${email})`,ipAddress:auditIp,userAgent:auditUa,path:reqUrl.pathname,query:reqUrl.search}),ctx.set.status=403,{success:!1,message:"Your access has expired. Please contact your administrator."}}}if(!await verifyPassword(password4,user.password)){let failedAttempts=(user.failedLoginAttempts||0)+1;await db.update(usersTable).set({failedLoginAttempts:failedAttempts,isLocked:failedAttempts>=5,lockedUntil:failedAttempts>=5?new Date(Date.now()+1800000):null}).where(eq53(usersTable.id,user.id)),log3.warn("Login failed - invalid password",{email,failedAttempts}),await logger2.audit({entityName:"users",entityId:user.id,operation:"LOGIN_FAILED",userId:user.id,summary:`Login failed: invalid password (${email}, attempt ${failedAttempts})`,ipAddress:auditIp,userAgent:auditUa,path:reqUrl.pathname,query:reqUrl.search});let captchaWillBeRequired=Boolean(captchaService?.isEnabled())&&captchaEnabledForLogin&&(captchaThreshold===void 0||failedAttempts>=captchaThreshold);return ctx.set.status=401,{success:!1,message:"Invalid email or password",requiresCaptcha:captchaWillBeRequired}}await db.update(usersTable).set({failedLoginAttempts:0,lastLoginAt:new Date,loginCount:(user.loginCount||0)+1}).where(eq53(usersTable.id,user.id));let ipAddress=ctx.request.headers.get("x-client-ip")?.trim()||"127.0.0.1",isPrivateOrLoopback=(ip)=>!ip||ip==="127.0.0.1"||ip==="::1"||ip==="localhost"||ip.startsWith("10.")||ip.startsWith("192.168.")||ip.startsWith("172."),userAgent=ctx.request.headers.get("user-agent")||"Unknown Browser";log3.info("Parsed device info",{ipAddress,userAgent});let deviceInfo=parseUserAgentForLogin(userAgent,ipAddress,deviceHint,deviceContextFromRequest(ctx.request).clientHints);if(!isPrivateOrLoopback(ipAddress)){let abortCtrl=new AbortController,geoTimeout=setTimeout(()=>abortCtrl.abort(),2000);fetch(`https://ip-api.com/json/${ipAddress}?fields=country,city`,{signal:abortCtrl.signal}).then(async(res)=>{if(clearTimeout(geoTimeout),!res.ok)return;let geo=await res.json();if(geo.country)deviceInfo.locationCountry=geo.country;if(geo.city)deviceInfo.locationCity=geo.city}).catch(()=>{clearTimeout(geoTimeout)})}let userRoles=[],userClaims=[],userClaimScopes={};if(db)try{let{fetchUserRolesAndClaims:fetchUserRolesAndClaims2}=await Promise.resolve().then(() => (init_fetchUserRolesAndClaims(),exports_fetchUserRolesAndClaims)),rc=await fetchUserRolesAndClaims2(db,user.id,resolved);userRoles=rc.roles,userClaims=rc.claims,userClaimScopes=rc.claimScopes}catch(roleError){log3.error("Failed to fetch user roles/claims for JWT",{userId:user.id,error:roleError instanceof Error?roleError.message:String(roleError),stack:roleError instanceof Error?roleError.stack:void 0})}let requestOrigin=ctx.request.headers.get("origin")||ctx.request.headers.get("referer")?.replace(/\/[^/]*$/,"")||void 0,sessionParams={userId:user.id,deviceInfo,loginMethod:"password",rememberMe,requestOrigin},sessionId=await createSession(sessionParams),accessToken=signAccessToken(user.id,userRoles.length>0?userRoles:void 0,userClaims.length>0?userClaims:void 0,ctx.request.headers.get("x-tenant-schema")??void 0,userClaimScopes,sessionId),refreshToken=signRefreshToken(user.id,rememberMe,sessionId);await recordIssuedRefreshToken(sessionId,refreshToken);let requiresApproval=!1,pendingSessionId=sessionId,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);let saveResult=await saveSessionToDb(sessionId,sessionParams,reqSchemaName);if(saveResult?.denied)return log3.warn("Login blocked by device-trust (unidentifiable device)",{userId:user.id}),ctx.set.status=403,{success:!1,message:"This device could not be recognized. Please contact support."};if(deviceTokenCookie=saveResult?.deviceTokenCookie,saveResult?.requiresApproval){if(requiresApproval=!0,saveResult.sessionId)pendingSessionId=saveResult.sessionId}}if(requiresApproval){ctx.set.status=202;let approvalHeaders={"Content-Type":"application/json"};if(deviceTokenCookie)approvalHeaders["Set-Cookie"]=deviceTokenCookie;return new Response(JSON.stringify({success:!1,requiresApproval:!0,sessionId:pendingSessionId,message:"Login requires approval. Please check your email to approve this device."}),{status:202,headers:approvalHeaders})}log3.info("Login successful",{userId:user.id,email,rememberMe}),await logger2.audit({entityName:"users",entityId:user.id,operation:"LOGIN",userId:user.id,summary:`${email} logged in successfully`,ipAddress,userAgent,path:reqUrl.pathname,query:reqUrl.search});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);ctx.set.headers["x-session-id"]=sessionId;let responseData={user:{id:user.id,email:user.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(withEnvelopeFlags({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 collectRoutes([{prefix:route,routes:[routePostRoute]}])}var dummyHashPromise=null,getTimingEqualizerHash=()=>{if(!dummyHashPromise)dummyHashPromise=hashPassword("nucleus-login-timing-equalizer");return dummyHashPromise};var init_login=__esm(()=>{init_routeCollector();init_response();init_DeviceTrust();init_scopes();init_recordIssued();init_utils6();init_identifier();init_identifierQuery();init_types12();init_deviceInfo();init_utils7();init_types12()});function clearAuthCookies(cookieConfig){let path7=cookieConfig?.path||"/",sameSite=cookieConfig?.sameSite||"Strict",securePart=cookieConfig?.secure!==!1?"; Secure":"",domainPart=cookieConfig?.domain?`; Domain=${cookieConfig.domain}`:"",accessName=cookieConfig?.accessTokenName||"access_token",refreshName=cookieConfig?.refreshTokenName||"refresh_token",sessionName=cookieConfig?.sessionTokenName||"session_token",clearOpts=`; Path=${path7}; HttpOnly; SameSite=${sameSite}${securePart}${domainPart}; Max-Age=0`;return[`${accessName}=${clearOpts}`,`${refreshName}=${clearOpts}`,`${sessionName}=${clearOpts}`,...IMPERSONATION_COOKIE_NAMES.map((name2)=>`${name2}=${clearOpts}`)]}var init_utils9=__esm(()=>{init_impersonationCookies()});var LogoutResponseSchema;var init_types13=__esm(()=>{init_esm3();LogoutResponseSchema=Type.Object({success:Type.Boolean(),message:Type.Optional(Type.String())})});function createLogoutRoute(config,logoutConfig,destroySession,revokeSessionInDb,cookieConfig){let{logger:logger2}=config,route=logoutConfig.route||"/auth/logout";if(!logoutConfig.enabled)return collectRoutes();let routePostRoute=defineRoute({method:"POST",path:"/",detail:{tags:["Authentication"],summary:"Logout",description:"Logout and invalidate session"},handler:async(ctx)=>{let sessionCookieName=cookieConfig?.sessionTokenName||"session_token",fromCookie=(ctx.request.headers.get("cookie")??"").split(";").map((one)=>one.trim()).find((one)=>one.startsWith(`${sessionCookieName}=`))?.slice(sessionCookieName.length+1),sessionId=ctx.request.headers.get("x-session-id")||fromCookie||null,userId=ctx.request.headers.get("x-user-id");if(sessionId){if(await destroySession(sessionId),revokeSessionInDb){let reqSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0;await revokeSessionInDb(sessionId,"user_logout",reqSchemaName)}}let clearCookies=clearAuthCookies(cookieConfig),responseHeaders=new Headers;responseHeaders.set("Content-Type","application/json");for(let cookie of clearCookies)responseHeaders.append("Set-Cookie",cookie);logger2.info("[AUTH] Logout successful",{userId,sessionId});let reqUrl=new URL(ctx.request.url);logger2.audit({entityName:"users",entityId:userId||void 0,operation:"LOGOUT",userId:userId||void 0,summary:`User logged out${sessionId?` (session: ${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});let jsonBody=JSON.stringify(withEnvelopeFlags({success:!0,message:"Logged out successfully"}));return new Response(jsonBody,{status:200,headers:responseHeaders})}});return collectRoutes([{prefix:route,routes:[routePostRoute]}])}var init_logout=__esm(()=>{init_response();init_routeCollector();init_utils9();init_types13()});var MagicLinkRequestSchema,MagicLinkVerifySchema,MagicLinkResponseSchema,MagicLinkVerifyResponseSchema;var init_types14=__esm(()=>{init_esm3();MagicLinkRequestSchema=Type.Object({email:Type.String({format:"email"})}),MagicLinkVerifySchema=Type.Object({token:Type.String()}),MagicLinkResponseSchema=Type.Object({success:Type.Boolean(),message:Type.Optional(Type.String())}),MagicLinkVerifyResponseSchema=Type.Object({success:Type.Boolean(),message:Type.Optional(Type.String()),data:Type.Optional(Type.Object({user:Type.Object({id:Type.String(),email:Type.String()}),accessToken:Type.String(),refreshToken:Type.String()}))})});import{eq as eq54}from"drizzle-orm";function createMagicLinkRoute(config,magicLinkConfig,getEmailService,signAccessToken,signRefreshToken,createSession,storeMagicToken,getMagicToken,deleteMagicToken,appName,cookieConfig,saveSessionToDb){let{db,logger:logger2}=config,requestRoute=magicLinkConfig.route||"/auth/magic-link",verifyRoute=magicLinkConfig.verifyRoute||"/auth/magic-link/verify",expiresIn=magicLinkConfig.expiresIn||"15m",redirectUrl=magicLinkConfig.redirectUrl||"";if(!magicLinkConfig.enabled)return collectRoutes();let routePostRequestroute=defineRoute({method:"POST",path:"/",body:MagicLinkRequestSchema,detail:{tags:["Authentication"],summary:"Request Magic Link",description:"Send a magic link to the user's email for passwordless login"},handler:async(ctx)=>{let{usersTable}=resolveAuthTablesForRequest(ctx.request,config);if(!db||!usersTable)return ctx.set.status=500,{success:!1,message:"Database not configured"};let emailService=getEmailService?.();if(!emailService?.isAvailable())return logger2.error("[AUTH] Magic link requested but email service not available"),ctx.set.status=503,{success:!1,message:"Email service not available"};let{email:rawEmail}=ctx.body,email=rawEmail.toLowerCase(),user=(await db.select().from(usersTable).where(eq54(usersTable.email,email)).limit(1))[0];if(!user)return logger2.info("[AUTH] Magic link requested for non-existent email",{email}),{success:!0,message:"If an account exists, a magic link has been sent"};if(user.isLocked)return logger2.warn("[AUTH] Magic link requested for locked account",{email}),{success:!0,message:"If an account exists, a magic link has been sent"};let token=generateMagicToken(),tokenHash=hashToken(token,TOKEN_PURPOSE.magicLink),expiresAt=new Date(Date.now()+parseTimeToMs3(expiresIn)),reqSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0;await storeMagicToken({userId:user.id,email,tokenHash,expiresAt},reqSchemaName);let magicLink=redirectUrl?buildEmailActionLink({request:ctx.request,configuredUrl:redirectUrl,path:extractConfiguredPath(redirectUrl,"/verify-magic-link"),query:{token},allowedOrigins:config.authentication?.trustedAppOrigins}):`${verifyRoute}?token=${token}`,result=await emailService.sendMagicLinkEmail(email,magicLink,appName);if(!result.success)return logger2.error("[AUTH] Failed to send magic link email",{email,error:result.error}),ctx.set.status=500,{success:!1,message:result.error?`The email could not be sent: ${result.error}`:"The email could not be sent"};return logger2.info("[AUTH] Magic link sent",{email,userId:user.id}),{success:!0,message:"If an account exists, a magic link has been sent"}}}),routeGetVerifyroute=defineRoute({method:"GET",path:"/",query:MagicLinkVerifySchema,detail:{tags:["Authentication"],summary:"Verify Magic Link",description:"Verify magic link token and login user"},handler:async(ctx)=>{let{usersTable}=resolveAuthTablesForRequest(ctx.request,config);if(!db||!usersTable)return ctx.set.status=500,{success:!1,message:"Database not configured"};let token=ctx.query.token;if(!token)return ctx.set.status=400,{success:!1,message:"Token is required"};let tokenHash=hashToken(token,TOKEN_PURPOSE.magicLink),reqSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0,storedToken=await getMagicToken(tokenHash,reqSchemaName);if(!storedToken)return logger2.warn("[AUTH] Invalid magic link token"),ctx.set.status=400,{success:!1,message:"Invalid or expired token"};if(new Date>storedToken.expiresAt)return await deleteMagicToken(tokenHash,reqSchemaName),logger2.warn("[AUTH] Expired magic link token",{email:storedToken.email}),ctx.set.status=400,{success:!1,message:"Invalid or expired token"};let user=(await db.select().from(usersTable).where(eq54(usersTable.id,storedToken.userId)).limit(1))[0];if(!user)return await deleteMagicToken(tokenHash,reqSchemaName),ctx.set.status=404,{success:!1,message:"User not found"};if(user.isActive===!1)return await deleteMagicToken(tokenHash,reqSchemaName),logger2.warn("[AUTH] Magic link denied: account inactive",{email:storedToken.email}),ctx.set.status=400,{success:!1,message:"Invalid or expired token"};if(user.isLocked){let lockedUntil=user.lockedUntil;if(!(lockedUntil&&new Date(lockedUntil)<new Date))return await deleteMagicToken(tokenHash,reqSchemaName),logger2.warn("[AUTH] Magic link denied: account locked",{email:storedToken.email}),ctx.set.status=400,{success:!1,message:"Invalid or expired token"}}await deleteMagicToken(tokenHash,reqSchemaName),await db.update(usersTable).set({lastLoginAt:new Date,loginCount:(user.loginCount||0)+1,emailVerified:!0}).where(eq54(usersTable.id,user.id));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")||"",mlUserRoles=[],mlUserClaims=[],mlUserClaimScopes={};if(db)try{let resolved=resolveAuthTablesForRequest(ctx.request,config),rc=await fetchUserRolesAndClaims(db,user.id,resolved);mlUserRoles=rc.roles,mlUserClaims=rc.claims,mlUserClaimScopes=rc.claimScopes}catch{}let sessionParams={userId:user.id,deviceInfo:ensureDeviceInfo({ipAddress,userAgent},deviceContextFromRequest(ctx.request).clientHints),loginMethod:"magic_link",rememberMe:!0},sessionId=await createSession(sessionParams),accessToken=signAccessToken(user.id,mlUserRoles.length>0?mlUserRoles:void 0,mlUserClaims.length>0?mlUserClaims:void 0,ctx.request.headers.get("x-tenant-schema")??void 0,mlUserClaimScopes,sessionId),refreshToken=signRefreshToken(user.id,void 0,sessionId),requiresApproval=!1,approvalSessionId=sessionId,deviceTokenCookie;if(saveSessionToDb){let reqSchemaName2=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,reqSchemaName2);if(saveResult?.denied)return ctx.set.status=403,{success:!1,message:"This device could not be recognized. Please contact support."};if(deviceTokenCookie=saveResult?.deviceTokenCookie,saveResult?.requiresApproval){if(requiresApproval=!0,saveResult.sessionId)approvalSessionId=saveResult.sessionId}}if(requiresApproval){if(deviceTokenCookie)ctx.set.headers["Set-Cookie"]=deviceTokenCookie;return logger2.info("[AUTH] Magic link login requires device approval",{userId:user.id,email:user.email,approvalSessionId}),ctx.set.status=202,{success:!1,requiresApproval:!0,sessionId:approvalSessionId,message:"Login requires approval. Please check your email to approve this device."}}logger2.info("[AUTH] Magic link login successful",{userId:user.id,email:user.email});let 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||2592000,secure:cookieConfig?.secure??!0,httpOnly:cookieConfig?.httpOnly??!0,sameSite:cookieConfig?.sameSite||"strict",path:cookieConfig?.path||"/",domain:cookieConfig?.domain},securePart=cookies.secure?"; Secure":"",domainPart=cookies.domain?`; Domain=${cookies.domain}`:"",cookieOptions=`; Path=${cookies.path}; HttpOnly; SameSite=${cookies.sameSite}${securePart}${domainPart}`,responseHeaders=new Headers;if(responseHeaders.set("Content-Type","application/json"),responseHeaders.set("x-session-id",sessionId),responseHeaders.append("Set-Cookie",`${cookies.accessTokenName}=${accessToken}${cookieOptions}; Max-Age=${cookies.accessTokenMaxAge}`),responseHeaders.append("Set-Cookie",`${cookies.refreshTokenName}=${refreshToken}${cookieOptions}; Max-Age=${cookies.refreshTokenMaxAge}`),responseHeaders.append("Set-Cookie",`${cookies.sessionTokenName}=${sessionId}${cookieOptions}; Max-Age=${cookies.sessionTokenMaxAge}`),deviceTokenCookie)responseHeaders.append("Set-Cookie",deviceTokenCookie);let jsonBody=JSON.stringify(withEnvelopeFlags({success:!0,data:{user:{id:user.id,email:user.email},accessToken,refreshToken,sessionId}}));return new Response(jsonBody,{status:200,headers:responseHeaders})}});return collectRoutes([{prefix:requestRoute,routes:[routePostRequestroute]},{prefix:verifyRoute,routes:[routeGetVerifyroute]}])}var init_magicLink=__esm(()=>{init_DeviceTrust();init_response();init_routeCollector();init_fetchUserRolesAndClaims();init_deviceInfo();init_types14();init_utils8();init_types14()});import{eq as eq55}from"drizzle-orm";function createMeRoute(config,meConfig,_schemaTables,_schemaRelations,_databaseUrl){let{db,logger:logger2}=config,route=meConfig.route||"/auth/me";if(!meConfig.enabled)return collectRoutes();let routeGetRoute=defineRoute({method:"GET",path:"/",detail:{tags:["Authentication"],summary:"Get current user",description:"Get the currently authenticated user with profile, addresses, phones and files"},handler:async(ctx)=>{let resolved=resolveAuthTablesForRequest(ctx.request,config),usersTable=resolved.usersTable,reqSchemaTables=resolved.schemaTables;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:"Unauthorized"};let user=(await db.select().from(usersTable).where(eq55(usersTable.id,userId)).limit(1))[0];if(!user)return ctx.set.status=404,{success:!1,message:"User not found"};let{password:rawPassword,...restUser}=user,hasPassword=typeof rawPassword==="string"&&rawPassword.length>0,safeUser={...stripUnclaimedSensitive(redactSensitiveOutput(restUser)),hasPassword},profile=null,addresses=[],phones=[],files=[],roles=[],claims=[];if(meConfig.includeProfile&&reqSchemaTables){let profilesTable=reqSchemaTables.profiles;if(profilesTable&&db)profile=(await db.select().from(profilesTable).where(eq55(profilesTable.userId,userId)).limit(1))[0]||null}if(meConfig.includeAddresses&&reqSchemaTables){let addressesTable=reqSchemaTables.addresses;if(addressesTable&&db)addresses=await db.select().from(addressesTable).where(eq55(addressesTable.ownerId,userId))}if(meConfig.includePhones&&reqSchemaTables){let phonesTable=reqSchemaTables.phones;if(phonesTable&&db)phones=await db.select().from(phonesTable).where(eq55(phonesTable.ownerId,userId))}if(meConfig.includeFiles&&reqSchemaTables){let filesTable=reqSchemaTables.files;if(filesTable&&db)files=await db.select().from(filesTable).where(eq55(filesTable.uploadedBy,userId))}if(meConfig.includeRoles&&reqSchemaTables){let{userRoles:userRolesTable,roles:rolesTable}=reqSchemaTables;if(userRolesTable&&rolesTable&&db){let roleIds=(await db.select().from(userRolesTable).where(eq55(userRolesTable.userId,userId))).map((ur)=>ur.roleId);if(roleIds.length>0){let{inArray:inArray19}=await import("drizzle-orm");roles=await db.select().from(rolesTable).where(inArray19(rolesTable.id,roleIds))}}}if(meConfig.includeClaims&&reqSchemaTables){let{userRoles:userRolesTable,roleClaims:roleClaimsTable,claims:claimsTable}=reqSchemaTables;if(userRolesTable&&roleClaimsTable&&claimsTable&&db){let urCols=userRolesTable,rcCols=roleClaimsTable,cCols=claimsTable,roleIds=(await db.select({roleId:urCols.roleId}).from(userRolesTable).where(eq55(urCols.userId,userId))).map((r)=>r.roleId);if(roleIds.length>0){let{inArray:inArray19}=await import("drizzle-orm"),claimRows=await db.select({action:cCols.action,scope:rcCols.scope}).from(roleClaimsTable).innerJoin(claimsTable,eq55(rcCols.claimId,cCols.id)).where(inArray19(rcCols.roleId,roleIds)),seen=new Set;for(let row of claimRows){let r=row,key2=`${r.action}|${r.scope??""}`;if(!seen.has(key2))seen.add(key2),claims.push({action:r.action,scope:r.scope??null})}}}}logger2.debug("[AUTH] Me endpoint accessed",{userId});let responseData={success:!0,data:{user:safeUser,profile:stripUnclaimedSensitive(profile),addresses:stripUnclaimedSensitive(addresses),phones:stripUnclaimedSensitive(phones),files:stripUnclaimedSensitive(files),roles:stripUnclaimedSensitive(roles),claims}};return JSON.parse(JSON.stringify(responseData,(_key,value2)=>typeof value2==="bigint"?Number(value2):value2))}});return collectRoutes([{prefix:route,routes:[routeGetRoute]}])}var init_me=__esm(()=>{init_routeCollector();init_utils5();init_sensitiveColumns()});var exports_accountState={};__export(exports_accountState,{evaluateAccountState:()=>evaluateAccountState});function evaluateAccountState(user,now=new Date){if(user.isActive===!1)return{allowed:!1,reason:"inactive",message:"Account is not active"};if(user.isLocked===!0){let raw=user.lockedUntil,lockedUntil=raw instanceof Date?raw:raw?new Date(raw):null;if(!lockedUntil||Number.isNaN(lockedUntil.getTime())||lockedUntil>now)return{allowed:!1,reason:"locked",message:"Account is locked"}}return{allowed:!0}}import{eq as eq56}from"drizzle-orm";async function applyProfileSync(args){let{usersTable,profilesTable,userId,rawProfile,config,isNewUser,logger:logger2}=args,db=args.db;try{if(config.enabled===!1)return;if(!isNewUser&&config.syncOnEveryLogin===!1)return;if(!config.mappings||config.mappings.length===0)return;let userIdCol=usersTable.id,currentUser=(await db.select().from(usersTable).where(eq56(userIdCol,userId)).limit(1))[0]??{},currentProfile={},profileExists=!1;if(profilesTable){let profileUserIdCol=profilesTable.userId,profileRows=await db.select().from(profilesTable).where(eq56(profileUserIdCol,userId)).limit(1);if(profileRows[0])currentProfile=profileRows[0],profileExists=!0}let{users,profiles,errors:errors2}=resolveProfileSyncUpdates({mappings:config.mappings,rawProfile,currentUser,currentProfile});for(let message of errors2)logger2.warn(`[OAUTH] profileSync: ${message}`);if(Object.keys(users).length>0)await db.update(usersTable).set({...users,updatedAt:new Date}).where(eq56(userIdCol,userId)),logger2.info("[OAUTH] profileSync updated users",{userId,columns:Object.keys(users)});if(profilesTable&&Object.keys(profiles).length>0){if(profileExists){let profileUserIdCol=profilesTable.userId;await db.update(profilesTable).set({...profiles,updatedAt:new Date}).where(eq56(profileUserIdCol,userId))}else if(config.createProfileIfMissing!==!1){let now=new Date;await db.insert(profilesTable).values({userId,firstName:rawProfile.givenName??"",lastName:rawProfile.surname??"",...profiles,createdAt:now,updatedAt:now})}else return;logger2.info("[OAUTH] profileSync updated profiles",{userId,columns:Object.keys(profiles),created:!profileExists})}}catch(err){logger2.warn("[OAUTH] profileSync failed \u2014 login continues",{userId,error:err instanceof Error?err.message:String(err)})}}var init_applyProfileSync=()=>{};import{and as and25,eq as eq57}from"drizzle-orm";function buildOAuthStateCookie(value2,opts){let securePart=opts.secure?"; Secure":"",domainPart=opts.domain?`; Domain=${opts.domain}`:"";return`${OAUTH_STATE_COOKIE}=${value2}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${opts.maxAge}${securePart}${domainPart}`}function createOAuthRoutes(config,oauthService,signAccessToken,signRefreshToken,createSession,saveSessionToDb,cookieConfig,tokenResponseConfig,getEmailService,storeMagicToken,appName){let{db,logger:logger2}=config,basePath=oauthService.basePath;for(let provider of oauthService.getEnabledProviders()){let p=oauthService.config.providers[provider];logger2.info("[OAUTH] Provider loaded",{provider,clientIdSuffix:p?.clientId?p.clientId.slice(-4):"none",tenantId:p?.tenantId??"common",redirectUri:p?.redirectUri,scopes:p?.scopes})}let 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||"lax",path:cookieConfig?.path||"/",domain:cookieConfig?.domain},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}},routeGetProvider=defineRoute({method:"GET",path:"/:provider",detail:{tags:["Authentication"],summary:"OAuth Redirect",description:"Redirects to the OAuth provider authorization page"},handler: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`};let redirectUrl=ctx.query.redirect_url,authUrl=await oauthService.buildAuthorizationUrl(provider,void 0,redirectUrl);logger2.debug("[OAUTH] Redirecting to provider",{provider,authUrl:authUrl.replace(/(client_id=)[^&]+/,"$1***")});let 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}}),routeGetProviderCallback=defineRoute({method:"GET",path:"/:provider/callback",detail:{tags:["Authentication"],summary:"OAuth Callback",description:"Handles the OAuth provider callback, creates session and sets cookies"},handler:async(ctx)=>{let provider=ctx.params.provider,query=ctx.query,{code,state,error:error3,error_description}=query,portalOrigin=resolveAppOrigin(ctx.request,oauthService.successRedirectUrl,config.authentication?.trustedAppOrigins),errorRedirect=absolutizeRedirect(oauthService.errorRedirectUrl,portalOrigin);if(error3)return logger2.warn("[OAUTH] Provider returned error",{provider,error:error3,error_description}),ctx.set.status=302,ctx.set.headers.Location=`${errorRedirect}?error=${encodeURIComponent(error_description||error3)}`,null;if(!code||!state)return ctx.set.status=302,ctx.set.headers.Location=`${errorRedirect}?error=missing_code_or_state`,null;let statePayload=await oauthService.consumeState(state);if(!statePayload)return logger2.warn("[OAUTH] Invalid or expired state",{provider,state}),ctx.set.status=302,ctx.set.headers.Location=`${errorRedirect}?error=invalid_state`,null;let cookieState=readCookie(ctx.request,OAUTH_STATE_COOKIE),clearStateCookie=buildOAuthStateCookie("",{maxAge:0,secure:cookies.secure,domain:cookies.domain});if(!cookieState||cookieState!==state)return logger2.warn("[OAUTH] State cookie missing or mismatched (possible CSRF)",{provider}),ctx.set.status=302,ctx.set.headers["Set-Cookie"]=clearStateCookie,ctx.set.headers.Location=`${errorRedirect}?error=invalid_state`,null;if(statePayload.provider!==provider)return logger2.warn("[OAUTH] State provider mismatch",{expected:statePayload.provider,got:provider}),ctx.set.status=302,ctx.set.headers.Location=`${errorRedirect}?error=provider_mismatch`,null;let resolved=resolveAuthTablesForRequest(ctx.request,config),usersTable=resolved.usersTable,oauthTable=resolved.oauthAccountsTable??config.oauthAccountsTable;if(!db||!usersTable)return ctx.set.status=302,ctx.set.headers.Location=`${errorRedirect}?error=server_error`,null;logger2.debug("[OAUTH] Exchanging code",{provider,codeLength:code.length,statePresent:!!state});let callbackResult;try{callbackResult=await oauthService.exchangeCode(provider,code)}catch(err){let e=err;return logger2.error("[OAUTH] Code exchange failed",err,{provider,errorMessage:e?.message,errorName:e?.name,errorCauseCode:e?.cause?.code,errorCauseMessage:e?.cause?.message}),ctx.set.status=302,ctx.set.headers.Location=`${errorRedirect}?error=token_exchange_failed`,null}let{profile,tokens}=callbackResult,userId=null;if(statePayload.linkUserId&&oauthService.allowAccountLinking){if(userId=statePayload.linkUserId,oauthTable){let existing=await db.select().from(oauthTable).where(and25(eq57(oauthTable.provider,provider),eq57(oauthTable.providerAccountId,profile.providerAccountId))).limit(1);if(existing.length>0){let existingRecord=existing[0];if(existingRecord.userId!==userId)return ctx.set.status=302,ctx.set.headers.Location=`${errorRedirect}?error=account_already_linked_to_another_user`,null;await db.update(oauthTable).set({accessToken:tokens.accessToken,refreshToken:tokens.refreshToken??null,tokenExpiresAt:tokens.expiresAt??null,scope:tokens.scope??null,rawProfile:profile.rawProfile,lastUsedAt:new Date}).where(eq57(oauthTable.id,existingRecord.id))}else await db.insert(oauthTable).values({userId,provider,providerAccountId:profile.providerAccountId,providerEmail:profile.email??null,providerName:profile.name??null,providerAvatarUrl:profile.avatarUrl??null,accessToken:tokens.accessToken,refreshToken:tokens.refreshToken??null,tokenExpiresAt:tokens.expiresAt??null,scope:tokens.scope??null,rawProfile:profile.rawProfile,isPrimary:!1,lastUsedAt:new Date})}let linkSyncConfig=oauthService.profileSync;if(linkSyncConfig){let linkSyncTables=resolveAuthTablesForRequest(ctx.request,config);await applyProfileSync({db,usersTable,profilesTable:linkSyncTables.schemaTables?.profiles,userId,rawProfile:profile.rawProfile,config:linkSyncConfig,isNewUser:!1,logger:logger2})}logger2.info("[OAUTH] Account linked successfully",{userId,provider});let successUrl2=absolutizeRedirect(resolveSafeOAuthRedirect(statePayload.redirectUrl,oauthService.successRedirectUrl,config.authentication?.trustedAppOrigins),portalOrigin);return ctx.set.status=302,ctx.set.headers.Location=`${successUrl2}?linked=true&provider=${provider}`,null}if(oauthTable){let existingOAuth=await db.select().from(oauthTable).where(and25(eq57(oauthTable.provider,provider),eq57(oauthTable.providerAccountId,profile.providerAccountId))).limit(1);if(existingOAuth.length>0){let oauthRecord=existingOAuth[0];userId=oauthRecord.userId,await db.update(oauthTable).set({accessToken:tokens.accessToken,refreshToken:tokens.refreshToken??null,tokenExpiresAt:tokens.expiresAt??null,scope:tokens.scope??null,rawProfile:profile.rawProfile,lastUsedAt:new Date}).where(eq57(oauthTable.id,oauthRecord.id))}}if(!userId&&profile.email){let normalizedOAuthEmail=profile.email.toLowerCase(),existingUsers=await db.select().from(usersTable).where(eq57(usersTable.email,normalizedOAuthEmail)).limit(1);if(existingUsers.length>0&&profile.emailVerified!==!0)return logger2.warn("[OAUTH] Refusing account merge: provider email not verified",{provider,email:profile.email}),ctx.set.status=302,ctx.set.headers.Location=`${errorRedirect}?error=email_not_verified`,null;if(existingUsers.length>0){if(userId=existingUsers[0].id,oauthTable)await db.insert(oauthTable).values({userId,provider,providerAccountId:profile.providerAccountId,providerEmail:profile.email??null,providerName:profile.name??null,providerAvatarUrl:profile.avatarUrl??null,accessToken:tokens.accessToken,refreshToken:tokens.refreshToken??null,tokenExpiresAt:tokens.expiresAt??null,scope:tokens.scope??null,rawProfile:profile.rawProfile,isPrimary:!1,lastUsedAt:new Date}).onConflictDoNothing();logger2.info("[OAUTH] Merged OAuth account with existing user by email",{userId,provider,email:profile.email})}}let createdNewUser=!1;if(!userId){if(!oauthService.autoCreateUser)return ctx.set.status=302,ctx.set.headers.Location=`${errorRedirect}?error=user_not_found`,null;let userEmail=(profile.email??`${provider}_${profile.providerAccountId}@oauth.local`).toLowerCase(),providerEmailVerified=profile.emailVerified===!0;if(userId=(await db.insert(usersTable).values({email:userEmail,password:null,emailVerified:providerEmailVerified,verifiedAt:providerEmailVerified?new Date:null,isActive:!0}).returning())[0].id,oauthTable)await db.insert(oauthTable).values({userId,provider,providerAccountId:profile.providerAccountId,providerEmail:profile.email??null,providerName:profile.name??null,providerAvatarUrl:profile.avatarUrl??null,accessToken:tokens.accessToken,refreshToken:tokens.refreshToken??null,tokenExpiresAt:tokens.expiresAt??null,scope:tokens.scope??null,rawProfile:profile.rawProfile,isPrimary:!0,lastUsedAt:new Date});if(createdNewUser=!0,logger2.info("[OAUTH] Created new user via OAuth",{userId,provider,email:profile.email}),oauthService.sendInviteOnCreate&&profile.email&&getEmailService?.()?.isAvailable()&&storeMagicToken)try{let rawToken=generateMagicToken(),tokenHash=hashToken(rawToken,TOKEN_PURPOSE.invite),expiresAt=new Date(Date.now()+parseTimeToMs3("7d")),oauthSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0;await storeMagicToken({userId,email:profile.email,tokenHash,expiresAt},oauthSchemaName);let inviteLink=buildEmailActionLink({request:ctx.request,configuredUrl:oauthService.successRedirectUrl,path:"/set-password",query:{token:rawToken},allowedOrigins:config.authentication?.trustedAppOrigins});await getEmailService?.()?.sendEmail({to:profile.email,subject:`Welcome to ${appName??"the platform"} \u2014 Set your password`,html:`
448
448
  <p>Hi${profile.name?` ${profile.name}`:""},</p>
449
449
  <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>
450
450
  <p><a href="${inviteLink}">Set your password</a></p>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nucleus-core-ts",
3
- "version": "0.10.99",
3
+ "version": "0.10.101",
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",