nucleus-core-ts 0.9.962 → 0.9.964
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/.build-ok
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.9.
|
|
1
|
+
0.9.964
|
package/dist/index.js
CHANGED
|
@@ -84,7 +84,7 @@ and limitations under the License.
|
|
|
84
84
|
AND state = 'active'
|
|
85
85
|
AND query_start IS NOT NULL
|
|
86
86
|
AND now() - query_start > interval '${thresholdMs} milliseconds'`);result.slowQueries=this.toNumber(rows[0]?.slow)}catch{}return result}parseThresholdMs(threshold){let match=threshold.match(/^(\d+)(ms|s|m)?$/);if(!match||!match[1])return 100;let value=Number.parseInt(match[1],10);switch(match[2]){case"s":return value*1000;case"m":return value*60*1000;default:return value}}}class RedisCollector{getClient;constructor(getClient){this.getClient=getClient}async collect(){let client=this.getClient();if(!client)return null;try{let info=await client.info(),fields=this.parseInfo(info),hits=this.toNumber(fields.keyspace_hits),misses=this.toNumber(fields.keyspace_misses),totalLookups=hits+misses;return{memory:{used:this.toNumber(fields.used_memory),peak:this.toNumber(fields.used_memory_peak)},connections:{connected:this.toNumber(fields.connected_clients)},operations:{totalCommands:this.toNumber(fields.total_commands_processed),opsPerSec:this.toNumber(fields.instantaneous_ops_per_sec)},hitRate:totalLookups>0?Math.round(hits/totalLookups*100*100)/100:0}}catch{return null}}parseInfo(info){let fields={};for(let line of info.split(`
|
|
87
|
-
`)){let trimmed=line.trim();if(!trimmed||trimmed.startsWith("#"))continue;let separatorIndex=trimmed.indexOf(":");if(separatorIndex===-1)continue;let key=trimmed.slice(0,separatorIndex),value=trimmed.slice(separatorIndex+1);fields[key]=value}return fields}toNumber(value){if(!value)return 0;let num=Number.parseFloat(value);return Number.isNaN(num)?0:num}}function parseCgroupLimit(raw){if(!raw)return null;let trimmed=raw.trim();if(trimmed===""||trimmed==="max")return null;let value=Number(trimmed);if(!Number.isFinite(value)||value<=0)return null;if(value>=1152921504606847000)return null;return value}function runtimeMemoryAdvice(input){let{cgroupLimit,hostTotal,forcedRamSize}=input;if(cgroupLimit===null||cgroupLimit<=0)return null;if(cgroupLimit>=hostTotal)return null;
|
|
87
|
+
`)){let trimmed=line.trim();if(!trimmed||trimmed.startsWith("#"))continue;let separatorIndex=trimmed.indexOf(":");if(separatorIndex===-1)continue;let key=trimmed.slice(0,separatorIndex),value=trimmed.slice(separatorIndex+1);fields[key]=value}return fields}toNumber(value){if(!value)return 0;let num=Number.parseFloat(value);return Number.isNaN(num)?0:num}}function parseCgroupLimit(raw){if(!raw)return null;let trimmed=raw.trim();if(trimmed===""||trimmed==="max")return null;let value=Number(trimmed);if(!Number.isFinite(value)||value<=0)return null;if(value>=1152921504606847000)return null;return value}function runtimeMemoryAdvice(input){let{cgroupLimit,hostTotal,forcedRamSize}=input;if(cgroupLimit===null||cgroupLimit<=0)return null;if(cgroupLimit>=hostTotal)return null;return{cgroupLimit,hostTotal}}function parseCgroupUsage(raw){if(!raw)return null;let value=Number(raw.trim());return Number.isFinite(value)&&value>=0?value:null}function parseInactiveFile(raw){if(!raw)return 0;for(let line of raw.split(`
|
|
88
88
|
`)){let[key,value]=line.trim().split(/\s+/);if(key==="inactive_file"||key==="total_inactive_file"){let parsed=Number(value);if(Number.isFinite(parsed)&&parsed>=0)return parsed}}return 0}function parseMemAvailable(raw){if(!raw)return null;for(let line of raw.split(`
|
|
89
89
|
`)){let match=line.match(/^MemAvailable:\s+(\d+)\s+kB/);if(match?.[1])return Number(match[1])*1024}return null}function percent(used,total){if(!Number.isFinite(total)||total<=0)return 0;return Math.round(used/total*100*100)/100}function resolveMemory(sources){let{limit,current,inactiveFile,hostTotal,hostAvailable,hostFree,process:process2}=sources;if(limit!==null&¤t!==null&&limit<hostTotal&&limit!==null&¤t!==null){let workingSet=Math.max(0,current-inactiveFile);return{total:limit,used:workingSet,free:Math.max(0,limit-workingSet),usagePercent:percent(workingSet,limit),rss:process2.rss,heapUsed:process2.heapUsed,heapTotal:process2.heapTotal,external:process2.external,arrayBuffers:process2.arrayBuffers,scope:"container"}}let free=hostAvailable??hostFree,used=Math.max(0,hostTotal-free);return{total:hostTotal,used,free,usagePercent:percent(used,hostTotal),rss:process2.rss,heapUsed:process2.heapUsed,heapTotal:process2.heapTotal,external:process2.external,arrayBuffers:process2.arrayBuffers,scope:"host"}}import*as fs3 from"fs";import*as os from"os";function readCgroupLimitBytes(){let read=(path2)=>{try{return fs3.readFileSync(path2,"utf-8")}catch{return null}};return parseCgroupLimit(read("/sys/fs/cgroup/memory.max")??read("/sys/fs/cgroup/memory/memory.limit_in_bytes"))}function readRuntimeMemoryAdvice(){let read=(path2)=>{try{return fs3.readFileSync(path2,"utf-8")}catch{return null}},limitRaw=read("/sys/fs/cgroup/memory.max")??read("/sys/fs/cgroup/memory/memory.limit_in_bytes"),forced=Number(process.env.BUN_JSC_forceRAMSize);return runtimeMemoryAdvice({cgroupLimit:parseCgroupLimit(limitRaw),hostTotal:os.totalmem(),forcedRamSize:Number.isFinite(forced)&&forced>0?forced:null})}class SystemCollector{config;lastCpuInfo=null;constructor(config){this.config=config}async collect(){if(!this.config?.enabled)return null;let metrics={cpu:{usage:0,cores:0},memory:{total:0,used:0,free:0,usagePercent:0,rss:0,heapUsed:0,heapTotal:0,external:0,arrayBuffers:0,scope:"host"},disk:{total:0,used:0,free:0,usagePercent:0},network:{bytesIn:0,bytesOut:0},process:{uptime:0,pid:0,eventLoopLag:0}};if(this.config.metrics?.cpu!==!1)metrics.cpu=this.collectCpu();if(this.config.metrics?.memory!==!1)metrics.memory=this.collectMemory();if(this.config.metrics?.disk!==!1)metrics.disk=await this.collectDisk();if(this.config.metrics?.network)metrics.network=this.collectNetwork();if(this.config.metrics?.process!==!1)metrics.process=await this.collectProcess();return metrics}collectCpu(){let cpus2=os.cpus(),cores=cpus2.length,idle=0,total=0;for(let cpu of cpus2)idle+=cpu.times.idle,total+=cpu.times.user+cpu.times.nice+cpu.times.sys+cpu.times.idle+cpu.times.irq;let usage=0;if(this.lastCpuInfo){let idleDiff=idle-this.lastCpuInfo.idle,totalDiff=total-this.lastCpuInfo.total;usage=totalDiff>0?Math.round((1-idleDiff/totalDiff)*100*100)/100:0}return this.lastCpuInfo={idle,total},{usage,cores}}read(path2){try{return fs3.readFileSync(path2,"utf-8")}catch{return null}}collectMemory(){let memUsage=process.memoryUsage(),limitRaw=this.read("/sys/fs/cgroup/memory.max")??this.read("/sys/fs/cgroup/memory/memory.limit_in_bytes"),usageRaw=this.read("/sys/fs/cgroup/memory.current")??this.read("/sys/fs/cgroup/memory/memory.usage_in_bytes"),statRaw=this.read("/sys/fs/cgroup/memory.stat")??this.read("/sys/fs/cgroup/memory/memory.stat");return resolveMemory({limit:parseCgroupLimit(limitRaw),current:parseCgroupUsage(usageRaw),inactiveFile:parseInactiveFile(statRaw),hostTotal:os.totalmem(),hostAvailable:parseMemAvailable(this.read("/proc/meminfo")),hostFree:os.freemem(),process:{rss:memUsage.rss,heapUsed:memUsage.heapUsed,heapTotal:memUsage.heapTotal,external:memUsage.external,arrayBuffers:memUsage.arrayBuffers}})}async collectDisk(){try{let stats=fs3.statfsSync("/"),total=stats.blocks*stats.bsize,free=stats.bfree*stats.bsize,used=total-free,usagePercent=Math.round(used/total*100*100)/100;return{total,used,free,usagePercent}}catch{return{total:0,used:0,free:0,usagePercent:0}}}collectNetwork(){try{let content=fs3.readFileSync("/proc/net/dev","utf-8"),bytesIn=0,bytesOut=0;for(let line of content.split(`
|
|
90
90
|
`).slice(2)){let[namePart,dataPart]=line.split(":");if(!namePart||!dataPart)continue;if(namePart.trim()==="lo")continue;let fields=dataPart.trim().split(/\s+/);bytesIn+=Number.parseInt(fields[0]??"0",10)||0,bytesOut+=Number.parseInt(fields[8]??"0",10)||0}return{bytesIn,bytesOut}}catch{return{bytesIn:0,bytesOut:0}}}async collectProcess(){let uptime=process.uptime(),pid=process.pid,lagStart=Date.now(),eventLoopLag=await new Promise((resolve)=>{setImmediate(()=>{resolve(Date.now()-lagStart)})});return{uptime,pid,eventLoopLag}}}var init_SystemCollector=()=>{};function peakContext(snapshot){let app=snapshot.application,busiest=Object.entries(app?.requests?.byEndpoint??{}).sort((a,b)=>b[1]-a[1])[0],tags={};if(busiest)tags.busiestEndpoint=busiest[0],tags.busiestEndpointCount=String(busiest[1]);if(app?.requests?.perMinute!==void 0)tags.requestsPerMinute=String(app.requests.perMinute);if(app?.responseTime?.p95!==void 0)tags.p95=String(app.responseTime.p95);if(app?.errors?.rate!==void 0)tags.errorRate=String(app.errors.rate);if(snapshot.system?.cpu?.usage!==void 0)tags.cpu=String(snapshot.system.cpu.usage);if(snapshot.system?.memory?.usagePercent!==void 0)tags.memory=String(snapshot.system.memory.usagePercent);if(snapshot.database?.connections?.active!==void 0)tags.dbConnections=String(snapshot.database.connections.active);return tags}function newPeaks(snapshot,previous,timestamp){let peaks=new Map(previous),points=[],tags=peakContext(snapshot);for(let watched of WATCHED_PEAKS){let value=watched.of(snapshot);if(value===void 0||!Number.isFinite(value))continue;let before=peaks.get(watched.name);if(before!==void 0&&value<=before)continue;peaks.set(watched.name,value),points.push({timestamp,metricType:"peak",metricName:watched.name,value,tags})}return{points,peaks}}var WATCHED_PEAKS;var init_peaks=__esm(()=>{WATCHED_PEAKS=[{name:"requests.perMinute",of:(s)=>s.application?.requests?.perMinute},{name:"responseTime.p95",of:(s)=>s.application?.responseTime?.p95},{name:"errors.rate",of:(s)=>s.application?.errors?.rate},{name:"cpu.usage",of:(s)=>s.system?.cpu?.usage},{name:"memory.usagePercent",of:(s)=>s.system?.memory?.usagePercent},{name:"database.connections.active",of:(s)=>s.database?.connections?.active}]});import{randomUUID}from"crypto";import*as os2 from"os";class LiveMonitoringService{store;memoryInterval=null;cpuInterval=null;lastCpuInfo=null;isRunning=!1;constructor(config){let merged={...DEFAULT_LIVE_CONFIG,...config};this.store={requests:[],configs:{logMemory:merged.logMemory,logCpu:merged.logCpu,logDapr:merged.logDapr,logWebSocket:merged.logWebSocket,cpuLogInterval:merged.cpuLogInterval,memoryLogInterval:merged.memoryLogInterval},logs:{memory:[],cpu:[],dapr:[],ws:[]},logLimits:{memory:merged.memoryLogLimit,cpu:merged.cpuLogLimit,dapr:merged.daprLogLimit,ws:merged.wsLogLimit,request:merged.requestLogLimit},worker:{pid:process.pid,workerId:null,memory:null,cpu:null,updatedAt:Date.now()},allWorkers:[],daprEvents:[],wsEvents:[]}}start(){if(this.isRunning)return;if(this.isRunning=!0,this.store.configs.logMemory)this.startMemoryCollector();if(this.store.configs.logCpu)this.startCpuCollector()}stop(){if(!this.isRunning)return;if(this.isRunning=!1,this.memoryInterval)clearInterval(this.memoryInterval),this.memoryInterval=null;if(this.cpuInterval)clearInterval(this.cpuInterval),this.cpuInterval=null}startMemoryCollector(){if(this.memoryInterval)clearInterval(this.memoryInterval);let collect=()=>{if(!this.store.configs.logMemory)return;let mem=process.memoryUsage(),entry={timestamp:Date.now(),rss:mem.rss,heapUsed:mem.heapUsed,heapTotal:mem.heapTotal};if(this.store.logs.memory.push(entry),this.store.logs.memory.length>this.store.logLimits.memory*2)this.store.logs.memory=this.store.logs.memory.slice(-this.store.logLimits.memory);this.store.worker.memory=entry,this.store.worker.updatedAt=Date.now()};collect(),this.memoryInterval=setInterval(collect,this.store.configs.memoryLogInterval)}startCpuCollector(){if(this.cpuInterval)clearInterval(this.cpuInterval);let collect=()=>{if(!this.store.configs.logCpu)return;let cpus3=os2.cpus(),userTime=0,sysTime=0,idle=0,total=0;for(let cpu of cpus3)userTime+=cpu.times.user,sysTime+=cpu.times.sys,idle+=cpu.times.idle,total+=cpu.times.user+cpu.times.nice+cpu.times.sys+cpu.times.idle+cpu.times.irq;let userPercent=0,sysPercent=0;if(this.lastCpuInfo){let totalDiff=total-this.lastCpuInfo.total,idleDiff=idle-this.lastCpuInfo.idle;if(totalDiff>0){let activeDiff=totalDiff-idleDiff;userPercent=Math.round((userTime-0)/(activeDiff||1)*100*100)/100,sysPercent=Math.round((sysTime-0)/(activeDiff||1)*100*100)/100;let totalActive=Math.round((1-idleDiff/totalDiff)*100*100)/100;userPercent=Math.round(totalActive*0.7*100)/100,sysPercent=Math.round(totalActive*0.3*100)/100}}this.lastCpuInfo={idle,total};let entry={timestamp:Date.now(),user:userPercent,system:sysPercent};if(this.store.logs.cpu.push(entry),this.store.logs.cpu.length>this.store.logLimits.cpu*2)this.store.logs.cpu=this.store.logs.cpu.slice(-this.store.logLimits.cpu);this.store.worker.cpu=entry,this.store.worker.updatedAt=Date.now()};collect(),this.cpuInterval=setInterval(collect,this.store.configs.cpuLogInterval)}recordRequest(request){if(this.store.requests.push(request),this.store.requests.length>this.store.logLimits.request*2)this.store.requests=this.store.requests.slice(-this.store.logLimits.request)}recordDaprEvent(type,details){if(!this.store.configs.logDapr)return;let event={id:randomUUID(),type,timestamp:Date.now(),...details};if(this.store.logs.dapr.push(event),this.store.daprEvents.push(event),this.store.logs.dapr.length>this.store.logLimits.dapr*2)this.store.logs.dapr=this.store.logs.dapr.slice(-this.store.logLimits.dapr);if(this.store.daprEvents.length>this.store.logLimits.dapr*2)this.store.daprEvents=this.store.daprEvents.slice(-this.store.logLimits.dapr)}recordWsEvent(type,details){if(!this.store.configs.logWebSocket)return;let event={id:randomUUID(),type,timestamp:Date.now(),...details};if(this.store.logs.ws.push(event),this.store.wsEvents.push(event),this.store.logs.ws.length>this.store.logLimits.ws*2)this.store.logs.ws=this.store.logs.ws.slice(-this.store.logLimits.ws);if(this.store.wsEvents.length>this.store.logLimits.ws*2)this.store.wsEvents=this.store.wsEvents.slice(-this.store.logLimits.ws)}getSnapshot(){return{memory:this.store.logs.memory.slice(-this.store.logLimits.memory),cpu:this.store.logs.cpu.slice(-this.store.logLimits.cpu),requests:this.store.requests.slice(-this.store.logLimits.request),dapr:this.store.logs.dapr.slice(-this.store.logLimits.dapr),ws:this.store.logs.ws.slice(-this.store.logLimits.ws),workers:this.store.allWorkers.length?this.store.allWorkers:[this.store.worker],logLimits:{...this.store.logLimits},configs:{...this.store.configs}}}getUpdatesSince(timestamps){let memoryUpdates=this.store.logs.memory.filter((m)=>m.timestamp>timestamps.memory),cpuUpdates=this.store.logs.cpu.filter((c)=>c.timestamp>timestamps.cpu),requestUpdates=this.store.requests.filter((r)=>r.timestamp>timestamps.request),daprUpdates=this.store.logs.dapr.filter((d)=>d.timestamp>timestamps.dapr),wsUpdates=this.store.logs.ws.filter((w)=>w.timestamp>timestamps.ws);if(!(memoryUpdates.length>0||cpuUpdates.length>0||requestUpdates.length>0||daprUpdates.length>0||wsUpdates.length>0))return null;return{memory:memoryUpdates,cpu:cpuUpdates,requests:requestUpdates,dapr:daprUpdates,ws:wsUpdates,timestamp:Date.now()}}getLogs(){return{memory:this.store.logs.memory,cpu:this.store.logs.cpu,requests:this.store.requests,dapr:this.store.logs.dapr,ws:this.store.logs.ws,daprEvents:this.store.daprEvents,wsEvents:this.store.wsEvents,configs:{logMemory:this.store.configs.logMemory,logCpu:this.store.configs.logCpu,logDapr:this.store.configs.logDapr,logWebSocket:this.store.configs.logWebSocket},limits:{...this.store.logLimits}}}getSettings(){return{configs:{...this.store.configs},logLimits:{...this.store.logLimits}}}changeSettings(payload){if(payload.logMemory!==void 0)this.store.configs.logMemory=payload.logMemory;if(payload.logCpu!==void 0)this.store.configs.logCpu=payload.logCpu;if(payload.logDapr!==void 0)this.store.configs.logDapr=payload.logDapr;if(payload.logWebSocket!==void 0)this.store.configs.logWebSocket=payload.logWebSocket;if(payload.cpuLogInterval!==void 0){if(this.store.configs.cpuLogInterval=payload.cpuLogInterval,this.isRunning&&this.store.configs.logCpu)this.startCpuCollector()}if(payload.memoryLogInterval!==void 0){if(this.store.configs.memoryLogInterval=payload.memoryLogInterval,this.isRunning&&this.store.configs.logMemory)this.startMemoryCollector()}if(payload.memoryLogLimit!==void 0)this.store.logLimits.memory=payload.memoryLogLimit;if(payload.cpuLogLimit!==void 0)this.store.logLimits.cpu=payload.cpuLogLimit;if(payload.daprLogLimit!==void 0)this.store.logLimits.dapr=payload.daprLogLimit;if(payload.wsLogLimit!==void 0)this.store.logLimits.ws=payload.wsLogLimit;if(payload.requestLogLimit!==void 0)this.store.logLimits.request=payload.requestLogLimit;return{message:"Settings updated successfully",configs:{...this.store.configs},logLimits:{...this.store.logLimits}}}getStore(){return this.store}isEnabled(){return this.isRunning}}var DEFAULT_LIVE_CONFIG;var init_LiveMonitoringService=__esm(()=>{DEFAULT_LIVE_CONFIG={enabled:!0,logMemory:!0,logCpu:!0,logDapr:!0,logWebSocket:!0,memoryLogInterval:1000,cpuLogInterval:1000,memoryLogLimit:100,cpuLogLimit:100,daprLogLimit:100,wsLogLimit:100,requestLogLimit:100,streamInterval:150}});class MonitoringService{redis;logger;config;appId;flushToDb;systemCollector;applicationCollector;databaseCollector=null;redisCollector=null;alertService;collectInterval=null;flushInterval=null;pendingMetrics=[];isRunning=!1;peaks=new Map;peaksSeeded=!1;dbQuery;loadPeaks;constructor(deps){if(this.redis=deps.redis,this.logger=deps.logger,this.config=this.mergeConfig(deps.config),this.appId=deps.appId,this.flushToDb=deps.flushToDb,this.dbQuery=deps.dbQuery,this.loadPeaks=deps.loadPeaks,this.config.database.enabled)if(deps.dbQuery)this.databaseCollector=new DatabaseCollector(this.config.database,deps.dbQuery);else this.logger.warn("[Monitoring] monitoring.database is enabled but no database query executor is wired \u2014 database metrics are skipped");if(this.config.redis.enabled)if(deps.redis.getDirectClient())this.redisCollector=new RedisCollector(()=>deps.redis.getDirectClient());else this.logger.warn("[Monitoring] monitoring.redis is enabled but Redis runs in Dapr mode \u2014 INFO is unavailable, Redis metrics are skipped");if(this.config.persistence.enabled&&!deps.flushToDb)this.logger.warn("[Monitoring] monitoring.persistence is enabled but no flushToDb handler is wired \u2014 metrics are kept in Redis only");this.systemCollector=new SystemCollector(this.config.system),this.applicationCollector=new ApplicationCollector(this.config.application),this.alertService=new AlertService({logger:deps.logger,getEmailService:deps.getEmailService,config:this.config,appId:deps.appId})}mergeConfig(config){return{enabled:config.enabled??DEFAULT_CONFIG3.enabled,system:{enabled:config.system?.enabled??DEFAULT_CONFIG3.system.enabled,collectInterval:config.system?.collectInterval??DEFAULT_CONFIG3.system.collectInterval,metrics:{cpu:config.system?.metrics?.cpu??DEFAULT_CONFIG3.system.metrics.cpu,memory:config.system?.metrics?.memory??DEFAULT_CONFIG3.system.metrics.memory,disk:config.system?.metrics?.disk??DEFAULT_CONFIG3.system.metrics.disk,network:config.system?.metrics?.network??DEFAULT_CONFIG3.system.metrics.network,process:config.system?.metrics?.process??DEFAULT_CONFIG3.system.metrics.process}},application:{enabled:config.application?.enabled??DEFAULT_CONFIG3.application.enabled,metrics:{requests:config.application?.metrics?.requests??DEFAULT_CONFIG3.application.metrics.requests,responseTime:config.application?.metrics?.responseTime??DEFAULT_CONFIG3.application.metrics.responseTime,errors:config.application?.metrics?.errors??DEFAULT_CONFIG3.application.metrics.errors,rateLimits:config.application?.metrics?.rateLimits??DEFAULT_CONFIG3.application.metrics.rateLimits}},database:{enabled:config.database?.enabled??DEFAULT_CONFIG3.database.enabled,metrics:{connections:config.database?.metrics?.connections??DEFAULT_CONFIG3.database.metrics.connections,queryTime:config.database?.metrics?.queryTime??DEFAULT_CONFIG3.database.metrics.queryTime,slowQueryThreshold:config.database?.metrics?.slowQueryThreshold??DEFAULT_CONFIG3.database.metrics.slowQueryThreshold}},redis:{enabled:config.redis?.enabled??DEFAULT_CONFIG3.redis.enabled},persistence:{enabled:config.persistence?.enabled??DEFAULT_CONFIG3.persistence.enabled,samples:config.persistence?.samples??DEFAULT_CONFIG3.persistence.samples,flushInterval:config.persistence?.flushInterval??DEFAULT_CONFIG3.persistence.flushInterval,retentionDays:config.persistence?.retentionDays??DEFAULT_CONFIG3.persistence.retentionDays},alerts:{enabled:config.alerts?.enabled??DEFAULT_CONFIG3.alerts.enabled,email:{enabled:config.alerts?.email?.enabled??DEFAULT_CONFIG3.alerts.email.enabled,recipients:config.alerts?.email?.recipients??DEFAULT_CONFIG3.alerts.email.recipients},thresholds:{cpuPercent:config.alerts?.thresholds?.cpuPercent??DEFAULT_CONFIG3.alerts.thresholds.cpuPercent,memoryPercent:config.alerts?.thresholds?.memoryPercent??DEFAULT_CONFIG3.alerts.thresholds.memoryPercent,diskPercent:config.alerts?.thresholds?.diskPercent??DEFAULT_CONFIG3.alerts.thresholds.diskPercent,errorRatePercent:config.alerts?.thresholds?.errorRatePercent??DEFAULT_CONFIG3.alerts.thresholds.errorRatePercent,responseTimeMs:config.alerts?.thresholds?.responseTimeMs??DEFAULT_CONFIG3.alerts.thresholds.responseTimeMs,rateLimitBlocksPerMinute:config.alerts?.thresholds?.rateLimitBlocksPerMinute??DEFAULT_CONFIG3.alerts.thresholds.rateLimitBlocksPerMinute},cooldown:config.alerts?.cooldown??DEFAULT_CONFIG3.alerts.cooldown}}}parseTimeToMs(time){let match=time.match(/^(\d+)(ms|s|m|h|d)$/);if(!match||!match[1]||!match[2])return 1e4;let value=parseInt(match[1],10);switch(match[2]){case"ms":return value;case"s":return value*1000;case"m":return value*60*1000;case"h":return value*60*60*1000;case"d":return value*24*60*60*1000;default:return 1e4}}start(){if(!this.config.enabled||this.isRunning)return;this.isRunning=!0,this.logger.info("[Monitoring] Starting monitoring service");let collectIntervalMs=this.parseTimeToMs(this.config.system.collectInterval);if(this.collectInterval=setInterval(()=>{this.collect()},collectIntervalMs),this.config.persistence.enabled&&this.flushToDb){let flushIntervalMs=this.parseTimeToMs(this.config.persistence.flushInterval);this.flushInterval=setInterval(()=>{this.flush()},flushIntervalMs)}this.collect()}stop(){if(!this.isRunning)return;if(this.isRunning=!1,this.logger.info("[Monitoring] Stopping monitoring service"),this.collectInterval)clearInterval(this.collectInterval),this.collectInterval=null;if(this.flushInterval)clearInterval(this.flushInterval),this.flushInterval=null;this.flush()}async collect(){let now=Date.now(),snapshot={timestamp:now};if(this.config.system.enabled){let systemMetrics=await this.systemCollector.collect();if(systemMetrics)snapshot.system=systemMetrics,this.addMetricPoints("system",systemMetrics,now)}if(this.config.application.enabled){let appMetrics=this.applicationCollector.collect();if(appMetrics)snapshot.application=appMetrics,this.addMetricPoints("application",appMetrics,now)}if(this.databaseCollector){let dbMetrics=await this.databaseCollector.collect();if(dbMetrics)snapshot.database=dbMetrics,this.addMetricPoints("database",dbMetrics,now)}if(this.redisCollector){let redisMetrics=await this.redisCollector.collect();if(redisMetrics)snapshot.redis=redisMetrics,this.addMetricPoints("redis",redisMetrics,now)}if(await this.recordPeaks(snapshot,now),await this.storeSnapshot(snapshot),this.config.alerts.enabled)await this.alertService.checkAndAlert(snapshot)}addMetricPoints(type,metrics,timestamp){if(!this.config.persistence.samples)return;let flatten=(obj,prefix="")=>{for(let key in obj){let value=obj[key],newKey=prefix?`${prefix}.${key}`:key;if(typeof value==="number")this.pendingMetrics.push({timestamp,metricType:type,metricName:newKey,value});else if(typeof value==="object"&&value!==null&&!Array.isArray(value))flatten(value,newKey)}};flatten(metrics)}async seedPeaks(){if(this.peaksSeeded)return;if(!this.loadPeaks)return;try{for(let row of await this.loadPeaks()){if(!row.metricName||this.peaks.has(row.metricName))continue;this.peaks.set(row.metricName,{value:row.value,at:row.at,context:row.context})}this.peaksSeeded=!0}catch(error){this.logger.warn(`[Monitoring] peak seed failed, peaks not recorded this cycle: ${error}`)}}async recordPeaks(snapshot,timestamp){if(await this.seedPeaks(),!this.peaksSeeded)return;let previous=new Map([...this.peaks].map(([name,peak])=>[name,peak.value])),{points}=newPeaks(snapshot,previous,timestamp);for(let point of points)this.peaks.set(point.metricName,{value:point.value,at:timestamp,context:point.tags}),this.pendingMetrics.push(point),this.logger.info(`[Monitoring] yeni tepe \u2014 ${point.metricName} = ${point.value}`);if(this.peaks.size>0)snapshot.peaks=Object.fromEntries(this.peaks)}async storeSnapshot(snapshot){let key=`monitoring:${this.appId}:latest`;await this.redis.create(key,snapshot,3600);let historyKey=`monitoring:${this.appId}:history`,historyResult=await this.redis.read(historyKey),history=historyResult.success&&historyResult.data?historyResult.data:[];history.push(snapshot);let oneHourAgo=Date.now()-3600000,filteredHistory=history.filter((s)=>s.timestamp>oneHourAgo);await this.redis.create(historyKey,filteredHistory,3600)}async flush(){if(this.pendingMetrics.length===0)return;if(!this.flushToDb)return;let metricsToFlush=[...this.pendingMetrics];this.pendingMetrics=[];try{await this.flushToDb(metricsToFlush),this.logger.debug(`[Monitoring] Flushed ${metricsToFlush.length} metrics to database`)}catch(error){this.logger.error(`[Monitoring] Failed to flush metrics: ${error}`),this.pendingMetrics=[...metricsToFlush,...this.pendingMetrics]}}recordRequest(params){if(!this.config.enabled||!this.config.application.enabled)return;this.applicationCollector.recordRequest(params)}recordRateLimitBlock(){if(!this.config.enabled||!this.config.application.enabled)return;this.applicationCollector.recordRateLimitBlock()}async getLatestSnapshot(){let key=`monitoring:${this.appId}:latest`,result=await this.redis.read(key);return result.success?result.data:null}async getHistory(minutes=60){let key=`monitoring:${this.appId}:history`,result=await this.redis.read(key);if(!result.success||!result.data)return[];let cutoff=Date.now()-minutes*60000;return result.data.filter((s)=>s.timestamp>cutoff)}getActiveAlerts(){return this.alertService.getActiveAlerts()}acknowledgeAlert(alertId){return this.alertService.acknowledgeAlert(alertId)}isEnabled(){return this.config.enabled}getConfig(){return this.config}}var DEFAULT_CONFIG3;var init_Monitoring=__esm(()=>{init_AlertService();init_SystemCollector();init_peaks();init_AlertService();init_LiveMonitoringService();DEFAULT_CONFIG3={enabled:!1,system:{enabled:!0,collectInterval:"10s",metrics:{cpu:!0,memory:!0,disk:!0,network:!1,process:!0}},application:{enabled:!0,metrics:{requests:!0,responseTime:!0,errors:!0,rateLimits:!0}},database:{enabled:!1,metrics:{connections:!0,queryTime:!0,slowQueryThreshold:"100ms"}},redis:{enabled:!1},persistence:{enabled:!0,samples:!1,flushInterval:"1m",retentionDays:30},alerts:{enabled:!1,email:{enabled:!1,recipients:[]},thresholds:{cpuPercent:80,memoryPercent:85,diskPercent:90,errorRatePercent:5,responseTimeMs:1000,rateLimitBlocksPerMinute:100},cooldown:"5m"}}});function snakeToCamel(name){return name.replace(/_([a-z])/g,(_,c)=>c.toUpperCase())}function resolveSchemaTable(schemaTables,name,logger2){let direct=schemaTables[name];if(direct)return direct;let camel=snakeToCamel(name);if(camel!==name){let viaCamel=schemaTables[camel];if(viaCamel)return viaCamel}if(logger2){let keys=Object.keys(schemaTables),preview=keys.slice(0,20).join(", "),suffix=keys.length>20?`, ...(+${keys.length-20} more)`:"";logger2.warn(`[SchemaTables] Table "${name}" not found (tried "${name}" and "${camel}"). Known keys: ${preview}${suffix}`)}return}var init_types4=()=>{};import{and as and4,desc,eq as eq9}from"drizzle-orm";function resolveEnvValue2(value){if(!value)return;return process.env[value]??value}function toCamel(obj){let result={};for(let[key,value]of Object.entries(obj)){let camelKey=key.replace(/_([a-z])/g,(_,c)=>c.toUpperCase());result[camelKey]=value}return result}function fromCamel(obj){let result={};for(let[key,value]of Object.entries(obj)){let snakeKey=key.replace(/[A-Z]/g,(c)=>`_${c.toLowerCase()}`);result[snakeKey]=value}return result}class NotificationService{db;schemaTables;config;logger;getEmailService;constructor(serviceConfig){this.db=serviceConfig.db,this.schemaTables=serviceConfig.schemaTables,this.config=serviceConfig.config,this.logger=serviceConfig.logger,this.getEmailService=serviceConfig.getEmailService}getTable(name){return resolveSchemaTable(this.schemaTables,name,this.logger)}getCol(table,col4){return table[col4]}isChannelEnabled(channel){let channels=this.config.channels;if(!channels)return channel==="portal";switch(channel){case"portal":return channels.portal!==!1;case"email":return channels.email===!0;case"telegram":return channels.telegram?.enabled===!0;case"webhook":return channels.webhook?.enabled===!0;default:return!1}}interpolateTemplate(template,context){let result=template;for(let[key,value]of Object.entries(context))result=result.replace(new RegExp(`{{${key}}}`,"g"),String(value??""));for(let[key,value]of Object.entries(this.config.templateVariables||{}))result=result.replace(new RegExp(`{{${key}}}`,"g"),value);return result}async triggerNotifications(params){let{trigger,flow_id,entity_name,entity_id,node_id,context={}}=params,rulesTable=this.getTable("verificationNotificationRules"),recipientsTable=this.getTable("verificationNotificationRecipients"),channelsTable=this.getTable("verificationNotificationChannels");if(!rulesTable||!recipientsTable){this.logger.warn("[Notification] Notification tables not found");return}let now=new Date,rules=await this.db.select().from(rulesTable).where(and4(eq9(this.getCol(rulesTable,"flowId"),flow_id),eq9(this.getCol(rulesTable,"trigger"),trigger)));this.logger.info(`[Notification] Found ${rules.length} rules for trigger=${trigger} flow_id=${flow_id}, filter_node_id=${node_id||"NONE"}`);for(let r of rules)this.logger.info(`[Notification] Rule ${r.id}: nodeId=${r.nodeId}, trigger=${r.trigger}, title=${JSON.stringify(r.titleTemplate)}`);let filteredRules=rules.filter((rule)=>{if(node_id&&rule.nodeId!==node_id)return this.logger.info(`[Notification] EXCLUDED rule ${rule.id}: rule.nodeId=${rule.nodeId} !== filter_node_id=${node_id}`),!1;if(rule.startsAt&&new Date(rule.startsAt)>now)return!1;if(rule.expiresAt&&new Date(rule.expiresAt)<now)return!1;return!0});for(let rule of filteredRules){let recipients=await this.db.select().from(recipientsTable).where(eq9(this.getCol(recipientsTable,"ruleId"),rule.id)),ruleChannels=["portal"];if(channelsTable){let channelEntries=await this.db.select().from(channelsTable).where(eq9(this.getCol(channelsTable,"ruleId"),rule.id));if(channelEntries.length>0)ruleChannels=channelEntries.map((c)=>c.channel)}let enabledChannels=ruleChannels.filter((ch)=>this.isChannelEnabled(ch));if(enabledChannels.length===0)continue;this.logger.info(`[Notification] Rule ${rule.id}: ${recipients.length} recipients, ${enabledChannels.length} channels (${enabledChannels.join(",")})`);let userIds=await this.resolveRecipients(recipients,params.verifier_id,flow_id,entity_name,entity_id);this.logger.info(`[Notification] Rule ${rule.id}: resolved ${userIds.length} user IDs: ${userIds.join(", ")}`);let enrichedContext={...context,entity_name,entity_id,trigger,decision:params.decision};this.logger.info(`[Notification] Rule ${rule.id}: titleTemplate=${JSON.stringify(rule.titleTemplate)}, bodyTemplate=${JSON.stringify(rule.bodyTemplate)}, context=${JSON.stringify(enrichedContext)}`);let title=rule.titleTemplate?this.interpolateTemplate(rule.titleTemplate,enrichedContext):`Verification ${trigger.replace("on_","").replace("_"," ")}`,body=rule.bodyTemplate?this.interpolateTemplate(rule.bodyTemplate,enrichedContext):void 0;this.logger.info(`[Notification] Rule ${rule.id}: final title="${title}", body="${body}"`);for(let userId of userIds)await this.send({user_id:userId,title,body,entity_name,entity_id,type:"verification",source:`flow:${flow_id}`,channels:enabledChannels})}this.logger.debug(`[Notification] Triggered ${filteredRules.length} rules for ${trigger} on ${entity_name}:${entity_id}`)}async resolveRecipients(recipients,currentVerifierId,flowId,entityName,entityId){let userIds=new Set,userRolesTable=this.getTable("userRoles"),rolesTable=this.getTable("roles");for(let recipient of recipients)switch(recipient.recipientType){case"user":if(recipient.recipientUserId)userIds.add(recipient.recipientUserId);break;case"role":if(recipient.recipientRole&&userRolesTable&&rolesTable){let rolesCols=rolesTable,userRolesCols=userRolesTable,role=(await this.db.select().from(rolesTable).where(eq9(rolesCols.name,recipient.recipientRole)).limit(1))[0];if(role){let usersInRole=await this.db.select({user_id:userRolesCols.userId}).from(userRolesTable).where(eq9(userRolesCols.roleId,role.id));for(let ur of usersInRole)userIds.add(ur.user_id)}}break;case"step_verifier":if(currentVerifierId)userIds.add(currentVerifierId);break;case"entity_creator":{if(entityName&&entityId){let instancesTable=this.getTable("verificationInstances");if(instancesTable){let inst=(await this.db.select().from(instancesTable).where(and4(eq9(this.getCol(instancesTable,"entityName"),entityName),eq9(this.getCol(instancesTable,"entityId"),entityId))).orderBy(desc(this.getCol(instancesTable,"createdAt"))).limit(1))[0];if(inst?.startedBy)userIds.add(inst.startedBy)}}break}case"all_verifiers":{if(flowId){let verifierConfigsTable=this.getTable("verificationVerifierConfigs");if(verifierConfigsTable){let configs=await this.db.select().from(verifierConfigsTable).where(eq9(this.getCol(verifierConfigsTable,"flowId"),flowId));this.logger.info(`[Notification] all_verifiers: found ${configs.length} verifier configs for flow ${flowId}`);for(let cfg of configs){let row=cfg;if(this.logger.info(`[Notification] all_verifiers: config node_id=${row.nodeId}, type=${row.verifierType}, userId=${row.verifierUserId}, role=${row.verifierRole}`),row.verifierUserId)userIds.add(row.verifierUserId);if(row.verifierType==="role"&&row.verifierRole&&userRolesTable&&rolesTable){let rCols=rolesTable,urCols=userRolesTable,roleRow=(await this.db.select().from(rolesTable).where(eq9(rCols.name,row.verifierRole)).limit(1))[0];if(roleRow){let usersInRole=await this.db.select({user_id:urCols.userId}).from(userRolesTable).where(eq9(urCols.roleId,roleRow.id));for(let ur of usersInRole)userIds.add(ur.user_id)}}}}}break}}return Array.from(userIds)}async send(params){let{user_id,title,body,entity_name,entity_id,type,source,channels}=params;for(let channel of channels){if(!this.isChannelEnabled(channel)){this.logger.debug?.(`[Notification] channel "${channel}" is disabled in config \u2014 skipping for user ${user_id}`);continue}switch(channel){case"portal":await this.sendPortalNotification(user_id,title,body,entity_name,entity_id,type,source);break;case"email":await this.sendEmailNotification(user_id,title,body);break;case"sms":this.logger.warn(`[Notification] SMS delivery is not supported \u2014 skipping sms notification for user ${user_id}`);break;case"telegram":await this.sendTelegramNotification(params);break;case"webhook":await this.sendWebhookNotification(params);break}}}async sendPortalNotification(userId,title,body,entityName,entityId,type,source){let notificationsTable=this.getTable("notifications");if(!notificationsTable){this.logger.warn("[Notification] notifications table not found");return}await this.db.insert(notificationsTable).values(toCamel({user_id:userId,title,body:body||null,entity_name:entityName||null,entity_id:entityId||null,type:type||"system",source:source||null,is_seen:!1})),this.logger.debug(`[Notification] Portal notification sent to ${userId}: ${title}`)}async sendTelegramNotification(params){let telegramConfig=this.config.channels?.telegram,botToken=resolveEnvValue2(telegramConfig?.botToken),chatId=resolveEnvValue2(telegramConfig?.chatId);if(!botToken||!chatId){this.logger.warn("[Notification] Telegram channel enabled but botToken/chatId is missing or unresolved \u2014 skipping delivery");return}let text=params.body?`*${params.title}*
|
|
@@ -442,7 +442,7 @@ ${strMDSAlgs}`)}if(attestationStatementAlg!==void 0&&authenticatorGetInfo?.algor
|
|
|
442
442
|
<p>Your account has been created via ${provider} login. You can optionally set a password to also sign in with your email and password.</p>
|
|
443
443
|
<p><a href="${inviteLink}">Set your password</a></p>
|
|
444
444
|
<p>This link expires in 7 days. If you don't want to set a password, you can always sign in with ${provider}.</p>
|
|
445
|
-
`}),logger2.info("[OAUTH] Invite email sent to new OAuth user",{userId,email:profile.email,provider})}catch(emailErr){logger2.warn("[OAUTH] Failed to send invite email",{userId,error:emailErr})}}{let stateUser=(await db.select().from(usersTable).where(eq50(usersTable.id,userId)).limit(1))[0],state2=stateUser?evaluateAccountState(stateUser):{allowed:!1,reason:"inactive",message:"Account is not active"};if(!state2.allowed)return logger2.warn(`[OAUTH] Login blocked - account ${state2.reason}`,{userId,provider}),ctx.set.status=302,ctx.set.headers.Location=`${errorRedirect}?error=account_${state2.reason}`,null}let defaultRoleNames=resolveDefaultRoleNames(config.authentication);if(defaultRoleNames.length>0){let resolvedForRole=resolveAuthTablesForRequest(ctx.request,config);await assignDefaultRoles({db,userId,roleNames:defaultRoleNames,rolesTable:resolvedForRole.rolesTable??config.rolesTable??null,userRolesTable:resolvedForRole.userRolesTable??config.userRolesTable??null,logger:logger2})}let syncConfig=oauthService.profileSync;if(syncConfig){let syncTables=resolveAuthTablesForRequest(ctx.request,config);await applyProfileSync({db,usersTable,profilesTable:syncTables.schemaTables?.profiles,userId,rawProfile:profile.rawProfile,config:syncConfig,isNewUser:createdNewUser,logger:logger2})}await db.update(usersTable).set({lastLoginAt:new Date}).where(eq50(usersTable.id,userId));let ipAddress=ctx.request.headers.get("cf-connecting-ip")?.trim()||ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||ctx.request.headers.get("x-real-ip")?.trim()||"127.0.0.1",userAgent=ctx.request.headers.get("user-agent")||"Unknown Browser",deviceInfo=parseUserAgentForLogin(userAgent,ipAddress,void 0,deviceContextFromRequest(ctx.request).clientHints),oauthUserRoles=[],oauthUserClaims=[],oauthUserClaimScopes={};if(config.db)try{let resolved2=resolveAuthTablesForRequest(ctx.request,config),rc=await fetchUserRolesAndClaims(config.db,userId,resolved2);oauthUserRoles=rc.roles,oauthUserClaims=rc.claims,oauthUserClaimScopes=rc.claimScopes}catch{}let accessToken=signAccessToken(userId,oauthUserRoles.length>0?oauthUserRoles:void 0,oauthUserClaims.length>0?oauthUserClaims:void 0,ctx.request.headers.get("x-tenant-schema")??void 0,oauthUserClaimScopes),refreshToken=signRefreshToken(userId),requestOrigin=ctx.request.headers.get("origin")||ctx.request.headers.get("referer")?.replace(/\/[^/]*$/,"")||void 0,sessionParams={userId,deviceInfo,loginMethod:`oauth:${provider}`,rememberMe:!0,requestOrigin},sessionId=await createSession(sessionParams),requiresApproval=!1,approvalSessionId=sessionId,deviceTokenCookie;if(saveSessionToDb){let oauthSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0,dtCfg=config.authentication?.deviceTrust;if(dtCfg?.enabled)sessionParams.deviceToken=readDeviceTokenFromCookieHeader(ctx.request.headers.get("cookie"),dtCfg);let saveResult=await saveSessionToDb(sessionId,sessionParams,oauthSchemaName);if(saveResult?.denied)return logger2.warn("[OAUTH] Login blocked by device-trust",{userId,provider}),ctx.set.status=302,ctx.set.headers.Location=`${errorRedirect}?error=device_not_recognized`,null;if(deviceTokenCookie=saveResult?.deviceTokenCookie,saveResult?.requiresApproval){if(requiresApproval=!0,saveResult.sessionId)approvalSessionId=saveResult.sessionId}}if(requiresApproval){if(deviceTokenCookie)ctx.set.headers["Set-Cookie"]=deviceTokenCookie;logger2.info("[OAUTH] Login requires device approval",{userId,provider,approvalSessionId});let pendingUrl=absolutizeRedirect(resolveSafeOAuthRedirect(statePayload.redirectUrl,oauthService.successRedirectUrl,config.authentication?.trustedAppOrigins),portalOrigin);return ctx.set.status=302,ctx.set.headers.Location=`${pendingUrl}?requires_approval=true&provider=${provider}&session_id=${approvalSessionId}`,null}await logger2.audit({entityName:"users",entityId:userId,operation:"LOGIN",userId,summary:`${profile.email??profile.providerAccountId} logged in via OAuth (${provider})`,ipAddress,userAgent,path:`${basePath}/${provider}/callback`,query:""});let securePart=cookies.secure?"; Secure":"",domainPart=cookies.domain?`; Domain=${cookies.domain}`:"",cookieOptions=`; Path=${cookies.path}; HttpOnly; SameSite=${cookies.sameSite}${securePart}${domainPart}`,cookiesToSet=[];if(tokenConfig.accessToken.setHeadersEnabled)cookiesToSet.push(`${cookies.accessTokenName}=${accessToken}${cookieOptions}; Max-Age=${cookies.accessTokenMaxAge}`);if(tokenConfig.refreshToken.setHeadersEnabled)cookiesToSet.push(`${cookies.refreshTokenName}=${refreshToken}${cookieOptions}; Max-Age=${cookies.refreshTokenMaxAge}`);if(tokenConfig.sessionToken.setHeadersEnabled)cookiesToSet.push(`${cookies.sessionTokenName}=${sessionId}${cookieOptions}; Max-Age=${cookies.sessionTokenMaxAge}`);if(deviceTokenCookie)cookiesToSet.push(deviceTokenCookie);let successUrl=absolutizeRedirect(resolveSafeOAuthRedirect(statePayload.redirectUrl,oauthService.successRedirectUrl,config.authentication?.trustedAppOrigins),portalOrigin),headers=new Headers;headers.set("Location",successUrl);for(let cookie of cookiesToSet)headers.append("Set-Cookie",cookie);return new Response(null,{status:302,headers})},{detail:{tags:["Authentication"],summary:"OAuth Callback",description:"Handles the OAuth provider callback, creates session and sets cookies"}}),routes.get(`${basePath}/providers`,()=>{return{success:!0,data:{providers:oauthService.getEnabledProviders()}}},{detail:{tags:["Authentication"],summary:"List OAuth Providers",description:"Returns the list of enabled OAuth providers"}}),routes.get(`${basePath}/link/:provider`,async(ctx)=>{let provider=ctx.params.provider;if(!oauthService.isProviderEnabled(provider))return ctx.set.status=404,{success:!1,message:`OAuth provider "${provider}" is not enabled`};if(!oauthService.allowAccountLinking)return ctx.set.status=403,{success:!1,message:"Account linking is disabled"};let userId=ctx.request.headers.get("x-user-id");if(!userId)return ctx.set.status=401,{success:!1,message:"Authentication required to link accounts"};let redirectUrl=ctx.query.redirect_url,authUrl=await oauthService.buildAuthorizationUrl(provider,userId,redirectUrl),issuedState=new URL(authUrl).searchParams.get("state");if(issuedState)ctx.set.headers["Set-Cookie"]=buildOAuthStateCookie(issuedState,{maxAge:600,secure:cookies.secure,domain:cookies.domain});return ctx.set.status=302,ctx.set.headers.Location=authUrl,null},{detail:{tags:["Authentication"],summary:"Link OAuth Account",description:"Redirects to provider to link an OAuth account to the current user"}}),routes.delete(`${basePath}/unlink/:provider`,async(ctx)=>{let provider=ctx.params.provider,userId=ctx.request.headers.get("x-user-id");if(!userId)return ctx.set.status=401,{success:!1,message:"Authentication required"};if(!db)return ctx.set.status=500,{success:!1,message:"Database not configured"};let oauthTable=config.oauthAccountsTable;if(!oauthTable)return ctx.set.status=500,{success:!1,message:"OAuth accounts table not configured"};if((await db.select().from(oauthTable).where(and18(eq50(oauthTable.userId,userId),eq50(oauthTable.provider,provider))).limit(1)).length===0)return{success:!1,message:`No linked ${provider} account found`};let{usersTable:resolvedUsersTable}=resolveAuthTablesForRequest(ctx.request,config);if(!resolvedUsersTable)return ctx.set.status=500,{success:!1,message:"Users table not configured"};let hasPassword=!!(await db.select().from(resolvedUsersTable).where(eq50(resolvedUsersTable.id,userId)).limit(1))[0]?.password,allOAuthAccounts=await db.select().from(oauthTable).where(eq50(oauthTable.userId,userId));if(!hasPassword&&allOAuthAccounts.length<=1)return ctx.set.status=400,{success:!1,message:"Cannot unlink the only login method. Set a password first."};return await db.delete(oauthTable).where(and18(eq50(oauthTable.userId,userId),eq50(oauthTable.provider,provider))),logger2.info("[OAUTH] Account unlinked",{userId,provider}),{success:!0,message:`${provider} account unlinked successfully`}},{detail:{tags:["Authentication"],summary:"Unlink OAuth Account",description:"Removes a linked OAuth account from the current user"}}),routes.get(`${basePath}/accounts`,async(ctx)=>{let userId=ctx.request.headers.get("x-user-id");if(!userId)return ctx.set.status=401,{success:!1,message:"Authentication required"};if(!db)return ctx.set.status=500,{success:!1,message:"Database not configured"};let oauthTable=config.oauthAccountsTable;if(!oauthTable)return{success:!0,data:{accounts:[]}};return{success:!0,data:{accounts:(await db.select().from(oauthTable).where(eq50(oauthTable.userId,userId))).map((a12)=>({id:a12.id,provider:a12.provider,providerEmail:a12.providerEmail,providerName:a12.providerName,providerAvatarUrl:a12.providerAvatarUrl,isPrimary:a12.isPrimary,lastUsedAt:a12.lastUsedAt,createdAt:a12.createdAt}))}}},{detail:{tags:["Authentication"],summary:"List Linked OAuth Accounts",description:"Returns all OAuth accounts linked to the current user"}}),routes}var OAUTH_STATE_COOKIE="oauth_state",readCookie=(request,name2)=>{let raw=request.headers.get("cookie");if(!raw)return;for(let part of raw.split(";")){let trimmed=part.trim(),eq51=trimmed.indexOf("=");if(eq51>0&&trimmed.slice(0,eq51)===name2)return trimmed.slice(eq51+1)}return},resolveSafeOAuthRedirect=(candidate,fallback,allowedOrigins)=>{if(!candidate)return fallback;let normalized=candidate.replace(/[\t\n\r]/g,"");if(!normalized)return fallback;if(normalized.startsWith("/")&&normalized[1]!=="/"&&normalized[1]!=="\\")return normalized;try{let candHost=new URL(normalized).host.toLowerCase(),trusted=[];try{trusted.push(new URL(fallback).host.toLowerCase())}catch{}for(let o of allowedOrigins??[])try{trusted.push(new URL(o.includes("://")?o:`https://${o}`).host.toLowerCase())}catch{}for(let t33 of trusted)if(t33.startsWith("*.")){let base=t33.slice(2);if(candHost===base||candHost.endsWith(`.${base}`))return normalized}else if(candHost===t33)return normalized}catch{}return fallback},absolutizeRedirect=(target2,portalOrigin)=>portalOrigin&&target2.startsWith("/")&&target2[1]!=="/"?`${portalOrigin}${target2}`:target2;var init_oauth=__esm(()=>{init_DeviceTrust();init_assignDefaultRole();init_fetchUserRolesAndClaims();init_deviceInfo();init_utils7();init_utils8();init_applyProfileSync()});import{t as t33}from"elysia";var PasswordChangeBodySchema,PasswordChangeResponseSchema;var init_types15=__esm(()=>{PasswordChangeBodySchema=t33.Object({currentPassword:t33.Optional(t33.String()),newPassword:t33.String({minLength:8}),confirmPassword:t33.String({minLength:8})}),PasswordChangeResponseSchema=t33.Object({success:t33.Boolean(),message:t33.Optional(t33.String())})});function validatePasswordsMatch(newPassword,confirmPassword){return newPassword===confirmPassword}var exports_store={};__export(exports_store,{userHasNoTrustedDevices:()=>userHasNoTrustedDevices,touchDevice:()=>touchDevice,revokeUserDevices:()=>revokeUserDevices,revokeDevicesBySessionIds:()=>revokeDevicesBySessionIds,revokeDeviceById:()=>revokeDeviceById,rejectDeviceForSession:()=>rejectDeviceForSession,listUserDevices:()=>listUserDevices,insertDeviceRow:()=>insertDeviceRow,findDeviceRowsByRawToken:()=>findDeviceRowsByRawToken,activateDeviceForSession:()=>activateDeviceForSession});import{and as and19,eq as eq51,inArray as inArray16}from"drizzle-orm";async function findDeviceRowsByRawToken(db,table,rawToken){let hash=hashDeviceToken(rawToken);return await db.select().from(table).where(eq51(col14(table,"tokenHash"),hash)).limit(2)}async function insertDeviceRow(db,table,v){let now=new Date;await db.insert(table).values({id:v.id,userId:v.userId,tokenHash:v.tokenHash,status:v.status,originSessionId:v.originSessionId??null,label:v.label??null,deviceFingerprint:v.deviceFingerprint??null,firstSeenIp:v.firstSeenIp??null,lastSeenIp:v.lastSeenIp??v.firstSeenIp??null,lastSeenAt:now,expiresAt:v.expiresAt,isActive:!0,createdAt:now,updatedAt:now})}async function activateDeviceForSession(db,table,sessionId){return(await db.update(table).set({status:"trusted",updatedAt:new Date}).where(and19(eq51(col14(table,"originSessionId"),sessionId),eq51(col14(table,"status"),"pending"))).returning({id:col14(table,"id")})).length}async function rejectDeviceForSession(db,table,sessionId){await db.update(table).set({status:"revoked",revokedAt:new Date,revokedReason:"replaced",isActive:!1,updatedAt:new Date}).where(and19(eq51(col14(table,"originSessionId"),sessionId),eq51(col14(table,"status"),"pending")))}async function touchDevice(db,table,id,ip){await db.update(table).set({lastSeenAt:new Date,lastSeenIp:ip??null,updatedAt:new Date}).where(eq51(col14(table,"id"),id))}async function revokeWhere(db,table,where,reason){await db.update(table).set({status:"revoked",revokedAt:new Date,revokedReason:reason,isActive:!1,updatedAt:new Date}).where(where)}async function revokeUserDevices(db,table,userId,reason){if(!userId)return;let notAlready=and19(eq51(col14(table,"userId"),userId),eq51(col14(table,"status"),"trusted")),alsoPending=and19(eq51(col14(table,"userId"),userId),eq51(col14(table,"status"),"pending"));await revokeWhere(db,table,notAlready,reason),await revokeWhere(db,table,alsoPending,reason)}async function revokeDevicesBySessionIds(db,table,sessionIds,reason){if(!sessionIds?.length)return;await revokeWhere(db,table,inArray16(col14(table,"originSessionId"),sessionIds),reason)}async function revokeDeviceById(db,table,id,userId,reason){return(await db.update(table).set({status:"revoked",revokedAt:new Date,revokedReason:reason,isActive:!1,updatedAt:new Date}).where(and19(eq51(col14(table,"id"),id),eq51(col14(table,"userId"),userId))).returning({id:col14(table,"id")})).length}async function listUserDevices(db,table,userId){return(await db.select().from(table).where(eq51(col14(table,"userId"),userId))).map((r2)=>{let{tokenHash:_omit,...safe}=r2;return safe})}async function userHasNoTrustedDevices(db,table,userId){return(await db.select({id:col14(table,"id")}).from(table).where(and19(eq51(col14(table,"userId"),userId),eq51(col14(table,"status"),"trusted"))).limit(1)).length===0}var col14=(t34,name2)=>t34[name2];var init_store2=__esm(()=>{init_DeviceTrust()});import{eq as eq52}from"drizzle-orm";import{Elysia as Elysia39}from"elysia";function createPasswordChangeRoute(config,passwordChangeConfig){let{db,logger:logger2}=config,log4=logger2.scoped(AUTH_PASSWORD_CHANGE),route=passwordChangeConfig.route||"/auth/password-change",passwordChangeRoutes=new Elysia39;if(!passwordChangeConfig.enabled)return passwordChangeRoutes;return passwordChangeRoutes.post(route,async(ctx)=>{let{usersTable,sessionsTable}=resolveAuthTablesForRequest(ctx.request,config);if(!db||!usersTable)return ctx.set.status=500,{success:!1,message:"Database not configured"};let userId=ctx.request.headers.get("x-user-id");if(!userId)return ctx.set.status=401,{success:!1,message:"Authentication required"};let{currentPassword,newPassword,confirmPassword}=ctx.body;if(!validatePasswordsMatch(newPassword,confirmPassword))return ctx.set.status=422,{success:!1,message:"New passwords do not match"};let strength=validatePasswordStrength(newPassword);if(!strength.valid)return ctx.set.status=400,{success:!1,message:"Password too weak",errors:strength.errors};let user=(await db.select().from(usersTable).where(eq52(usersTable.id,userId)).limit(1))[0];if(!user)return ctx.set.status=404,{success:!1,message:"User not found"};let existingHash=user.password??"",userHasPassword=existingHash.length>0,isInitialSet=!userHasPassword,reqUrl=new URL(ctx.request.url),auditIp=ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||ctx.request.headers.get("x-real-ip")?.trim()||"unknown",auditUa=ctx.request.headers.get("user-agent")||"unknown";if(userHasPassword){if(!currentPassword)return ctx.set.status=400,{success:!1,message:"Current password is required"};if(!await verifyPassword(currentPassword,existingHash))return log4.warn("Password change failed - invalid current password",{userId}),logger2.audit({entityName:"users",entityId:userId,operation:"PASSWORD_CHANGE_FAILED",userId,summary:"Password change failed: invalid current password",ipAddress:auditIp,userAgent:auditUa,path:reqUrl.pathname,query:reqUrl.search}),ctx.set.status=400,{success:!1,message:"Current password is incorrect"}}let hashedNewPassword=await hashPassword(newPassword);if(await db.update(usersTable).set({password:hashedNewPassword,updatedAt:new Date}).where(eq52(usersTable.id,userId)),!isInitialSet&&sessionsTable)try{await db.update(sessionsTable).set({isActive:!1,revokedAt:new Date,revokedReason:"password_change"}).where(eq52(sessionsTable.userId,userId))}catch(err){log4.warn("Failed to revoke sessions after password change",{userId,error:err instanceof Error?err.message:String(err)})}if(!isInitialSet&&config.authentication?.deviceTrust?.enabled)try{let{schemaTables}=resolveAuthTablesForRequest(ctx.request,config),dtTable=schemaTables?.trustedDevices??schemaTables?.trusted_devices;if(dtTable&&db){let{revokeUserDevices:revokeUserDevices2}=await Promise.resolve().then(() => (init_store2(),exports_store));await revokeUserDevices2(db,dtTable,userId,"password_reset")}}catch(err){log4.warn("Failed to revoke device tokens after password change",{userId,error:err instanceof Error?err.message:String(err)})}return log4.info(isInitialSet?"Initial password set successful":"Password change successful",{userId}),logger2.audit({entityName:"users",entityId:userId,operation:isInitialSet?"PASSWORD_SET":"PASSWORD_CHANGE",userId,summary:isInitialSet?"Initial password set successfully":"Password changed successfully",ipAddress:auditIp,userAgent:auditUa,path:reqUrl.pathname,query:reqUrl.search}),{success:!0,message:isInitialSet?"Password set successfully":"Password changed successfully"}},{body:PasswordChangeBodySchema,detail:{tags:["Authentication"],summary:"Change Password",description:"Change password for authenticated user"}}),passwordChangeRoutes}var init_passwordChange=__esm(()=>{init_scopes();init_utils7();init_utils6();init_types15();init_types15()});function describeResetEmail(outcome){if(!outcome||outcome.sent)return{sent:!0,level:"info",message:"Password reset email sent"};let base=SENTENCES[outcome.reason],detail=outcome.detail?.trim();return{sent:!1,level:outcome.reason==="exempt"?"info":"warn",message:detail?`${base} (${detail})`:base}}var SENTENCES;var init_outcome=__esm(()=>{SENTENCES={no_provider:"Password reset email NOT sent \u2014 no email provider is configured. The token was stored, so a link exists, but nothing carried it.",exempt:"Password reset email not sent \u2014 the address is on an exempt domain this install never mails.",rejected:"Password reset email NOT sent \u2014 the provider refused the recipient. The address is accepted at login but not deliverable.",error:"Password reset email NOT sent \u2014 the send failed."}});import{t as t34}from"elysia";var PasswordResetRequestSchema,PasswordResetConfirmSchema,PasswordResetResponseSchema;var init_types16=__esm(()=>{PasswordResetRequestSchema=t34.Object({email:t34.String({format:"email"})}),PasswordResetConfirmSchema=t34.Object({token:t34.String(),newPassword:t34.String({minLength:8}),confirmPassword:t34.String({minLength:8})}),PasswordResetResponseSchema=t34.Object({success:t34.Boolean(),message:t34.Optional(t34.String())})});import{randomBytes as randomBytes7}from"crypto";function generateResetToken(){return randomBytes7(32).toString("hex")}function isTokenExpired(expiresAt){return new Date>expiresAt}var init_utils9=()=>{};import{Elysia as Elysia40}from"elysia";function createPasswordResetRoute(config,passwordResetConfig,storeResetToken,getResetToken,deleteResetToken,sendResetEmail){let{db,logger:logger2}=config,log4=logger2.scoped(AUTH_PASSWORD_RESET),baseRoute=passwordResetConfig.route||"/auth/password-reset",passwordResetRoutes=new Elysia40;if(!passwordResetConfig.enabled)return passwordResetRoutes;return passwordResetRoutes.post(`${baseRoute}/request`,async(ctx)=>{let{usersTable}=resolveAuthTablesForRequest(ctx.request,config);if(!db||!usersTable)return{success:!1,message:"Database not configured"};let{email:rawEmail}=ctx.body,email=rawEmail.toLowerCase(),{eq:eq53}=await import("drizzle-orm"),user=(await db.select().from(usersTable).where(eq53(usersTable.email,email)).limit(1))[0];if(!user)return{success:!0,message:"If email exists, reset link will be sent"};let token=generateResetToken(),expiresAt=new Date(Date.now()+3600000),reqSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0;await storeResetToken(user.id,token,expiresAt,reqSchemaName);let delivery={sent:!1,level:"warn",message:"Password reset email NOT sent \u2014 no send helper is configured."};if(sendResetEmail)try{delivery=describeResetEmail(await sendResetEmail(email,token,ctx.request))}catch(error3){delivery=describeResetEmail({sent:!1,reason:"error",detail:error3 instanceof Error?error3.message:String(error3)})}if(delivery.level==="warn")log4.warn(delivery.message,{email});else log4.info(delivery.message,{email});log4.info("Password reset requested",{userId:user.id,email});let reqUrl=new URL(ctx.request.url);return logger2.audit({entityName:"users",entityId:user.id,operation:"PASSWORD_RESET_REQUEST",userId:user.id,summary:`Password reset requested for ${email} \u2014 ${delivery.sent?"e-posta g\xF6nderildi":"E-POSTA G\xD6NDER\u0130LEMED\u0130"}`,ipAddress:ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||ctx.request.headers.get("x-real-ip")?.trim()||"unknown",userAgent:ctx.request.headers.get("user-agent")||"unknown",path:reqUrl.pathname,query:reqUrl.search}),{success:!0,message:"If email exists, reset link will be sent"}},{body:PasswordResetRequestSchema,detail:{tags:["Authentication"],summary:"Request Password Reset",description:"Request a password reset email"}}),passwordResetRoutes.post(`${baseRoute}/confirm`,async(ctx)=>{let{usersTable,sessionsTable}=resolveAuthTablesForRequest(ctx.request,config);if(!db||!usersTable)return{success:!1,message:"Database not configured"};let{token,newPassword,confirmPassword}=ctx.body;if(newPassword!==confirmPassword)return{success:!1,message:"Passwords do not match"};let strength=validatePasswordStrength(newPassword);if(!strength.valid)return ctx.set.status=400,{success:!1,message:"Password too weak",errors:strength.errors};let reqSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0,resetData=await getResetToken(token,reqSchemaName);if(!resetData)return{success:!1,message:"Invalid or expired reset token"};if(isTokenExpired(resetData.expiresAt))return await deleteResetToken(token,reqSchemaName),{success:!1,message:"Reset token has expired"};let hashedPassword=await hashPassword(newPassword),{eq:eq53}=await import("drizzle-orm");if(await db.update(usersTable).set({password:hashedPassword}).where(eq53(usersTable.id,resetData.userId)),await deleteResetToken(token,reqSchemaName),sessionsTable)try{await db.update(sessionsTable).set({isActive:!1,revokedAt:new Date,revokedReason:"password_reset"}).where(eq53(sessionsTable.userId,resetData.userId))}catch(err){log4.warn("Failed to revoke sessions after password reset",{userId:resetData.userId,error:err instanceof Error?err.message:String(err)})}try{if(config.authentication?.deviceTrust?.enabled){let{schemaTables}=resolveAuthTablesForRequest(ctx.request,config),dtTable=schemaTables?.trustedDevices??schemaTables?.trusted_devices;if(dtTable&&db){let{revokeUserDevices:revokeUserDevices2}=await Promise.resolve().then(() => (init_store2(),exports_store));await revokeUserDevices2(db,dtTable,resetData.userId,"password_reset")}}}catch(err){log4.warn("Failed to revoke device tokens after password reset",{userId:resetData.userId,error:err instanceof Error?err.message:String(err)})}log4.info("Password reset successful",{userId:resetData.userId});let reqUrl=new URL(ctx.request.url);return logger2.audit({entityName:"users",entityId:resetData.userId,operation:"PASSWORD_RESET",userId:resetData.userId,summary:"Password reset completed successfully",ipAddress:ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||ctx.request.headers.get("x-real-ip")?.trim()||"unknown",userAgent:ctx.request.headers.get("user-agent")||"unknown",path:reqUrl.pathname,query:reqUrl.search}),{success:!0,message:"Password has been reset"}},{body:PasswordResetConfirmSchema,detail:{tags:["Authentication"],summary:"Confirm Password Reset",description:"Reset password with token"}}),passwordResetRoutes}var init_passwordReset=__esm(()=>{init_scopes();init_utils6();init_outcome();init_types16();init_utils9();init_types16()});import{t as t35}from"elysia";var PasswordSetBodySchema,PasswordSetResponseSchema;var init_types17=__esm(()=>{PasswordSetBodySchema=t35.Object({newPassword:t35.String({minLength:8}),userId:t35.Optional(t35.String()),token:t35.Optional(t35.String())}),PasswordSetResponseSchema=t35.Object({success:t35.Boolean(),message:t35.String()})});import{eq as eq53}from"drizzle-orm";import{Elysia as Elysia41}from"elysia";function createPasswordSetRoute(config,passwordSetConfig,getMagicToken,deleteMagicToken){let{db,logger:logger2}=config,log4=logger2.scoped(AUTH_PASSWORD_SET),route=passwordSetConfig.route||"/auth/password-set",passwordSetRoutes=new Elysia41;if(!passwordSetConfig.enabled)return passwordSetRoutes;return passwordSetRoutes.post(route,async(ctx)=>{log4.info("Password set request received");let{usersTable}=resolveAuthTablesForRequest(ctx.request,config);if(!db||!usersTable)return log4.error("Password set failed - database not configured"),{success:!1,message:"Database not configured"};let{newPassword,token}=ctx.body,strength=validatePasswordStrength(newPassword);if(!strength.valid)return ctx.set.status=400,{success:!1,message:"Password too weak",errors:strength.errors};let userId=null;if(token&&getMagicToken&&deleteMagicToken){let reqSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0,tokenHash=hashToken(token,TOKEN_PURPOSE.invite),storedToken=await getMagicToken(tokenHash,reqSchemaName);if(!storedToken)return log4.warn("Password set failed - invalid token"),{success:!1,message:"Invalid or expired token"};if(new Date>storedToken.expiresAt)return await deleteMagicToken(tokenHash,reqSchemaName),log4.warn("Password set failed - expired token",{email:storedToken.email}),{success:!1,message:"Invalid or expired token"};userId=storedToken.userId,log4.info("Password set - userId resolved from token",{userId,email:storedToken.email})}else userId=ctx.request.headers.get("x-user-id");if(!userId)return log4.warn("Password set failed - no token and no authenticated user"),{success:!1,message:"Valid token or authentication required"};let user=(await db.select().from(usersTable).where(eq53(usersTable.id,userId)).limit(1))[0];if(log4.info("Password set - user found",{found:!!user,hasPassword:!!user?.password}),!user)return{success:!1,message:"User not found"};if(user.password)return log4.warn("Password set failed - user already has password",{userId}),{success:!1,message:"Password already set. Use password change instead."};let hashedPassword=await hashPassword(newPassword);log4.info("Password set - updating user with verifiedAt",{userId});let updateResult=await db.update(usersTable).set({password:hashedPassword,verifiedAt:new Date,updatedAt:new Date}).where(eq53(usersTable.id,userId));if(log4.info("Password set successful for invited user",{userId,updateResult}),token&&deleteMagicToken){let setSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0,tokenHash=hashToken(token,TOKEN_PURPOSE.invite);await deleteMagicToken(tokenHash,setSchemaName),log4.info("Invite token consumed after password set",{userId})}return{success:!0,message:"Password set successfully"}},{body:PasswordSetBodySchema,detail:{tags:["Authentication"],summary:"Set Password",description:"Set password for the first time (for invited users who do not have a password yet)"}}),passwordSetRoutes}var init_passwordSet=__esm(()=>{init_scopes();init_utils8();init_utils6();init_types17();init_types17()});function parseTimeToSeconds3(time2){let match=time2.match(/^(\d+)([smhd])$/);if(!match)return 900;let value2=match[1],unit=match[2];if(!value2||!unit)return 900;let num3=parseInt(value2,10);switch(unit){case"s":return num3;case"m":return num3*60;case"h":return num3*3600;case"d":return num3*86400;default:return 900}}import{t as t36}from"elysia";var RefreshResponseSchema;var init_types18=__esm(()=>{RefreshResponseSchema=t36.Object({success:t36.Boolean(),message:t36.Optional(t36.String()),data:t36.Optional(t36.Object({accessToken:t36.String()}))})});import{eq as eq54}from"drizzle-orm";import{Elysia as Elysia42}from"elysia";function createRefreshRoute(config,refreshConfig,verifyRefreshToken,signAccessToken,signRefreshToken,tokenResponseConfig,cookieMaxAgeBufferSeconds,cookieConfig){let{logger:logger2,authentication}=config,route=refreshConfig.route||"/auth/refresh",refreshRoutes=new Elysia42;if(!refreshConfig.enabled)return refreshRoutes;return refreshRoutes.post(route,async(ctx)=>{let refreshTokenName=authentication?.refreshToken?.name||"refresh_token",accessTokenName=authentication?.accessToken?.name||"access_token",refreshToken=ctx.request.headers.get("x-refresh-token");if(!refreshToken){let cookieHeader=ctx.request.headers.get("cookie");if(cookieHeader)refreshToken=cookieHeader.split(";").reduce((acc,cookie)=>{let trimmed=cookie.trim(),eqIndex=trimmed.indexOf("=");if(eqIndex>0)acc[trimmed.slice(0,eqIndex)]=trimmed.slice(eqIndex+1);return acc},{})[refreshTokenName]||null}if(!refreshToken)return ctx.set.status=401,{success:!1,message:"Refresh token required"};let result=verifyRefreshToken(refreshToken);if(!result.valid||!result.payload)return logger2.warn("[AUTH] Refresh failed - invalid token"),ctx.set.status=401,{success:!1,message:"Invalid refresh token"};let userId=result.payload.sub,userRoles=[],userClaims=[],userClaimScopes={},resolved=resolveAuthTablesForRequest(ctx.request,config);if(config.db){try{let rc=await fetchUserRolesAndClaims(config.db,userId,resolved);userRoles=rc.roles,userClaims=rc.claims,userClaimScopes=rc.claimScopes}catch(roleError){logger2.error("[AUTH] Failed to fetch roles/claims during refresh",{userId,error:roleError instanceof Error?roleError.message:String(roleError)})}try{let usersTable=resolved.usersTable;if(usersTable){let user=(await config.db.select().from(usersTable).where(eq54(usersTable.id,userId)).limit(1))[0];if(!user||user.isActive===!1)return logger2.warn("[AUTH] Refresh blocked - account inactive",{userId}),ctx.set.status=401,{success:!1,message:"Account is not active"};let lockedUntilRaw=user.lockedUntil,lockedUntil=lockedUntilRaw?new Date(lockedUntilRaw):null;if(user.isLocked===!0&&(!lockedUntil||lockedUntil>new Date))return logger2.warn("[AUTH] Refresh blocked - account locked",{userId}),ctx.set.status=401,{success:!1,message:"Account is locked"};let userCohortId=user.cohortId;if(userCohortId){let cohortsTable=resolved.schemaTables?.userCohorts??resolved.schemaTables?.user_cohorts;if(cohortsTable){let cohortExpiresAt=(await config.db.select().from(cohortsTable).where(eq54(cohortsTable.id,userCohortId)).limit(1))[0]?.expiresAt;if(cohortExpiresAt&&new Date(cohortExpiresAt)<new Date)return logger2.warn("[AUTH] Refresh blocked - cohort expired",{userId,cohortId:userCohortId}),ctx.set.status=401,{success:!1,message:"Your access has expired. Please contact your administrator."}}}}}catch(cohortErr){logger2.error("[AUTH] Failed cohort expiry check during refresh",{userId,error:cohortErr instanceof Error?cohortErr.message:String(cohortErr)})}}let newAccessToken=signAccessToken(userId,userRoles.length>0?userRoles:void 0,userClaims.length>0?userClaims:void 0,ctx.request.headers.get("x-tenant-schema")??void 0,userClaimScopes),newRefreshToken=signRefreshToken?signRefreshToken(userId):null,accessSetHeaders=tokenResponseConfig?.accessToken?.setHeadersEnabled??!0,accessReturnJson=tokenResponseConfig?.accessToken?.returnJson??!0,refreshSetHeaders=tokenResponseConfig?.refreshToken?.setHeadersEnabled??!0,refreshReturnJson=tokenResponseConfig?.refreshToken?.returnJson??!1,accessExpiresIn=authentication?.accessToken?.expiresIn||"15m",accessMaxAge=Math.max(0,parseTimeToSeconds3(accessExpiresIn)-(cookieMaxAgeBufferSeconds??0)),refreshTtl=lifetimeSeconds(authentication?.refreshToken?.expiresIn,604800),refreshMaxAge=cookieMaxAge(refreshTtl),headers=new Headers;headers.set("Content-Type","application/json");let cookiePath=cookieConfig?.path||"/",cookieSameSite=cookieConfig?.sameSite||"Strict",securePart=cookieConfig?.secure!==!1?"; Secure":"",domainPart=cookieConfig?.domain?`; Domain=${cookieConfig.domain}`:"",cookieOpts=`; Path=${cookiePath}; HttpOnly; SameSite=${cookieSameSite}${securePart}${domainPart}`;if(accessSetHeaders)headers.append("Set-Cookie",`${accessTokenName}=${newAccessToken}${cookieOpts}; Max-Age=${accessMaxAge}`);if(newRefreshToken&&refreshSetHeaders)headers.append("Set-Cookie",`${refreshTokenName}=${newRefreshToken}${cookieOpts}; Max-Age=${refreshMaxAge}`);logger2.info("[AUTH] Token refresh successful",{userId,rotatedRefreshToken:!!newRefreshToken});let responseData={};if(accessReturnJson)responseData.accessToken=newAccessToken;if(newRefreshToken&&refreshReturnJson)responseData.refreshToken=newRefreshToken;let jsonBody=JSON.stringify({success:!0,data:responseData});return new Response(jsonBody,{status:200,headers})},{detail:{tags:["Authentication"],summary:"Refresh Token",description:"Get new access token using refresh token"}}),refreshRoutes}var init_refresh=__esm(()=>{init_lifetime();init_fetchUserRolesAndClaims();init_types18()});import{t as t37}from"elysia";var RegisterBodySchema,RegisterResponseSchema;var init_types19=__esm(()=>{RegisterBodySchema=t37.Object({email:t37.String({format:"email"}),password:t37.String({minLength:8}),confirmPassword:t37.Optional(t37.String({minLength:8})),firstName:t37.Optional(t37.String({maxLength:100})),lastName:t37.Optional(t37.String({maxLength:100})),createProfile:t37.Optional(t37.Boolean())}),RegisterResponseSchema=t37.Object({success:t37.Boolean(),message:t37.Optional(t37.String()),data:t37.Optional(t37.Object({user:t37.Object({id:t37.String(),email:t37.String()})}))})});import{eq as eq55}from"drizzle-orm";import{Elysia as Elysia43}from"elysia";function createRegisterRoute(config,registerConfig,sendWelcomeEmail,signAccessToken,signRefreshToken,createSession,cookieConfig,tokenResponseConfig,getEmailService,appName,saveSessionToDb){let{db,logger:logger2}=config,log4=logger2.scoped(AUTH_REGISTER),route=registerConfig.route||"/auth/register",cookies={accessTokenName:cookieConfig?.accessTokenName||"access_token",refreshTokenName:cookieConfig?.refreshTokenName||"refresh_token",sessionTokenName:cookieConfig?.sessionTokenName||"session_token",accessTokenMaxAge:cookieConfig?.accessTokenMaxAge||900,refreshTokenMaxAge:cookieConfig?.refreshTokenMaxAge||604800,sessionTokenMaxAge:cookieConfig?.sessionTokenMaxAge||900,secure:cookieConfig?.secure??!0,httpOnly:cookieConfig?.httpOnly??!0,sameSite:cookieConfig?.sameSite||"strict",path:cookieConfig?.path||"/",domain:cookieConfig?.domain},registerRoutes=new Elysia43;if(!registerConfig.enabled)return registerRoutes;return registerRoutes.post(route,async(ctx)=>{let resolved=resolveAuthTablesForRequest(ctx.request,config),usersTable=resolved.usersTable;if(!db||!usersTable)return ctx.set.status=500,{success:!1,message:"Database not configured"};let{email:rawEmail,password:password4,confirmPassword,firstName,lastName,createProfile}=ctx.body,email=rawEmail.toLowerCase();if(confirmPassword&&password4!==confirmPassword)return ctx.set.status=400,{success:!1,message:"Passwords do not match"};let passwordValidation=validatePasswordStrength(password4);if(!passwordValidation.valid)return ctx.set.status=400,{success:!1,message:"Password too weak",errors:passwordValidation.errors};let existingUsers=await db.select().from(usersTable).where(eq55(usersTable.email,email)).limit(1),reqUrl=new URL(ctx.request.url),auditIp=ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||ctx.request.headers.get("x-real-ip")?.trim()||"unknown",auditUa=ctx.request.headers.get("user-agent")||"unknown";if(existingUsers.length>0){log4.warn("Registration failed - email exists",{email}),logger2.audit({entityName:"users",operation:"REGISTER_FAILED",summary:`Registration failed: email already exists (${email})`,ipAddress:auditIp,userAgent:auditUa,path:reqUrl.pathname,query:reqUrl.search});let verifGatesSignup=registerConfig.emailVerification?.enabled&&getEmailService?.()?.isAvailable();if(config.authentication?.preventUserEnumeration&&verifGatesSignup)return{success:!0,message:"Registration successful. Please check your email to verify your account."};return ctx.set.status=409,{success:!1,message:"Email already registered"}}let hashedPassword=await hashPassword(password4),domainExempt=isEmailExempt(email,config.authentication?.emailExemptDomains),emailService=getEmailService?.()??null,emailVerificationEnabled=!domainExempt&®isterConfig.emailVerification?.enabled&&emailService?.isAvailable(),verificationToken=emailVerificationEnabled?generateVerificationToken():null,tokenExpiresIn=registerConfig.emailVerification?.tokenExpiresIn||"24h",tokenExpiresAt=emailVerificationEnabled?new Date(Date.now()+parseTimeToMs(tokenExpiresIn)):null,insertValues={email,password:hashedPassword};if(emailVerificationEnabled&&verificationToken)insertValues.emailVerificationToken=hashVerificationToken(verificationToken),insertValues.emailVerificationTokenExpiresAt=tokenExpiresAt,insertValues.emailVerificationSentAt=new Date,insertValues.emailVerificationAttempts=1;let newUser=(await db.insert(usersTable).values(insertValues).returning())[0],registerDefaultRoles=resolveDefaultRoleNames(config.authentication);if(registerDefaultRoles.length>0)await assignDefaultRoles({db,userId:newUser.id,roleNames:registerDefaultRoles,rolesTable:resolved.rolesTable??config.rolesTable??null,userRolesTable:resolved.userRolesTable??config.userRolesTable??null,logger:logger2});let wantsProfile=registerConfig.createProfileOnRegister||createProfile,profilesTable=resolved.schemaTables?.profiles;if(wantsProfile&&profilesTable&&(firstName||lastName))try{let profileNow=new Date;await db.insert(profilesTable).values({userId:newUser.id,firstName:firstName??"",lastName:lastName??"",createdAt:profileNow,updatedAt:profileNow})}catch(err){log4.warn("Failed to create profile on register \u2014 continuing",{userId:newUser.id,error:err instanceof Error?err.message:String(err)})}if(log4.info("Registration successful",{userId:newUser.id,email,emailVerificationEnabled}),logger2.audit({entityName:"users",entityId:newUser.id,operation:"REGISTER",userId:newUser.id,summary:`New user registered: ${email}`,ipAddress:auditIp,userAgent:auditUa,path:reqUrl.pathname,query:reqUrl.search}),emailVerificationEnabled&&emailService&&verificationToken){let verificationLink=buildEmailActionLink({request:ctx.request,configuredUrl:registerConfig.emailVerification?.redirectUrl,path:"/verify-email",query:{token:verificationToken},allowedOrigins:config.authentication?.trustedAppOrigins});emailService.sendVerificationEmail(email,email.split("@")[0]||"User",verificationLink,appName||"Nucleus").then((result2)=>{if(result2.success)log4.info("Verification email sent",{email});else log4.error("Failed to send verification email",{email,error:result2.error})}).catch((err)=>{log4.error("Failed to send verification email",{email,error:err})});let resendCooldown=registerConfig.emailVerification?.resendCooldown||"60s",cooldownSeconds=parseTimeToMs(resendCooldown)/1000,maxResendAttempts=registerConfig.emailVerification?.maxResendAttempts||3;if(config.authentication?.preventUserEnumeration)return{success:!0,message:"Registration successful. Please check your email to verify your account."};return{success:!0,message:"Registration successful. Please check your email to verify your account.",data:{user:{id:newUser.id,email:newUser.email},emailVerificationRequired:!0,verification:{cooldownSeconds,canResendAt:new Date(Date.now()+cooldownSeconds*1000).toISOString(),attemptsRemaining:maxResendAttempts-1,maxAttempts:maxResendAttempts}}}}if(sendWelcomeEmail)sendWelcomeEmail(email,firstName||email.split("@")[0]||"User").catch((err)=>{log4.error("Failed to send welcome email",{email,error:err})});if(signAccessToken&&signRefreshToken&&createSession){let ipAddress=ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||ctx.request.headers.get("x-real-ip")?.trim()||"unknown",userAgent=ctx.request.headers.get("user-agent")||"",accessToken=signAccessToken(newUser.id,void 0,void 0,ctx.request.headers.get("x-tenant-schema")??void 0),refreshToken=signRefreshToken(newUser.id),sessionParams={userId:newUser.id,deviceInfo:ensureDeviceInfo({ipAddress,userAgent},deviceContextFromRequest(ctx.request).clientHints),loginMethod:"register",rememberMe:!0},sessionId=await createSession(sessionParams),deviceTokenCookie;if(saveSessionToDb){let reqSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0,dtCfg=config.authentication?.deviceTrust;if(dtCfg?.enabled)sessionParams.deviceToken=readDeviceTokenFromCookieHeader(ctx.request.headers.get("cookie"),dtCfg);deviceTokenCookie=(await saveSessionToDb(sessionId,sessionParams,reqSchemaName))?.deviceTokenCookie}let tokenConfig={accessToken:{setHeadersEnabled:tokenResponseConfig?.accessToken?.setHeadersEnabled??!0,returnJson:tokenResponseConfig?.accessToken?.returnJson??!0},refreshToken:{setHeadersEnabled:tokenResponseConfig?.refreshToken?.setHeadersEnabled??!0,returnJson:tokenResponseConfig?.refreshToken?.returnJson??!0},sessionToken:{setHeadersEnabled:tokenResponseConfig?.sessionToken?.setHeadersEnabled??!0,returnJson:tokenResponseConfig?.sessionToken?.returnJson??!0}},securePart=cookies.secure?"; Secure":"",domainPart=cookies.domain?`; Domain=${cookies.domain}`:"",cookieOptions=`; Path=${cookies.path}; HttpOnly; SameSite=${cookies.sameSite}${securePart}${domainPart}`,cookiesToSet=[];if(tokenConfig.accessToken.setHeadersEnabled)cookiesToSet.push(`${cookies.accessTokenName}=${accessToken}${cookieOptions}; Max-Age=${cookies.accessTokenMaxAge}`);if(tokenConfig.refreshToken.setHeadersEnabled)cookiesToSet.push(`${cookies.refreshTokenName}=${refreshToken}${cookieOptions}; Max-Age=${cookies.refreshTokenMaxAge}`);if(tokenConfig.sessionToken.setHeadersEnabled)cookiesToSet.push(`${cookies.sessionTokenName}=${sessionId}${cookieOptions}; Max-Age=${cookies.sessionTokenMaxAge}`);if(deviceTokenCookie)cookiesToSet.push(deviceTokenCookie);let responseData={user:{id:newUser.id,email:newUser.email}};if(tokenConfig.accessToken.returnJson)responseData.accessToken=accessToken;if(tokenConfig.refreshToken.returnJson)responseData.refreshToken=refreshToken;if(tokenConfig.sessionToken.returnJson)responseData.sessionId=sessionId;let jsonBody=JSON.stringify({success:!0,data:responseData}),headers=new Headers;headers.set("Content-Type","application/json"),headers.set("x-session-id",sessionId);for(let cookie of cookiesToSet)headers.append("Set-Cookie",cookie);return new Response(jsonBody,{status:200,headers})}return{success:!0,data:{user:{id:newUser.id,email:newUser.email}}}},{body:RegisterBodySchema,detail:{tags:["Authentication"],summary:"Register",description:"Register a new user account"}}),registerRoutes}var init_register=__esm(()=>{init_DeviceTrust();init_scopes();init_assignDefaultRole();init_deviceInfo();init_types19();init_utils6();init_types19()});import{t as t38}from"elysia";var SessionResponseSchema,SessionListResponseSchema,RevokeSessionBodySchema,RevokeAllSessionsBodySchema,SessionActivityResponseSchema,AdminRevokeSessionsBodySchema;var init_types20=__esm(()=>{SessionResponseSchema=t38.Object({id:t38.String(),deviceName:t38.Optional(t38.String()),deviceType:t38.Optional(t38.String()),browserName:t38.Optional(t38.String()),browserVersion:t38.Optional(t38.String()),osName:t38.Optional(t38.String()),osVersion:t38.Optional(t38.String()),ipAddress:t38.String(),locationCountry:t38.Optional(t38.String()),locationCity:t38.Optional(t38.String()),lastActivityAt:t38.String(),createdAt:t38.String(),isCurrent:t38.Boolean(),loginMethod:t38.Optional(t38.String()),trustScore:t38.Optional(t38.Number())}),SessionListResponseSchema=t38.Object({success:t38.Boolean(),data:t38.Optional(t38.Object({sessions:t38.Array(SessionResponseSchema),currentSessionId:t38.Optional(t38.String()),totalCount:t38.Number()})),message:t38.Optional(t38.String())}),RevokeSessionBodySchema=t38.Object({reason:t38.Optional(t38.String())}),RevokeAllSessionsBodySchema=t38.Object({excludeCurrent:t38.Optional(t38.Boolean()),reason:t38.Optional(t38.String())}),SessionActivityResponseSchema=t38.Object({success:t38.Boolean(),data:t38.Optional(t38.Object({recentActivity:t38.Array(t38.Object({sessionId:t38.String(),action:t38.String(),ipAddress:t38.String(),timestamp:t38.String(),deviceInfo:t38.Optional(t38.String())}))})),message:t38.Optional(t38.String())}),AdminRevokeSessionsBodySchema=t38.Optional(t38.Object({sessionId:t38.Optional(t38.String()),reason:t38.Optional(t38.String())}))});function formatSessionForResponse(session){let get=(snake,camel3)=>session[snake]??session[camel3],getDate=(snake,camel3)=>{let val=session[snake]??session[camel3];if(!val)return;return val instanceof Date?val.toISOString():String(val)};return{id:session.id,deviceName:get("device_name","deviceName"),deviceType:get("device_type","deviceType"),deviceFingerprint:get("device_fingerprint","deviceFingerprint"),browserName:get("browser_name","browserName"),browserVersion:get("browser_version","browserVersion"),osName:get("os_name","osName"),osVersion:get("os_version","osVersion"),ipAddress:get("ip_address","ipAddress"),locationCountry:get("location_country","locationCountry"),locationCity:get("location_city","locationCity"),lastActivityAt:getDate("last_activity_at","lastActivityAt"),createdAt:getDate("created_at","createdAt"),isCurrent:get("is_current","isCurrent"),loginMethod:get("login_method","loginMethod"),trustScore:get("trust_score","trustScore")}}var init_utils10=()=>{};import{and as and20,desc as desc6,eq as eq56,isNull as isNull3}from"drizzle-orm";import{Elysia as Elysia44}from"elysia";function createSessionsRoute(config,sessionsConfig,sessionsTable){let{db,logger:logger2}=config,log4=logger2.scoped(AUTH_SESSION),baseRoute=sessionsConfig.route||"/auth/sessions",sessionsRoutes=new Elysia44;if(!sessionsConfig.enabled||!sessionsTable)return sessionsRoutes;let tableColumns=sessionsTable,_col=(name2)=>{let snakeCase=name2.replace(/([A-Z])/g,"_$1").toLowerCase();return tableColumns[name2]||tableColumns[snakeCase]},getSessionsForRequest=(request)=>{let resolved=resolveAuthTablesForRequest(request,config),reqTable=resolved.sessionsTable||sessionsTable,reqCols=reqTable,reqCol=(name2)=>{let snakeCase=name2.replace(/([A-Z])/g,"_$1").toLowerCase();return reqCols[name2]||reqCols[snakeCase]},trustedDevicesTable=resolved.schemaTables?.trustedDevices??resolved.schemaTables?.trusted_devices;return{sessionsTable:reqTable,col:reqCol,trustedDevicesTable}};sessionsRoutes.get(baseRoute,async(ctx)=>{let{sessionsTable:sessionsTable2,col:col15}=getSessionsForRequest(ctx.request);if(!db)return{success:!1,message:"Database not configured"};let userId=ctx.request.headers.get("x-user-id");if(!userId)return{success:!1,message:"Authentication required"};let currentSessionId=ctx.request.headers.get("x-session-id"),sessions=await db.select().from(sessionsTable2).where(and20(eq56(col15("userId"),userId),eq56(col15("isActive"),!0),isNull3(col15("revokedAt")))).orderBy(desc6(col15("lastActivityAt"))),realSessions=sessions.filter((s)=>{let sess=s,fp=(sess.deviceFingerprint||"").toLowerCase(),ip=sess.ipAddress||"";return!((!fp||fp==="--"||fp==="--unknown"||fp.includes("bot/crawler")||fp.includes("headless")||fp.includes("unknown-unknown"))&&(ip==="127.0.0.1"||ip==="::1"||ip==="localhost"||!ip))}),formattedSessions=realSessions.map((s)=>{let session=s,formatted=formatSessionForResponse(session);return formatted.isCurrent=session.id===currentSessionId,formatted});return log4.info("Sessions list retrieved",{userId,totalInDb:sessions.length,filteredCount:realSessions.length,hiddenBotSessions:sessions.length-realSessions.length}),{success:!0,data:{sessions:formattedSessions,currentSessionId,totalCount:realSessions.length}}},{detail:{tags:["Authentication"],summary:"List active sessions",description:"Get all active sessions for the current user"}}),sessionsRoutes.get(`${baseRoute}/current`,async(ctx)=>{let{sessionsTable:sessionsTable2,col:col15}=getSessionsForRequest(ctx.request);if(!db)return{success:!1,message:"Database not configured"};let userId=ctx.request.headers.get("x-user-id"),sessionId=ctx.request.headers.get("x-session-id");if(!userId||!sessionId)return{success:!1,message:"Authentication required"};let sessions=await db.select().from(sessionsTable2).where(eq56(col15("id"),sessionId)).limit(1);if(sessions.length===0)return{success:!1,message:"Session not found"};let session=sessions[0],formatted=formatSessionForResponse(session);return formatted.isCurrent=!0,{success:!0,data:formatted}},{detail:{tags:["Authentication"],summary:"Get current session",description:"Get details of the current session"}}),sessionsRoutes.delete(`${baseRoute}/:sessionId`,async(ctx)=>{let{sessionsTable:sessionsTable2,col:col15}=getSessionsForRequest(ctx.request);if(!db)return{success:!1,message:"Database not configured"};let userId=ctx.request.headers.get("x-user-id");if(!userId)return{success:!1,message:"Authentication required"};let{sessionId}=ctx.params,body=ctx.body,currentSessionId=ctx.request.headers.get("x-session-id");if((await db.select().from(sessionsTable2).where(and20(eq56(col15("id"),sessionId),eq56(col15("userId"),userId))).limit(1)).length===0)return{success:!1,message:"Session not found"};let isCurrentSession=sessionId===currentSessionId;await db.update(sessionsTable2).set({isActive:!1,revokedAt:new Date,revokedReason:isCurrentSession?"user_logout":body.reason||"user_revoked"}).where(eq56(col15("id"),sessionId)),log4.info("Session revoked",{userId,sessionId,isCurrentSession,reason:body.reason||"user_revoked"});let reqUrl=new URL(ctx.request.url);return logger2.audit({entityName:"user_sessions",entityId:sessionId,operation:isCurrentSession?"LOGOUT":"SESSION_REVOKE",userId:userId||void 0,summary:isCurrentSession?"User logged out via session revoke":`Session revoked (${sessionId.substring(0,8)}...)`,ipAddress:ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||ctx.request.headers.get("x-real-ip")?.trim()||"unknown",userAgent:ctx.request.headers.get("user-agent")||"unknown",path:reqUrl.pathname,query:reqUrl.search}),{success:!0,message:isCurrentSession?"Logged out successfully":"Session revoked successfully"}},{body:RevokeSessionBodySchema,detail:{tags:["Authentication"],summary:"Revoke session",description:"Revoke a specific session by ID"}}),sessionsRoutes.delete(`${baseRoute}/all`,async(ctx)=>{let{sessionsTable:sessionsTable2,col:col15}=getSessionsForRequest(ctx.request);if(!db)return{success:!1,message:"Database not configured"};let userId=ctx.request.headers.get("x-user-id");if(!userId)return{success:!1,message:"Authentication required"};let body=ctx.body,currentSessionId=ctx.request.headers.get("x-session-id"),query=db.update(sessionsTable2).set({isActive:!1,revokedAt:new Date,revokedReason:body.reason||"user_revoked"}).where(and20(eq56(col15("userId"),userId),eq56(col15("isActive"),!0)));if(body.excludeCurrent&¤tSessionId){let{ne:ne3}=await import("drizzle-orm");query=db.update(sessionsTable2).set({isActive:!1,revokedAt:new Date,revokedReason:body.reason||"user_revoked"}).where(and20(eq56(col15("userId"),userId),eq56(col15("isActive"),!0),ne3(col15("id"),currentSessionId)))}await query,log4.info("All sessions revoked",{userId,excludeCurrent:body.excludeCurrent,reason:body.reason||"user_revoked"});let reqUrl=new URL(ctx.request.url);return logger2.audit({entityName:"user_sessions",operation:"SESSION_REVOKE_ALL",userId:userId||void 0,summary:body.excludeCurrent?"All other sessions revoked":"All sessions revoked",ipAddress:ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||ctx.request.headers.get("x-real-ip")?.trim()||"unknown",userAgent:ctx.request.headers.get("user-agent")||"unknown",path:reqUrl.pathname,query:reqUrl.search}),{success:!0,message:body.excludeCurrent?"All other sessions revoked successfully":"All sessions revoked successfully"}},{body:RevokeAllSessionsBodySchema,detail:{tags:["Authentication"],summary:"Revoke all sessions",description:"Revoke all sessions for the current user (optionally exclude current)"}}),sessionsRoutes.get(`${baseRoute}/stats`,async(ctx)=>{let{sessionsTable:sessionsTable2,col:col15}=getSessionsForRequest(ctx.request);if(!db)return{success:!1,message:"Database not configured"};let userId=ctx.request.headers.get("x-user-id");if(!userId)return{success:!1,message:"Authentication required"};let{count:count3,countDistinct}=await import("drizzle-orm"),activeSessions=await db.select({count:count3()}).from(sessionsTable2).where(and20(eq56(col15("userId"),userId),eq56(col15("isActive"),!0),isNull3(col15("revokedAt")))),uniqueDevices=await db.select({count:countDistinct(col15("deviceFingerprint"))}).from(sessionsTable2).where(and20(eq56(col15("userId"),userId),eq56(col15("isActive"),!0))),uniqueIps=await db.select({count:countDistinct(col15("ipAddress"))}).from(sessionsTable2).where(and20(eq56(col15("userId"),userId),eq56(col15("isActive"),!0)));return{success:!0,data:{activeSessions:activeSessions[0]?.count||0,uniqueDevices:uniqueDevices[0]?.count||0,uniqueIpAddresses:uniqueIps[0]?.count||0}}},{detail:{tags:["Authentication"],summary:"Session statistics",description:"Get session statistics for the current user"}});let APPROVAL_TOKEN_TTL_MS=86400000,buildResultHtml=(action,success,message)=>{let icon=success?action==="approve"?"\u2705":"\uD83D\uDEAB":"\u274C",title=success?action==="approve"?"Device Approved":"Device Rejected":"Action Failed",bName=sessionsConfig.brandName||"Nucleus";return`<!DOCTYPE html>
|
|
445
|
+
`}),logger2.info("[OAUTH] Invite email sent to new OAuth user",{userId,email:profile.email,provider})}catch(emailErr){logger2.warn("[OAUTH] Failed to send invite email",{userId,error:emailErr})}}{let stateUser=(await db.select().from(usersTable).where(eq50(usersTable.id,userId)).limit(1))[0],state2=stateUser?evaluateAccountState(stateUser):{allowed:!1,reason:"inactive",message:"Account is not active"};if(!state2.allowed)return logger2.warn(`[OAUTH] Login blocked - account ${state2.reason}`,{userId,provider}),ctx.set.status=302,ctx.set.headers.Location=`${errorRedirect}?error=account_${state2.reason}`,null}let defaultRoleNames=resolveDefaultRoleNames(config.authentication);if(defaultRoleNames.length>0){let resolvedForRole=resolveAuthTablesForRequest(ctx.request,config);await assignDefaultRoles({db,userId,roleNames:defaultRoleNames,rolesTable:resolvedForRole.rolesTable??config.rolesTable??null,userRolesTable:resolvedForRole.userRolesTable??config.userRolesTable??null,logger:logger2})}let syncConfig=oauthService.profileSync;if(syncConfig){let syncTables=resolveAuthTablesForRequest(ctx.request,config);await applyProfileSync({db,usersTable,profilesTable:syncTables.schemaTables?.profiles,userId,rawProfile:profile.rawProfile,config:syncConfig,isNewUser:createdNewUser,logger:logger2})}await db.update(usersTable).set({lastLoginAt:new Date}).where(eq50(usersTable.id,userId));let ipAddress=ctx.request.headers.get("cf-connecting-ip")?.trim()||ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||ctx.request.headers.get("x-real-ip")?.trim()||"127.0.0.1",userAgent=ctx.request.headers.get("user-agent")||"Unknown Browser",deviceInfo=parseUserAgentForLogin(userAgent,ipAddress,void 0,deviceContextFromRequest(ctx.request).clientHints),oauthUserRoles=[],oauthUserClaims=[],oauthUserClaimScopes={};if(config.db)try{let resolved2=resolveAuthTablesForRequest(ctx.request,config),rc=await fetchUserRolesAndClaims(config.db,userId,resolved2);oauthUserRoles=rc.roles,oauthUserClaims=rc.claims,oauthUserClaimScopes=rc.claimScopes}catch{}let accessToken=signAccessToken(userId,oauthUserRoles.length>0?oauthUserRoles:void 0,oauthUserClaims.length>0?oauthUserClaims:void 0,ctx.request.headers.get("x-tenant-schema")??void 0,oauthUserClaimScopes),refreshToken=signRefreshToken(userId),requestOrigin=ctx.request.headers.get("origin")||ctx.request.headers.get("referer")?.replace(/\/[^/]*$/,"")||void 0,sessionParams={userId,deviceInfo,loginMethod:`oauth:${provider}`,rememberMe:!0,requestOrigin},sessionId=await createSession(sessionParams),requiresApproval=!1,approvalSessionId=sessionId,deviceTokenCookie;if(saveSessionToDb){let oauthSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0,dtCfg=config.authentication?.deviceTrust;if(dtCfg?.enabled)sessionParams.deviceToken=readDeviceTokenFromCookieHeader(ctx.request.headers.get("cookie"),dtCfg);let saveResult=await saveSessionToDb(sessionId,sessionParams,oauthSchemaName);if(saveResult?.denied)return logger2.warn("[OAUTH] Login blocked by device-trust",{userId,provider}),ctx.set.status=302,ctx.set.headers.Location=`${errorRedirect}?error=device_not_recognized`,null;if(deviceTokenCookie=saveResult?.deviceTokenCookie,saveResult?.requiresApproval){if(requiresApproval=!0,saveResult.sessionId)approvalSessionId=saveResult.sessionId}}if(requiresApproval){if(deviceTokenCookie)ctx.set.headers["Set-Cookie"]=deviceTokenCookie;logger2.info("[OAUTH] Login requires device approval",{userId,provider,approvalSessionId});let pendingUrl=absolutizeRedirect(resolveSafeOAuthRedirect(statePayload.redirectUrl,oauthService.successRedirectUrl,config.authentication?.trustedAppOrigins),portalOrigin);return ctx.set.status=302,ctx.set.headers.Location=`${pendingUrl}?requires_approval=true&provider=${provider}&session_id=${approvalSessionId}`,null}await logger2.audit({entityName:"users",entityId:userId,operation:"LOGIN",userId,summary:`${profile.email??profile.providerAccountId} logged in via OAuth (${provider})`,ipAddress,userAgent,path:`${basePath}/${provider}/callback`,query:""});let securePart=cookies.secure?"; Secure":"",domainPart=cookies.domain?`; Domain=${cookies.domain}`:"",cookieOptions=`; Path=${cookies.path}; HttpOnly; SameSite=${cookies.sameSite}${securePart}${domainPart}`,cookiesToSet=[];if(tokenConfig.accessToken.setHeadersEnabled)cookiesToSet.push(`${cookies.accessTokenName}=${accessToken}${cookieOptions}; Max-Age=${cookies.accessTokenMaxAge}`);if(tokenConfig.refreshToken.setHeadersEnabled)cookiesToSet.push(`${cookies.refreshTokenName}=${refreshToken}${cookieOptions}; Max-Age=${cookies.refreshTokenMaxAge}`);if(tokenConfig.sessionToken.setHeadersEnabled)cookiesToSet.push(`${cookies.sessionTokenName}=${sessionId}${cookieOptions}; Max-Age=${cookies.sessionTokenMaxAge}`);if(deviceTokenCookie)cookiesToSet.push(deviceTokenCookie);let successUrl=absolutizeRedirect(resolveSafeOAuthRedirect(statePayload.redirectUrl,oauthService.successRedirectUrl,config.authentication?.trustedAppOrigins),portalOrigin),headers=new Headers;headers.set("Location",successUrl);for(let cookie of cookiesToSet)headers.append("Set-Cookie",cookie);return new Response(null,{status:302,headers})},{detail:{tags:["Authentication"],summary:"OAuth Callback",description:"Handles the OAuth provider callback, creates session and sets cookies"}}),routes.get(`${basePath}/providers`,()=>{return{success:!0,data:{providers:oauthService.getEnabledProviders()}}},{detail:{tags:["Authentication"],summary:"List OAuth Providers",description:"Returns the list of enabled OAuth providers"}}),routes.get(`${basePath}/link/:provider`,async(ctx)=>{let provider=ctx.params.provider;if(!oauthService.isProviderEnabled(provider))return ctx.set.status=404,{success:!1,message:`OAuth provider "${provider}" is not enabled`};if(!oauthService.allowAccountLinking)return ctx.set.status=403,{success:!1,message:"Account linking is disabled"};let userId=ctx.request.headers.get("x-user-id");if(!userId)return ctx.set.status=401,{success:!1,message:"Authentication required to link accounts"};let redirectUrl=ctx.query.redirect_url,authUrl=await oauthService.buildAuthorizationUrl(provider,userId,redirectUrl),issuedState=new URL(authUrl).searchParams.get("state");if(issuedState)ctx.set.headers["Set-Cookie"]=buildOAuthStateCookie(issuedState,{maxAge:600,secure:cookies.secure,domain:cookies.domain});return ctx.set.status=302,ctx.set.headers.Location=authUrl,null},{detail:{tags:["Authentication"],summary:"Link OAuth Account",description:"Redirects to provider to link an OAuth account to the current user"}}),routes.delete(`${basePath}/unlink/:provider`,async(ctx)=>{let provider=ctx.params.provider,userId=ctx.request.headers.get("x-user-id");if(!userId)return ctx.set.status=401,{success:!1,message:"Authentication required"};if(!db)return ctx.set.status=500,{success:!1,message:"Database not configured"};let oauthTable=config.oauthAccountsTable;if(!oauthTable)return ctx.set.status=500,{success:!1,message:"OAuth accounts table not configured"};if((await db.select().from(oauthTable).where(and18(eq50(oauthTable.userId,userId),eq50(oauthTable.provider,provider))).limit(1)).length===0)return{success:!1,message:`No linked ${provider} account found`};let{usersTable:resolvedUsersTable}=resolveAuthTablesForRequest(ctx.request,config);if(!resolvedUsersTable)return ctx.set.status=500,{success:!1,message:"Users table not configured"};let hasPassword=!!(await db.select().from(resolvedUsersTable).where(eq50(resolvedUsersTable.id,userId)).limit(1))[0]?.password,allOAuthAccounts=await db.select().from(oauthTable).where(eq50(oauthTable.userId,userId));if(!hasPassword&&allOAuthAccounts.length<=1)return ctx.set.status=400,{success:!1,message:"Cannot unlink the only login method. Set a password first."};return await db.delete(oauthTable).where(and18(eq50(oauthTable.userId,userId),eq50(oauthTable.provider,provider))),logger2.info("[OAUTH] Account unlinked",{userId,provider}),{success:!0,message:`${provider} account unlinked successfully`}},{detail:{tags:["Authentication"],summary:"Unlink OAuth Account",description:"Removes a linked OAuth account from the current user"}}),routes.get(`${basePath}/accounts`,async(ctx)=>{let userId=ctx.request.headers.get("x-user-id");if(!userId)return ctx.set.status=401,{success:!1,message:"Authentication required"};if(!db)return ctx.set.status=500,{success:!1,message:"Database not configured"};let oauthTable=config.oauthAccountsTable;if(!oauthTable)return{success:!0,data:{accounts:[]}};return{success:!0,data:{accounts:(await db.select().from(oauthTable).where(eq50(oauthTable.userId,userId))).map((a12)=>({id:a12.id,provider:a12.provider,providerEmail:a12.providerEmail,providerName:a12.providerName,providerAvatarUrl:a12.providerAvatarUrl,isPrimary:a12.isPrimary,lastUsedAt:a12.lastUsedAt,createdAt:a12.createdAt}))}}},{detail:{tags:["Authentication"],summary:"List Linked OAuth Accounts",description:"Returns all OAuth accounts linked to the current user"}}),routes}var OAUTH_STATE_COOKIE="oauth_state",readCookie=(request,name2)=>{let raw=request.headers.get("cookie");if(!raw)return;for(let part of raw.split(";")){let trimmed=part.trim(),eq51=trimmed.indexOf("=");if(eq51>0&&trimmed.slice(0,eq51)===name2)return trimmed.slice(eq51+1)}return},resolveSafeOAuthRedirect=(candidate,fallback,allowedOrigins)=>{if(!candidate)return fallback;let normalized=candidate.replace(/[\t\n\r]/g,"");if(!normalized)return fallback;if(normalized.startsWith("/")&&normalized[1]!=="/"&&normalized[1]!=="\\")return normalized;try{let candHost=new URL(normalized).host.toLowerCase(),trusted=[];try{trusted.push(new URL(fallback).host.toLowerCase())}catch{}for(let o of allowedOrigins??[])try{trusted.push(new URL(o.includes("://")?o:`https://${o}`).host.toLowerCase())}catch{}for(let t33 of trusted)if(t33.startsWith("*.")){let base=t33.slice(2);if(candHost===base||candHost.endsWith(`.${base}`))return normalized}else if(candHost===t33)return normalized}catch{}return fallback},absolutizeRedirect=(target2,portalOrigin)=>portalOrigin&&target2.startsWith("/")&&target2[1]!=="/"?`${portalOrigin}${target2}`:target2;var init_oauth=__esm(()=>{init_DeviceTrust();init_assignDefaultRole();init_fetchUserRolesAndClaims();init_deviceInfo();init_utils7();init_utils8();init_applyProfileSync()});import{t as t33}from"elysia";var PasswordChangeBodySchema,PasswordChangeResponseSchema;var init_types15=__esm(()=>{PasswordChangeBodySchema=t33.Object({currentPassword:t33.Optional(t33.String()),newPassword:t33.String({minLength:8}),confirmPassword:t33.String({minLength:8})}),PasswordChangeResponseSchema=t33.Object({success:t33.Boolean(),message:t33.Optional(t33.String())})});function validatePasswordsMatch(newPassword,confirmPassword){return newPassword===confirmPassword}var exports_store={};__export(exports_store,{userHasNoTrustedDevices:()=>userHasNoTrustedDevices,touchDevice:()=>touchDevice,revokeUserDevices:()=>revokeUserDevices,revokeDevicesBySessionIds:()=>revokeDevicesBySessionIds,revokeDeviceById:()=>revokeDeviceById,rejectDeviceForSession:()=>rejectDeviceForSession,listUserDevices:()=>listUserDevices,insertDeviceRow:()=>insertDeviceRow,findDeviceRowsByRawToken:()=>findDeviceRowsByRawToken,activateDeviceForSession:()=>activateDeviceForSession});import{and as and19,eq as eq51,inArray as inArray16}from"drizzle-orm";async function findDeviceRowsByRawToken(db,table,rawToken){let hash=hashDeviceToken(rawToken);return await db.select().from(table).where(eq51(col14(table,"tokenHash"),hash)).limit(2)}async function insertDeviceRow(db,table,v){let now=new Date;await db.insert(table).values({id:v.id,userId:v.userId,tokenHash:v.tokenHash,status:v.status,originSessionId:v.originSessionId??null,label:v.label??null,deviceFingerprint:v.deviceFingerprint??null,firstSeenIp:v.firstSeenIp??null,lastSeenIp:v.lastSeenIp??v.firstSeenIp??null,lastSeenAt:now,expiresAt:v.expiresAt,isActive:!0,createdAt:now,updatedAt:now})}async function activateDeviceForSession(db,table,sessionId){return(await db.update(table).set({status:"trusted",updatedAt:new Date}).where(and19(eq51(col14(table,"originSessionId"),sessionId),eq51(col14(table,"status"),"pending"))).returning({id:col14(table,"id")})).length}async function rejectDeviceForSession(db,table,sessionId){await db.update(table).set({status:"revoked",revokedAt:new Date,revokedReason:"replaced",isActive:!1,updatedAt:new Date}).where(and19(eq51(col14(table,"originSessionId"),sessionId),eq51(col14(table,"status"),"pending")))}async function touchDevice(db,table,id,ip){await db.update(table).set({lastSeenAt:new Date,lastSeenIp:ip??null,updatedAt:new Date}).where(eq51(col14(table,"id"),id))}async function revokeWhere(db,table,where,reason){await db.update(table).set({status:"revoked",revokedAt:new Date,revokedReason:reason,isActive:!1,updatedAt:new Date}).where(where)}async function revokeUserDevices(db,table,userId,reason){if(!userId)return;let notAlready=and19(eq51(col14(table,"userId"),userId),eq51(col14(table,"status"),"trusted")),alsoPending=and19(eq51(col14(table,"userId"),userId),eq51(col14(table,"status"),"pending"));await revokeWhere(db,table,notAlready,reason),await revokeWhere(db,table,alsoPending,reason)}async function revokeDevicesBySessionIds(db,table,sessionIds,reason){if(!sessionIds?.length)return;await revokeWhere(db,table,inArray16(col14(table,"originSessionId"),sessionIds),reason)}async function revokeDeviceById(db,table,id,userId,reason){return(await db.update(table).set({status:"revoked",revokedAt:new Date,revokedReason:reason,isActive:!1,updatedAt:new Date}).where(and19(eq51(col14(table,"id"),id),eq51(col14(table,"userId"),userId))).returning({id:col14(table,"id")})).length}async function listUserDevices(db,table,userId){return(await db.select().from(table).where(eq51(col14(table,"userId"),userId))).map((r2)=>{let{tokenHash:_omit,...safe}=r2;return safe})}async function userHasNoTrustedDevices(db,table,userId){return(await db.select({id:col14(table,"id")}).from(table).where(and19(eq51(col14(table,"userId"),userId),eq51(col14(table,"status"),"trusted"))).limit(1)).length===0}var col14=(t34,name2)=>t34[name2];var init_store2=__esm(()=>{init_DeviceTrust()});import{eq as eq52}from"drizzle-orm";import{Elysia as Elysia39}from"elysia";function createPasswordChangeRoute(config,passwordChangeConfig){let{db,logger:logger2}=config,log4=logger2.scoped(AUTH_PASSWORD_CHANGE),route=passwordChangeConfig.route||"/auth/password-change",passwordChangeRoutes=new Elysia39;if(!passwordChangeConfig.enabled)return passwordChangeRoutes;return passwordChangeRoutes.post(route,async(ctx)=>{let{usersTable,sessionsTable}=resolveAuthTablesForRequest(ctx.request,config);if(!db||!usersTable)return ctx.set.status=500,{success:!1,message:"Database not configured"};let userId=ctx.request.headers.get("x-user-id");if(!userId)return ctx.set.status=401,{success:!1,message:"Authentication required"};let{currentPassword,newPassword,confirmPassword}=ctx.body;if(!validatePasswordsMatch(newPassword,confirmPassword))return ctx.set.status=422,{success:!1,message:"New passwords do not match"};let strength=validatePasswordStrength(newPassword);if(!strength.valid)return ctx.set.status=400,{success:!1,message:"Password too weak",errors:strength.errors};let user=(await db.select().from(usersTable).where(eq52(usersTable.id,userId)).limit(1))[0];if(!user)return ctx.set.status=404,{success:!1,message:"User not found"};let existingHash=user.password??"",userHasPassword=existingHash.length>0,isInitialSet=!userHasPassword,reqUrl=new URL(ctx.request.url),auditIp=ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||ctx.request.headers.get("x-real-ip")?.trim()||"unknown",auditUa=ctx.request.headers.get("user-agent")||"unknown";if(userHasPassword){if(!currentPassword)return ctx.set.status=400,{success:!1,message:"Current password is required"};if(!await verifyPassword(currentPassword,existingHash))return log4.warn("Password change failed - invalid current password",{userId}),logger2.audit({entityName:"users",entityId:userId,operation:"PASSWORD_CHANGE_FAILED",userId,summary:"Password change failed: invalid current password",ipAddress:auditIp,userAgent:auditUa,path:reqUrl.pathname,query:reqUrl.search}),ctx.set.status=400,{success:!1,message:"Current password is incorrect"}}let hashedNewPassword=await hashPassword(newPassword);if(await db.update(usersTable).set({password:hashedNewPassword,updatedAt:new Date}).where(eq52(usersTable.id,userId)),!isInitialSet&&sessionsTable)try{await db.update(sessionsTable).set({isActive:!1,revokedAt:new Date,revokedReason:"password_change"}).where(eq52(sessionsTable.userId,userId))}catch(err){log4.warn("Failed to revoke sessions after password change",{userId,error:err instanceof Error?err.message:String(err)})}if(!isInitialSet&&config.authentication?.deviceTrust?.enabled)try{let{schemaTables}=resolveAuthTablesForRequest(ctx.request,config),dtTable=schemaTables?.trustedDevices??schemaTables?.trusted_devices;if(dtTable&&db){let{revokeUserDevices:revokeUserDevices2}=await Promise.resolve().then(() => (init_store2(),exports_store));await revokeUserDevices2(db,dtTable,userId,"password_reset")}}catch(err){log4.warn("Failed to revoke device tokens after password change",{userId,error:err instanceof Error?err.message:String(err)})}return log4.info(isInitialSet?"Initial password set successful":"Password change successful",{userId}),logger2.audit({entityName:"users",entityId:userId,operation:isInitialSet?"PASSWORD_SET":"PASSWORD_CHANGE",userId,summary:isInitialSet?"Initial password set successfully":"Password changed successfully",ipAddress:auditIp,userAgent:auditUa,path:reqUrl.pathname,query:reqUrl.search}),{success:!0,message:isInitialSet?"Password set successfully":"Password changed successfully"}},{body:PasswordChangeBodySchema,detail:{tags:["Authentication"],summary:"Change Password",description:"Change password for authenticated user"}}),passwordChangeRoutes}var init_passwordChange=__esm(()=>{init_scopes();init_utils7();init_utils6();init_types15();init_types15()});function describeResetEmail(outcome){if(!outcome||outcome.sent)return{sent:!0,level:"info",message:"Password reset email sent"};let base=SENTENCES[outcome.reason],detail=outcome.detail?.trim();return{sent:!1,level:outcome.reason==="exempt"?"info":"warn",message:detail?`${base} (${detail})`:base}}var SENTENCES;var init_outcome=__esm(()=>{SENTENCES={no_provider:"Password reset email NOT sent \u2014 no email provider is configured. The token was stored, so a link exists, but nothing carried it.",exempt:"Password reset email not sent \u2014 the address is on an exempt domain this install never mails.",rejected:"Password reset email NOT sent \u2014 the provider refused the recipient. The address is accepted at login but not deliverable.",error:"Password reset email NOT sent \u2014 the send failed."}});import{t as t34}from"elysia";var PasswordResetRequestSchema,PasswordResetConfirmSchema,PasswordResetResponseSchema;var init_types16=__esm(()=>{PasswordResetRequestSchema=t34.Object({email:t34.String({format:"email"})}),PasswordResetConfirmSchema=t34.Object({token:t34.String(),newPassword:t34.String({minLength:8}),confirmPassword:t34.String({minLength:8})}),PasswordResetResponseSchema=t34.Object({success:t34.Boolean(),message:t34.Optional(t34.String())})});import{randomBytes as randomBytes7}from"crypto";function generateResetToken(){return randomBytes7(32).toString("hex")}function isTokenExpired(expiresAt){return new Date>expiresAt}var init_utils9=()=>{};import{Elysia as Elysia40}from"elysia";function createPasswordResetRoute(config,passwordResetConfig,storeResetToken,getResetToken,deleteResetToken,sendResetEmail){let{db,logger:logger2}=config,log4=logger2.scoped(AUTH_PASSWORD_RESET),baseRoute=passwordResetConfig.route||"/auth/password-reset",passwordResetRoutes=new Elysia40;if(!passwordResetConfig.enabled)return passwordResetRoutes;return passwordResetRoutes.post(`${baseRoute}/request`,async(ctx)=>{let{usersTable}=resolveAuthTablesForRequest(ctx.request,config);if(!db||!usersTable)return{success:!1,message:"Database not configured"};let{email:rawEmail}=ctx.body,email=rawEmail.toLowerCase(),{eq:eq53}=await import("drizzle-orm"),user=(await db.select().from(usersTable).where(eq53(usersTable.email,email)).limit(1))[0];if(!user)return{success:!0,message:"If email exists, reset link will be sent"};let token=generateResetToken(),expiresAt=new Date(Date.now()+3600000),reqSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0;await storeResetToken(user.id,token,expiresAt,reqSchemaName);let delivery={sent:!1,level:"warn",message:"Password reset email NOT sent \u2014 no send helper is configured."};if(sendResetEmail)try{delivery=describeResetEmail(await sendResetEmail(email,token,ctx.request))}catch(error3){delivery=describeResetEmail({sent:!1,reason:"error",detail:error3 instanceof Error?error3.message:String(error3)})}if(delivery.level==="warn")log4.warn(delivery.message,{email});else log4.info(delivery.message,{email});log4.info("Password reset requested",{userId:user.id,email});let reqUrl=new URL(ctx.request.url);return logger2.audit({entityName:"users",entityId:user.id,operation:"PASSWORD_RESET_REQUEST",userId:user.id,summary:`Password reset requested for ${email} \u2014 ${delivery.sent?"e-posta g\xF6nderildi":"E-POSTA G\xD6NDER\u0130LEMED\u0130"}`,ipAddress:ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||ctx.request.headers.get("x-real-ip")?.trim()||"unknown",userAgent:ctx.request.headers.get("user-agent")||"unknown",path:reqUrl.pathname,query:reqUrl.search}),{success:!0,message:"If email exists, reset link will be sent"}},{body:PasswordResetRequestSchema,detail:{tags:["Authentication"],summary:"Request Password Reset",description:"Request a password reset email"}}),passwordResetRoutes.post(`${baseRoute}/confirm`,async(ctx)=>{let{usersTable,sessionsTable}=resolveAuthTablesForRequest(ctx.request,config);if(!db||!usersTable)return{success:!1,message:"Database not configured"};let{token,newPassword,confirmPassword}=ctx.body;if(newPassword!==confirmPassword)return{success:!1,message:"Passwords do not match"};let strength=validatePasswordStrength(newPassword);if(!strength.valid)return ctx.set.status=400,{success:!1,message:"Password too weak",errors:strength.errors};let reqSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0,resetData=await getResetToken(token,reqSchemaName);if(!resetData)return{success:!1,message:"Invalid or expired reset token"};if(isTokenExpired(resetData.expiresAt))return await deleteResetToken(token,reqSchemaName),{success:!1,message:"Reset token has expired"};let hashedPassword=await hashPassword(newPassword),{eq:eq53}=await import("drizzle-orm");if(await db.update(usersTable).set({password:hashedPassword}).where(eq53(usersTable.id,resetData.userId)),await deleteResetToken(token,reqSchemaName),sessionsTable)try{await db.update(sessionsTable).set({isActive:!1,revokedAt:new Date,revokedReason:"password_reset"}).where(eq53(sessionsTable.userId,resetData.userId))}catch(err){log4.warn("Failed to revoke sessions after password reset",{userId:resetData.userId,error:err instanceof Error?err.message:String(err)})}try{if(config.authentication?.deviceTrust?.enabled){let{schemaTables}=resolveAuthTablesForRequest(ctx.request,config),dtTable=schemaTables?.trustedDevices??schemaTables?.trusted_devices;if(dtTable&&db){let{revokeUserDevices:revokeUserDevices2}=await Promise.resolve().then(() => (init_store2(),exports_store));await revokeUserDevices2(db,dtTable,resetData.userId,"password_reset")}}}catch(err){log4.warn("Failed to revoke device tokens after password reset",{userId:resetData.userId,error:err instanceof Error?err.message:String(err)})}log4.info("Password reset successful",{userId:resetData.userId});let reqUrl=new URL(ctx.request.url);return logger2.audit({entityName:"users",entityId:resetData.userId,operation:"PASSWORD_RESET",userId:resetData.userId,summary:"Password reset completed successfully",ipAddress:ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||ctx.request.headers.get("x-real-ip")?.trim()||"unknown",userAgent:ctx.request.headers.get("user-agent")||"unknown",path:reqUrl.pathname,query:reqUrl.search}),{success:!0,message:"Password has been reset"}},{body:PasswordResetConfirmSchema,detail:{tags:["Authentication"],summary:"Confirm Password Reset",description:"Reset password with token"}}),passwordResetRoutes}var init_passwordReset=__esm(()=>{init_scopes();init_utils6();init_outcome();init_types16();init_utils9();init_types16()});import{t as t35}from"elysia";var PasswordSetBodySchema,PasswordSetResponseSchema;var init_types17=__esm(()=>{PasswordSetBodySchema=t35.Object({newPassword:t35.String({minLength:8}),userId:t35.Optional(t35.String()),token:t35.Optional(t35.String())}),PasswordSetResponseSchema=t35.Object({success:t35.Boolean(),message:t35.String()})});import{eq as eq53}from"drizzle-orm";import{Elysia as Elysia41}from"elysia";function createPasswordSetRoute(config,passwordSetConfig,getMagicToken,deleteMagicToken){let{db,logger:logger2}=config,log4=logger2.scoped(AUTH_PASSWORD_SET),route=passwordSetConfig.route||"/auth/password-set",passwordSetRoutes=new Elysia41;if(!passwordSetConfig.enabled)return passwordSetRoutes;return passwordSetRoutes.post(route,async(ctx)=>{log4.info("Password set request received");let{usersTable}=resolveAuthTablesForRequest(ctx.request,config);if(!db||!usersTable)return log4.error("Password set failed - database not configured"),{success:!1,message:"Database not configured"};let{newPassword,token}=ctx.body,strength=validatePasswordStrength(newPassword);if(!strength.valid)return ctx.set.status=400,{success:!1,message:"Password too weak",errors:strength.errors};let userId=null;if(token&&getMagicToken&&deleteMagicToken){let reqSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0,tokenHash=hashToken(token,TOKEN_PURPOSE.invite),storedToken=await getMagicToken(tokenHash,reqSchemaName);if(!storedToken)return log4.warn("Password set failed - invalid token"),{success:!1,message:"Invalid or expired token"};if(new Date>storedToken.expiresAt)return await deleteMagicToken(tokenHash,reqSchemaName),log4.warn("Password set failed - expired token",{email:storedToken.email}),{success:!1,message:"Invalid or expired token"};userId=storedToken.userId,log4.info("Password set - userId resolved from token",{userId,email:storedToken.email})}else userId=ctx.request.headers.get("x-user-id");if(!userId)return log4.warn("Password set failed - no token and no authenticated user"),{success:!1,message:"Valid token or authentication required"};let user=(await db.select().from(usersTable).where(eq53(usersTable.id,userId)).limit(1))[0];if(log4.info("Password set - user found",{found:!!user,hasPassword:!!user?.password}),!user)return{success:!1,message:"User not found"};if(user.password)return log4.warn("Password set failed - user already has password",{userId}),{success:!1,message:"Password already set. Use password change instead."};let hashedPassword=await hashPassword(newPassword);log4.info("Password set - updating user with verifiedAt",{userId});let updateResult=await db.update(usersTable).set({password:hashedPassword,verifiedAt:new Date,updatedAt:new Date}).where(eq53(usersTable.id,userId));if(log4.info("Password set successful for invited user",{userId,updateResult}),token&&deleteMagicToken){let setSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0,tokenHash=hashToken(token,TOKEN_PURPOSE.invite);await deleteMagicToken(tokenHash,setSchemaName),log4.info("Invite token consumed after password set",{userId})}return{success:!0,message:"Password set successfully"}},{body:PasswordSetBodySchema,detail:{tags:["Authentication"],summary:"Set Password",description:"Set password for the first time (for invited users who do not have a password yet)"}}),passwordSetRoutes}var init_passwordSet=__esm(()=>{init_scopes();init_utils8();init_utils6();init_types17();init_types17()});function parseTimeToSeconds3(time2){let match=time2.match(/^(\d+)([smhd])$/);if(!match)return 900;let value2=match[1],unit=match[2];if(!value2||!unit)return 900;let num3=parseInt(value2,10);switch(unit){case"s":return num3;case"m":return num3*60;case"h":return num3*3600;case"d":return num3*86400;default:return 900}}import{t as t36}from"elysia";var RefreshResponseSchema;var init_types18=__esm(()=>{RefreshResponseSchema=t36.Object({success:t36.Boolean(),message:t36.Optional(t36.String()),data:t36.Optional(t36.Object({accessToken:t36.String()}))})});import{eq as eq54}from"drizzle-orm";import{Elysia as Elysia42}from"elysia";function createRefreshRoute(config,refreshConfig,verifyRefreshToken,signAccessToken,signRefreshToken,tokenResponseConfig,cookieMaxAgeBufferSeconds,cookieConfig){let{logger:logger2,authentication}=config,route=refreshConfig.route||"/auth/refresh",refreshRoutes=new Elysia42;if(!refreshConfig.enabled)return refreshRoutes;return refreshRoutes.post(route,async(ctx)=>{let refreshTokenName=authentication?.refreshToken?.name||"refresh_token",accessTokenName=authentication?.accessToken?.name||"access_token",refreshToken=ctx.request.headers.get("x-refresh-token");if(!refreshToken){let cookieHeader=ctx.request.headers.get("cookie");if(cookieHeader)refreshToken=cookieHeader.split(";").reduce((acc,cookie)=>{let trimmed=cookie.trim(),eqIndex=trimmed.indexOf("=");if(eqIndex>0)acc[trimmed.slice(0,eqIndex)]=trimmed.slice(eqIndex+1);return acc},{})[refreshTokenName]||null}if(!refreshToken)return logger2.debug("[AUTH] Refresh skipped - no refresh cookie on the request"),ctx.set.status=401,{success:!1,message:"Refresh token required"};let result=verifyRefreshToken(refreshToken);if(!result.valid||!result.payload)return logger2.warn("[AUTH] Refresh failed - invalid token"),ctx.set.status=401,{success:!1,message:"Invalid refresh token"};let userId=result.payload.sub,userRoles=[],userClaims=[],userClaimScopes={},resolved=resolveAuthTablesForRequest(ctx.request,config);if(config.db){try{let rc=await fetchUserRolesAndClaims(config.db,userId,resolved);userRoles=rc.roles,userClaims=rc.claims,userClaimScopes=rc.claimScopes}catch(roleError){logger2.error("[AUTH] Failed to fetch roles/claims during refresh",{userId,error:roleError instanceof Error?roleError.message:String(roleError)})}try{let usersTable=resolved.usersTable;if(usersTable){let user=(await config.db.select().from(usersTable).where(eq54(usersTable.id,userId)).limit(1))[0];if(!user||user.isActive===!1)return logger2.warn("[AUTH] Refresh blocked - account inactive",{userId}),ctx.set.status=401,{success:!1,message:"Account is not active"};let lockedUntilRaw=user.lockedUntil,lockedUntil=lockedUntilRaw?new Date(lockedUntilRaw):null;if(user.isLocked===!0&&(!lockedUntil||lockedUntil>new Date))return logger2.warn("[AUTH] Refresh blocked - account locked",{userId}),ctx.set.status=401,{success:!1,message:"Account is locked"};let userCohortId=user.cohortId;if(userCohortId){let cohortsTable=resolved.schemaTables?.userCohorts??resolved.schemaTables?.user_cohorts;if(cohortsTable){let cohortExpiresAt=(await config.db.select().from(cohortsTable).where(eq54(cohortsTable.id,userCohortId)).limit(1))[0]?.expiresAt;if(cohortExpiresAt&&new Date(cohortExpiresAt)<new Date)return logger2.warn("[AUTH] Refresh blocked - cohort expired",{userId,cohortId:userCohortId}),ctx.set.status=401,{success:!1,message:"Your access has expired. Please contact your administrator."}}}}}catch(cohortErr){logger2.error("[AUTH] Failed cohort expiry check during refresh",{userId,error:cohortErr instanceof Error?cohortErr.message:String(cohortErr)})}}let newAccessToken=signAccessToken(userId,userRoles.length>0?userRoles:void 0,userClaims.length>0?userClaims:void 0,ctx.request.headers.get("x-tenant-schema")??void 0,userClaimScopes),newRefreshToken=signRefreshToken?signRefreshToken(userId):null,accessSetHeaders=tokenResponseConfig?.accessToken?.setHeadersEnabled??!0,accessReturnJson=tokenResponseConfig?.accessToken?.returnJson??!0,refreshSetHeaders=tokenResponseConfig?.refreshToken?.setHeadersEnabled??!0,refreshReturnJson=tokenResponseConfig?.refreshToken?.returnJson??!1,accessExpiresIn=authentication?.accessToken?.expiresIn||"15m",accessMaxAge=Math.max(0,parseTimeToSeconds3(accessExpiresIn)-(cookieMaxAgeBufferSeconds??0)),refreshTtl=lifetimeSeconds(authentication?.refreshToken?.expiresIn,604800),refreshMaxAge=cookieMaxAge(refreshTtl),headers=new Headers;headers.set("Content-Type","application/json");let cookiePath=cookieConfig?.path||"/",cookieSameSite=cookieConfig?.sameSite||"Strict",securePart=cookieConfig?.secure!==!1?"; Secure":"",domainPart=cookieConfig?.domain?`; Domain=${cookieConfig.domain}`:"",cookieOpts=`; Path=${cookiePath}; HttpOnly; SameSite=${cookieSameSite}${securePart}${domainPart}`;if(accessSetHeaders)headers.append("Set-Cookie",`${accessTokenName}=${newAccessToken}${cookieOpts}; Max-Age=${accessMaxAge}`);if(newRefreshToken&&refreshSetHeaders)headers.append("Set-Cookie",`${refreshTokenName}=${newRefreshToken}${cookieOpts}; Max-Age=${refreshMaxAge}`);logger2.info("[AUTH] Token refresh successful",{userId,rotatedRefreshToken:!!newRefreshToken});let responseData={};if(accessReturnJson)responseData.accessToken=newAccessToken;if(newRefreshToken&&refreshReturnJson)responseData.refreshToken=newRefreshToken;let jsonBody=JSON.stringify({success:!0,data:responseData});return new Response(jsonBody,{status:200,headers})},{detail:{tags:["Authentication"],summary:"Refresh Token",description:"Get new access token using refresh token"}}),refreshRoutes}var init_refresh=__esm(()=>{init_lifetime();init_fetchUserRolesAndClaims();init_types18()});import{t as t37}from"elysia";var RegisterBodySchema,RegisterResponseSchema;var init_types19=__esm(()=>{RegisterBodySchema=t37.Object({email:t37.String({format:"email"}),password:t37.String({minLength:8}),confirmPassword:t37.Optional(t37.String({minLength:8})),firstName:t37.Optional(t37.String({maxLength:100})),lastName:t37.Optional(t37.String({maxLength:100})),createProfile:t37.Optional(t37.Boolean())}),RegisterResponseSchema=t37.Object({success:t37.Boolean(),message:t37.Optional(t37.String()),data:t37.Optional(t37.Object({user:t37.Object({id:t37.String(),email:t37.String()})}))})});import{eq as eq55}from"drizzle-orm";import{Elysia as Elysia43}from"elysia";function createRegisterRoute(config,registerConfig,sendWelcomeEmail,signAccessToken,signRefreshToken,createSession,cookieConfig,tokenResponseConfig,getEmailService,appName,saveSessionToDb){let{db,logger:logger2}=config,log4=logger2.scoped(AUTH_REGISTER),route=registerConfig.route||"/auth/register",cookies={accessTokenName:cookieConfig?.accessTokenName||"access_token",refreshTokenName:cookieConfig?.refreshTokenName||"refresh_token",sessionTokenName:cookieConfig?.sessionTokenName||"session_token",accessTokenMaxAge:cookieConfig?.accessTokenMaxAge||900,refreshTokenMaxAge:cookieConfig?.refreshTokenMaxAge||604800,sessionTokenMaxAge:cookieConfig?.sessionTokenMaxAge||900,secure:cookieConfig?.secure??!0,httpOnly:cookieConfig?.httpOnly??!0,sameSite:cookieConfig?.sameSite||"strict",path:cookieConfig?.path||"/",domain:cookieConfig?.domain},registerRoutes=new Elysia43;if(!registerConfig.enabled)return registerRoutes;return registerRoutes.post(route,async(ctx)=>{let resolved=resolveAuthTablesForRequest(ctx.request,config),usersTable=resolved.usersTable;if(!db||!usersTable)return ctx.set.status=500,{success:!1,message:"Database not configured"};let{email:rawEmail,password:password4,confirmPassword,firstName,lastName,createProfile}=ctx.body,email=rawEmail.toLowerCase();if(confirmPassword&&password4!==confirmPassword)return ctx.set.status=400,{success:!1,message:"Passwords do not match"};let passwordValidation=validatePasswordStrength(password4);if(!passwordValidation.valid)return ctx.set.status=400,{success:!1,message:"Password too weak",errors:passwordValidation.errors};let existingUsers=await db.select().from(usersTable).where(eq55(usersTable.email,email)).limit(1),reqUrl=new URL(ctx.request.url),auditIp=ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||ctx.request.headers.get("x-real-ip")?.trim()||"unknown",auditUa=ctx.request.headers.get("user-agent")||"unknown";if(existingUsers.length>0){log4.warn("Registration failed - email exists",{email}),logger2.audit({entityName:"users",operation:"REGISTER_FAILED",summary:`Registration failed: email already exists (${email})`,ipAddress:auditIp,userAgent:auditUa,path:reqUrl.pathname,query:reqUrl.search});let verifGatesSignup=registerConfig.emailVerification?.enabled&&getEmailService?.()?.isAvailable();if(config.authentication?.preventUserEnumeration&&verifGatesSignup)return{success:!0,message:"Registration successful. Please check your email to verify your account."};return ctx.set.status=409,{success:!1,message:"Email already registered"}}let hashedPassword=await hashPassword(password4),domainExempt=isEmailExempt(email,config.authentication?.emailExemptDomains),emailService=getEmailService?.()??null,emailVerificationEnabled=!domainExempt&®isterConfig.emailVerification?.enabled&&emailService?.isAvailable(),verificationToken=emailVerificationEnabled?generateVerificationToken():null,tokenExpiresIn=registerConfig.emailVerification?.tokenExpiresIn||"24h",tokenExpiresAt=emailVerificationEnabled?new Date(Date.now()+parseTimeToMs(tokenExpiresIn)):null,insertValues={email,password:hashedPassword};if(emailVerificationEnabled&&verificationToken)insertValues.emailVerificationToken=hashVerificationToken(verificationToken),insertValues.emailVerificationTokenExpiresAt=tokenExpiresAt,insertValues.emailVerificationSentAt=new Date,insertValues.emailVerificationAttempts=1;let newUser=(await db.insert(usersTable).values(insertValues).returning())[0],registerDefaultRoles=resolveDefaultRoleNames(config.authentication);if(registerDefaultRoles.length>0)await assignDefaultRoles({db,userId:newUser.id,roleNames:registerDefaultRoles,rolesTable:resolved.rolesTable??config.rolesTable??null,userRolesTable:resolved.userRolesTable??config.userRolesTable??null,logger:logger2});let wantsProfile=registerConfig.createProfileOnRegister||createProfile,profilesTable=resolved.schemaTables?.profiles;if(wantsProfile&&profilesTable&&(firstName||lastName))try{let profileNow=new Date;await db.insert(profilesTable).values({userId:newUser.id,firstName:firstName??"",lastName:lastName??"",createdAt:profileNow,updatedAt:profileNow})}catch(err){log4.warn("Failed to create profile on register \u2014 continuing",{userId:newUser.id,error:err instanceof Error?err.message:String(err)})}if(log4.info("Registration successful",{userId:newUser.id,email,emailVerificationEnabled}),logger2.audit({entityName:"users",entityId:newUser.id,operation:"REGISTER",userId:newUser.id,summary:`New user registered: ${email}`,ipAddress:auditIp,userAgent:auditUa,path:reqUrl.pathname,query:reqUrl.search}),emailVerificationEnabled&&emailService&&verificationToken){let verificationLink=buildEmailActionLink({request:ctx.request,configuredUrl:registerConfig.emailVerification?.redirectUrl,path:"/verify-email",query:{token:verificationToken},allowedOrigins:config.authentication?.trustedAppOrigins});emailService.sendVerificationEmail(email,email.split("@")[0]||"User",verificationLink,appName||"Nucleus").then((result2)=>{if(result2.success)log4.info("Verification email sent",{email});else log4.error("Failed to send verification email",{email,error:result2.error})}).catch((err)=>{log4.error("Failed to send verification email",{email,error:err})});let resendCooldown=registerConfig.emailVerification?.resendCooldown||"60s",cooldownSeconds=parseTimeToMs(resendCooldown)/1000,maxResendAttempts=registerConfig.emailVerification?.maxResendAttempts||3;if(config.authentication?.preventUserEnumeration)return{success:!0,message:"Registration successful. Please check your email to verify your account."};return{success:!0,message:"Registration successful. Please check your email to verify your account.",data:{user:{id:newUser.id,email:newUser.email},emailVerificationRequired:!0,verification:{cooldownSeconds,canResendAt:new Date(Date.now()+cooldownSeconds*1000).toISOString(),attemptsRemaining:maxResendAttempts-1,maxAttempts:maxResendAttempts}}}}if(sendWelcomeEmail)sendWelcomeEmail(email,firstName||email.split("@")[0]||"User").catch((err)=>{log4.error("Failed to send welcome email",{email,error:err})});if(signAccessToken&&signRefreshToken&&createSession){let ipAddress=ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||ctx.request.headers.get("x-real-ip")?.trim()||"unknown",userAgent=ctx.request.headers.get("user-agent")||"",accessToken=signAccessToken(newUser.id,void 0,void 0,ctx.request.headers.get("x-tenant-schema")??void 0),refreshToken=signRefreshToken(newUser.id),sessionParams={userId:newUser.id,deviceInfo:ensureDeviceInfo({ipAddress,userAgent},deviceContextFromRequest(ctx.request).clientHints),loginMethod:"register",rememberMe:!0},sessionId=await createSession(sessionParams),deviceTokenCookie;if(saveSessionToDb){let reqSchemaName=ctx.request.headers.get("x-tenant-schema")||void 0,dtCfg=config.authentication?.deviceTrust;if(dtCfg?.enabled)sessionParams.deviceToken=readDeviceTokenFromCookieHeader(ctx.request.headers.get("cookie"),dtCfg);deviceTokenCookie=(await saveSessionToDb(sessionId,sessionParams,reqSchemaName))?.deviceTokenCookie}let tokenConfig={accessToken:{setHeadersEnabled:tokenResponseConfig?.accessToken?.setHeadersEnabled??!0,returnJson:tokenResponseConfig?.accessToken?.returnJson??!0},refreshToken:{setHeadersEnabled:tokenResponseConfig?.refreshToken?.setHeadersEnabled??!0,returnJson:tokenResponseConfig?.refreshToken?.returnJson??!0},sessionToken:{setHeadersEnabled:tokenResponseConfig?.sessionToken?.setHeadersEnabled??!0,returnJson:tokenResponseConfig?.sessionToken?.returnJson??!0}},securePart=cookies.secure?"; Secure":"",domainPart=cookies.domain?`; Domain=${cookies.domain}`:"",cookieOptions=`; Path=${cookies.path}; HttpOnly; SameSite=${cookies.sameSite}${securePart}${domainPart}`,cookiesToSet=[];if(tokenConfig.accessToken.setHeadersEnabled)cookiesToSet.push(`${cookies.accessTokenName}=${accessToken}${cookieOptions}; Max-Age=${cookies.accessTokenMaxAge}`);if(tokenConfig.refreshToken.setHeadersEnabled)cookiesToSet.push(`${cookies.refreshTokenName}=${refreshToken}${cookieOptions}; Max-Age=${cookies.refreshTokenMaxAge}`);if(tokenConfig.sessionToken.setHeadersEnabled)cookiesToSet.push(`${cookies.sessionTokenName}=${sessionId}${cookieOptions}; Max-Age=${cookies.sessionTokenMaxAge}`);if(deviceTokenCookie)cookiesToSet.push(deviceTokenCookie);let responseData={user:{id:newUser.id,email:newUser.email}};if(tokenConfig.accessToken.returnJson)responseData.accessToken=accessToken;if(tokenConfig.refreshToken.returnJson)responseData.refreshToken=refreshToken;if(tokenConfig.sessionToken.returnJson)responseData.sessionId=sessionId;let jsonBody=JSON.stringify({success:!0,data:responseData}),headers=new Headers;headers.set("Content-Type","application/json"),headers.set("x-session-id",sessionId);for(let cookie of cookiesToSet)headers.append("Set-Cookie",cookie);return new Response(jsonBody,{status:200,headers})}return{success:!0,data:{user:{id:newUser.id,email:newUser.email}}}},{body:RegisterBodySchema,detail:{tags:["Authentication"],summary:"Register",description:"Register a new user account"}}),registerRoutes}var init_register=__esm(()=>{init_DeviceTrust();init_scopes();init_assignDefaultRole();init_deviceInfo();init_types19();init_utils6();init_types19()});import{t as t38}from"elysia";var SessionResponseSchema,SessionListResponseSchema,RevokeSessionBodySchema,RevokeAllSessionsBodySchema,SessionActivityResponseSchema,AdminRevokeSessionsBodySchema;var init_types20=__esm(()=>{SessionResponseSchema=t38.Object({id:t38.String(),deviceName:t38.Optional(t38.String()),deviceType:t38.Optional(t38.String()),browserName:t38.Optional(t38.String()),browserVersion:t38.Optional(t38.String()),osName:t38.Optional(t38.String()),osVersion:t38.Optional(t38.String()),ipAddress:t38.String(),locationCountry:t38.Optional(t38.String()),locationCity:t38.Optional(t38.String()),lastActivityAt:t38.String(),createdAt:t38.String(),isCurrent:t38.Boolean(),loginMethod:t38.Optional(t38.String()),trustScore:t38.Optional(t38.Number())}),SessionListResponseSchema=t38.Object({success:t38.Boolean(),data:t38.Optional(t38.Object({sessions:t38.Array(SessionResponseSchema),currentSessionId:t38.Optional(t38.String()),totalCount:t38.Number()})),message:t38.Optional(t38.String())}),RevokeSessionBodySchema=t38.Object({reason:t38.Optional(t38.String())}),RevokeAllSessionsBodySchema=t38.Object({excludeCurrent:t38.Optional(t38.Boolean()),reason:t38.Optional(t38.String())}),SessionActivityResponseSchema=t38.Object({success:t38.Boolean(),data:t38.Optional(t38.Object({recentActivity:t38.Array(t38.Object({sessionId:t38.String(),action:t38.String(),ipAddress:t38.String(),timestamp:t38.String(),deviceInfo:t38.Optional(t38.String())}))})),message:t38.Optional(t38.String())}),AdminRevokeSessionsBodySchema=t38.Optional(t38.Object({sessionId:t38.Optional(t38.String()),reason:t38.Optional(t38.String())}))});function formatSessionForResponse(session){let get=(snake,camel3)=>session[snake]??session[camel3],getDate=(snake,camel3)=>{let val=session[snake]??session[camel3];if(!val)return;return val instanceof Date?val.toISOString():String(val)};return{id:session.id,deviceName:get("device_name","deviceName"),deviceType:get("device_type","deviceType"),deviceFingerprint:get("device_fingerprint","deviceFingerprint"),browserName:get("browser_name","browserName"),browserVersion:get("browser_version","browserVersion"),osName:get("os_name","osName"),osVersion:get("os_version","osVersion"),ipAddress:get("ip_address","ipAddress"),locationCountry:get("location_country","locationCountry"),locationCity:get("location_city","locationCity"),lastActivityAt:getDate("last_activity_at","lastActivityAt"),createdAt:getDate("created_at","createdAt"),isCurrent:get("is_current","isCurrent"),loginMethod:get("login_method","loginMethod"),trustScore:get("trust_score","trustScore")}}var init_utils10=()=>{};import{and as and20,desc as desc6,eq as eq56,isNull as isNull3}from"drizzle-orm";import{Elysia as Elysia44}from"elysia";function createSessionsRoute(config,sessionsConfig,sessionsTable){let{db,logger:logger2}=config,log4=logger2.scoped(AUTH_SESSION),baseRoute=sessionsConfig.route||"/auth/sessions",sessionsRoutes=new Elysia44;if(!sessionsConfig.enabled||!sessionsTable)return sessionsRoutes;let tableColumns=sessionsTable,_col=(name2)=>{let snakeCase=name2.replace(/([A-Z])/g,"_$1").toLowerCase();return tableColumns[name2]||tableColumns[snakeCase]},getSessionsForRequest=(request)=>{let resolved=resolveAuthTablesForRequest(request,config),reqTable=resolved.sessionsTable||sessionsTable,reqCols=reqTable,reqCol=(name2)=>{let snakeCase=name2.replace(/([A-Z])/g,"_$1").toLowerCase();return reqCols[name2]||reqCols[snakeCase]},trustedDevicesTable=resolved.schemaTables?.trustedDevices??resolved.schemaTables?.trusted_devices;return{sessionsTable:reqTable,col:reqCol,trustedDevicesTable}};sessionsRoutes.get(baseRoute,async(ctx)=>{let{sessionsTable:sessionsTable2,col:col15}=getSessionsForRequest(ctx.request);if(!db)return{success:!1,message:"Database not configured"};let userId=ctx.request.headers.get("x-user-id");if(!userId)return{success:!1,message:"Authentication required"};let currentSessionId=ctx.request.headers.get("x-session-id"),sessions=await db.select().from(sessionsTable2).where(and20(eq56(col15("userId"),userId),eq56(col15("isActive"),!0),isNull3(col15("revokedAt")))).orderBy(desc6(col15("lastActivityAt"))),realSessions=sessions.filter((s)=>{let sess=s,fp=(sess.deviceFingerprint||"").toLowerCase(),ip=sess.ipAddress||"";return!((!fp||fp==="--"||fp==="--unknown"||fp.includes("bot/crawler")||fp.includes("headless")||fp.includes("unknown-unknown"))&&(ip==="127.0.0.1"||ip==="::1"||ip==="localhost"||!ip))}),formattedSessions=realSessions.map((s)=>{let session=s,formatted=formatSessionForResponse(session);return formatted.isCurrent=session.id===currentSessionId,formatted});return log4.info("Sessions list retrieved",{userId,totalInDb:sessions.length,filteredCount:realSessions.length,hiddenBotSessions:sessions.length-realSessions.length}),{success:!0,data:{sessions:formattedSessions,currentSessionId,totalCount:realSessions.length}}},{detail:{tags:["Authentication"],summary:"List active sessions",description:"Get all active sessions for the current user"}}),sessionsRoutes.get(`${baseRoute}/current`,async(ctx)=>{let{sessionsTable:sessionsTable2,col:col15}=getSessionsForRequest(ctx.request);if(!db)return{success:!1,message:"Database not configured"};let userId=ctx.request.headers.get("x-user-id"),sessionId=ctx.request.headers.get("x-session-id");if(!userId||!sessionId)return{success:!1,message:"Authentication required"};let sessions=await db.select().from(sessionsTable2).where(eq56(col15("id"),sessionId)).limit(1);if(sessions.length===0)return{success:!1,message:"Session not found"};let session=sessions[0],formatted=formatSessionForResponse(session);return formatted.isCurrent=!0,{success:!0,data:formatted}},{detail:{tags:["Authentication"],summary:"Get current session",description:"Get details of the current session"}}),sessionsRoutes.delete(`${baseRoute}/:sessionId`,async(ctx)=>{let{sessionsTable:sessionsTable2,col:col15}=getSessionsForRequest(ctx.request);if(!db)return{success:!1,message:"Database not configured"};let userId=ctx.request.headers.get("x-user-id");if(!userId)return{success:!1,message:"Authentication required"};let{sessionId}=ctx.params,body=ctx.body,currentSessionId=ctx.request.headers.get("x-session-id");if((await db.select().from(sessionsTable2).where(and20(eq56(col15("id"),sessionId),eq56(col15("userId"),userId))).limit(1)).length===0)return{success:!1,message:"Session not found"};let isCurrentSession=sessionId===currentSessionId;await db.update(sessionsTable2).set({isActive:!1,revokedAt:new Date,revokedReason:isCurrentSession?"user_logout":body.reason||"user_revoked"}).where(eq56(col15("id"),sessionId)),log4.info("Session revoked",{userId,sessionId,isCurrentSession,reason:body.reason||"user_revoked"});let reqUrl=new URL(ctx.request.url);return logger2.audit({entityName:"user_sessions",entityId:sessionId,operation:isCurrentSession?"LOGOUT":"SESSION_REVOKE",userId:userId||void 0,summary:isCurrentSession?"User logged out via session revoke":`Session revoked (${sessionId.substring(0,8)}...)`,ipAddress:ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||ctx.request.headers.get("x-real-ip")?.trim()||"unknown",userAgent:ctx.request.headers.get("user-agent")||"unknown",path:reqUrl.pathname,query:reqUrl.search}),{success:!0,message:isCurrentSession?"Logged out successfully":"Session revoked successfully"}},{body:RevokeSessionBodySchema,detail:{tags:["Authentication"],summary:"Revoke session",description:"Revoke a specific session by ID"}}),sessionsRoutes.delete(`${baseRoute}/all`,async(ctx)=>{let{sessionsTable:sessionsTable2,col:col15}=getSessionsForRequest(ctx.request);if(!db)return{success:!1,message:"Database not configured"};let userId=ctx.request.headers.get("x-user-id");if(!userId)return{success:!1,message:"Authentication required"};let body=ctx.body,currentSessionId=ctx.request.headers.get("x-session-id"),query=db.update(sessionsTable2).set({isActive:!1,revokedAt:new Date,revokedReason:body.reason||"user_revoked"}).where(and20(eq56(col15("userId"),userId),eq56(col15("isActive"),!0)));if(body.excludeCurrent&¤tSessionId){let{ne:ne3}=await import("drizzle-orm");query=db.update(sessionsTable2).set({isActive:!1,revokedAt:new Date,revokedReason:body.reason||"user_revoked"}).where(and20(eq56(col15("userId"),userId),eq56(col15("isActive"),!0),ne3(col15("id"),currentSessionId)))}await query,log4.info("All sessions revoked",{userId,excludeCurrent:body.excludeCurrent,reason:body.reason||"user_revoked"});let reqUrl=new URL(ctx.request.url);return logger2.audit({entityName:"user_sessions",operation:"SESSION_REVOKE_ALL",userId:userId||void 0,summary:body.excludeCurrent?"All other sessions revoked":"All sessions revoked",ipAddress:ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||ctx.request.headers.get("x-real-ip")?.trim()||"unknown",userAgent:ctx.request.headers.get("user-agent")||"unknown",path:reqUrl.pathname,query:reqUrl.search}),{success:!0,message:body.excludeCurrent?"All other sessions revoked successfully":"All sessions revoked successfully"}},{body:RevokeAllSessionsBodySchema,detail:{tags:["Authentication"],summary:"Revoke all sessions",description:"Revoke all sessions for the current user (optionally exclude current)"}}),sessionsRoutes.get(`${baseRoute}/stats`,async(ctx)=>{let{sessionsTable:sessionsTable2,col:col15}=getSessionsForRequest(ctx.request);if(!db)return{success:!1,message:"Database not configured"};let userId=ctx.request.headers.get("x-user-id");if(!userId)return{success:!1,message:"Authentication required"};let{count:count3,countDistinct}=await import("drizzle-orm"),activeSessions=await db.select({count:count3()}).from(sessionsTable2).where(and20(eq56(col15("userId"),userId),eq56(col15("isActive"),!0),isNull3(col15("revokedAt")))),uniqueDevices=await db.select({count:countDistinct(col15("deviceFingerprint"))}).from(sessionsTable2).where(and20(eq56(col15("userId"),userId),eq56(col15("isActive"),!0))),uniqueIps=await db.select({count:countDistinct(col15("ipAddress"))}).from(sessionsTable2).where(and20(eq56(col15("userId"),userId),eq56(col15("isActive"),!0)));return{success:!0,data:{activeSessions:activeSessions[0]?.count||0,uniqueDevices:uniqueDevices[0]?.count||0,uniqueIpAddresses:uniqueIps[0]?.count||0}}},{detail:{tags:["Authentication"],summary:"Session statistics",description:"Get session statistics for the current user"}});let APPROVAL_TOKEN_TTL_MS=86400000,buildResultHtml=(action,success,message)=>{let icon=success?action==="approve"?"\u2705":"\uD83D\uDEAB":"\u274C",title=success?action==="approve"?"Device Approved":"Device Rejected":"Action Failed",bName=sessionsConfig.brandName||"Nucleus";return`<!DOCTYPE html>
|
|
446
446
|
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
447
447
|
<title>${title} \u2014 ${bName}</title>
|
|
448
448
|
<style>*{box-sizing:border-box}body{margin:0;padding:0;background-color:#f4f4f5;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;}</style>
|
|
@@ -2028,4 +2028,4 @@ ${content}
|
|
|
2028
2028
|
${warningBanner}
|
|
2029
2029
|
${deviceTable}
|
|
2030
2030
|
<p style="margin:16px 0 0;color:#6b7280;font-size:13px;">If this wasn't you, please secure your account immediately.</p>
|
|
2031
|
-
`)}deliver(getEmailService(),{to:user.email,subject,html:emailHtml},"[AUTH] login notification").then((report)=>{if(report.level==="warn")logger2.warn(report.message,{userId:params.userId,trustScore,requiresApproval});else logger2.info(report.message,{userId:params.userId})})}}}if(logger2.info("[AUTH] Session saved to DB",{sessionId,userId:params.userId,isNewDevice,requiresApproval,deviceFingerprint,ipAddress:deviceInfo.ipAddress}),deviceTrustMint&&dtTable&&deviceTrustCfg?.enabled)try{let DT2=await Promise.resolve().then(() => (init_DeviceTrust(),exports_DeviceTrust)),DTStore2=await Promise.resolve().then(() => (init_store2(),exports_store)),{randomUUID:randomUUID10}=await import("crypto"),{raw,hash}=DT2.mintDeviceToken(),ttlSec=parseTimeToSeconds2(deviceTrustCfg.tokenExpiresIn??"30d");await DTStore2.insertDeviceRow(db,dtTable,{id:randomUUID10(),userId:params.userId,tokenHash:hash,status:deviceTrustMint,originSessionId:sessionId,label:deviceInfo.deviceName||`${deviceInfo.browserName||"Unknown"} on ${deviceInfo.osName||"Unknown"}`,deviceFingerprint,firstSeenIp:deviceInfo.ipAddress,expiresAt:new Date(Date.now()+ttlSec*1000)});let rawDomainDT=authentication.cookieDomain,resolvedDomainDT=rawDomainDT?process.env[rawDomainDT]??rawDomainDT:void 0,domainDT=resolvedDomainDT==="localhost"||resolvedDomainDT===".localhost"?void 0:resolvedDomainDT;deviceTokenToSet=DT2.buildDeviceTokenCookie(raw,{cookieName:deviceTrustCfg.cookieName,cookiePrefix:deviceTrustCfg.cookiePrefix,sameSite:deviceTrustCfg.sameSite,maxAgeSeconds:ttlSec,domain:domainDT,secure:!!domainDT})}catch(dtErr){logger2.warn("[AUTH] Device-trust token mint failed \u2014 continuing",{userId:params.userId,error:dtErr instanceof Error?dtErr.message:String(dtErr)})}return{requiresApproval,sessionId,deviceTokenCookie:deviceTokenToSet??void 0}},storeResetToken:async(userId,token,expiresAt,reqSchemaName)=>{let resetTokensTable=resolveTableForTenant("passwordResetTokens",reqSchemaName);if(!resetTokensTable||!db)return;await db.insert(resetTokensTable).values({userId,tokenHash:createHash4("sha256").update(token).digest("hex"),expiresAt})},getResetToken:async(token,reqSchemaName)=>{let{eq:eq64}=__require("drizzle-orm"),resetTokensTable=resolveTableForTenant("passwordResetTokens",reqSchemaName);if(!resetTokensTable||!db)return null;let tokenHash=createHash4("sha256").update(token).digest("hex"),row=(await db.select().from(resetTokensTable).where(eq64(resetTokensTable.tokenHash,tokenHash)).limit(1))[0];if(!row||row.usedAt)return null;return{userId:row.userId,expiresAt:row.expiresAt}},deleteResetToken:async(token,reqSchemaName)=>{let{eq:eq64}=__require("drizzle-orm"),resetTokensTable=resolveTableForTenant("passwordResetTokens",reqSchemaName);if(!resetTokensTable||!db)return;let tokenHash=createHash4("sha256").update(token).digest("hex");await db.delete(resetTokensTable).where(eq64(resetTokensTable.tokenHash,tokenHash))},revokeSessionInDb:async(sessionId,reason,reqSchemaName)=>{let revokeTable=resolveTableForTenant("userSessions",reqSchemaName)||resolveTableForTenant("user_sessions",reqSchemaName)||resolveTableForTenant("sessions",reqSchemaName)||sessionsTable;if(!revokeTable||!db)return;await db.update(revokeTable).set({isActive:!1,revokedAt:new Date,revokedReason:reason}).where(eq63(revokeTable.id,sessionId)),logger2.info("[AUTH] Session revoked in DB",{sessionId,reason})},sendResetEmail:async(email,token,request)=>{if(isEmailExempt(email,authentication?.emailExemptDomains))return logger2.info("[AUTH] Skipping reset email \u2014 domain exempt",{email}),{sent:!1,reason:"exempt"};let mailer=getEmailService();if(!mailer?.isAvailable())return logger2.warn("[AUTH] Cannot send reset email \u2014 no email provider configured"),{sent:!1,reason:"no_provider"};let configuredResetUrl=authentication.passwordReset?.redirectUrl||"http://localhost:3000/reset-password",resetUrl=request?buildEmailActionLink({request,configuredUrl:configuredResetUrl,path:extractConfiguredPath(configuredResetUrl,"/reset-password"),query:{token},allowedOrigins:trustedAppOrigins()}):`${configuredResetUrl}?token=${token}`,result=await mailer.sendEmail({to:email,subject:"Password Reset Request",html:`<p>Click the link to reset your password:</p><a href="${resetUrl}">${resetUrl}</a>`});if(result?.success===!1){let refused=result.rejected;return{sent:!1,reason:Array.isArray(refused)&&refused.length>0?"rejected":"error",detail:result.error}}return{sent:!0}},storeMagicToken:async(params,reqSchemaName)=>{let magicTokensTable=resolveTableForTenant("magicLinkTokens",reqSchemaName);if(!magicTokensTable||!db)return;await db.insert(magicTokensTable).values({userId:params.userId,email:params.email,tokenHash:params.tokenHash,expiresAt:params.expiresAt})},getMagicToken:async(tokenHash,reqSchemaName)=>{let{eq:eq64}=__require("drizzle-orm"),magicTokensTable=resolveTableForTenant("magicLinkTokens",reqSchemaName);if(!magicTokensTable||!db)return null;let row=(await db.select().from(magicTokensTable).where(eq64(magicTokensTable.tokenHash,tokenHash)).limit(1))[0];if(!row||row.usedAt)return null;return{userId:row.userId,email:row.email,tokenHash:row.tokenHash,expiresAt:row.expiresAt}},deleteMagicToken:async(tokenHash,reqSchemaName)=>{let{eq:eq64}=__require("drizzle-orm"),magicTokensTable=resolveTableForTenant("magicLinkTokens",reqSchemaName);if(!magicTokensTable||!db)return;await db.delete(magicTokensTable).where(eq64(magicTokensTable.tokenHash,tokenHash))}}}),logger2.info("[AUTH] Routes registered")}}if(resolvedOptions.storage?.enabled&&resolvedOptions.storage?.cdn?.enabled){let{createCdnRoutes:createCdnRoutes2,mergeCdnConfig:mergeCdnConfig2,mergeStorageConfig:mergeStorageConfig2}=(init_storage2(),__toCommonJS(exports_storage)),cdnConfig=mergeCdnConfig2(resolvedOptions.storage.cdn),storageConfig=mergeStorageConfig2(resolvedOptions.storage),filesTable=schemaTables.files;if(plugin.use(createCdnRoutes2({cdn:cdnConfig,storagePath:storageConfig.basePath,logger:logger2,getStorageProvider:()=>storageProvider,buildStorageProvider,getFileRecord:filesTable&&db?async(id,reqSchemaName)=>{let tbl=filesTable;if(reqSchemaName&&tenantRegistry){let ctx=tenantRegistry.getSchemaContext(reqSchemaName);if(ctx?.schemaTables.files)tbl=ctx.schemaTables.files}let t43=tbl,result=await db.select().from(t43).where(eq63(t43.id,id)).limit(1);if(result.length===0)return null;let record3=result[0];return{id:record3.id,name:record3.name,path:record3.path,mime_type:record3.mimeType||record3.mime_type,original_name:record3.originalName||record3.original_name}}:void 0})),logger2.info(`[Storage] CDN routes enabled at ${cdnConfig.basePath}`),filesTable&&db){let{createResumableRoutes:createResumableRoutes2}=(init_resumable(),__toCommonJS(exports_resumable)),resumable=createResumableRoutes2({basePath:storageConfig.basePath,storage:storageConfig,logger:logger2,getUserId:(request)=>request.headers.get("x-user-id"),canUpload:(request)=>{if(resolvedOptions.authorization?.enabled===!1)return!0;return checkAuthorizationFromJWT({userClaims:decodeHeaderList(request.headers.get("x-user-claims")),userRoles:decodeHeaderList(request.headers.get("x-user-roles")),claimScopes:decodeClaimScopesHeader(request.headers.get("x-user-claim-scopes")),method:"POST",entity:"files",logger:logger2,requireClaimSpecificity:resolvedOptions.authorization?.requireClaimSpecificity}).authorized},register:async(input)=>{let nodePath=__require("path"),{rename:rename6}=__require("fs/promises"),{randomUUID:randomUUID10}=__require("crypto"),id=randomUUID10(),extension2=nodePath.extname(input.originalName),storedName=`${id}${extension2}`,destination=nodePath.join(storageConfig.basePath,storedName);try{await rename6(input.partPath,destination)}catch(error3){return logger2.error("[Storage] A finished upload could not be moved into place",{error:error3}),null}let uploaded={id,name:storedName,originalName:input.originalName,path:storageConfig.basePath,mimeType:input.mimeType,size:input.size,createdAt:new Date},{buildFileRecordPayload:buildFileRecordPayload2,scheduleUploadMediaProcessing:scheduleUploadMediaProcessing2}=(init_storage2(),__toCommonJS(exports_storage)),t43=filesTable,payload={...buildFileRecordPayload2(uploaded,input.userId),type:input.type},row=(await db.insert(t43).values(payload).returning())[0];if(!row?.id)return null;return scheduleUploadMediaProcessing2([uploaded],{storagePath:storageConfig.basePath,media:{transform:mergeTransformConfig(resolvedOptions.storage?.cdn?.transform),video:mergeVideoConfig(resolvedOptions.storage?.cdn?.video),pdf:mergePdfConfig(resolvedOptions.storage?.cdn?.pdf)},logger:logger2}),{id:String(row.id)}}});plugin.use(resumable.plugin),setInterval(()=>{resumable.sweep(Date.now()).then((removed)=>{if(removed>0)logger2.info(`[Storage] Reclaimed ${removed} abandoned upload(s)`)}).catch(()=>{})},3600000).unref?.(),logger2.info("[Storage] Resumable uploads enabled at /uploads")}}if(resolvedOptions.notification?.enabled&&db){let notificationConfig=resolvedOptions.notification;if(notificationConfig.endpoints?.enabled!==!1){warnOnEntityBasePathCollision("Notification",notificationConfig.endpoints?.basePath||"/notifications",resolvedOptions.entities,logger2);let{routes:notificationRoutes}=createNotificationRoutes({db,schemaTables,config:notificationConfig,logger:logger2,getEmailService,tenantRegistry});plugin.use(notificationRoutes),logger2.info(`[Notification] Routes registered at ${notificationConfig.endpoints?.basePath||"/notifications"}`)}}if(resolvedOptions.verification?.enabled&&db){let{routes:verificationRoutes}=createVerificationRoutes({db,schemaTables,config:resolvedOptions.verification,notificationConfig:resolvedOptions.notification,logger:logger2,getEmailService,tenantRegistry});plugin.use(verificationRoutes)}if(resolvedOptions.backup?.enabled&&db){let{createBackupRoutes:createBackupRoutes2}=(init_backup(),__toCommonJS(exports_backup)),{routes:backupRoutes}=createBackupRoutes2({db,logger:logger2,config:{enabled:!0,basePath:resolvedOptions.backup.basePath||"/admin/backup",storagePath:resolvedOptions.backup.storagePath||"./backups",format:resolvedOptions.backup.format||"json",maxBackups:resolvedOptions.backup.maxBackups||50,allowRestore:resolvedOptions.backup.allowRestore??!0,excludeTables:resolvedOptions.backup.excludeTables||["audit_logs","backup_logs"],encryptionKey:resolvedOptions.backup.encryptionKey?process.env[resolvedOptions.backup.encryptionKey]||resolvedOptions.backup.encryptionKey:void 0,schedule:{enabled:resolvedOptions.backup.schedule?.enabled??!1,cron:resolvedOptions.backup.schedule?.cron||"0 2 * * *",retentionDays:resolvedOptions.backup.schedule?.retentionDays||30,timeZone:resolvedOptions.backup.schedule?.timeZone}},schemaTables,schemaName:targetSchemaName,tenantRegistry,lock:getRedisManager()});plugin.use(backupRoutes),logger2.info("[Backup] Routes registered",{basePath:resolvedOptions.backup.basePath||"/admin/backup",scheduleEnabled:resolvedOptions.backup.schedule?.enabled??!1})}if(resolvedOptions.authorization?.endpointDiscovery?.enabled&&db&&resolvedOptions.authentication?.mode!=="consumer"){let{createAuthorizationDiscoveryRoutes:createAuthorizationDiscoveryRoutes2}=(init_authorization(),__toCommonJS(exports_authorization));plugin.use(createAuthorizationDiscoveryRoutes2({db,schemaTables,logger:logger2,discovery:resolvedOptions.authorization.endpointDiscovery,readState:async(key2)=>{let rm3=getRedisManager();if(!rm3)return null;let res=await rm3.read(key2);return res?.success?res.data:null}})),logger2.info("[Authorization] Endpoint-discovery routes registered",{discover:"/authorization/discover",manifest:"/authorization/route-manifest"})}let pubsubClientManager=null;if(resolvedOptions.pubsub?.enabled){let redis=getRedisManager();if(redis){let pubsubConfig=resolvedOptions.pubsub,{plugin:pubsubPlugin,clientManager}=createPubSubRoutes({redis,logger:logger2,basePath:pubsubConfig.basePath||"/subs",wsPath:pubsubConfig.wsPath||"/api/events/subscribe",pubsubName:pubsubConfig.pubsubName||"pubsub-redis",maxClientsPerUser:pubsubConfig.maxClientsPerUser??10,maxTopicsPerClient:pubsubConfig.maxTopicsPerClient??64,wsIdleTimeout:pubsubConfig.wsIdleTimeout??120,ack:{enabled:pubsubConfig.ack?.enabled??!0,ttlSeconds:pubsubConfig.ack?.ttlSeconds??300,maxRetries:pubsubConfig.ack?.maxRetries??3,retryIntervalMs:pubsubConfig.ack?.retryIntervalMs??5000},presence:{enabled:pubsubConfig.presence?.enabled??!0,debounceMs:pubsubConfig.presence?.debounceMs??5000},cleanupIntervalMs:pubsubConfig.cleanupIntervalMs??60000,daprApiToken:pubsubConfig.daprApiToken,getLiveMonitoringService:()=>liveMonitoringService,authenticate:envResolved.accessTokenSecret?(headers)=>{let wsTokens=parseTokenValuesFromHeaders(headers,tokenNames);if(!wsTokens.access_token)return logger2.debug("[PubSub] WS handshake rejected: no access token presented"),null;let jwtResult=verifyJWT(wsTokens.access_token,envResolved.accessTokenSecret||"",{issuer:authentication?.accessToken?.issuer,audience:authentication?.accessToken?.audience});if(!jwtResult.valid)return logger2.warn("[PubSub] WS handshake rejected: JWT verification failed",{reason:jwtResult.error}),null;let sub=jwtResult.payload?.sub;return typeof sub==="string"&&sub.length>0?{userId:sub}:null}:void 0});plugin.use(pubsubPlugin),pubsubClientManager=clientManager,logger2.info("[PubSub] Enabled",{basePath:pubsubConfig.basePath||"/subs",wsPath:pubsubConfig.wsPath||"/api/events/subscribe"})}else logger2.warn("[PubSub] pubsub is enabled but Redis is not configured. Disabling PubSub.")}if(resolvedOptions.chat?.enabled&&db){let chatConfig=mergeChatConfig(resolvedOptions.chat),chatStorageConfig=mergeStorageConfig(resolvedOptions.storage),attachmentsAvailable=resolvedOptions.storage?.enabled===!0&&resolvedOptions.storage?.cdn?.enabled===!0&&chatConfig.attachments.enabled,cdnBasePath=resolvedOptions.storage?.cdn?.basePath||"/cdn",cdnMedia={transform:mergeTransformConfig(resolvedOptions.storage?.cdn?.transform),video:mergeVideoConfig(resolvedOptions.storage?.cdn?.video),pdf:mergePdfConfig(resolvedOptions.storage?.cdn?.pdf)},{routes:chatRoutes}=createChatRoutes({db,schemaTables,config:chatConfig,logger:logger2,broadcaster:pubsubClientManager,tenantRegistry,storageConfig:chatStorageConfig,attachmentsAvailable,cdnBasePath,cdnMedia});plugin.use(chatRoutes),logger2.info("[Chat] Enabled",{basePath:chatConfig.basePath,attachmentsAvailable,realtime:pubsubClientManager!==null})}if(resolvedOptions.payment?.enabled&&db){let{createPaymentService:createPaymentService2}=(init_Payment(),__toCommonJS(exports_Payment)),{createPaymentRoutes:createPaymentRoutes2}=(init_payment(),__toCommonJS(exports_payment)),paymentService=createPaymentService2(resolvedOptions);if(paymentService){let paymentConfig=resolvedOptions.payment,marketplaceServices=null;if(paymentConfig?.marketplace?.enabled){let{createMarketplaceServices:createMarketplaceServices2}=(init_Payment(),__toCommonJS(exports_Payment));marketplaceServices=createMarketplaceServices2({options:resolvedOptions,logger:logger2,getDb:()=>db,provider:paymentService.providerInstance,schemaTables})}let paymentRoutes=createPaymentRoutes2({provider:paymentService.providerInstance,webhookSecret:paymentService.webhookSecret,getPayoutService:()=>marketplaceServices?.payoutService??null,basePath:paymentConfig?.basePath||"/payment",defaultCurrency:paymentConfig?.defaultCurrency||"TRY",defaultLocale:paymentConfig?.defaultLocale||"tr",successRedirectUrl:paymentConfig?.successRedirectUrl||"/payment/success",failedRedirectUrl:paymentConfig?.failedRedirectUrl||"/payment/failed",errorRedirectUrl:paymentConfig?.errorRedirectUrl||"/payment/error",callbackUrl:paymentConfig?.callbackUrl,savedMethodsEnabled:paymentConfig?.savedMethodsEnabled??!0,threeDSecureEnabled:paymentConfig?.threeDSecureEnabled??!0,subMerchantsEnabled:paymentConfig?.subMerchantsEnabled??!1,transactionsTable:schemaTables.paymentTransactions??schemaTables.payment_transactions,methodsTable:schemaTables.paymentMethods??schemaTables.payment_methods,webhookLogsTable:schemaTables.paymentWebhookLogs??schemaTables.payment_webhook_logs,subMerchantsTable:schemaTables.paymentSubMerchants??schemaTables.payment_sub_merchants,commissionSplitsTable:schemaTables.paymentCommissionSplits??schemaTables.payment_commission_splits,productsTable:schemaTables.paymentProducts??schemaTables.payment_products,pricesTable:schemaTables.paymentPrices??schemaTables.payment_prices,customersTable:schemaTables.paymentCustomers??schemaTables.payment_customers,subscriptionsTable:schemaTables.paymentSubscriptions??schemaTables.payment_subscriptions,invoicesTable:schemaTables.paymentInvoices??schemaTables.payment_invoices,db,logger:logger2});if(plugin.use(paymentRoutes),logger2.info("[Payment] Routes registered",{basePath:paymentConfig?.basePath||"/payment",provider:paymentService.provider}),marketplaceServices){let{createMarketplaceRoutes:createMarketplaceRoutes2}=(init_marketplace(),__toCommonJS(exports_marketplace)),services=marketplaceServices;createMarketplaceRoutes2(plugin,{getMarketplaceService:()=>services.marketplaceService,getPayoutService:()=>services.payoutService,basePath:paymentConfig?.basePath||"/payment",logger:logger2}),logger2.info("[Payment] Marketplace routes registered",{basePath:`${paymentConfig?.basePath||"/payment"}/marketplace`})}}}let cohortConfig=authentication?.cohorts;if(cohortConfig?.enabled!==!1&&!isConsumerMode&&db){let cohortBasePath=cohortConfig?.basePath||"/auth/admin/cohorts",resolveTable=(key2,fallback)=>schemaTables[key2]??(fallback?schemaTables[fallback]:null)??null,resolvedCohortsTable=resolveTable("userCohorts","user_cohorts");if(!resolvedCohortsTable)logger2.warn("[Cohort] user_cohorts table not found in schema \u2014 cohort routes will be inactive. Re-generate schema with nucleus-core >= 0.9.120");if(plugin.use(createCohortRoutes({db,logger:logger2,cohortsTable:resolvedCohortsTable,usersTable:resolveTable("users"),rolesTable:resolveTable("roles"),userRolesTable:resolveTable("userRoles","user_roles"),profilesTable:resolveTable("profiles"),basePath:cohortBasePath,get defaultRole(){return resolvedOptions.authentication?.defaultRole},get defaultRoles(){return resolvedOptions.authentication?.defaultRoles},emailExemptDomains:authentication?.emailExemptDomains,passwordPolicy:authentication?.passwordPolicy})),resolvedCohortsTable)logger2.info("[Cohort] Routes registered",{basePath:cohortBasePath})}if(resolvedOptions.configManagement?.enabled){let configBasePath=resolvedOptions.configManagement.basePath||"/nucleus/config";plugin.use(createConfigRoutes({logger:logger2,resolvedOptions,configFilePath,basePath:configBasePath,db,schemaTables,getRedis:()=>{let redis=getRedisManager();if(!redis)return null;return{read:async(key2)=>{let r2=await redis.read(key2);return{success:r2.success,data:r2.success?r2.data:null}},create:async(key2,value2)=>{return{success:(await redis.create(key2,value2)).success}}}},onConfigUpdate:(section,_newValue)=>{logger2.info(`[ConfigManagement] Section "${section}" updated in-memory`,{section})}})),logger2.info(`[ConfigManagement] Routes enabled at ${configBasePath}`)}if(resolvedOptions.secrets?.enabled){let secretsBasePath=resolvedOptions.secrets.basePath||"/secrets";warnOnEntityBasePathCollision("Secrets",secretsBasePath,resolvedOptions.entities,logger2),plugin.use(createSecretsRoutes({logger:logger2,basePath:secretsBasePath,db,schemaTables,resolvedOptions,getService:()=>secretsService,getEmailService,getStorageProvider:()=>storageProvider,buildStorageProvider,adminRoles:resolvedOptions.secrets.adminRoles})),logger2.info(`[Secrets] Routes enabled at ${secretsBasePath}`)}if(resolvedOptions.integrations?.enabled){let integrationsBasePath=resolvedOptions.integrations.basePath||"/integrations";warnOnEntityBasePathCollision("Integrations",integrationsBasePath,resolvedOptions.entities,logger2),plugin.use(createIntegrationRoutes({logger:logger2,basePath:integrationsBasePath,db,schemaTables,resolvedOptions,getEncryptionKey:()=>resolveMasterKey(resolvedOptions.secrets?.encryptionKey)??null,getLockStore:()=>getRedisManager(),publish:(topic,payload)=>pubsubClientManager?.broadcastEvent(topic,payload),adminRoles:resolvedOptions.integrations.adminRoles}))}plugin.onStart((app)=>{let requiredBody=requiredBodyCeiling({storageEnabled:resolvedOptions.storage?.enabled,maxFileSizeBytes:resolvedOptions.storage?.maxFileSizeBytes,maxFiles:resolvedOptions.storage?.formData?.maxFiles}),complaint=bodyCeilingComplaint({configured:app.config?.serve?.maxRequestBodySize,required:requiredBody});if(complaint)logger2.error(`[Storage] ${complaint}`);let memoryComplaint=memoryCeilingComplaint({required:requiredBody,cgroupLimit:readCgroupLimitBytes()});if(memoryComplaint)logger2.error(`[Storage] ${memoryComplaint}`);let memoryAdvice=readRuntimeMemoryAdvice();if(memoryAdvice){let mib=(bytes)=>`${Math.round(bytes/1024/1024)} MiB`;logger2.warn(`[Runtime] The JS engine sizes its heap for ${mib(memoryAdvice.hostTotal)} but this container is capped at ${mib(memoryAdvice.cgroupLimit)} \u2014 it will grow past the `+`limit and be OOM-killed. Set BUN_JSC_forceRAMSize=${memoryAdvice.recommended} (${mib(memoryAdvice.recommended)}) in the deployment env.`)}let port=Number(process.env.PORT)||3000,appId=resolvedOptions.appId||"nucleus",mode=resolvedOptions.mode||"production";console.log(""),console.log(` \x1B[32m\uD83D\uDE80 ${appId}\x1B[0m \x1B[90mv${Date.now()}\x1B[0m`),console.log(` \x1B[36m\u279C\x1B[0m Local: \x1B[36mhttp://localhost:${port}\x1B[0m`),console.log(` \x1B[36m\u279C\x1B[0m Mode: \x1B[33m${mode}\x1B[0m`),console.log("")});let shuttingDown=!1,drainResources=async(trigger)=>{if(shuttingDown)return;shuttingDown=!0,logger2.info(`[Shutdown] ${trigger} \u2014 draining resources`);try{liveMonitoringService?.stop(),monitoringService?.stop(),secretsService?.stop(),storageProvider?.close()}catch(err){logger2.warn("[Shutdown] failed stopping monitoring",{error:err instanceof Error?err.message:String(err)})}try{let redisClient=getRedisManager()?.getDirectClient?.();if(redisClient)await redisClient.quit()}catch(err){logger2.warn("[Shutdown] failed closing Redis",{error:err instanceof Error?err.message:String(err)})}try{for(let timer of backgroundIntervals)clearInterval(timer);backgroundIntervals.length=0}catch(err){logger2.warn("[Shutdown] failed clearing background intervals",{error:err instanceof Error?err.message:String(err)})}try{let{destroyAckCleanup:destroyAckCleanup2}=await Promise.resolve().then(() => (init_ack_manager(),exports_ack_manager));destroyAckCleanup2()}catch{}try{if(dbPool)await dbPool.end()}catch(err){logger2.warn("[Shutdown] failed draining DB pool",{error:err instanceof Error?err.message:String(err)})}logger2.info("[Shutdown] complete")};if(plugin.onStop(async()=>{await drainResources("Elysia onStop")}),resolvedOptions.gracefulShutdown!==!1){let onSignal=(signal)=>{drainResources(signal).finally(()=>process.exit(0))};process.once("SIGTERM",()=>onSignal("SIGTERM")),process.once("SIGINT",()=>onSignal("SIGINT"))}return plugin}init_AzureEmailService();init_Gmail();export{usePubSubStore,usePubSub,serverFetch,requiredBodyCeiling,parseCron2 as parseCron,matchesCron,isDue,generateVerificationEndpoints,generateTenantEndpoints,generateSystemTableEndpoints,generateMonitoringEndpoints,generateMarketplaceEndpoints,generateEndpointsFromConfig,generateDomainEndpoints,generateCohortEndpoints,generateChatEndpoints,generateAuthEndpoints,generateAllEndpoints,generateAdminEndpoints,describeCron,decodeHeaderList,createServerFactory,createApiHook,VERIFICATION_ENDPOINTS,TENANT_ENDPOINTS,ServerFetch,SYSTEM_TABLES,PAYMENT_ENDPOINTS,NucleusElysiaPlugin,MONITORING_ENDPOINTS,MARKETPLACE_ENDPOINTS,GmailService,DOMAIN_ENDPOINTS,CONFIG_ENDPOINTS,COHORT_ENDPOINTS,CHAT_ENDPOINTS,AzureEmailService,AUTH_ENDPOINT_CONFIGS,AUTH_ENDPOINTS};
|
|
2031
|
+
`)}deliver(getEmailService(),{to:user.email,subject,html:emailHtml},"[AUTH] login notification").then((report)=>{if(report.level==="warn")logger2.warn(report.message,{userId:params.userId,trustScore,requiresApproval});else logger2.info(report.message,{userId:params.userId})})}}}if(logger2.info("[AUTH] Session saved to DB",{sessionId,userId:params.userId,isNewDevice,requiresApproval,deviceFingerprint,ipAddress:deviceInfo.ipAddress}),deviceTrustMint&&dtTable&&deviceTrustCfg?.enabled)try{let DT2=await Promise.resolve().then(() => (init_DeviceTrust(),exports_DeviceTrust)),DTStore2=await Promise.resolve().then(() => (init_store2(),exports_store)),{randomUUID:randomUUID10}=await import("crypto"),{raw,hash}=DT2.mintDeviceToken(),ttlSec=parseTimeToSeconds2(deviceTrustCfg.tokenExpiresIn??"30d");await DTStore2.insertDeviceRow(db,dtTable,{id:randomUUID10(),userId:params.userId,tokenHash:hash,status:deviceTrustMint,originSessionId:sessionId,label:deviceInfo.deviceName||`${deviceInfo.browserName||"Unknown"} on ${deviceInfo.osName||"Unknown"}`,deviceFingerprint,firstSeenIp:deviceInfo.ipAddress,expiresAt:new Date(Date.now()+ttlSec*1000)});let rawDomainDT=authentication.cookieDomain,resolvedDomainDT=rawDomainDT?process.env[rawDomainDT]??rawDomainDT:void 0,domainDT=resolvedDomainDT==="localhost"||resolvedDomainDT===".localhost"?void 0:resolvedDomainDT;deviceTokenToSet=DT2.buildDeviceTokenCookie(raw,{cookieName:deviceTrustCfg.cookieName,cookiePrefix:deviceTrustCfg.cookiePrefix,sameSite:deviceTrustCfg.sameSite,maxAgeSeconds:ttlSec,domain:domainDT,secure:!!domainDT})}catch(dtErr){logger2.warn("[AUTH] Device-trust token mint failed \u2014 continuing",{userId:params.userId,error:dtErr instanceof Error?dtErr.message:String(dtErr)})}return{requiresApproval,sessionId,deviceTokenCookie:deviceTokenToSet??void 0}},storeResetToken:async(userId,token,expiresAt,reqSchemaName)=>{let resetTokensTable=resolveTableForTenant("passwordResetTokens",reqSchemaName);if(!resetTokensTable||!db)return;await db.insert(resetTokensTable).values({userId,tokenHash:createHash4("sha256").update(token).digest("hex"),expiresAt})},getResetToken:async(token,reqSchemaName)=>{let{eq:eq64}=__require("drizzle-orm"),resetTokensTable=resolveTableForTenant("passwordResetTokens",reqSchemaName);if(!resetTokensTable||!db)return null;let tokenHash=createHash4("sha256").update(token).digest("hex"),row=(await db.select().from(resetTokensTable).where(eq64(resetTokensTable.tokenHash,tokenHash)).limit(1))[0];if(!row||row.usedAt)return null;return{userId:row.userId,expiresAt:row.expiresAt}},deleteResetToken:async(token,reqSchemaName)=>{let{eq:eq64}=__require("drizzle-orm"),resetTokensTable=resolveTableForTenant("passwordResetTokens",reqSchemaName);if(!resetTokensTable||!db)return;let tokenHash=createHash4("sha256").update(token).digest("hex");await db.delete(resetTokensTable).where(eq64(resetTokensTable.tokenHash,tokenHash))},revokeSessionInDb:async(sessionId,reason,reqSchemaName)=>{let revokeTable=resolveTableForTenant("userSessions",reqSchemaName)||resolveTableForTenant("user_sessions",reqSchemaName)||resolveTableForTenant("sessions",reqSchemaName)||sessionsTable;if(!revokeTable||!db)return;await db.update(revokeTable).set({isActive:!1,revokedAt:new Date,revokedReason:reason}).where(eq63(revokeTable.id,sessionId)),logger2.info("[AUTH] Session revoked in DB",{sessionId,reason})},sendResetEmail:async(email,token,request)=>{if(isEmailExempt(email,authentication?.emailExemptDomains))return logger2.info("[AUTH] Skipping reset email \u2014 domain exempt",{email}),{sent:!1,reason:"exempt"};let mailer=getEmailService();if(!mailer?.isAvailable())return logger2.warn("[AUTH] Cannot send reset email \u2014 no email provider configured"),{sent:!1,reason:"no_provider"};let configuredResetUrl=authentication.passwordReset?.redirectUrl||"http://localhost:3000/reset-password",resetUrl=request?buildEmailActionLink({request,configuredUrl:configuredResetUrl,path:extractConfiguredPath(configuredResetUrl,"/reset-password"),query:{token},allowedOrigins:trustedAppOrigins()}):`${configuredResetUrl}?token=${token}`,result=await mailer.sendEmail({to:email,subject:"Password Reset Request",html:`<p>Click the link to reset your password:</p><a href="${resetUrl}">${resetUrl}</a>`});if(result?.success===!1){let refused=result.rejected;return{sent:!1,reason:Array.isArray(refused)&&refused.length>0?"rejected":"error",detail:result.error}}return{sent:!0}},storeMagicToken:async(params,reqSchemaName)=>{let magicTokensTable=resolveTableForTenant("magicLinkTokens",reqSchemaName);if(!magicTokensTable||!db)return;await db.insert(magicTokensTable).values({userId:params.userId,email:params.email,tokenHash:params.tokenHash,expiresAt:params.expiresAt})},getMagicToken:async(tokenHash,reqSchemaName)=>{let{eq:eq64}=__require("drizzle-orm"),magicTokensTable=resolveTableForTenant("magicLinkTokens",reqSchemaName);if(!magicTokensTable||!db)return null;let row=(await db.select().from(magicTokensTable).where(eq64(magicTokensTable.tokenHash,tokenHash)).limit(1))[0];if(!row||row.usedAt)return null;return{userId:row.userId,email:row.email,tokenHash:row.tokenHash,expiresAt:row.expiresAt}},deleteMagicToken:async(tokenHash,reqSchemaName)=>{let{eq:eq64}=__require("drizzle-orm"),magicTokensTable=resolveTableForTenant("magicLinkTokens",reqSchemaName);if(!magicTokensTable||!db)return;await db.delete(magicTokensTable).where(eq64(magicTokensTable.tokenHash,tokenHash))}}}),logger2.info("[AUTH] Routes registered")}}if(resolvedOptions.storage?.enabled&&resolvedOptions.storage?.cdn?.enabled){let{createCdnRoutes:createCdnRoutes2,mergeCdnConfig:mergeCdnConfig2,mergeStorageConfig:mergeStorageConfig2}=(init_storage2(),__toCommonJS(exports_storage)),cdnConfig=mergeCdnConfig2(resolvedOptions.storage.cdn),storageConfig=mergeStorageConfig2(resolvedOptions.storage),filesTable=schemaTables.files;if(plugin.use(createCdnRoutes2({cdn:cdnConfig,storagePath:storageConfig.basePath,logger:logger2,getStorageProvider:()=>storageProvider,buildStorageProvider,getFileRecord:filesTable&&db?async(id,reqSchemaName)=>{let tbl=filesTable;if(reqSchemaName&&tenantRegistry){let ctx=tenantRegistry.getSchemaContext(reqSchemaName);if(ctx?.schemaTables.files)tbl=ctx.schemaTables.files}let t43=tbl,result=await db.select().from(t43).where(eq63(t43.id,id)).limit(1);if(result.length===0)return null;let record3=result[0];return{id:record3.id,name:record3.name,path:record3.path,mime_type:record3.mimeType||record3.mime_type,original_name:record3.originalName||record3.original_name}}:void 0})),logger2.info(`[Storage] CDN routes enabled at ${cdnConfig.basePath}`),filesTable&&db){let{createResumableRoutes:createResumableRoutes2}=(init_resumable(),__toCommonJS(exports_resumable)),resumable=createResumableRoutes2({basePath:storageConfig.basePath,storage:storageConfig,logger:logger2,getUserId:(request)=>request.headers.get("x-user-id"),canUpload:(request)=>{if(resolvedOptions.authorization?.enabled===!1)return!0;return checkAuthorizationFromJWT({userClaims:decodeHeaderList(request.headers.get("x-user-claims")),userRoles:decodeHeaderList(request.headers.get("x-user-roles")),claimScopes:decodeClaimScopesHeader(request.headers.get("x-user-claim-scopes")),method:"POST",entity:"files",logger:logger2,requireClaimSpecificity:resolvedOptions.authorization?.requireClaimSpecificity}).authorized},register:async(input)=>{let nodePath=__require("path"),{rename:rename6}=__require("fs/promises"),{randomUUID:randomUUID10}=__require("crypto"),id=randomUUID10(),extension2=nodePath.extname(input.originalName),storedName=`${id}${extension2}`,destination=nodePath.join(storageConfig.basePath,storedName);try{await rename6(input.partPath,destination)}catch(error3){return logger2.error("[Storage] A finished upload could not be moved into place",{error:error3}),null}let uploaded={id,name:storedName,originalName:input.originalName,path:storageConfig.basePath,mimeType:input.mimeType,size:input.size,createdAt:new Date},{buildFileRecordPayload:buildFileRecordPayload2,scheduleUploadMediaProcessing:scheduleUploadMediaProcessing2}=(init_storage2(),__toCommonJS(exports_storage)),t43=filesTable,payload={...buildFileRecordPayload2(uploaded,input.userId),type:input.type},row=(await db.insert(t43).values(payload).returning())[0];if(!row?.id)return null;return scheduleUploadMediaProcessing2([uploaded],{storagePath:storageConfig.basePath,media:{transform:mergeTransformConfig(resolvedOptions.storage?.cdn?.transform),video:mergeVideoConfig(resolvedOptions.storage?.cdn?.video),pdf:mergePdfConfig(resolvedOptions.storage?.cdn?.pdf)},logger:logger2}),{id:String(row.id)}}});plugin.use(resumable.plugin),setInterval(()=>{resumable.sweep(Date.now()).then((removed)=>{if(removed>0)logger2.info(`[Storage] Reclaimed ${removed} abandoned upload(s)`)}).catch(()=>{})},3600000).unref?.(),logger2.info("[Storage] Resumable uploads enabled at /uploads")}}if(resolvedOptions.notification?.enabled&&db){let notificationConfig=resolvedOptions.notification;if(notificationConfig.endpoints?.enabled!==!1){warnOnEntityBasePathCollision("Notification",notificationConfig.endpoints?.basePath||"/notifications",resolvedOptions.entities,logger2);let{routes:notificationRoutes}=createNotificationRoutes({db,schemaTables,config:notificationConfig,logger:logger2,getEmailService,tenantRegistry});plugin.use(notificationRoutes),logger2.info(`[Notification] Routes registered at ${notificationConfig.endpoints?.basePath||"/notifications"}`)}}if(resolvedOptions.verification?.enabled&&db){let{routes:verificationRoutes}=createVerificationRoutes({db,schemaTables,config:resolvedOptions.verification,notificationConfig:resolvedOptions.notification,logger:logger2,getEmailService,tenantRegistry});plugin.use(verificationRoutes)}if(resolvedOptions.backup?.enabled&&db){let{createBackupRoutes:createBackupRoutes2}=(init_backup(),__toCommonJS(exports_backup)),{routes:backupRoutes}=createBackupRoutes2({db,logger:logger2,config:{enabled:!0,basePath:resolvedOptions.backup.basePath||"/admin/backup",storagePath:resolvedOptions.backup.storagePath||"./backups",format:resolvedOptions.backup.format||"json",maxBackups:resolvedOptions.backup.maxBackups||50,allowRestore:resolvedOptions.backup.allowRestore??!0,excludeTables:resolvedOptions.backup.excludeTables||["audit_logs","backup_logs"],encryptionKey:resolvedOptions.backup.encryptionKey?process.env[resolvedOptions.backup.encryptionKey]||resolvedOptions.backup.encryptionKey:void 0,schedule:{enabled:resolvedOptions.backup.schedule?.enabled??!1,cron:resolvedOptions.backup.schedule?.cron||"0 2 * * *",retentionDays:resolvedOptions.backup.schedule?.retentionDays||30,timeZone:resolvedOptions.backup.schedule?.timeZone}},schemaTables,schemaName:targetSchemaName,tenantRegistry,lock:getRedisManager()});plugin.use(backupRoutes),logger2.info("[Backup] Routes registered",{basePath:resolvedOptions.backup.basePath||"/admin/backup",scheduleEnabled:resolvedOptions.backup.schedule?.enabled??!1})}if(resolvedOptions.authorization?.endpointDiscovery?.enabled&&db&&resolvedOptions.authentication?.mode!=="consumer"){let{createAuthorizationDiscoveryRoutes:createAuthorizationDiscoveryRoutes2}=(init_authorization(),__toCommonJS(exports_authorization));plugin.use(createAuthorizationDiscoveryRoutes2({db,schemaTables,logger:logger2,discovery:resolvedOptions.authorization.endpointDiscovery,readState:async(key2)=>{let rm3=getRedisManager();if(!rm3)return null;let res=await rm3.read(key2);return res?.success?res.data:null}})),logger2.info("[Authorization] Endpoint-discovery routes registered",{discover:"/authorization/discover",manifest:"/authorization/route-manifest"})}let pubsubClientManager=null;if(resolvedOptions.pubsub?.enabled){let redis=getRedisManager();if(redis){let pubsubConfig=resolvedOptions.pubsub,{plugin:pubsubPlugin,clientManager}=createPubSubRoutes({redis,logger:logger2,basePath:pubsubConfig.basePath||"/subs",wsPath:pubsubConfig.wsPath||"/api/events/subscribe",pubsubName:pubsubConfig.pubsubName||"pubsub-redis",maxClientsPerUser:pubsubConfig.maxClientsPerUser??10,maxTopicsPerClient:pubsubConfig.maxTopicsPerClient??64,wsIdleTimeout:pubsubConfig.wsIdleTimeout??120,ack:{enabled:pubsubConfig.ack?.enabled??!0,ttlSeconds:pubsubConfig.ack?.ttlSeconds??300,maxRetries:pubsubConfig.ack?.maxRetries??3,retryIntervalMs:pubsubConfig.ack?.retryIntervalMs??5000},presence:{enabled:pubsubConfig.presence?.enabled??!0,debounceMs:pubsubConfig.presence?.debounceMs??5000},cleanupIntervalMs:pubsubConfig.cleanupIntervalMs??60000,daprApiToken:pubsubConfig.daprApiToken,getLiveMonitoringService:()=>liveMonitoringService,authenticate:envResolved.accessTokenSecret?(headers)=>{let wsTokens=parseTokenValuesFromHeaders(headers,tokenNames);if(!wsTokens.access_token)return logger2.debug("[PubSub] WS handshake rejected: no access token presented"),null;let jwtResult=verifyJWT(wsTokens.access_token,envResolved.accessTokenSecret||"",{issuer:authentication?.accessToken?.issuer,audience:authentication?.accessToken?.audience});if(!jwtResult.valid)return logger2.warn("[PubSub] WS handshake rejected: JWT verification failed",{reason:jwtResult.error}),null;let sub=jwtResult.payload?.sub;return typeof sub==="string"&&sub.length>0?{userId:sub}:null}:void 0});plugin.use(pubsubPlugin),pubsubClientManager=clientManager,logger2.info("[PubSub] Enabled",{basePath:pubsubConfig.basePath||"/subs",wsPath:pubsubConfig.wsPath||"/api/events/subscribe"})}else logger2.warn("[PubSub] pubsub is enabled but Redis is not configured. Disabling PubSub.")}if(resolvedOptions.chat?.enabled&&db){let chatConfig=mergeChatConfig(resolvedOptions.chat),chatStorageConfig=mergeStorageConfig(resolvedOptions.storage),attachmentsAvailable=resolvedOptions.storage?.enabled===!0&&resolvedOptions.storage?.cdn?.enabled===!0&&chatConfig.attachments.enabled,cdnBasePath=resolvedOptions.storage?.cdn?.basePath||"/cdn",cdnMedia={transform:mergeTransformConfig(resolvedOptions.storage?.cdn?.transform),video:mergeVideoConfig(resolvedOptions.storage?.cdn?.video),pdf:mergePdfConfig(resolvedOptions.storage?.cdn?.pdf)},{routes:chatRoutes}=createChatRoutes({db,schemaTables,config:chatConfig,logger:logger2,broadcaster:pubsubClientManager,tenantRegistry,storageConfig:chatStorageConfig,attachmentsAvailable,cdnBasePath,cdnMedia});plugin.use(chatRoutes),logger2.info("[Chat] Enabled",{basePath:chatConfig.basePath,attachmentsAvailable,realtime:pubsubClientManager!==null})}if(resolvedOptions.payment?.enabled&&db){let{createPaymentService:createPaymentService2}=(init_Payment(),__toCommonJS(exports_Payment)),{createPaymentRoutes:createPaymentRoutes2}=(init_payment(),__toCommonJS(exports_payment)),paymentService=createPaymentService2(resolvedOptions);if(paymentService){let paymentConfig=resolvedOptions.payment,marketplaceServices=null;if(paymentConfig?.marketplace?.enabled){let{createMarketplaceServices:createMarketplaceServices2}=(init_Payment(),__toCommonJS(exports_Payment));marketplaceServices=createMarketplaceServices2({options:resolvedOptions,logger:logger2,getDb:()=>db,provider:paymentService.providerInstance,schemaTables})}let paymentRoutes=createPaymentRoutes2({provider:paymentService.providerInstance,webhookSecret:paymentService.webhookSecret,getPayoutService:()=>marketplaceServices?.payoutService??null,basePath:paymentConfig?.basePath||"/payment",defaultCurrency:paymentConfig?.defaultCurrency||"TRY",defaultLocale:paymentConfig?.defaultLocale||"tr",successRedirectUrl:paymentConfig?.successRedirectUrl||"/payment/success",failedRedirectUrl:paymentConfig?.failedRedirectUrl||"/payment/failed",errorRedirectUrl:paymentConfig?.errorRedirectUrl||"/payment/error",callbackUrl:paymentConfig?.callbackUrl,savedMethodsEnabled:paymentConfig?.savedMethodsEnabled??!0,threeDSecureEnabled:paymentConfig?.threeDSecureEnabled??!0,subMerchantsEnabled:paymentConfig?.subMerchantsEnabled??!1,transactionsTable:schemaTables.paymentTransactions??schemaTables.payment_transactions,methodsTable:schemaTables.paymentMethods??schemaTables.payment_methods,webhookLogsTable:schemaTables.paymentWebhookLogs??schemaTables.payment_webhook_logs,subMerchantsTable:schemaTables.paymentSubMerchants??schemaTables.payment_sub_merchants,commissionSplitsTable:schemaTables.paymentCommissionSplits??schemaTables.payment_commission_splits,productsTable:schemaTables.paymentProducts??schemaTables.payment_products,pricesTable:schemaTables.paymentPrices??schemaTables.payment_prices,customersTable:schemaTables.paymentCustomers??schemaTables.payment_customers,subscriptionsTable:schemaTables.paymentSubscriptions??schemaTables.payment_subscriptions,invoicesTable:schemaTables.paymentInvoices??schemaTables.payment_invoices,db,logger:logger2});if(plugin.use(paymentRoutes),logger2.info("[Payment] Routes registered",{basePath:paymentConfig?.basePath||"/payment",provider:paymentService.provider}),marketplaceServices){let{createMarketplaceRoutes:createMarketplaceRoutes2}=(init_marketplace(),__toCommonJS(exports_marketplace)),services=marketplaceServices;createMarketplaceRoutes2(plugin,{getMarketplaceService:()=>services.marketplaceService,getPayoutService:()=>services.payoutService,basePath:paymentConfig?.basePath||"/payment",logger:logger2}),logger2.info("[Payment] Marketplace routes registered",{basePath:`${paymentConfig?.basePath||"/payment"}/marketplace`})}}}let cohortConfig=authentication?.cohorts;if(cohortConfig?.enabled!==!1&&!isConsumerMode&&db){let cohortBasePath=cohortConfig?.basePath||"/auth/admin/cohorts",resolveTable=(key2,fallback)=>schemaTables[key2]??(fallback?schemaTables[fallback]:null)??null,resolvedCohortsTable=resolveTable("userCohorts","user_cohorts");if(!resolvedCohortsTable)logger2.warn("[Cohort] user_cohorts table not found in schema \u2014 cohort routes will be inactive. Re-generate schema with nucleus-core >= 0.9.120");if(plugin.use(createCohortRoutes({db,logger:logger2,cohortsTable:resolvedCohortsTable,usersTable:resolveTable("users"),rolesTable:resolveTable("roles"),userRolesTable:resolveTable("userRoles","user_roles"),profilesTable:resolveTable("profiles"),basePath:cohortBasePath,get defaultRole(){return resolvedOptions.authentication?.defaultRole},get defaultRoles(){return resolvedOptions.authentication?.defaultRoles},emailExemptDomains:authentication?.emailExemptDomains,passwordPolicy:authentication?.passwordPolicy})),resolvedCohortsTable)logger2.info("[Cohort] Routes registered",{basePath:cohortBasePath})}if(resolvedOptions.configManagement?.enabled){let configBasePath=resolvedOptions.configManagement.basePath||"/nucleus/config";plugin.use(createConfigRoutes({logger:logger2,resolvedOptions,configFilePath,basePath:configBasePath,db,schemaTables,getRedis:()=>{let redis=getRedisManager();if(!redis)return null;return{read:async(key2)=>{let r2=await redis.read(key2);return{success:r2.success,data:r2.success?r2.data:null}},create:async(key2,value2)=>{return{success:(await redis.create(key2,value2)).success}}}},onConfigUpdate:(section,_newValue)=>{logger2.info(`[ConfigManagement] Section "${section}" updated in-memory`,{section})}})),logger2.info(`[ConfigManagement] Routes enabled at ${configBasePath}`)}if(resolvedOptions.secrets?.enabled){let secretsBasePath=resolvedOptions.secrets.basePath||"/secrets";warnOnEntityBasePathCollision("Secrets",secretsBasePath,resolvedOptions.entities,logger2),plugin.use(createSecretsRoutes({logger:logger2,basePath:secretsBasePath,db,schemaTables,resolvedOptions,getService:()=>secretsService,getEmailService,getStorageProvider:()=>storageProvider,buildStorageProvider,adminRoles:resolvedOptions.secrets.adminRoles})),logger2.info(`[Secrets] Routes enabled at ${secretsBasePath}`)}if(resolvedOptions.integrations?.enabled){let integrationsBasePath=resolvedOptions.integrations.basePath||"/integrations";warnOnEntityBasePathCollision("Integrations",integrationsBasePath,resolvedOptions.entities,logger2),plugin.use(createIntegrationRoutes({logger:logger2,basePath:integrationsBasePath,db,schemaTables,resolvedOptions,getEncryptionKey:()=>resolveMasterKey(resolvedOptions.secrets?.encryptionKey)??null,getLockStore:()=>getRedisManager(),publish:(topic,payload)=>pubsubClientManager?.broadcastEvent(topic,payload),adminRoles:resolvedOptions.integrations.adminRoles}))}plugin.onStart((app)=>{let requiredBody=requiredBodyCeiling({storageEnabled:resolvedOptions.storage?.enabled,maxFileSizeBytes:resolvedOptions.storage?.maxFileSizeBytes,maxFiles:resolvedOptions.storage?.formData?.maxFiles}),complaint=bodyCeilingComplaint({configured:app.config?.serve?.maxRequestBodySize,required:requiredBody});if(complaint)logger2.error(`[Storage] ${complaint}`);let memoryComplaint=memoryCeilingComplaint({required:requiredBody,cgroupLimit:readCgroupLimitBytes()});if(memoryComplaint)logger2.error(`[Storage] ${memoryComplaint}`);let memoryAdvice=readRuntimeMemoryAdvice();if(memoryAdvice){let mib=(bytes)=>`${Math.round(bytes/1024/1024)} MiB`;logger2.warn(`[Runtime] The JS engine sizes its heap for ${mib(memoryAdvice.hostTotal)} but this container is capped at ${mib(memoryAdvice.cgroupLimit)} \u2014 it feels no memory `+"pressure and may grow past the limit and be OOM-killed. Raising the container memory limit delays this; no runtime option we have measured prevents it.")}let port=Number(process.env.PORT)||3000,appId=resolvedOptions.appId||"nucleus",mode=resolvedOptions.mode||"production";console.log(""),console.log(` \x1B[32m\uD83D\uDE80 ${appId}\x1B[0m \x1B[90mv${Date.now()}\x1B[0m`),console.log(` \x1B[36m\u279C\x1B[0m Local: \x1B[36mhttp://localhost:${port}\x1B[0m`),console.log(` \x1B[36m\u279C\x1B[0m Mode: \x1B[33m${mode}\x1B[0m`),console.log("")});let shuttingDown=!1,drainResources=async(trigger)=>{if(shuttingDown)return;shuttingDown=!0,logger2.info(`[Shutdown] ${trigger} \u2014 draining resources`);try{liveMonitoringService?.stop(),monitoringService?.stop(),secretsService?.stop(),storageProvider?.close()}catch(err){logger2.warn("[Shutdown] failed stopping monitoring",{error:err instanceof Error?err.message:String(err)})}try{let redisClient=getRedisManager()?.getDirectClient?.();if(redisClient)await redisClient.quit()}catch(err){logger2.warn("[Shutdown] failed closing Redis",{error:err instanceof Error?err.message:String(err)})}try{for(let timer of backgroundIntervals)clearInterval(timer);backgroundIntervals.length=0}catch(err){logger2.warn("[Shutdown] failed clearing background intervals",{error:err instanceof Error?err.message:String(err)})}try{let{destroyAckCleanup:destroyAckCleanup2}=await Promise.resolve().then(() => (init_ack_manager(),exports_ack_manager));destroyAckCleanup2()}catch{}try{if(dbPool)await dbPool.end()}catch(err){logger2.warn("[Shutdown] failed draining DB pool",{error:err instanceof Error?err.message:String(err)})}logger2.info("[Shutdown] complete")};if(plugin.onStop(async()=>{await drainResources("Elysia onStop")}),resolvedOptions.gracefulShutdown!==!1){let onSignal=(signal)=>{drainResources(signal).finally(()=>process.exit(0))};process.once("SIGTERM",()=>onSignal("SIGTERM")),process.once("SIGINT",()=>onSignal("SIGINT"))}return plugin}init_AzureEmailService();init_Gmail();export{usePubSubStore,usePubSub,serverFetch,requiredBodyCeiling,parseCron2 as parseCron,matchesCron,isDue,generateVerificationEndpoints,generateTenantEndpoints,generateSystemTableEndpoints,generateMonitoringEndpoints,generateMarketplaceEndpoints,generateEndpointsFromConfig,generateDomainEndpoints,generateCohortEndpoints,generateChatEndpoints,generateAuthEndpoints,generateAllEndpoints,generateAdminEndpoints,describeCron,decodeHeaderList,createServerFactory,createApiHook,VERIFICATION_ENDPOINTS,TENANT_ENDPOINTS,ServerFetch,SYSTEM_TABLES,PAYMENT_ENDPOINTS,NucleusElysiaPlugin,MONITORING_ENDPOINTS,MARKETPLACE_ENDPOINTS,GmailService,DOMAIN_ENDPOINTS,CONFIG_ENDPOINTS,COHORT_ENDPOINTS,CHAT_ENDPOINTS,AzureEmailService,AUTH_ENDPOINT_CONFIGS,AUTH_ENDPOINTS};
|
|
@@ -52,8 +52,6 @@ export interface RuntimeMemoryAdvice {
|
|
|
52
52
|
cgroupLimit: number;
|
|
53
53
|
/** What the JS engine believes it has — the NODE's RAM. */
|
|
54
54
|
hostTotal: number;
|
|
55
|
-
/** What `BUN_JSC_forceRAMSize` should be set to, in bytes. */
|
|
56
|
-
recommended: number;
|
|
57
55
|
}
|
|
58
56
|
/**
|
|
59
57
|
* The gap between the ceiling the kernel enforces and the one the engine believes.
|
|
@@ -69,20 +67,20 @@ export interface RuntimeMemoryAdvice {
|
|
|
69
67
|
*
|
|
70
68
|
* It cannot be fixed from inside the process — JSC reads its options when the
|
|
71
69
|
* runtime boots, long before any of this code runs. So the honest thing is to
|
|
72
|
-
* SAY it at startup
|
|
73
|
-
* restart itself twice a day.
|
|
70
|
+
* SAY it at startup rather than let a pod quietly restart itself twice a day.
|
|
74
71
|
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* at
|
|
72
|
+
* This used to carry a remedy: set `BUN_JSC_forceRAMSize` to a quarter of the
|
|
73
|
+
* limit. That remedy is GONE. Measured inside a live container on 15 Aug 2026
|
|
74
|
+
* (Bun 1.3.10), across three settings and two workloads — garbage-heavy and
|
|
75
|
+
* live-heavy — the option moved peak RSS by under 1 MB; quartering it and
|
|
76
|
+
* removing it entirely gave the same figure. The probe was not blind (it saw
|
|
77
|
+
* RSS fall 150 MB → 35 MB on the garbage run), and the option IS recognised (a
|
|
78
|
+
* bogus `BUN_JSC_*` name is still rejected at startup). It simply does nothing
|
|
79
|
+
* here. A remedy that does not work is worse than none: it reads as done, it
|
|
80
|
+
* costs a deploy, and it stops the search.
|
|
82
81
|
*
|
|
83
|
-
* Returns `null` when there is nothing to say: uncapped container, a
|
|
84
|
-
* that is not really a limit
|
|
85
|
-
* below the advice.
|
|
82
|
+
* Returns `null` when there is nothing to say: an uncapped container, or a
|
|
83
|
+
* "limit" that is not really a limit.
|
|
86
84
|
*/
|
|
87
85
|
export declare function runtimeMemoryAdvice(input: {
|
|
88
86
|
cgroupLimit: number | null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nucleus-core-ts",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.964",
|
|
4
4
|
"description": "Production-ready, enterprise-grade TypeScript framework for building multi-tenant APIs",
|
|
5
5
|
"author": "Hidayet Can Özcan <hidayetcan@gmail.com>",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|