nucleus-core-ts 0.9.963 → 0.9.965

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.963
1
+ 0.9.965
package/dist/index.js CHANGED
@@ -30,11 +30,11 @@ and limitations under the License.
30
30
  ${textElements}
31
31
  </g>
32
32
  ${overlayLines}
33
- </svg>`}function generatePuzzleChallenge(difficulty){let pieceCount=DIFFICULTY_CONFIG[difficulty].puzzlePieces,pieces=[],correctOrder=[];for(let i=0;i<pieceCount;i++)pieces.push({id:i,x:i%3*100,y:Math.floor(i/3)*100}),correctOrder.push(i);let shuffledPieces=[...pieces];for(let i=shuffledPieces.length-1;i>0;i--){let j=getSecureRandomInt(0,i),itemI=shuffledPieces[i],itemJ=shuffledPieces[j];if(itemI&&itemJ)shuffledPieces[i]=itemJ,shuffledPieces[j]=itemI}return{question:"Arrange the pieces in correct order",answer:correctOrder.join(","),puzzleData:{pieces:shuffledPieces,correctOrder}}}class CaptchaService{redis;logger;config;constructor(deps){this.redis=deps.redis,this.logger=deps.logger,this.config={enabled:deps.config.enabled??!0,type:deps.config.type??"math",difficulty:deps.config.difficulty??"medium",expiresIn:deps.config.expiresIn??"5m",maxAttempts:deps.config.maxAttempts??3,caseSensitive:deps.config.caseSensitive??!1,rateLimit:{maxGeneratePerMinute:deps.config.rateLimit?.maxGeneratePerMinute??10,maxGeneratePerHour:deps.config.rateLimit?.maxGeneratePerHour??100}}}async generate(type,difficulty,ipAddress){if(ipAddress){if(await this.checkRateLimit(ipAddress))return this.logger.warn("[CAPTCHA] Rate limit exceeded",{ipAddress}),{success:!1,challengeId:"",type:type??this.config.type,question:"",expiresAt:0,rateLimited:!0,message:"Too many requests. Please try again later."};await this.incrementRateLimit(ipAddress)}let captchaType=type??this.config.type,captchaDifficulty=difficulty??this.config.difficulty,challengeId=generateSecureId(),expiresInSeconds=parseTimeToSeconds(this.config.expiresIn),expiresAt=Date.now()+expiresInSeconds*1000,question,answer,imageData,puzzleData;switch(captchaType){case"math":{let mathResult=generateMathChallenge(captchaDifficulty);question=mathResult.question,answer=mathResult.answer;break}case"text":{let textResult=generateTextChallenge(captchaDifficulty);question=textResult.question,answer=textResult.answer;break}case"image":{let imageResult=generateImageChallenge(captchaDifficulty);question=imageResult.question,answer=imageResult.answer,imageData=imageResult.imageData;break}case"puzzle":{let puzzleResult=generatePuzzleChallenge(captchaDifficulty);question=puzzleResult.question,answer=puzzleResult.answer,puzzleData=puzzleResult.puzzleData;break}default:{let defaultResult=generateMathChallenge(captchaDifficulty);question=defaultResult.question,answer=defaultResult.answer}}let normalizedAnswer=this.config.caseSensitive?answer:answer.toUpperCase(),answerHash=hashAnswer(normalizedAnswer),challenge={id:challengeId,type:captchaType,question,answer:answerHash,imageData,puzzleData,expiresAt,attempts:0,maxAttempts:this.config.maxAttempts,caseSensitive:this.config.caseSensitive};await this.redis.set(`captcha:${challengeId}`,JSON.stringify(challenge),{ex:expiresInSeconds}),this.logger.info("[CAPTCHA] Challenge generated",{challengeId,type:captchaType,difficulty:captchaDifficulty,expiresAt});let result={success:!0,challengeId,type:captchaType,question,expiresAt};if(imageData)result.imageData=imageData;if(puzzleData)result.puzzleData={pieces:puzzleData.pieces};return result}async validate(challengeId,userAnswer){let key=`captcha:${challengeId}`,stored=await this.redis.get(key);if(!stored)return this.logger.warn("[CAPTCHA] Challenge not found or expired",{challengeId}),{success:!1,valid:!1,message:"Challenge expired or not found"};let challenge=JSON.parse(stored);if(Date.now()>challenge.expiresAt)return await this.redis.del(key),this.logger.warn("[CAPTCHA] Challenge expired",{challengeId}),{success:!1,valid:!1,message:"Challenge expired"};let ttl=Math.max(1,Math.floor((challenge.expiresAt-Date.now())/1000)),attemptsKey=`${key}:att`,attempts;if(this.redis.incrementCounter)attempts=await this.redis.incrementCounter(attemptsKey,1,ttl);else challenge.attempts+=1,attempts=challenge.attempts,await this.redis.set(key,JSON.stringify(challenge),{ex:ttl});let clearChallenge=async()=>{if(await this.redis.del(key),this.redis.incrementCounter)await this.redis.del(attemptsKey)};if(attempts>challenge.maxAttempts)return await clearChallenge(),this.logger.warn("[CAPTCHA] Max attempts exceeded",{challengeId,attempts}),{success:!1,valid:!1,message:"Maximum attempts exceeded"};let normalizedUserAnswer=challenge.caseSensitive?userAnswer.trim():userAnswer.trim().toUpperCase(),userAnswerHash=hashAnswer(normalizedUserAnswer);if(timingSafeEqual(challenge.answer,userAnswerHash))return await clearChallenge(),this.logger.info("[CAPTCHA] Challenge validated successfully",{challengeId}),{success:!0,valid:!0,message:"Captcha validated successfully"};let remainingAttempts=challenge.maxAttempts-attempts;if(remainingAttempts<=0)await clearChallenge();return this.logger.warn("[CAPTCHA] Invalid answer",{challengeId,attemptsRemaining:remainingAttempts}),{success:!0,valid:!1,message:"Incorrect answer",attemptsRemaining:remainingAttempts}}async invalidate(challengeId){await this.redis.del(`captcha:${challengeId}`),this.logger.info("[CAPTCHA] Challenge invalidated",{challengeId})}isEnabled(){return this.config.enabled}async checkRateLimit(ipAddress){let minuteKey=`captcha_rate:${ipAddress}:minute`,hourKey=`captcha_rate:${ipAddress}:hour`,[minuteCount,hourCount]=await Promise.all([this.redis.get(minuteKey),this.redis.get(hourKey)]),minuteRequests=minuteCount?Number.parseInt(minuteCount,10):0,hourRequests=hourCount?Number.parseInt(hourCount,10):0,maxPerMinute=this.config.rateLimit.maxGeneratePerMinute??10,maxPerHour=this.config.rateLimit.maxGeneratePerHour??100;return minuteRequests>=maxPerMinute||hourRequests>=maxPerHour}async incrementRateLimit(ipAddress){let minuteKey=`captcha_rate:${ipAddress}:minute`,hourKey=`captcha_rate:${ipAddress}:hour`,[minuteCount,hourCount]=await Promise.all([this.redis.get(minuteKey),this.redis.get(hourKey)]),newMinuteCount=(minuteCount?Number.parseInt(minuteCount,10):0)+1,newHourCount=(hourCount?Number.parseInt(hourCount,10):0)+1;await Promise.all([this.redis.set(minuteKey,newMinuteCount.toString(),{ex:60}),this.redis.set(hourKey,newHourCount.toString(),{ex:3600})])}}var DIFFICULTY_CONFIG;var init_Captcha=__esm(()=>{DIFFICULTY_CONFIG={easy:{mathRange:{min:1,max:10},textLength:4,puzzlePieces:4},medium:{mathRange:{min:10,max:50},textLength:6,puzzlePieces:6},hard:{mathRange:{min:50,max:100},textLength:8,puzzlePieces:9}}});import{promises as dns}from"dns";var DEFAULT_TXT_PREFIX="_nucleus-challenge",DEFAULT_CHALLENGE_TTL=604800,DEFAULT_BASE_PATH="/domains",resolveEnvValue=(value)=>{if(value==null)return;let fromEnv=process.env[value];return fromEnv!=null&&fromEnv!==""?fromEnv:value},normalizeHostname=(raw)=>{let trimmed=(raw||"").trim();if(!trimmed)return"";let normalized=((trimmed.replace(/^https?:\/\//i,"").split("/")[0]?.split("?")[0]||"").split(":")[0]||"").toLowerCase().replace(/\.$/,"");try{normalized=new URL(`http://${normalized}`).hostname.replace(/\.$/,"")}catch{}return normalized},isValidHostname=(hostname)=>{if(!hostname||hostname.length>253)return!1;let labels=hostname.split(".");if(labels.length<2)return!1;return labels.every((label)=>/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/.test(label))},classifyHostname=(normalized,platformBaseDomain)=>{if(platformBaseDomain){let base=platformBaseDomain.toLowerCase().replace(/\.$/,"");if(normalized===base)return"apex";if(normalized.endsWith(`.${base}`)){let prefix=normalized.slice(0,-(base.length+1));if(prefix&&!prefix.includes("."))return"platform_subdomain"}}let labels=normalized.split(".");if(labels[0]==="www")return"www";if(labels.length<=2)return"apex";return"subdomain"},generateChallengeToken=()=>{let bytes=new Uint8Array(24);return crypto.getRandomValues(bytes),Array.from(bytes,(b)=>b.toString(16).padStart(2,"0")).join("")},resolveDomainsConfig=(raw)=>{let provider=raw?.provider??"manual_dns",cf=raw?.cloudflare,cloudflare=cf&&resolveEnvValue(cf.accountId)&&resolveEnvValue(cf.zoneId)&&resolveEnvValue(cf.apiToken)?{accountId:resolveEnvValue(cf.accountId),zoneId:resolveEnvValue(cf.zoneId),apiToken:resolveEnvValue(cf.apiToken),fallbackOrigin:resolveEnvValue(cf.fallbackOrigin)??null,cnameTarget:resolveEnvValue(cf.cnameTarget)??null}:null;return{enabled:raw?.enabled===!0,basePath:raw?.basePath??DEFAULT_BASE_PATH,provider,platformBaseDomain:raw?.platformBaseDomain?normalizeHostname(raw.platformBaseDomain):null,cnameTarget:resolveEnvValue(raw?.cnameTarget)??cloudflare?.cnameTarget??null,allowApexDomains:raw?.allowApexDomains!==!1,allowWwwDomains:raw?.allowWwwDomains!==!1,allowManagedRegistrations:raw?.allowManagedRegistrations===!0,reservedHostnames:(raw?.reservedHostnames??[]).map((h)=>normalizeHostname(h)),verification:{defaultMethod:raw?.verification?.defaultMethod??"txt",txtRecordPrefix:raw?.verification?.txtRecordPrefix??DEFAULT_TXT_PREFIX,challengeTtlSeconds:raw?.verification?.challengeTtlSeconds??DEFAULT_CHALLENGE_TTL},cloudflare,registrar:raw?.registrar?{provider:raw.registrar.provider??"cloudflare_registrar",defaultRegistrantOwnerType:raw.registrar.defaultRegistrantOwnerType??"customer"}:null}},str=(v)=>v==null?"":String(v),strOrNull=(v)=>v==null?null:String(v),parseMetadata=(v)=>{if(!v)return{};let obj=typeof v==="string"?safeJson(v):v;if(!obj||typeof obj!=="object")return{};let out={};for(let[k,val]of Object.entries(obj))if(typeof val==="string"||typeof val==="number"||typeof val==="boolean")out[k]=val;return out},safeJson=(v)=>{try{return JSON.parse(v)}catch{return{}}},rowToDomainHostname=(row)=>({id:str(row.id),ownerType:str(row.ownerType??row.owner_type)||"tenant",ownerId:str(row.ownerId??row.owner_id),tenantId:strOrNull(row.tenantId??row.tenant_id),schemaName:strOrNull(row.schemaName??row.schema_name),hostname:str(row.hostname),normalizedHostname:str(row.normalizedHostname??row.normalized_hostname),hostnameKind:str(row.hostnameKind??row.hostname_kind)||"apex",status:str(row.status),routingStatus:str(row.routingStatus??row.routing_status),certificateStatus:str(row.certificateStatus??row.certificate_status),verificationStatus:str(row.verificationStatus??row.verification_status),provider:str(row.provider)||"manual_dns",providerHostnameId:strOrNull(row.providerHostnameId??row.provider_hostname_id),dnsTarget:strOrNull(row.dnsTarget??row.dns_target),isPrimary:Boolean(row.isPrimary??row.is_primary),redirectToHostnameId:strOrNull(row.redirectToHostnameId??row.redirect_to_hostname_id),activatedAt:strOrNull(row.activatedAt??row.activated_at),disabledAt:strOrNull(row.disabledAt??row.disabled_at),failureReason:strOrNull(row.failureReason??row.failure_reason),metadata:parseMetadata(row.metadata)}),verifyTxtRecord=async(recordName,expectedValue)=>{try{let flat=(await dns.resolveTxt(recordName)).map((chunks)=>chunks.join("")),verified=flat.some((v)=>v===expectedValue);return{verified,observed:flat,reason:verified?void 0:"TXT value not found"}}catch(err){return{verified:!1,reason:err instanceof Error?err.message:"DNS lookup failed"}}},verifyCnameRecord=async(recordName,expectedTarget)=>{let target=expectedTarget.toLowerCase().replace(/\.$/,"");try{let flat=(await dns.resolveCname(recordName)).map((v)=>v.toLowerCase().replace(/\.$/,"")),verified=flat.includes(target);return{verified,observed:flat,reason:verified?void 0:"CNAME target mismatch"}}catch(err){return{verified:!1,reason:err instanceof Error?err.message:"DNS lookup failed"}}};var init_Domain_helpers=()=>{};import{eq as eq7}from"drizzle-orm";class DomainRegistrationService{getDb;getRegistry;logger;config;constructor(cfg){this.getDb=cfg.getDb,this.getRegistry=cfg.getTenantRegistry,this.logger=cfg.logger.scoped("domains.registration"),this.config=cfg.config}get enabled(){return this.config.allowManagedRegistrations}table(name){let registry=this.getRegistry();if(!registry)return null;return registry.getMainContext().schemaTables[name]??null}async event(registrationId,eventType,message){let db=this.getDb(),table=this.table("domainEvents");if(!db||!table)return;try{await db.insert(table).values({domainRegistrationId:registrationId,eventType,message:message??null,metadata:JSON.stringify({})})}catch(err){this.logger.warn("Failed to record registration event",{eventType,error:err instanceof Error?err.message:String(err)})}}async createRequest(input){if(!this.enabled)throw Error("Managed domain registration is not enabled");if(!input.termsAccepted)throw Error("Registration requires accepting the domain terms");let db=this.getDb(),table=this.table("domainRegistrations");if(!db||!table)throw Error("Domain registration service not ready");let normalized=normalizeHostname(input.requestedDomain),rows=await db.insert(table).values({ownerType:input.ownerType??"tenant",ownerId:input.ownerId,tenantId:input.tenantId??null,requestedDomain:normalized,provider:this.config.registrar?.provider??"cloudflare_registrar",registrantOwnerType:input.registrantOwnerType??this.config.registrar?.defaultRegistrantOwnerType??"customer",status:"awaiting_payment",autoRenew:input.autoRenew??!0,termsVersion:input.termsVersion,termsAcceptedAt:new Date,metadata:JSON.stringify(input.metadata??{})}).returning(),record=rowToRegistration(rows[0]);return await this.event(record.id,"registration_requested",`Requested ${normalized}`),record}async listRegistrations(filter){let db=this.getDb(),table=this.table("domainRegistrations");if(!db||!table)return[];let where=filter.tenantId?eq7(col2(table,"tenantId"),filter.tenantId):filter.ownerId?eq7(col2(table,"ownerId"),filter.ownerId):void 0;return(where?await db.select().from(table).where(where):await db.select().from(table)).map((r)=>rowToRegistration(r))}async getRegistration(id){let db=this.getDb(),table=this.table("domainRegistrations");if(!db||!table)return null;let rows=await db.select().from(table).where(eq7(col2(table,"id"),id)).limit(1);return rows[0]?rowToRegistration(rows[0]):null}async transition(id,status,patch){let db=this.getDb(),table=this.table("domainRegistrations");if(!db||!table)return null;return await db.update(table).set({status,registeredDomain:patch?.registeredDomain,providerRegistrationId:patch?.providerRegistrationId,priceAmount:patch?.priceAmount,currency:patch?.currency,renewalDate:patch?.renewalDate,failureReason:patch?.failureReason??null,updatedAt:new Date}).where(eq7(col2(table,"id"),id)),await this.event(id,`registration_${status}`),this.getRegistration(id)}async cancel(id){return this.transition(id,"cancelled")}async requestTransferOut(id){return this.transition(id,"transfer_requested")}}var col2=(table,name)=>table[name],rowToRegistration=(row)=>({id:String(row.id??""),ownerType:String(row.ownerType??row.owner_type??"tenant"),ownerId:String(row.ownerId??row.owner_id??""),tenantId:row.tenantId??row.tenant_id?String(row.tenantId??row.tenant_id):null,requestedDomain:String(row.requestedDomain??row.requested_domain??""),registeredDomain:row.registeredDomain??row.registered_domain?String(row.registeredDomain??row.registered_domain):null,provider:String(row.provider??"cloudflare_registrar"),status:String(row.status??"draft"),registrantOwnerType:String(row.registrantOwnerType??row.registrant_owner_type??"customer"),priceAmount:row.priceAmount??row.price_amount?String(row.priceAmount??row.price_amount):null,currency:row.currency?String(row.currency):null,autoRenew:Boolean(row.autoRenew??row.auto_renew??!0),termsVersion:row.termsVersion??row.terms_version?String(row.termsVersion??row.terms_version):null,termsAcceptedAt:row.termsAcceptedAt??row.terms_accepted_at?String(row.termsAcceptedAt??row.terms_accepted_at):null,failureReason:row.failureReason??row.failure_reason?String(row.failureReason??row.failure_reason):null});var init_DomainRegistrationService=__esm(()=>{init_Domain_helpers()});import{and as and3,eq as eq8}from"drizzle-orm";class DomainService{getDb;getRegistry;logger;config;provider;constructor(cfg){this.getDb=cfg.getDb,this.getRegistry=cfg.getTenantRegistry,this.logger=cfg.logger.scoped("domains.service"),this.config=cfg.config,this.provider=cfg.provider}table(name){let registry=this.getRegistry();if(!registry)return null;return registry.getMainContext().schemaTables[name]??null}toProviderInput(record){return{hostname:record.hostname,normalizedHostname:record.normalizedHostname,hostnameKind:record.hostnameKind}}isReserved(normalized){if(this.config.platformBaseDomain&&normalized===this.config.platformBaseDomain)return!0;return this.config.reservedHostnames.includes(normalized)}async createHostname(input){let db=this.getDb(),table=this.table("domainHostnames");if(!db||!table)throw Error("Domain service not ready");let normalized=normalizeHostname(input.hostname);if(!isValidHostname(normalized))throw Error(`Invalid hostname: ${input.hostname}`);if(this.isReserved(normalized))throw Error(`Hostname is reserved: ${normalized}`);let kind=classifyHostname(normalized,this.config.platformBaseDomain);if(kind==="apex"&&!this.config.allowApexDomains)throw Error("Apex domains are not allowed");if(kind==="www"&&!this.config.allowWwwDomains)throw Error("www domains are not allowed");if((await db.select().from(table).where(eq8(col3(table,"normalizedHostname"),normalized)).limit(1))[0])throw Error(`Hostname already registered: ${normalized}`);let providerInput={hostname:input.hostname,normalizedHostname:normalized,hostnameKind:kind},provisioned=await this.provider.createCustomHostname(providerInput),rows=await db.insert(table).values({ownerType:input.ownerType??"tenant",ownerId:input.ownerId,tenantId:input.tenantId??null,schemaName:input.schemaName??null,hostname:input.hostname,normalizedHostname:normalized,hostnameKind:kind,status:"pending_verification",routingStatus:provisioned.routingStatus,certificateStatus:provisioned.certificateStatus,verificationStatus:provisioned.verificationStatus,provider:this.provider.name,providerHostnameId:provisioned.providerHostnameId,dnsTarget:provisioned.dnsTarget,isPrimary:input.isPrimary??!1,metadata:JSON.stringify(input.metadata??{})}).returning(),record=rowToDomainHostname(rows[0]);return await this.persistChallenges(record.id,provisioned.challenges),await this.recordEvent({domainHostnameId:record.id,eventType:"hostname_requested",message:`Custom hostname requested: ${normalized}`,metadata:{provider:this.provider.name,kind}}),{hostname:record,instructions:provisioned.instructions}}async persistChallenges(hostnameId,challenges){let db=this.getDb(),table=this.table("domainVerificationChallenges");if(!db||!table||challenges.length===0)return;let expiresAt=new Date(Date.now()+this.config.verification.challengeTtlSeconds*1000);for(let c of challenges)await db.insert(table).values({domainHostnameId:hostnameId,challengeType:c.challengeType,recordType:c.recordType,recordName:c.recordName,recordValue:c.recordValue,expectedTarget:c.expectedTarget??null,status:"pending",expiresAt})}async listHostnames(filter){let db=this.getDb(),table=this.table("domainHostnames");if(!db||!table)return[];let where=filter.tenantId?eq8(col3(table,"tenantId"),filter.tenantId):filter.ownerId?eq8(col3(table,"ownerId"),filter.ownerId):void 0;return(where?await db.select().from(table).where(where):await db.select().from(table)).map((r)=>rowToDomainHostname(r))}async getHostname(id){let db=this.getDb(),table=this.table("domainHostnames");if(!db||!table)return null;let rows=await db.select().from(table).where(eq8(col3(table,"id"),id)).limit(1);return rows[0]?rowToDomainHostname(rows[0]):null}async getInstructions(id){let record=await this.getHostname(id);if(!record)return[];let db=this.getDb(),table=this.table("domainVerificationChallenges"),instructions=[];if(record.dnsTarget)instructions.push({recordType:record.hostnameKind==="apex"?"A":"CNAME",name:record.normalizedHostname,value:record.dnsTarget,note:record.hostnameKind==="apex"?"Apex domains require an A/ALIAS record at your DNS provider":"Point this hostname at the platform routing target"});if(db&&table){let rows=await db.select().from(table).where(eq8(col3(table,"domainHostnameId"),id));for(let r of rows)instructions.push({recordType:String(r.recordType??"TXT"),name:String(r.recordName??""),value:String(r.recordValue??""),note:String(r.challengeType??"")==="certificate_validation"?"SSL validation":"Ownership verification"})}return instructions}async loadPendingChallenge(hostnameId){let db=this.getDb(),table=this.table("domainVerificationChallenges");if(!db||!table)return null;let row=(await db.select().from(table).where(and3(eq8(col3(table,"domainHostnameId"),hostnameId),eq8(col3(table,"status"),"pending"))).limit(1))[0];if(!row)return null;return{challengeType:String(row.challengeType??"ownership_validation"),recordType:String(row.recordType??"TXT"),recordName:String(row.recordName??""),recordValue:String(row.recordValue??""),expectedTarget:row.expectedTarget!=null?String(row.expectedTarget):void 0}}async verifyHostname(id){let record=await this.getHostname(id);if(!record)return null;let providerInput=this.toProviderInput(record),routingStatus=record.routingStatus,certificateStatus=record.certificateStatus,verificationStatus=record.verificationStatus,failureReason=null;if(this.provider.name==="manual_dns"){let challenge=await this.loadPendingChallenge(id);if(challenge){let result=await this.provider.verifyOwnership(providerInput,challenge);if(verificationStatus=result.verified?"verified":"failed",failureReason=result.verified?null:result.reason??"Verification failed",result.verified)await this.markChallengesVerified(id)}let status2=await this.provider.getCustomHostnameStatus(record.providerHostnameId,providerInput);routingStatus=status2.routingStatus,certificateStatus=status2.certificateStatus}else{let status2=await this.provider.getCustomHostnameStatus(record.providerHostnameId,providerInput);if(routingStatus=status2.routingStatus,certificateStatus=status2.certificateStatus,verificationStatus=status2.verificationStatus,failureReason=status2.failureReason??null,verificationStatus==="verified")await this.markChallengesVerified(id)}let ready=verificationStatus==="verified"&&routingStatus==="active"&&(certificateStatus==="active"||certificateStatus==="not_required"),status=verificationStatus==="failed"?"failed":ready?"active":"verifying";if(await this.applyStatus(id,{status,routingStatus,certificateStatus,verificationStatus,failureReason,activate:ready}),ready)await this.syncRegistry(id);return await this.recordEvent({domainHostnameId:id,eventType:ready?"verification_succeeded":"verification_attempted",message:failureReason??`verification=${verificationStatus} routing=${routingStatus} ssl=${certificateStatus}`}),this.getHostname(id)}async activateHostname(id){let record=await this.getHostname(id);if(!record)return null;if(record.verificationStatus!=="verified")throw Error("Cannot activate a hostname before ownership is verified");return await this.applyStatus(id,{status:"active",routingStatus:"active",certificateStatus:record.certificateStatus==="failed"?"pending":record.certificateStatus,verificationStatus:"verified",failureReason:null,activate:!0}),await this.syncRegistry(id),await this.recordEvent({domainHostnameId:id,eventType:"routing_active",message:"Hostname activated"}),this.getHostname(id)}async disableHostname(id){let db=this.getDb(),table=this.table("domainHostnames"),record=await this.getHostname(id);if(!db||!table||!record)return null;return await this.provider.deleteCustomHostname(record.providerHostnameId),await db.update(table).set({status:"disabled",routingStatus:"inactive",disabledAt:new Date,updatedAt:new Date}).where(eq8(col3(table,"id"),id)),this.getRegistry()?.unregisterDomainHostname(record.normalizedHostname),await this.recordEvent({domainHostnameId:id,eventType:"hostname_disabled",message:"Hostname disabled"}),this.getHostname(id)}async makePrimary(id){let db=this.getDb(),table=this.table("domainHostnames"),record=await this.getHostname(id);if(!db||!table||!record)return null;let scope=record.tenantId?eq8(col3(table,"tenantId"),record.tenantId):eq8(col3(table,"ownerId"),record.ownerId);return await db.update(table).set({isPrimary:!1,updatedAt:new Date}).where(scope),await db.update(table).set({isPrimary:!0,updatedAt:new Date}).where(eq8(col3(table,"id"),id)),await this.recordEvent({domainHostnameId:id,eventType:"primary_changed",message:"Primary hostname set"}),this.getHostname(id)}async refreshStatus(id){return this.verifyHostname(id)}async resolveHostname(hostname){let db=this.getDb(),table=this.table("domainHostnames");if(!db||!table)return null;let normalized=normalizeHostname(hostname),row=(await db.select().from(table).where(and3(eq8(col3(table,"normalizedHostname"),normalized),eq8(col3(table,"status"),"active"))).limit(1))[0];if(!row)return null;let record=rowToDomainHostname(row),redirectToHostname=null;if(record.redirectToHostnameId)redirectToHostname=(await this.getHostname(record.redirectToHostnameId))?.normalizedHostname??null;return{hostnameId:record.id,tenantId:record.tenantId,schemaName:record.schemaName,isPrimary:record.isPrimary,redirectToHostname}}async recordEvent(input){let db=this.getDb(),table=this.table("domainEvents");if(!db||!table)return;try{await db.insert(table).values({domainHostnameId:input.domainHostnameId??null,domainRegistrationId:input.domainRegistrationId??null,eventType:input.eventType,actorType:input.actorType??null,actorId:input.actorId??null,message:input.message??null,ipAddress:input.ipAddress??null,metadata:JSON.stringify(input.metadata??{})})}catch(err){this.logger.warn("Failed to record domain event",{eventType:input.eventType,error:err instanceof Error?err.message:String(err)})}}async markChallengesVerified(hostnameId){let db=this.getDb(),table=this.table("domainVerificationChallenges");if(!db||!table)return;await db.update(table).set({status:"verified",verifiedAt:new Date,updatedAt:new Date}).where(eq8(col3(table,"domainHostnameId"),hostnameId))}async applyStatus(id,patch){let db=this.getDb(),table=this.table("domainHostnames");if(!db||!table)return;await db.update(table).set({status:patch.status,routingStatus:patch.routingStatus,certificateStatus:patch.certificateStatus,verificationStatus:patch.verificationStatus,failureReason:patch.failureReason,activatedAt:patch.activate?new Date:void 0,updatedAt:new Date}).where(eq8(col3(table,"id"),id))}async syncRegistry(id){let record=await this.getHostname(id),registry=this.getRegistry();if(!record||!registry)return;if(record.status==="active"&&record.tenantId)registry.registerDomainHostname(record.normalizedHostname,record.tenantId)}}var col3=(table,name)=>table[name];var init_DomainService=__esm(()=>{init_Domain_helpers()});class CloudflareForSaasProvider{name="cloudflare_for_saas";config;logger;zoneId;apiToken;constructor(config,logger2){if(!config.cloudflare)throw Error("[Domain] Cloudflare for SaaS provider requires domains.cloudflare config");this.config=config,this.logger=logger2.scoped("domains.cloudflare"),this.zoneId=config.cloudflare.zoneId,this.apiToken=config.cloudflare.apiToken}get cnameTarget(){return this.config.cloudflare?.cnameTarget??this.config.cnameTarget}async call(path2,init){let res=await fetch(`https://api.cloudflare.com/client/v4${path2}`,{...init,headers:{Authorization:`Bearer ${this.apiToken}`,"Content-Type":"application/json",...init.headers??{}}}),json=await res.json().catch(()=>({}));if(!res.ok||json.success===!1){let msg=json.errors?.map((e)=>e.message).join("; ")||`HTTP ${res.status}`;throw Error(`Cloudflare API error: ${msg}`)}return json}mapRouting(status){if(status==="active")return"active";if(status==="blocked"||status==="moved"||status==="deleted")return"failed";return"pending"}mapCertificate(sslStatus){if(sslStatus==="active")return"active";if(sslStatus==="validation_timed_out"||sslStatus==="deactivating"||sslStatus==="deleted")return"failed";return"pending"}mapVerification(result){if(result.status==="active")return"verified";if((result.ssl?.validation_errors?.length??0)>0)return"failed";return"pending"}buildChallenges(result){let challenges=[],ov=result.ownership_verification;if(ov?.name&&ov.value)challenges.push({challengeType:"hostname_validation",recordType:"TXT",recordName:ov.name,recordValue:ov.value});for(let vr of result.ssl?.validation_records??[])if(vr.txt_name&&vr.txt_value)challenges.push({challengeType:"certificate_validation",recordType:"TXT",recordName:vr.txt_name,recordValue:vr.txt_value});return challenges}buildInstructions(input,challenges){let target=this.cnameTarget,instructions=[];if(target)instructions.push({recordType:input.hostnameKind==="apex"?"A":"CNAME",name:input.normalizedHostname,value:target,note:input.hostnameKind==="apex"?"Apex domains require an A/ALIAS record pointing at the SaaS target":"Point this hostname at the Cloudflare for SaaS target"});for(let c of challenges)instructions.push({recordType:c.recordType,name:c.recordName,value:c.recordValue,note:c.challengeType==="certificate_validation"?"SSL validation":"Hostname validation"});return instructions}toResult(result,input){let challenges=this.buildChallenges(result);return{providerHostnameId:result.id??null,dnsTarget:this.cnameTarget,routingStatus:this.mapRouting(result.status),certificateStatus:this.mapCertificate(result.ssl?.status),verificationStatus:this.mapVerification(result),instructions:this.buildInstructions(input,challenges),challenges,raw:result}}async createCustomHostname(input){let body=JSON.stringify({hostname:input.normalizedHostname,ssl:{method:"txt",type:"dv",settings:{min_tls_version:"1.2"}}}),json=await this.call(`/zones/${this.zoneId}/custom_hostnames`,{method:"POST",body});return this.toResult(json.result??{},input)}async getCustomHostnameStatus(providerHostnameId,input){let result=await this.fetchHostname(providerHostnameId,input.normalizedHostname);if(!result)return{routingStatus:"failed",certificateStatus:"failed",verificationStatus:"failed",failureReason:"Custom hostname not found at provider"};return{routingStatus:this.mapRouting(result.status),certificateStatus:this.mapCertificate(result.ssl?.status),verificationStatus:this.mapVerification(result),providerStatus:`${result.status??"unknown"}/${result.ssl?.status??"unknown"}`,failureReason:result.ssl?.validation_errors?.map((e)=>e.message).join("; ")||void 0,raw:result}}async fetchHostname(providerHostnameId,normalizedHostname){if(providerHostnameId)return(await this.call(`/zones/${this.zoneId}/custom_hostnames/${providerHostnameId}`,{method:"GET"})).result??null;let list=(await this.call(`/zones/${this.zoneId}/custom_hostnames?hostname=${encodeURIComponent(normalizedHostname)}`,{method:"GET"})).result;return Array.isArray(list)?list[0]??null:null}async deleteCustomHostname(providerHostnameId){if(!providerHostnameId)return;try{await this.call(`/zones/${this.zoneId}/custom_hostnames/${providerHostnameId}`,{method:"DELETE"})}catch(err){this.logger.warn("Failed to delete Cloudflare custom hostname",{providerHostnameId,error:err instanceof Error?err.message:String(err)})}}async verifyOwnership(input,_challenge){let status=await this.getCustomHostnameStatus(null,input);return{verified:status.verificationStatus==="verified",reason:status.failureReason??status.providerStatus}}}class ManualDnsProvider{name="manual_dns";config;constructor(config){this.config=config}buildChallenge(input){let prefix=this.config.verification.txtRecordPrefix,token=`nucleus-domain-verification=${generateChallengeToken()}`;return{challengeType:"ownership_validation",recordType:"TXT",recordName:`${prefix}.${input.normalizedHostname}`,recordValue:token}}buildInstructions(input,challenge){let target=this.config.cnameTarget,instructions=[{recordType:"TXT",name:challenge.recordName,value:challenge.recordValue,note:"Ownership verification record"}];if(input.hostnameKind==="apex")instructions.push({recordType:"A",name:input.normalizedHostname,value:target??"<platform-ingress-ip>",note:"Apex domains require an A/ALIAS record at your DNS provider"});else if(target)instructions.push({recordType:"CNAME",name:input.normalizedHostname,value:target,note:"Point this hostname at the platform routing target"});return instructions}async createCustomHostname(input){let challenge=this.buildChallenge(input);return{providerHostnameId:null,dnsTarget:this.config.cnameTarget,routingStatus:"pending",certificateStatus:"not_required",verificationStatus:"pending",instructions:this.buildInstructions(input,challenge),challenges:[challenge]}}async getCustomHostnameStatus(_providerHostnameId,input){if(input.hostnameKind!=="apex"&&this.config.cnameTarget){let dns2=await verifyCnameRecord(input.normalizedHostname,this.config.cnameTarget);return{routingStatus:dns2.verified?"active":"pending",certificateStatus:"not_required",verificationStatus:"pending",providerStatus:dns2.verified?"cname_active":"cname_pending"}}return{routingStatus:"pending",certificateStatus:"not_required",verificationStatus:"pending",providerStatus:"manual"}}async deleteCustomHostname(_providerHostnameId){}async verifyOwnership(_input,challenge){if(challenge.recordType==="CNAME"&&challenge.expectedTarget)return verifyCnameRecord(challenge.recordName,challenge.expectedTarget);return verifyTxtRecord(challenge.recordName,challenge.recordValue)}}var init_ManualDnsProvider=__esm(()=>{init_Domain_helpers()});var createDomainProvider=(config,logger2)=>{if(config.provider==="cloudflare_for_saas"){if(!config.cloudflare)return logger2.warn("[Domain] cloudflare_for_saas selected but cloudflare config missing \u2014 using manual DNS"),new ManualDnsProvider(config);return new CloudflareForSaasProvider(config,logger2)}return new ManualDnsProvider(config)};var init_Providers=__esm(()=>{init_ManualDnsProvider();init_ManualDnsProvider()});var createDomainServices=(params)=>{let config=resolveDomainsConfig(params.options.domains);if(!config.enabled)return null;let provider=createDomainProvider(config,params.logger),serviceConfig={getDb:params.getDb,getTenantRegistry:params.getTenantRegistry,logger:params.logger,config,provider};return{domainService:new DomainService(serviceConfig),registrationService:new DomainRegistrationService(serviceConfig)}};var init_Domain=__esm(()=>{init_Domain_helpers();init_DomainRegistrationService();init_DomainService();init_Providers();init_Domain_helpers();init_DomainRegistrationService();init_DomainService();init_Providers()});class TemplatedEmailService{emailWrapper(content,appName){return`<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><meta name="color-scheme" content="light dark"><meta name="supported-color-schemes" content="light dark"><title>${appName}</title><style type="text/css">:root{color-scheme:light dark;supported-color-schemes:light dark;}@media(prefers-color-scheme:dark){.em-bg{background-color:#18181b!important;}.em-card{background-color:#27272a!important;border-color:#3f3f46!important;}.em-h{color:#fafafa!important;}.em-t{color:#d4d4d8!important;}.em-m{color:#a1a1aa!important;}.em-hr{background-color:#3f3f46!important;}.em-btn{background-color:#fafafa!important;}.em-btn-text{color:#18181b!important;}.em-link{color:#60a5fa!important;}.em-lb{background-color:#3f3f46!important;border-color:#52525b!important;}.em-lb-t{color:#a1a1aa!important;}.em-sn{background-color:#3f3f46!important;border-color:#52525b!important;color:#a1a1aa!important;}}</style></head><body style="margin:0;padding:0;background-color:#f4f4f5;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;-webkit-font-smoothing:antialiased;"><table role="presentation" class="em-bg" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f4f4f5;"><tr><td align="center" style="padding:40px 16px;"><table role="presentation" width="480" cellpadding="0" cellspacing="0" style="max-width:480px;width:100%;"><tr><td align="center" style="padding-bottom:24px;"><span class="em-m" style="font-size:14px;font-weight:600;color:#71717a;letter-spacing:-0.2px;">${appName}</span></td></tr><tr><td class="em-card" style="background-color:#ffffff;border:1px solid #e4e4e7;border-radius:12px;overflow:hidden;">${content}</td></tr><tr><td align="center" style="padding-top:24px;"><p class="em-m" style="margin:0;font-size:12px;color:#a1a1aa;line-height:1.6;">This email was sent by <strong>${appName}</strong>.<br>Please do not reply to this email.</p></td></tr></table></td></tr></table></body></html>`}emailButton(href,label){return`<table role="presentation" cellpadding="0" cellspacing="0" width="100%"><tr><td align="center" style="padding:28px 32px;"><table role="presentation" cellpadding="0" cellspacing="0"><tr><td class="em-btn" align="center" style="background-color:#18181b;border-radius:8px;"><a class="em-btn-text" href="${href}" target="_blank" style="display:inline-block;padding:12px 32px;font-size:14px;font-weight:600;color:#fafafa;text-decoration:none;letter-spacing:-0.1px;">${label}</a></td></tr></table></td></tr></table>`}emailDivider(){return'<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:0 32px;"><div class="em-hr" style="height:1px;background-color:#e4e4e7;"></div></td></tr></table>'}emailLinkFallback(link,notice){let html=`<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:4px 32px 32px 32px;"><p class="em-m" style="margin:0 0 8px 0;font-size:11px;font-weight:600;color:#a1a1aa;text-transform:uppercase;letter-spacing:0.5px;">Or copy this link</p><table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td class="em-lb" style="background-color:#f4f4f5;border:1px solid #e4e4e7;border-radius:8px;padding:12px 14px;"><a class="em-link" href="${link}" style="font-size:12px;color:#2563eb;word-break:break-all;line-height:1.5;text-decoration:none;">${link}</a></td></tr></table>`;if(notice)html+=`<p class="em-m" style="margin:16px 0 0 0;font-size:12px;color:#a1a1aa;">${notice}</p>`;return html+="</td></tr></table>",html}async sendWelcomeEmail(email,name,appName="Nucleus"){let content=`<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:40px 32px 24px 32px;text-align:center;"><h1 class="em-h" style="margin:0;font-size:24px;font-weight:700;color:#18181b;letter-spacing:-0.5px;">Welcome to ${appName}</h1><p class="em-t" style="margin:8px 0 0 0;font-size:15px;color:#52525b;line-height:1.6;">Your account has been created successfully</p></td></tr></table>${this.emailDivider()}<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:24px 32px 32px 32px;"><p class="em-t" style="margin:0 0 16px 0;font-size:15px;color:#3f3f46;line-height:1.7;">Hello <strong class="em-h" style="color:#18181b;">${name}</strong>,</p><p class="em-t" style="margin:0;font-size:15px;color:#3f3f46;line-height:1.7;">Welcome aboard! You now have full access to <strong>${appName}</strong>. We're excited to have you.</p></td></tr></table>`;return this.sendEmail({to:email,subject:`Welcome to ${appName}`,html:this.emailWrapper(content,appName)})}async sendVerificationEmail(email,name,verificationLink,appName="Nucleus"){let content=`<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:40px 32px 24px 32px;text-align:center;"><h1 class="em-h" style="margin:0;font-size:24px;font-weight:700;color:#18181b;letter-spacing:-0.5px;">Verify your email</h1><p class="em-t" style="margin:8px 0 0 0;font-size:15px;color:#52525b;line-height:1.6;">Confirm your address to get started</p></td></tr></table>${this.emailDivider()}<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:24px 32px 0 32px;"><p class="em-t" style="margin:0 0 4px 0;font-size:15px;color:#3f3f46;line-height:1.7;">Hello <strong class="em-h" style="color:#18181b;">${name}</strong>,</p><p class="em-t" style="margin:0;font-size:15px;color:#3f3f46;line-height:1.7;">Please verify your email address by clicking the button below.</p></td></tr><tr><td>${this.emailButton(verificationLink,"Verify Email Address")}</td></tr></table>${this.emailDivider()}${this.emailLinkFallback(verificationLink)}`;return this.sendEmail({to:email,subject:`Verify your email \u2014 ${appName}`,html:this.emailWrapper(content,appName)})}async sendPasswordResetEmail(email,name,resetLink,appName="Nucleus"){let content=`<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:40px 32px 24px 32px;text-align:center;"><h1 class="em-h" style="margin:0;font-size:24px;font-weight:700;color:#18181b;letter-spacing:-0.5px;">Reset your password</h1><p class="em-t" style="margin:8px 0 0 0;font-size:15px;color:#52525b;line-height:1.6;">We received a request to reset your password</p></td></tr></table>${this.emailDivider()}<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:24px 32px 0 32px;"><p class="em-t" style="margin:0 0 4px 0;font-size:15px;color:#3f3f46;line-height:1.7;">Hello <strong class="em-h" style="color:#18181b;">${name}</strong>,</p><p class="em-t" style="margin:0;font-size:15px;color:#3f3f46;line-height:1.7;">Click the button below to choose a new password. If you didn't make this request, you can safely ignore this email.</p></td></tr><tr><td>${this.emailButton(resetLink,"Reset Password")}</td></tr></table>${this.emailDivider()}${this.emailLinkFallback(resetLink,"This link expires in 1 hour and can only be used once.")}`;return this.sendEmail({to:email,subject:`Reset your password \u2014 ${appName}`,html:this.emailWrapper(content,appName)})}async sendMagicLinkEmail(email,magicLink,appName="Nucleus"){let content=`<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:40px 32px 24px 32px;text-align:center;"><h1 class="em-h" style="margin:0;font-size:24px;font-weight:700;color:#18181b;letter-spacing:-0.5px;">Sign in to ${appName}</h1><p class="em-t" style="margin:8px 0 0 0;font-size:15px;color:#52525b;line-height:1.6;">No password needed \u2014 just click to sign in</p></td></tr></table>${this.emailDivider()}<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:24px 32px 0 32px;"><p class="em-t" style="margin:0;font-size:15px;color:#3f3f46;line-height:1.7;">Click the button below to securely sign in to your <strong class="em-h" style="color:#18181b;">${appName}</strong> account.</p></td></tr><tr><td>${this.emailButton(magicLink,"Sign In")}</td></tr></table>${this.emailDivider()}${this.emailLinkFallback(magicLink,"This link expires in 15 minutes and can only be used once.")}`;return this.sendEmail({to:email,subject:`Sign in to ${appName}`,html:this.emailWrapper(content,appName)})}async sendAlertEmail(to,subject,html){return this.sendEmail({to,subject,html})}async sendInvitationEmail(email,inviteLink,appName="Nucleus"){let content=`<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:40px 32px 24px 32px;text-align:center;"><h1 class="em-h" style="margin:0;font-size:24px;font-weight:700;color:#18181b;letter-spacing:-0.5px;">You're Invited</h1><p class="em-t" style="margin:8px 0 0 0;font-size:15px;color:#52525b;line-height:1.6;">Join <strong class="em-h" style="color:#18181b;">${appName}</strong> and start collaborating</p></td></tr></table>${this.emailDivider()}<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:0 32px 8px 32px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:8px 0;"><table role="presentation" cellpadding="0" cellspacing="0"><tr><td class="em-sn" style="width:28px;height:28px;background-color:#f4f4f5;border:1px solid #e4e4e7;border-radius:50%;text-align:center;line-height:28px;font-size:12px;font-weight:600;color:#71717a;">1</td><td class="em-t" style="padding-left:14px;font-size:14px;color:#52525b;">Click the button below</td></tr></table></td></tr><tr><td style="padding:8px 0;"><table role="presentation" cellpadding="0" cellspacing="0"><tr><td class="em-sn" style="width:28px;height:28px;background-color:#f4f4f5;border:1px solid #e4e4e7;border-radius:50%;text-align:center;line-height:28px;font-size:12px;font-weight:600;color:#71717a;">2</td><td class="em-t" style="padding-left:14px;font-size:14px;color:#52525b;">Choose a secure password</td></tr></table></td></tr><tr><td style="padding:8px 0;"><table role="presentation" cellpadding="0" cellspacing="0"><tr><td class="em-sn" style="width:28px;height:28px;background-color:#f4f4f5;border:1px solid #e4e4e7;border-radius:50%;text-align:center;line-height:28px;font-size:12px;font-weight:600;color:#71717a;">3</td><td class="em-t" style="padding-left:14px;font-size:14px;color:#52525b;">Start collaborating</td></tr></table></td></tr></table></td></tr></table>${this.emailButton(inviteLink,"Accept Invitation")}${this.emailDivider()}${this.emailLinkFallback(inviteLink,"This invitation expires in 7 days.")}`;return this.sendEmail({to:email,subject:`You're invited to join ${appName}`,html:this.emailWrapper(content,appName)})}}var AzureEmailService;var init_AzureEmailService=__esm(()=>{AzureEmailService=class AzureEmailService extends TemplatedEmailService{client=null;senderAddress;fromName;logger;initialized=!1;credentialsFingerprint="";constructor(config,logger2){super();if(this.senderAddress=config.senderAddress,this.fromName=config.fromName||"Nucleus",this.logger=logger2,config.enabled)this.credentialsFingerprint=`${config.connectionString??""}|${config.senderAddress}`,this.initialize(config.connectionString)}reconfigure(config){let fingerprint=`${config.connectionString??""}|${config.senderAddress}`;if(fingerprint===this.credentialsFingerprint&&this.initialized)return;if(this.credentialsFingerprint=fingerprint,this.senderAddress=config.senderAddress||this.senderAddress,config.fromName)this.fromName=config.fromName;if(this.initialized=!1,this.client=null,config.enabled)this.initialize(config.connectionString)}isAvailable(){return this.initialized&&this.client!==null}initialize(connectionString){try{if(!connectionString){this.logger.warn("[AzureEmailService] No connection string provided");return}if(!this.senderAddress){this.logger.warn("[AzureEmailService] No sender address configured");return}let{EmailClient}=__require("@azure/communication-email");this.client=new EmailClient(connectionString),this.initialized=!0,this.logger.info("[AzureEmailService] Initialized",{senderAddress:this.senderAddress})}catch(error){let message=error instanceof Error?error.message:String(error);this.logger.error("[AzureEmailService] Initialization failed",{error:message})}}async sendEmail(payload){if(!this.client)return this.logger.error("[AzureEmailService] Not initialized"),{success:!1,error:"Azure Email service not initialized"};try{let toAddresses=Array.isArray(payload.to)?payload.to:[payload.to],result=await(await this.client.beginSend({senderAddress:payload.from||this.senderAddress,content:{subject:payload.subject,plainText:payload.text,html:payload.html},recipients:{to:toAddresses.map((addr)=>({address:addr}))},attachments:payload.attachments?.map((att)=>({name:att.filename,contentType:att.contentType,contentInBase64:att.content.toString("base64")}))})).pollUntilDone();if(result.status==="Succeeded")return this.logger.info("[AzureEmailService] Email sent",{to:payload.to,subject:payload.subject,operationId:result.id}),{success:!0};let errorMsg=result.error?.message||`Send failed with status: ${result.status}`;return this.logger.error("[AzureEmailService] Send failed",{status:result.status,error:errorMsg,to:payload.to}),{success:!1,error:errorMsg}}catch(error){let message=error instanceof Error?error.message:"Unknown error";return this.logger.error("[AzureEmailService] Send failed",{error:message,to:payload.to}),{success:!1,error:message}}}}});import{createRequire}from"module";function stripAppPasswordGroups(password2){return APP_PASSWORD_SHAPE.test(password2)?password2.replace(/\s+/g,""):password2.trim()}function explainSmtpFailure(message){let text=message.toLowerCase();if(text.includes("5.7.3")||text.includes("authentication unsuccessful"))return"Sunucu kimlik do\u011Frulamay\u0131 reddetti. En s\u0131k sebebi parola de\u011Fil: Exchange Online'da "+"SMTP AUTH kirac\u0131 d\xFCzeyinde varsay\u0131lan olarak KAPALIDIR ve do\u011Fru parolay\u0131 da reddeder. "+"IT'den o posta kutusu i\xE7in \u201CAuthenticated SMTP\u201Dyi a\xE7mas\u0131n\u0131 isteyin. Di\u011Fer ihtimal, "+"uygulama parolas\u0131n\u0131n \u201CPosta kutusu\u201D alan\u0131na yaz\u0131lan adresten ba\u015Fka bir hesaba ait olmas\u0131. "+`Sunucunun kendi yan\u0131t\u0131: ${message}`;if(text.includes("5.7.57")||text.includes("must issue a starttls"))return`Sunucu \u015Fifreli ba\u011Flant\u0131 istiyor. Portu 587 (STARTTLS) veya 465 olarak deneyin. Sunucunun yan\u0131t\u0131: ${message}`;if(text.includes("enotfound")||text.includes("eai_again"))return`Sunucu ad\u0131 \xE7\xF6z\xFClemedi \u2014 \u201CSunucu\u201D alan\u0131n\u0131 kontrol edin (Microsoft 365 i\xE7in smtp.office365.com). Ayr\u0131nt\u0131: ${message}`;if(text.includes("etimedout")||text.includes("econnrefused"))return`Sunucuya ba\u011Flan\u0131lamad\u0131. A\u011F bu porta \xE7\u0131k\u0131\u015Fa izin vermiyor olabilir. Ayr\u0131nt\u0131: ${message}`;if(text.includes("5.7.60")||text.includes("send as"))return`G\xF6nderen adresi bu posta kutusuna ait de\u011Fil. Bo\u015F b\u0131rak\u0131n ya da Exchange'de \u201CSend As\u201D yetkisi verdirin. Sunucunun yan\u0131t\u0131: ${message}`;return message}var APP_PASSWORD_SHAPE,IMPLICIT_TLS_PORT=465,DEFAULT_PORT=587,SmtpEmailService;var init_SmtpEmailService=__esm(()=>{APP_PASSWORD_SHAPE=/^\s*(?:[A-Za-z0-9]{4}\s+){3}[A-Za-z0-9]{4}\s*$/;SmtpEmailService=class SmtpEmailService extends TemplatedEmailService{transporter=null;senderAddress;fromName;logger;initialized=!1;credentialsFingerprint="";constructor(config,logger2){super();this.senderAddress=config.senderAddress,this.fromName=config.fromName||"Nucleus",this.logger=logger2,this.applyConfig(config)}applyConfig(config){let fingerprint=[config.host??"",String(config.port??""),String(config.secure??""),config.user??"",config.password??"",config.senderAddress??"",config.fromName??"",String(config.enabled)].join("|");if(fingerprint===this.credentialsFingerprint&&this.initialized)return;if(this.credentialsFingerprint=fingerprint,this.senderAddress=config.senderAddress||this.senderAddress,config.fromName)this.fromName=config.fromName;if(this.initialized=!1,this.transporter=null,config.enabled)this.initialize(config)}isAvailable(){return this.initialized&&this.transporter!==null}initialize(config){try{let host=config.host?.trim();if(!host){this.logger.warn("[SmtpEmailService] No host configured");return}let user=config.user?.trim();if(!user||!config.password){this.logger.warn("[SmtpEmailService] No credentials configured");return}if(!this.senderAddress)this.senderAddress=user;let port=config.port??DEFAULT_PORT,nodemailer=createRequire(import.meta.url)("nodemailer"),secure=config.secure??port===IMPLICIT_TLS_PORT,pass=stripAppPasswordGroups(config.password);this.transporter=nodemailer.createTransport({host,port,secure,requireTLS:!secure,tls:{minVersion:"TLSv1.2"},auth:{user,pass},pool:!1,connectionTimeout:20000,greetingTimeout:20000,socketTimeout:60000}),this.initialized=!0,this.logger.info("[SmtpEmailService] Initialized",{host,port,senderAddress:this.senderAddress})}catch(error){let message=error instanceof Error?error.message:String(error);this.logger.error("[SmtpEmailService] Initialization failed \u2014 is `nodemailer` installed in this app?",{error:message,host:config.host,port:config.port??DEFAULT_PORT})}}sanitizeHeaderValue(value){return value.replace(/[\r\n\u0000-\u001f\u007f]+/g," ").trim()}async sendEmail(payload){if(!this.transporter)return this.logger.error("[SmtpEmailService] Not initialized"),{success:!1,error:"SMTP email service not initialized"};try{let address=this.sanitizeHeaderValue(payload.from||this.senderAddress),from=this.fromName?`"${this.sanitizeHeaderValue(this.fromName).replace(/"/g,"")}" <${address}>`:address,to=(Array.isArray(payload.to)?payload.to:[payload.to]).map((a)=>this.sanitizeHeaderValue(a)),result=await this.transporter.sendMail({from,to,subject:this.sanitizeHeaderValue(payload.subject),text:payload.text,html:payload.html,replyTo:payload.replyTo?this.sanitizeHeaderValue(payload.replyTo):void 0,attachments:payload.attachments?.map((att)=>({filename:att.filename,content:att.content,contentType:att.contentType}))}),rejected=(result.rejected??[]).filter(Boolean);if(rejected.length>0)return this.logger.warn("[SmtpEmailService] Some recipients were refused",{rejected,accepted:result.accepted?.length??0,subject:payload.subject}),{success:(result.accepted?.length??0)>0,rejected,error:`Bu adresler sunucu taraf\u0131ndan reddedildi: ${rejected.join(", ")}`};return this.logger.info("[SmtpEmailService] Email sent",{to:payload.to,subject:payload.subject,messageId:result.messageId}),{success:!0}}catch(error){let message=error instanceof Error?error.message:"Unknown error";return this.logger.error("[SmtpEmailService] Send failed",{error:message,to:payload.to}),{success:!1,error:explainSmtpFailure(message)}}}}});var init_Email=__esm(()=>{init_AzureEmailService();init_SmtpEmailService()});import fs2 from"fs";import{google}from"googleapis";class GmailService{gmail=null;fromEmail;fromName;logger;initialized=!1;credentialsFingerprint="";constructor(config,logger2){if(this.fromEmail=config.fromEmail,this.fromName=config.fromName||"Vorion",this.logger=logger2,config.enabled)this.initialize(config)}reconfigure(config){if(`${config.serviceAccountJson??""}|${config.jsonFilePath??""}|${config.fromEmail}`===this.credentialsFingerprint&&this.initialized)return;if(this.fromEmail=config.fromEmail||this.fromEmail,config.fromName)this.fromName=config.fromName;if(this.initialized=!1,this.gmail=null,config.enabled)this.initialize(config)}sanitizeHeaderValue(value){return value.replace(/[\r\n\u0000-\u001f\u007f]+/g," ").trim()}encodeHeaderValue(rawValue){let value=this.sanitizeHeaderValue(rawValue),hasNonAscii=!1;for(let i=0;i<value.length;i++)if(value.charCodeAt(i)>127){hasNonAscii=!0;break}if(!hasNonAscii)return value;return`=?UTF-8?B?${Buffer.from(value,"utf-8").toString("base64")}?=`}isAvailable(){return this.initialized&&this.gmail!==null}initialize(config){let{serviceAccountJson,jsonFilePath}=config;this.credentialsFingerprint=`${serviceAccountJson??""}|${jsonFilePath??""}|${config.fromEmail}`;try{let keyContent;if(serviceAccountJson?.trim())keyContent=serviceAccountJson;else if(jsonFilePath){if(!fs2.existsSync(jsonFilePath)){this.logger.error("[GmailService] JSON file not found",{path:jsonFilePath});return}keyContent=fs2.readFileSync(jsonFilePath,"utf8")}else{this.logger.warn("[GmailService] No service-account JSON or file path provided");return}let credentials;try{credentials=JSON.parse(keyContent)}catch{this.logger.error("[GmailService] Service-account JSON is not valid JSON",{source:serviceAccountJson?.trim()?"inline":"file"});return}if(!this.fromEmail){this.logger.warn("[GmailService] From email not configured");return}if(!credentials?.client_email||!credentials?.private_key){this.logger.error("[GmailService] Invalid credentials format");return}let privateKey=credentials.private_key;if(!privateKey.includes(`
33
+ </svg>`}function generatePuzzleChallenge(difficulty){let pieceCount=DIFFICULTY_CONFIG[difficulty].puzzlePieces,pieces=[],correctOrder=[];for(let i=0;i<pieceCount;i++)pieces.push({id:i,x:i%3*100,y:Math.floor(i/3)*100}),correctOrder.push(i);let shuffledPieces=[...pieces];for(let i=shuffledPieces.length-1;i>0;i--){let j=getSecureRandomInt(0,i),itemI=shuffledPieces[i],itemJ=shuffledPieces[j];if(itemI&&itemJ)shuffledPieces[i]=itemJ,shuffledPieces[j]=itemI}return{question:"Arrange the pieces in correct order",answer:correctOrder.join(","),puzzleData:{pieces:shuffledPieces,correctOrder}}}class CaptchaService{redis;logger;config;constructor(deps){this.redis=deps.redis,this.logger=deps.logger,this.config={enabled:deps.config.enabled??!0,type:deps.config.type??"math",difficulty:deps.config.difficulty??"medium",expiresIn:deps.config.expiresIn??"5m",maxAttempts:deps.config.maxAttempts??3,caseSensitive:deps.config.caseSensitive??!1,rateLimit:{maxGeneratePerMinute:deps.config.rateLimit?.maxGeneratePerMinute??10,maxGeneratePerHour:deps.config.rateLimit?.maxGeneratePerHour??100}}}async generate(type,difficulty,ipAddress){if(ipAddress){if(await this.checkRateLimit(ipAddress))return this.logger.warn("[CAPTCHA] Rate limit exceeded",{ipAddress}),{success:!1,challengeId:"",type:type??this.config.type,question:"",expiresAt:0,rateLimited:!0,message:"Too many requests. Please try again later."};await this.incrementRateLimit(ipAddress)}let captchaType=type??this.config.type,captchaDifficulty=difficulty??this.config.difficulty,challengeId=generateSecureId(),expiresInSeconds=parseTimeToSeconds(this.config.expiresIn),expiresAt=Date.now()+expiresInSeconds*1000,question,answer,imageData,puzzleData;switch(captchaType){case"math":{let mathResult=generateMathChallenge(captchaDifficulty);question=mathResult.question,answer=mathResult.answer;break}case"text":{let textResult=generateTextChallenge(captchaDifficulty);question=textResult.question,answer=textResult.answer;break}case"image":{let imageResult=generateImageChallenge(captchaDifficulty);question=imageResult.question,answer=imageResult.answer,imageData=imageResult.imageData;break}case"puzzle":{let puzzleResult=generatePuzzleChallenge(captchaDifficulty);question=puzzleResult.question,answer=puzzleResult.answer,puzzleData=puzzleResult.puzzleData;break}default:{let defaultResult=generateMathChallenge(captchaDifficulty);question=defaultResult.question,answer=defaultResult.answer}}let normalizedAnswer=this.config.caseSensitive?answer:answer.toUpperCase(),answerHash=hashAnswer(normalizedAnswer),challenge={id:challengeId,type:captchaType,question,answer:answerHash,imageData,puzzleData,expiresAt,attempts:0,maxAttempts:this.config.maxAttempts,caseSensitive:this.config.caseSensitive};await this.redis.set(`captcha:${challengeId}`,JSON.stringify(challenge),{ex:expiresInSeconds}),this.logger.info("[CAPTCHA] Challenge generated",{challengeId,type:captchaType,difficulty:captchaDifficulty,expiresAt});let result={success:!0,challengeId,type:captchaType,question,expiresAt};if(imageData)result.imageData=imageData;if(puzzleData)result.puzzleData={pieces:puzzleData.pieces};return result}async validate(challengeId,userAnswer){let key=`captcha:${challengeId}`,stored=await this.redis.get(key);if(!stored)return this.logger.warn("[CAPTCHA] Challenge not found or expired",{challengeId}),{success:!1,valid:!1,message:"Challenge expired or not found"};let challenge=JSON.parse(stored);if(Date.now()>challenge.expiresAt)return await this.redis.del(key),this.logger.warn("[CAPTCHA] Challenge expired",{challengeId}),{success:!1,valid:!1,message:"Challenge expired"};let ttl=Math.max(1,Math.floor((challenge.expiresAt-Date.now())/1000)),attemptsKey=`${key}:att`,attempts;if(this.redis.incrementCounter)attempts=await this.redis.incrementCounter(attemptsKey,1,ttl);else challenge.attempts+=1,attempts=challenge.attempts,await this.redis.set(key,JSON.stringify(challenge),{ex:ttl});let clearChallenge=async()=>{if(await this.redis.del(key),this.redis.incrementCounter)await this.redis.del(attemptsKey)};if(attempts>challenge.maxAttempts)return await clearChallenge(),this.logger.warn("[CAPTCHA] Max attempts exceeded",{challengeId,attempts}),{success:!1,valid:!1,message:"Maximum attempts exceeded"};let normalizedUserAnswer=challenge.caseSensitive?userAnswer.trim():userAnswer.trim().toUpperCase(),userAnswerHash=hashAnswer(normalizedUserAnswer);if(timingSafeEqual(challenge.answer,userAnswerHash))return await clearChallenge(),this.logger.info("[CAPTCHA] Challenge validated successfully",{challengeId}),{success:!0,valid:!0,message:"Captcha validated successfully"};let remainingAttempts=challenge.maxAttempts-attempts;if(remainingAttempts<=0)await clearChallenge();return this.logger.warn("[CAPTCHA] Invalid answer",{challengeId,attemptsRemaining:remainingAttempts}),{success:!0,valid:!1,message:"Incorrect answer",attemptsRemaining:remainingAttempts}}async invalidate(challengeId){await this.redis.del(`captcha:${challengeId}`),this.logger.info("[CAPTCHA] Challenge invalidated",{challengeId})}isEnabled(){return this.config.enabled}async checkRateLimit(ipAddress){let minuteKey=`captcha_rate:${ipAddress}:minute`,hourKey=`captcha_rate:${ipAddress}:hour`,[minuteCount,hourCount]=await Promise.all([this.redis.get(minuteKey),this.redis.get(hourKey)]),minuteRequests=minuteCount?Number.parseInt(minuteCount,10):0,hourRequests=hourCount?Number.parseInt(hourCount,10):0,maxPerMinute=this.config.rateLimit.maxGeneratePerMinute??10,maxPerHour=this.config.rateLimit.maxGeneratePerHour??100;return minuteRequests>=maxPerMinute||hourRequests>=maxPerHour}async incrementRateLimit(ipAddress){let minuteKey=`captcha_rate:${ipAddress}:minute`,hourKey=`captcha_rate:${ipAddress}:hour`,[minuteCount,hourCount]=await Promise.all([this.redis.get(minuteKey),this.redis.get(hourKey)]),newMinuteCount=(minuteCount?Number.parseInt(minuteCount,10):0)+1,newHourCount=(hourCount?Number.parseInt(hourCount,10):0)+1;await Promise.all([this.redis.set(minuteKey,newMinuteCount.toString(),{ex:60}),this.redis.set(hourKey,newHourCount.toString(),{ex:3600})])}}var DIFFICULTY_CONFIG;var init_Captcha=__esm(()=>{DIFFICULTY_CONFIG={easy:{mathRange:{min:1,max:10},textLength:4,puzzlePieces:4},medium:{mathRange:{min:10,max:50},textLength:6,puzzlePieces:6},hard:{mathRange:{min:50,max:100},textLength:8,puzzlePieces:9}}});import{promises as dns}from"dns";var DEFAULT_TXT_PREFIX="_nucleus-challenge",DEFAULT_CHALLENGE_TTL=604800,DEFAULT_BASE_PATH="/domains",resolveEnvValue=(value)=>{if(value==null)return;let fromEnv=process.env[value];return fromEnv!=null&&fromEnv!==""?fromEnv:value},normalizeHostname=(raw)=>{let trimmed=(raw||"").trim();if(!trimmed)return"";let normalized=((trimmed.replace(/^https?:\/\//i,"").split("/")[0]?.split("?")[0]||"").split(":")[0]||"").toLowerCase().replace(/\.$/,"");try{normalized=new URL(`http://${normalized}`).hostname.replace(/\.$/,"")}catch{}return normalized},isValidHostname=(hostname)=>{if(!hostname||hostname.length>253)return!1;let labels=hostname.split(".");if(labels.length<2)return!1;return labels.every((label)=>/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/.test(label))},classifyHostname=(normalized,platformBaseDomain)=>{if(platformBaseDomain){let base=platformBaseDomain.toLowerCase().replace(/\.$/,"");if(normalized===base)return"apex";if(normalized.endsWith(`.${base}`)){let prefix=normalized.slice(0,-(base.length+1));if(prefix&&!prefix.includes("."))return"platform_subdomain"}}let labels=normalized.split(".");if(labels[0]==="www")return"www";if(labels.length<=2)return"apex";return"subdomain"},generateChallengeToken=()=>{let bytes=new Uint8Array(24);return crypto.getRandomValues(bytes),Array.from(bytes,(b)=>b.toString(16).padStart(2,"0")).join("")},resolveDomainsConfig=(raw)=>{let provider=raw?.provider??"manual_dns",cf=raw?.cloudflare,cloudflare=cf&&resolveEnvValue(cf.accountId)&&resolveEnvValue(cf.zoneId)&&resolveEnvValue(cf.apiToken)?{accountId:resolveEnvValue(cf.accountId),zoneId:resolveEnvValue(cf.zoneId),apiToken:resolveEnvValue(cf.apiToken),fallbackOrigin:resolveEnvValue(cf.fallbackOrigin)??null,cnameTarget:resolveEnvValue(cf.cnameTarget)??null}:null;return{enabled:raw?.enabled===!0,basePath:raw?.basePath??DEFAULT_BASE_PATH,provider,platformBaseDomain:raw?.platformBaseDomain?normalizeHostname(raw.platformBaseDomain):null,cnameTarget:resolveEnvValue(raw?.cnameTarget)??cloudflare?.cnameTarget??null,allowApexDomains:raw?.allowApexDomains!==!1,allowWwwDomains:raw?.allowWwwDomains!==!1,allowManagedRegistrations:raw?.allowManagedRegistrations===!0,reservedHostnames:(raw?.reservedHostnames??[]).map((h)=>normalizeHostname(h)),verification:{defaultMethod:raw?.verification?.defaultMethod??"txt",txtRecordPrefix:raw?.verification?.txtRecordPrefix??DEFAULT_TXT_PREFIX,challengeTtlSeconds:raw?.verification?.challengeTtlSeconds??DEFAULT_CHALLENGE_TTL},cloudflare,registrar:raw?.registrar?{provider:raw.registrar.provider??"cloudflare_registrar",defaultRegistrantOwnerType:raw.registrar.defaultRegistrantOwnerType??"customer"}:null}},str=(v)=>v==null?"":String(v),strOrNull=(v)=>v==null?null:String(v),parseMetadata=(v)=>{if(!v)return{};let obj=typeof v==="string"?safeJson(v):v;if(!obj||typeof obj!=="object")return{};let out={};for(let[k,val]of Object.entries(obj))if(typeof val==="string"||typeof val==="number"||typeof val==="boolean")out[k]=val;return out},safeJson=(v)=>{try{return JSON.parse(v)}catch{return{}}},rowToDomainHostname=(row)=>({id:str(row.id),ownerType:str(row.ownerType??row.owner_type)||"tenant",ownerId:str(row.ownerId??row.owner_id),tenantId:strOrNull(row.tenantId??row.tenant_id),schemaName:strOrNull(row.schemaName??row.schema_name),hostname:str(row.hostname),normalizedHostname:str(row.normalizedHostname??row.normalized_hostname),hostnameKind:str(row.hostnameKind??row.hostname_kind)||"apex",status:str(row.status),routingStatus:str(row.routingStatus??row.routing_status),certificateStatus:str(row.certificateStatus??row.certificate_status),verificationStatus:str(row.verificationStatus??row.verification_status),provider:str(row.provider)||"manual_dns",providerHostnameId:strOrNull(row.providerHostnameId??row.provider_hostname_id),dnsTarget:strOrNull(row.dnsTarget??row.dns_target),isPrimary:Boolean(row.isPrimary??row.is_primary),redirectToHostnameId:strOrNull(row.redirectToHostnameId??row.redirect_to_hostname_id),activatedAt:strOrNull(row.activatedAt??row.activated_at),disabledAt:strOrNull(row.disabledAt??row.disabled_at),failureReason:strOrNull(row.failureReason??row.failure_reason),metadata:parseMetadata(row.metadata)}),verifyTxtRecord=async(recordName,expectedValue)=>{try{let flat=(await dns.resolveTxt(recordName)).map((chunks)=>chunks.join("")),verified=flat.some((v)=>v===expectedValue);return{verified,observed:flat,reason:verified?void 0:"TXT value not found"}}catch(err){return{verified:!1,reason:err instanceof Error?err.message:"DNS lookup failed"}}},verifyCnameRecord=async(recordName,expectedTarget)=>{let target=expectedTarget.toLowerCase().replace(/\.$/,"");try{let flat=(await dns.resolveCname(recordName)).map((v)=>v.toLowerCase().replace(/\.$/,"")),verified=flat.includes(target);return{verified,observed:flat,reason:verified?void 0:"CNAME target mismatch"}}catch(err){return{verified:!1,reason:err instanceof Error?err.message:"DNS lookup failed"}}};var init_Domain_helpers=()=>{};import{eq as eq7}from"drizzle-orm";class DomainRegistrationService{getDb;getRegistry;logger;config;constructor(cfg){this.getDb=cfg.getDb,this.getRegistry=cfg.getTenantRegistry,this.logger=cfg.logger.scoped("domains.registration"),this.config=cfg.config}get enabled(){return this.config.allowManagedRegistrations}table(name){let registry=this.getRegistry();if(!registry)return null;return registry.getMainContext().schemaTables[name]??null}async event(registrationId,eventType,message){let db=this.getDb(),table=this.table("domainEvents");if(!db||!table)return;try{await db.insert(table).values({domainRegistrationId:registrationId,eventType,message:message??null,metadata:JSON.stringify({})})}catch(err){this.logger.warn("Failed to record registration event",{eventType,error:err instanceof Error?err.message:String(err)})}}async createRequest(input){if(!this.enabled)throw Error("Managed domain registration is not enabled");if(!input.termsAccepted)throw Error("Registration requires accepting the domain terms");let db=this.getDb(),table=this.table("domainRegistrations");if(!db||!table)throw Error("Domain registration service not ready");let normalized=normalizeHostname(input.requestedDomain),rows=await db.insert(table).values({ownerType:input.ownerType??"tenant",ownerId:input.ownerId,tenantId:input.tenantId??null,requestedDomain:normalized,provider:this.config.registrar?.provider??"cloudflare_registrar",registrantOwnerType:input.registrantOwnerType??this.config.registrar?.defaultRegistrantOwnerType??"customer",status:"awaiting_payment",autoRenew:input.autoRenew??!0,termsVersion:input.termsVersion,termsAcceptedAt:new Date,metadata:JSON.stringify(input.metadata??{})}).returning(),record=rowToRegistration(rows[0]);return await this.event(record.id,"registration_requested",`Requested ${normalized}`),record}async listRegistrations(filter){let db=this.getDb(),table=this.table("domainRegistrations");if(!db||!table)return[];let where=filter.tenantId?eq7(col2(table,"tenantId"),filter.tenantId):filter.ownerId?eq7(col2(table,"ownerId"),filter.ownerId):void 0;return(where?await db.select().from(table).where(where):await db.select().from(table)).map((r)=>rowToRegistration(r))}async getRegistration(id){let db=this.getDb(),table=this.table("domainRegistrations");if(!db||!table)return null;let rows=await db.select().from(table).where(eq7(col2(table,"id"),id)).limit(1);return rows[0]?rowToRegistration(rows[0]):null}async transition(id,status,patch){let db=this.getDb(),table=this.table("domainRegistrations");if(!db||!table)return null;return await db.update(table).set({status,registeredDomain:patch?.registeredDomain,providerRegistrationId:patch?.providerRegistrationId,priceAmount:patch?.priceAmount,currency:patch?.currency,renewalDate:patch?.renewalDate,failureReason:patch?.failureReason??null,updatedAt:new Date}).where(eq7(col2(table,"id"),id)),await this.event(id,`registration_${status}`),this.getRegistration(id)}async cancel(id){return this.transition(id,"cancelled")}async requestTransferOut(id){return this.transition(id,"transfer_requested")}}var col2=(table,name)=>table[name],rowToRegistration=(row)=>({id:String(row.id??""),ownerType:String(row.ownerType??row.owner_type??"tenant"),ownerId:String(row.ownerId??row.owner_id??""),tenantId:row.tenantId??row.tenant_id?String(row.tenantId??row.tenant_id):null,requestedDomain:String(row.requestedDomain??row.requested_domain??""),registeredDomain:row.registeredDomain??row.registered_domain?String(row.registeredDomain??row.registered_domain):null,provider:String(row.provider??"cloudflare_registrar"),status:String(row.status??"draft"),registrantOwnerType:String(row.registrantOwnerType??row.registrant_owner_type??"customer"),priceAmount:row.priceAmount??row.price_amount?String(row.priceAmount??row.price_amount):null,currency:row.currency?String(row.currency):null,autoRenew:Boolean(row.autoRenew??row.auto_renew??!0),termsVersion:row.termsVersion??row.terms_version?String(row.termsVersion??row.terms_version):null,termsAcceptedAt:row.termsAcceptedAt??row.terms_accepted_at?String(row.termsAcceptedAt??row.terms_accepted_at):null,failureReason:row.failureReason??row.failure_reason?String(row.failureReason??row.failure_reason):null});var init_DomainRegistrationService=__esm(()=>{init_Domain_helpers()});import{and as and3,eq as eq8}from"drizzle-orm";class DomainService{getDb;getRegistry;logger;config;provider;constructor(cfg){this.getDb=cfg.getDb,this.getRegistry=cfg.getTenantRegistry,this.logger=cfg.logger.scoped("domains.service"),this.config=cfg.config,this.provider=cfg.provider}table(name){let registry=this.getRegistry();if(!registry)return null;return registry.getMainContext().schemaTables[name]??null}toProviderInput(record){return{hostname:record.hostname,normalizedHostname:record.normalizedHostname,hostnameKind:record.hostnameKind}}isReserved(normalized){if(this.config.platformBaseDomain&&normalized===this.config.platformBaseDomain)return!0;return this.config.reservedHostnames.includes(normalized)}async createHostname(input){let db=this.getDb(),table=this.table("domainHostnames");if(!db||!table)throw Error("Domain service not ready");let normalized=normalizeHostname(input.hostname);if(!isValidHostname(normalized))throw Error(`Invalid hostname: ${input.hostname}`);if(this.isReserved(normalized))throw Error(`Hostname is reserved: ${normalized}`);let kind=classifyHostname(normalized,this.config.platformBaseDomain);if(kind==="apex"&&!this.config.allowApexDomains)throw Error("Apex domains are not allowed");if(kind==="www"&&!this.config.allowWwwDomains)throw Error("www domains are not allowed");if((await db.select().from(table).where(eq8(col3(table,"normalizedHostname"),normalized)).limit(1))[0])throw Error(`Hostname already registered: ${normalized}`);let providerInput={hostname:input.hostname,normalizedHostname:normalized,hostnameKind:kind},provisioned=await this.provider.createCustomHostname(providerInput),rows=await db.insert(table).values({ownerType:input.ownerType??"tenant",ownerId:input.ownerId,tenantId:input.tenantId??null,schemaName:input.schemaName??null,hostname:input.hostname,normalizedHostname:normalized,hostnameKind:kind,status:"pending_verification",routingStatus:provisioned.routingStatus,certificateStatus:provisioned.certificateStatus,verificationStatus:provisioned.verificationStatus,provider:this.provider.name,providerHostnameId:provisioned.providerHostnameId,dnsTarget:provisioned.dnsTarget,isPrimary:input.isPrimary??!1,metadata:JSON.stringify(input.metadata??{})}).returning(),record=rowToDomainHostname(rows[0]);return await this.persistChallenges(record.id,provisioned.challenges),await this.recordEvent({domainHostnameId:record.id,eventType:"hostname_requested",message:`Custom hostname requested: ${normalized}`,metadata:{provider:this.provider.name,kind}}),{hostname:record,instructions:provisioned.instructions}}async persistChallenges(hostnameId,challenges){let db=this.getDb(),table=this.table("domainVerificationChallenges");if(!db||!table||challenges.length===0)return;let expiresAt=new Date(Date.now()+this.config.verification.challengeTtlSeconds*1000);for(let c of challenges)await db.insert(table).values({domainHostnameId:hostnameId,challengeType:c.challengeType,recordType:c.recordType,recordName:c.recordName,recordValue:c.recordValue,expectedTarget:c.expectedTarget??null,status:"pending",expiresAt})}async listHostnames(filter){let db=this.getDb(),table=this.table("domainHostnames");if(!db||!table)return[];let where=filter.tenantId?eq8(col3(table,"tenantId"),filter.tenantId):filter.ownerId?eq8(col3(table,"ownerId"),filter.ownerId):void 0;return(where?await db.select().from(table).where(where):await db.select().from(table)).map((r)=>rowToDomainHostname(r))}async getHostname(id){let db=this.getDb(),table=this.table("domainHostnames");if(!db||!table)return null;let rows=await db.select().from(table).where(eq8(col3(table,"id"),id)).limit(1);return rows[0]?rowToDomainHostname(rows[0]):null}async getInstructions(id){let record=await this.getHostname(id);if(!record)return[];let db=this.getDb(),table=this.table("domainVerificationChallenges"),instructions=[];if(record.dnsTarget)instructions.push({recordType:record.hostnameKind==="apex"?"A":"CNAME",name:record.normalizedHostname,value:record.dnsTarget,note:record.hostnameKind==="apex"?"Apex domains require an A/ALIAS record at your DNS provider":"Point this hostname at the platform routing target"});if(db&&table){let rows=await db.select().from(table).where(eq8(col3(table,"domainHostnameId"),id));for(let r of rows)instructions.push({recordType:String(r.recordType??"TXT"),name:String(r.recordName??""),value:String(r.recordValue??""),note:String(r.challengeType??"")==="certificate_validation"?"SSL validation":"Ownership verification"})}return instructions}async loadPendingChallenge(hostnameId){let db=this.getDb(),table=this.table("domainVerificationChallenges");if(!db||!table)return null;let row=(await db.select().from(table).where(and3(eq8(col3(table,"domainHostnameId"),hostnameId),eq8(col3(table,"status"),"pending"))).limit(1))[0];if(!row)return null;return{challengeType:String(row.challengeType??"ownership_validation"),recordType:String(row.recordType??"TXT"),recordName:String(row.recordName??""),recordValue:String(row.recordValue??""),expectedTarget:row.expectedTarget!=null?String(row.expectedTarget):void 0}}async verifyHostname(id){let record=await this.getHostname(id);if(!record)return null;let providerInput=this.toProviderInput(record),routingStatus=record.routingStatus,certificateStatus=record.certificateStatus,verificationStatus=record.verificationStatus,failureReason=null;if(this.provider.name==="manual_dns"){let challenge=await this.loadPendingChallenge(id);if(challenge){let result=await this.provider.verifyOwnership(providerInput,challenge);if(verificationStatus=result.verified?"verified":"failed",failureReason=result.verified?null:result.reason??"Verification failed",result.verified)await this.markChallengesVerified(id)}let status2=await this.provider.getCustomHostnameStatus(record.providerHostnameId,providerInput);routingStatus=status2.routingStatus,certificateStatus=status2.certificateStatus}else{let status2=await this.provider.getCustomHostnameStatus(record.providerHostnameId,providerInput);if(routingStatus=status2.routingStatus,certificateStatus=status2.certificateStatus,verificationStatus=status2.verificationStatus,failureReason=status2.failureReason??null,verificationStatus==="verified")await this.markChallengesVerified(id)}let ready=verificationStatus==="verified"&&routingStatus==="active"&&(certificateStatus==="active"||certificateStatus==="not_required"),status=verificationStatus==="failed"?"failed":ready?"active":"verifying";if(await this.applyStatus(id,{status,routingStatus,certificateStatus,verificationStatus,failureReason,activate:ready}),ready)await this.syncRegistry(id);return await this.recordEvent({domainHostnameId:id,eventType:ready?"verification_succeeded":"verification_attempted",message:failureReason??`verification=${verificationStatus} routing=${routingStatus} ssl=${certificateStatus}`}),this.getHostname(id)}async activateHostname(id){let record=await this.getHostname(id);if(!record)return null;if(record.verificationStatus!=="verified")throw Error("Cannot activate a hostname before ownership is verified");return await this.applyStatus(id,{status:"active",routingStatus:"active",certificateStatus:record.certificateStatus==="failed"?"pending":record.certificateStatus,verificationStatus:"verified",failureReason:null,activate:!0}),await this.syncRegistry(id),await this.recordEvent({domainHostnameId:id,eventType:"routing_active",message:"Hostname activated"}),this.getHostname(id)}async disableHostname(id){let db=this.getDb(),table=this.table("domainHostnames"),record=await this.getHostname(id);if(!db||!table||!record)return null;return await this.provider.deleteCustomHostname(record.providerHostnameId),await db.update(table).set({status:"disabled",routingStatus:"inactive",disabledAt:new Date,updatedAt:new Date}).where(eq8(col3(table,"id"),id)),this.getRegistry()?.unregisterDomainHostname(record.normalizedHostname),await this.recordEvent({domainHostnameId:id,eventType:"hostname_disabled",message:"Hostname disabled"}),this.getHostname(id)}async makePrimary(id){let db=this.getDb(),table=this.table("domainHostnames"),record=await this.getHostname(id);if(!db||!table||!record)return null;let scope=record.tenantId?eq8(col3(table,"tenantId"),record.tenantId):eq8(col3(table,"ownerId"),record.ownerId);return await db.update(table).set({isPrimary:!1,updatedAt:new Date}).where(scope),await db.update(table).set({isPrimary:!0,updatedAt:new Date}).where(eq8(col3(table,"id"),id)),await this.recordEvent({domainHostnameId:id,eventType:"primary_changed",message:"Primary hostname set"}),this.getHostname(id)}async refreshStatus(id){return this.verifyHostname(id)}async resolveHostname(hostname){let db=this.getDb(),table=this.table("domainHostnames");if(!db||!table)return null;let normalized=normalizeHostname(hostname),row=(await db.select().from(table).where(and3(eq8(col3(table,"normalizedHostname"),normalized),eq8(col3(table,"status"),"active"))).limit(1))[0];if(!row)return null;let record=rowToDomainHostname(row),redirectToHostname=null;if(record.redirectToHostnameId)redirectToHostname=(await this.getHostname(record.redirectToHostnameId))?.normalizedHostname??null;return{hostnameId:record.id,tenantId:record.tenantId,schemaName:record.schemaName,isPrimary:record.isPrimary,redirectToHostname}}async recordEvent(input){let db=this.getDb(),table=this.table("domainEvents");if(!db||!table)return;try{await db.insert(table).values({domainHostnameId:input.domainHostnameId??null,domainRegistrationId:input.domainRegistrationId??null,eventType:input.eventType,actorType:input.actorType??null,actorId:input.actorId??null,message:input.message??null,ipAddress:input.ipAddress??null,metadata:JSON.stringify(input.metadata??{})})}catch(err){this.logger.warn("Failed to record domain event",{eventType:input.eventType,error:err instanceof Error?err.message:String(err)})}}async markChallengesVerified(hostnameId){let db=this.getDb(),table=this.table("domainVerificationChallenges");if(!db||!table)return;await db.update(table).set({status:"verified",verifiedAt:new Date,updatedAt:new Date}).where(eq8(col3(table,"domainHostnameId"),hostnameId))}async applyStatus(id,patch){let db=this.getDb(),table=this.table("domainHostnames");if(!db||!table)return;await db.update(table).set({status:patch.status,routingStatus:patch.routingStatus,certificateStatus:patch.certificateStatus,verificationStatus:patch.verificationStatus,failureReason:patch.failureReason,activatedAt:patch.activate?new Date:void 0,updatedAt:new Date}).where(eq8(col3(table,"id"),id))}async syncRegistry(id){let record=await this.getHostname(id),registry=this.getRegistry();if(!record||!registry)return;if(record.status==="active"&&record.tenantId)registry.registerDomainHostname(record.normalizedHostname,record.tenantId)}}var col3=(table,name)=>table[name];var init_DomainService=__esm(()=>{init_Domain_helpers()});class CloudflareForSaasProvider{name="cloudflare_for_saas";config;logger;zoneId;apiToken;constructor(config,logger2){if(!config.cloudflare)throw Error("[Domain] Cloudflare for SaaS provider requires domains.cloudflare config");this.config=config,this.logger=logger2.scoped("domains.cloudflare"),this.zoneId=config.cloudflare.zoneId,this.apiToken=config.cloudflare.apiToken}get cnameTarget(){return this.config.cloudflare?.cnameTarget??this.config.cnameTarget}async call(path2,init){let res=await fetch(`https://api.cloudflare.com/client/v4${path2}`,{...init,headers:{Authorization:`Bearer ${this.apiToken}`,"Content-Type":"application/json",...init.headers??{}}}),json=await res.json().catch(()=>({}));if(!res.ok||json.success===!1){let msg=json.errors?.map((e)=>e.message).join("; ")||`HTTP ${res.status}`;throw Error(`Cloudflare API error: ${msg}`)}return json}mapRouting(status){if(status==="active")return"active";if(status==="blocked"||status==="moved"||status==="deleted")return"failed";return"pending"}mapCertificate(sslStatus){if(sslStatus==="active")return"active";if(sslStatus==="validation_timed_out"||sslStatus==="deactivating"||sslStatus==="deleted")return"failed";return"pending"}mapVerification(result){if(result.status==="active")return"verified";if((result.ssl?.validation_errors?.length??0)>0)return"failed";return"pending"}buildChallenges(result){let challenges=[],ov=result.ownership_verification;if(ov?.name&&ov.value)challenges.push({challengeType:"hostname_validation",recordType:"TXT",recordName:ov.name,recordValue:ov.value});for(let vr of result.ssl?.validation_records??[])if(vr.txt_name&&vr.txt_value)challenges.push({challengeType:"certificate_validation",recordType:"TXT",recordName:vr.txt_name,recordValue:vr.txt_value});return challenges}buildInstructions(input,challenges){let target=this.cnameTarget,instructions=[];if(target)instructions.push({recordType:input.hostnameKind==="apex"?"A":"CNAME",name:input.normalizedHostname,value:target,note:input.hostnameKind==="apex"?"Apex domains require an A/ALIAS record pointing at the SaaS target":"Point this hostname at the Cloudflare for SaaS target"});for(let c of challenges)instructions.push({recordType:c.recordType,name:c.recordName,value:c.recordValue,note:c.challengeType==="certificate_validation"?"SSL validation":"Hostname validation"});return instructions}toResult(result,input){let challenges=this.buildChallenges(result);return{providerHostnameId:result.id??null,dnsTarget:this.cnameTarget,routingStatus:this.mapRouting(result.status),certificateStatus:this.mapCertificate(result.ssl?.status),verificationStatus:this.mapVerification(result),instructions:this.buildInstructions(input,challenges),challenges,raw:result}}async createCustomHostname(input){let body=JSON.stringify({hostname:input.normalizedHostname,ssl:{method:"txt",type:"dv",settings:{min_tls_version:"1.2"}}}),json=await this.call(`/zones/${this.zoneId}/custom_hostnames`,{method:"POST",body});return this.toResult(json.result??{},input)}async getCustomHostnameStatus(providerHostnameId,input){let result=await this.fetchHostname(providerHostnameId,input.normalizedHostname);if(!result)return{routingStatus:"failed",certificateStatus:"failed",verificationStatus:"failed",failureReason:"Custom hostname not found at provider"};return{routingStatus:this.mapRouting(result.status),certificateStatus:this.mapCertificate(result.ssl?.status),verificationStatus:this.mapVerification(result),providerStatus:`${result.status??"unknown"}/${result.ssl?.status??"unknown"}`,failureReason:result.ssl?.validation_errors?.map((e)=>e.message).join("; ")||void 0,raw:result}}async fetchHostname(providerHostnameId,normalizedHostname){if(providerHostnameId)return(await this.call(`/zones/${this.zoneId}/custom_hostnames/${providerHostnameId}`,{method:"GET"})).result??null;let list=(await this.call(`/zones/${this.zoneId}/custom_hostnames?hostname=${encodeURIComponent(normalizedHostname)}`,{method:"GET"})).result;return Array.isArray(list)?list[0]??null:null}async deleteCustomHostname(providerHostnameId){if(!providerHostnameId)return;try{await this.call(`/zones/${this.zoneId}/custom_hostnames/${providerHostnameId}`,{method:"DELETE"})}catch(err){this.logger.warn("Failed to delete Cloudflare custom hostname",{providerHostnameId,error:err instanceof Error?err.message:String(err)})}}async verifyOwnership(input,_challenge){let status=await this.getCustomHostnameStatus(null,input);return{verified:status.verificationStatus==="verified",reason:status.failureReason??status.providerStatus}}}class ManualDnsProvider{name="manual_dns";config;constructor(config){this.config=config}buildChallenge(input){let prefix=this.config.verification.txtRecordPrefix,token=`nucleus-domain-verification=${generateChallengeToken()}`;return{challengeType:"ownership_validation",recordType:"TXT",recordName:`${prefix}.${input.normalizedHostname}`,recordValue:token}}buildInstructions(input,challenge){let target=this.config.cnameTarget,instructions=[{recordType:"TXT",name:challenge.recordName,value:challenge.recordValue,note:"Ownership verification record"}];if(input.hostnameKind==="apex")instructions.push({recordType:"A",name:input.normalizedHostname,value:target??"<platform-ingress-ip>",note:"Apex domains require an A/ALIAS record at your DNS provider"});else if(target)instructions.push({recordType:"CNAME",name:input.normalizedHostname,value:target,note:"Point this hostname at the platform routing target"});return instructions}async createCustomHostname(input){let challenge=this.buildChallenge(input);return{providerHostnameId:null,dnsTarget:this.config.cnameTarget,routingStatus:"pending",certificateStatus:"not_required",verificationStatus:"pending",instructions:this.buildInstructions(input,challenge),challenges:[challenge]}}async getCustomHostnameStatus(_providerHostnameId,input){if(input.hostnameKind!=="apex"&&this.config.cnameTarget){let dns2=await verifyCnameRecord(input.normalizedHostname,this.config.cnameTarget);return{routingStatus:dns2.verified?"active":"pending",certificateStatus:"not_required",verificationStatus:"pending",providerStatus:dns2.verified?"cname_active":"cname_pending"}}return{routingStatus:"pending",certificateStatus:"not_required",verificationStatus:"pending",providerStatus:"manual"}}async deleteCustomHostname(_providerHostnameId){}async verifyOwnership(_input,challenge){if(challenge.recordType==="CNAME"&&challenge.expectedTarget)return verifyCnameRecord(challenge.recordName,challenge.expectedTarget);return verifyTxtRecord(challenge.recordName,challenge.recordValue)}}var init_ManualDnsProvider=__esm(()=>{init_Domain_helpers()});var createDomainProvider=(config,logger2)=>{if(config.provider==="cloudflare_for_saas"){if(!config.cloudflare)return logger2.warn("[Domain] cloudflare_for_saas selected but cloudflare config missing \u2014 using manual DNS"),new ManualDnsProvider(config);return new CloudflareForSaasProvider(config,logger2)}return new ManualDnsProvider(config)};var init_Providers=__esm(()=>{init_ManualDnsProvider();init_ManualDnsProvider()});var createDomainServices=(params)=>{let config=resolveDomainsConfig(params.options.domains);if(!config.enabled)return null;let provider=createDomainProvider(config,params.logger),serviceConfig={getDb:params.getDb,getTenantRegistry:params.getTenantRegistry,logger:params.logger,config,provider};return{domainService:new DomainService(serviceConfig),registrationService:new DomainRegistrationService(serviceConfig)}};var init_Domain=__esm(()=>{init_Domain_helpers();init_DomainRegistrationService();init_DomainService();init_Providers();init_Domain_helpers();init_DomainRegistrationService();init_DomainService();init_Providers()});class TemplatedEmailService{emailWrapper(content,appName){return`<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><meta name="color-scheme" content="light dark"><meta name="supported-color-schemes" content="light dark"><title>${appName}</title><style type="text/css">:root{color-scheme:light dark;supported-color-schemes:light dark;}@media(prefers-color-scheme:dark){.em-bg{background-color:#18181b!important;}.em-card{background-color:#27272a!important;border-color:#3f3f46!important;}.em-h{color:#fafafa!important;}.em-t{color:#d4d4d8!important;}.em-m{color:#a1a1aa!important;}.em-hr{background-color:#3f3f46!important;}.em-btn{background-color:#fafafa!important;}.em-btn-text{color:#18181b!important;}.em-link{color:#60a5fa!important;}.em-lb{background-color:#3f3f46!important;border-color:#52525b!important;}.em-lb-t{color:#a1a1aa!important;}.em-sn{background-color:#3f3f46!important;border-color:#52525b!important;color:#a1a1aa!important;}}</style></head><body style="margin:0;padding:0;background-color:#f4f4f5;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;-webkit-font-smoothing:antialiased;"><table role="presentation" class="em-bg" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f4f4f5;"><tr><td align="center" style="padding:40px 16px;"><table role="presentation" width="480" cellpadding="0" cellspacing="0" style="max-width:480px;width:100%;"><tr><td align="center" style="padding-bottom:24px;"><span class="em-m" style="font-size:14px;font-weight:600;color:#71717a;letter-spacing:-0.2px;">${appName}</span></td></tr><tr><td class="em-card" style="background-color:#ffffff;border:1px solid #e4e4e7;border-radius:12px;overflow:hidden;">${content}</td></tr><tr><td align="center" style="padding-top:24px;"><p class="em-m" style="margin:0;font-size:12px;color:#a1a1aa;line-height:1.6;">This email was sent by <strong>${appName}</strong>.<br>Please do not reply to this email.</p></td></tr></table></td></tr></table></body></html>`}emailButton(href,label){return`<table role="presentation" cellpadding="0" cellspacing="0" width="100%"><tr><td align="center" style="padding:28px 32px;"><table role="presentation" cellpadding="0" cellspacing="0"><tr><td class="em-btn" align="center" style="background-color:#18181b;border-radius:8px;"><a class="em-btn-text" href="${href}" target="_blank" style="display:inline-block;padding:12px 32px;font-size:14px;font-weight:600;color:#fafafa;text-decoration:none;letter-spacing:-0.1px;">${label}</a></td></tr></table></td></tr></table>`}emailDivider(){return'<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:0 32px;"><div class="em-hr" style="height:1px;background-color:#e4e4e7;"></div></td></tr></table>'}emailLinkFallback(link,notice){let html=`<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:4px 32px 32px 32px;"><p class="em-m" style="margin:0 0 8px 0;font-size:11px;font-weight:600;color:#a1a1aa;text-transform:uppercase;letter-spacing:0.5px;">Or copy this link</p><table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td class="em-lb" style="background-color:#f4f4f5;border:1px solid #e4e4e7;border-radius:8px;padding:12px 14px;"><a class="em-link" href="${link}" style="font-size:12px;color:#2563eb;word-break:break-all;line-height:1.5;text-decoration:none;">${link}</a></td></tr></table>`;if(notice)html+=`<p class="em-m" style="margin:16px 0 0 0;font-size:12px;color:#a1a1aa;">${notice}</p>`;return html+="</td></tr></table>",html}async sendWelcomeEmail(email,name,appName="Nucleus"){let content=`<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:40px 32px 24px 32px;text-align:center;"><h1 class="em-h" style="margin:0;font-size:24px;font-weight:700;color:#18181b;letter-spacing:-0.5px;">Welcome to ${appName}</h1><p class="em-t" style="margin:8px 0 0 0;font-size:15px;color:#52525b;line-height:1.6;">Your account has been created successfully</p></td></tr></table>${this.emailDivider()}<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:24px 32px 32px 32px;"><p class="em-t" style="margin:0 0 16px 0;font-size:15px;color:#3f3f46;line-height:1.7;">Hello <strong class="em-h" style="color:#18181b;">${name}</strong>,</p><p class="em-t" style="margin:0;font-size:15px;color:#3f3f46;line-height:1.7;">Welcome aboard! You now have full access to <strong>${appName}</strong>. We're excited to have you.</p></td></tr></table>`;return this.sendEmail({to:email,subject:`Welcome to ${appName}`,html:this.emailWrapper(content,appName)})}async sendVerificationEmail(email,name,verificationLink,appName="Nucleus"){let content=`<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:40px 32px 24px 32px;text-align:center;"><h1 class="em-h" style="margin:0;font-size:24px;font-weight:700;color:#18181b;letter-spacing:-0.5px;">Verify your email</h1><p class="em-t" style="margin:8px 0 0 0;font-size:15px;color:#52525b;line-height:1.6;">Confirm your address to get started</p></td></tr></table>${this.emailDivider()}<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:24px 32px 0 32px;"><p class="em-t" style="margin:0 0 4px 0;font-size:15px;color:#3f3f46;line-height:1.7;">Hello <strong class="em-h" style="color:#18181b;">${name}</strong>,</p><p class="em-t" style="margin:0;font-size:15px;color:#3f3f46;line-height:1.7;">Please verify your email address by clicking the button below.</p></td></tr><tr><td>${this.emailButton(verificationLink,"Verify Email Address")}</td></tr></table>${this.emailDivider()}${this.emailLinkFallback(verificationLink)}`;return this.sendEmail({to:email,subject:`Verify your email \u2014 ${appName}`,html:this.emailWrapper(content,appName)})}async sendPasswordResetEmail(email,name,resetLink,appName="Nucleus"){let content=`<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:40px 32px 24px 32px;text-align:center;"><h1 class="em-h" style="margin:0;font-size:24px;font-weight:700;color:#18181b;letter-spacing:-0.5px;">Reset your password</h1><p class="em-t" style="margin:8px 0 0 0;font-size:15px;color:#52525b;line-height:1.6;">We received a request to reset your password</p></td></tr></table>${this.emailDivider()}<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:24px 32px 0 32px;"><p class="em-t" style="margin:0 0 4px 0;font-size:15px;color:#3f3f46;line-height:1.7;">Hello <strong class="em-h" style="color:#18181b;">${name}</strong>,</p><p class="em-t" style="margin:0;font-size:15px;color:#3f3f46;line-height:1.7;">Click the button below to choose a new password. If you didn't make this request, you can safely ignore this email.</p></td></tr><tr><td>${this.emailButton(resetLink,"Reset Password")}</td></tr></table>${this.emailDivider()}${this.emailLinkFallback(resetLink,"This link expires in 1 hour and can only be used once.")}`;return this.sendEmail({to:email,subject:`Reset your password \u2014 ${appName}`,html:this.emailWrapper(content,appName)})}async sendMagicLinkEmail(email,magicLink,appName="Nucleus"){let content=`<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:40px 32px 24px 32px;text-align:center;"><h1 class="em-h" style="margin:0;font-size:24px;font-weight:700;color:#18181b;letter-spacing:-0.5px;">Sign in to ${appName}</h1><p class="em-t" style="margin:8px 0 0 0;font-size:15px;color:#52525b;line-height:1.6;">No password needed \u2014 just click to sign in</p></td></tr></table>${this.emailDivider()}<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:24px 32px 0 32px;"><p class="em-t" style="margin:0;font-size:15px;color:#3f3f46;line-height:1.7;">Click the button below to securely sign in to your <strong class="em-h" style="color:#18181b;">${appName}</strong> account.</p></td></tr><tr><td>${this.emailButton(magicLink,"Sign In")}</td></tr></table>${this.emailDivider()}${this.emailLinkFallback(magicLink,"This link expires in 15 minutes and can only be used once.")}`;return this.sendEmail({to:email,subject:`Sign in to ${appName}`,html:this.emailWrapper(content,appName)})}async sendAlertEmail(to,subject,html){return this.sendEmail({to,subject,html})}async sendInvitationEmail(email,inviteLink,appName="Nucleus"){let content=`<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:40px 32px 24px 32px;text-align:center;"><h1 class="em-h" style="margin:0;font-size:24px;font-weight:700;color:#18181b;letter-spacing:-0.5px;">You're Invited</h1><p class="em-t" style="margin:8px 0 0 0;font-size:15px;color:#52525b;line-height:1.6;">Join <strong class="em-h" style="color:#18181b;">${appName}</strong> and start collaborating</p></td></tr></table>${this.emailDivider()}<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:0 32px 8px 32px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:8px 0;"><table role="presentation" cellpadding="0" cellspacing="0"><tr><td class="em-sn" style="width:28px;height:28px;background-color:#f4f4f5;border:1px solid #e4e4e7;border-radius:50%;text-align:center;line-height:28px;font-size:12px;font-weight:600;color:#71717a;">1</td><td class="em-t" style="padding-left:14px;font-size:14px;color:#52525b;">Click the button below</td></tr></table></td></tr><tr><td style="padding:8px 0;"><table role="presentation" cellpadding="0" cellspacing="0"><tr><td class="em-sn" style="width:28px;height:28px;background-color:#f4f4f5;border:1px solid #e4e4e7;border-radius:50%;text-align:center;line-height:28px;font-size:12px;font-weight:600;color:#71717a;">2</td><td class="em-t" style="padding-left:14px;font-size:14px;color:#52525b;">Choose a secure password</td></tr></table></td></tr><tr><td style="padding:8px 0;"><table role="presentation" cellpadding="0" cellspacing="0"><tr><td class="em-sn" style="width:28px;height:28px;background-color:#f4f4f5;border:1px solid #e4e4e7;border-radius:50%;text-align:center;line-height:28px;font-size:12px;font-weight:600;color:#71717a;">3</td><td class="em-t" style="padding-left:14px;font-size:14px;color:#52525b;">Start collaborating</td></tr></table></td></tr></table></td></tr></table>${this.emailButton(inviteLink,"Accept Invitation")}${this.emailDivider()}${this.emailLinkFallback(inviteLink,"This invitation expires in 7 days.")}`;return this.sendEmail({to:email,subject:`You're invited to join ${appName}`,html:this.emailWrapper(content,appName)})}}var AzureEmailService;var init_AzureEmailService=__esm(()=>{AzureEmailService=class AzureEmailService extends TemplatedEmailService{client=null;senderAddress;fromName;logger;initialized=!1;credentialsFingerprint="";constructor(config,logger2){super();if(this.senderAddress=config.senderAddress,this.fromName=config.fromName||"Nucleus",this.logger=logger2,config.enabled)this.credentialsFingerprint=`${config.connectionString??""}|${config.senderAddress}`,this.initialize(config.connectionString)}reconfigure(config){let fingerprint=`${config.connectionString??""}|${config.senderAddress}`;if(fingerprint===this.credentialsFingerprint&&this.initialized)return;if(this.credentialsFingerprint=fingerprint,this.senderAddress=config.senderAddress||this.senderAddress,config.fromName)this.fromName=config.fromName;if(this.initialized=!1,this.client=null,config.enabled)this.initialize(config.connectionString)}isAvailable(){return this.initialized&&this.client!==null}initialize(connectionString){try{if(!connectionString){this.logger.warn("[AzureEmailService] No connection string provided");return}if(!this.senderAddress){this.logger.warn("[AzureEmailService] No sender address configured");return}let{EmailClient}=__require("@azure/communication-email");this.client=new EmailClient(connectionString),this.initialized=!0,this.logger.info("[AzureEmailService] Initialized",{senderAddress:this.senderAddress})}catch(error){let message=error instanceof Error?error.message:String(error);this.logger.error("[AzureEmailService] Initialization failed",{error:message})}}async sendEmail(payload){if(!this.client)return this.logger.error("[AzureEmailService] Not initialized"),{success:!1,error:"Azure Email service not initialized"};try{let toAddresses=Array.isArray(payload.to)?payload.to:[payload.to],result=await(await this.client.beginSend({senderAddress:payload.from||this.senderAddress,content:{subject:payload.subject,plainText:payload.text,html:payload.html},recipients:{to:toAddresses.map((addr)=>({address:addr}))},attachments:payload.attachments?.map((att)=>({name:att.filename,contentType:att.contentType,contentInBase64:att.content.toString("base64")}))})).pollUntilDone();if(result.status==="Succeeded")return this.logger.info("[AzureEmailService] Email sent",{to:payload.to,subject:payload.subject,operationId:result.id}),{success:!0};let errorMsg=result.error?.message||`Send failed with status: ${result.status}`;return this.logger.error("[AzureEmailService] Send failed",{status:result.status,error:errorMsg,to:payload.to}),{success:!1,error:errorMsg}}catch(error){let message=error instanceof Error?error.message:"Unknown error";return this.logger.error("[AzureEmailService] Send failed",{error:message,to:payload.to}),{success:!1,error:message}}}}});import{createRequire}from"module";function stripAppPasswordGroups(password2){return APP_PASSWORD_SHAPE.test(password2)?password2.replace(/\s+/g,""):password2.trim()}function explainSmtpFailure(message){let text=message.toLowerCase();if(text.includes("5.7.3")||text.includes("authentication unsuccessful"))return"Sunucu kimlik do\u011Frulamay\u0131 reddetti. En s\u0131k sebebi parola de\u011Fil: Exchange Online'da "+"SMTP AUTH kirac\u0131 d\xFCzeyinde varsay\u0131lan olarak KAPALIDIR ve do\u011Fru parolay\u0131 da reddeder. "+"IT'den o posta kutusu i\xE7in \u201CAuthenticated SMTP\u201Dyi a\xE7mas\u0131n\u0131 isteyin. Di\u011Fer ihtimal, "+"uygulama parolas\u0131n\u0131n \u201CPosta kutusu\u201D alan\u0131na yaz\u0131lan adresten ba\u015Fka bir hesaba ait olmas\u0131. "+`Sunucunun kendi yan\u0131t\u0131: ${message}`;if(text.includes("5.7.57")||text.includes("must issue a starttls"))return`Sunucu \u015Fifreli ba\u011Flant\u0131 istiyor. Portu 587 (STARTTLS) veya 465 olarak deneyin. Sunucunun yan\u0131t\u0131: ${message}`;if(text.includes("enotfound")||text.includes("eai_again"))return`Sunucu ad\u0131 \xE7\xF6z\xFClemedi \u2014 \u201CSunucu\u201D alan\u0131n\u0131 kontrol edin (Microsoft 365 i\xE7in smtp.office365.com). Ayr\u0131nt\u0131: ${message}`;if(text.includes("etimedout")||text.includes("econnrefused"))return`Sunucuya ba\u011Flan\u0131lamad\u0131. A\u011F bu porta \xE7\u0131k\u0131\u015Fa izin vermiyor olabilir. Ayr\u0131nt\u0131: ${message}`;if(text.includes("5.7.60")||text.includes("send as"))return`G\xF6nderen adresi bu posta kutusuna ait de\u011Fil. Bo\u015F b\u0131rak\u0131n ya da Exchange'de \u201CSend As\u201D yetkisi verdirin. Sunucunun yan\u0131t\u0131: ${message}`;return message}var APP_PASSWORD_SHAPE,IMPLICIT_TLS_PORT=465,DEFAULT_PORT=587,SmtpEmailService;var init_SmtpEmailService=__esm(()=>{APP_PASSWORD_SHAPE=/^\s*(?:[A-Za-z0-9]{4}\s+){3}[A-Za-z0-9]{4}\s*$/;SmtpEmailService=class SmtpEmailService extends TemplatedEmailService{transporter=null;senderAddress;fromName;logger;initialized=!1;credentialsFingerprint="";constructor(config,logger2){super();this.senderAddress=config.senderAddress,this.fromName=config.fromName||"Nucleus",this.logger=logger2,this.applyConfig(config)}applyConfig(config){let fingerprint=[config.host??"",String(config.port??""),String(config.secure??""),config.user??"",config.password??"",config.senderAddress??"",config.fromName??"",String(config.enabled)].join("|");if(fingerprint===this.credentialsFingerprint&&this.initialized)return;if(this.credentialsFingerprint=fingerprint,this.senderAddress=config.senderAddress||this.senderAddress,config.fromName)this.fromName=config.fromName;if(this.initialized=!1,this.transporter=null,config.enabled)this.initialize(config)}isAvailable(){return this.initialized&&this.transporter!==null}initialize(config){try{let host=config.host?.trim();if(!host){this.logger.warn("[SmtpEmailService] No host configured");return}let user=config.user?.trim();if(!user||!config.password){this.logger.warn("[SmtpEmailService] No credentials configured");return}if(!this.senderAddress)this.senderAddress=user;let port=config.port??DEFAULT_PORT,nodemailer=createRequire(import.meta.url)("nodemailer"),secure=config.secure??port===IMPLICIT_TLS_PORT,pass=stripAppPasswordGroups(config.password);this.transporter=nodemailer.createTransport({host,port,secure,requireTLS:!secure,tls:{minVersion:"TLSv1.2"},auth:{user,pass},pool:!1,connectionTimeout:20000,greetingTimeout:20000,socketTimeout:60000}),this.initialized=!0,this.logger.info("[SmtpEmailService] Initialized",{host,port,senderAddress:this.senderAddress})}catch(error){let message=error instanceof Error?error.message:String(error);this.logger.error("[SmtpEmailService] Initialization failed \u2014 is `nodemailer` installed in this app?",{error:message,host:config.host,port:config.port??DEFAULT_PORT})}}sanitizeHeaderValue(value){return value.replace(/[\r\n\u0000-\u001f\u007f]+/g," ").trim()}async sendEmail(payload){if(!this.transporter)return this.logger.error("[SmtpEmailService] Not initialized"),{success:!1,error:"SMTP email service not initialized"};try{let address=this.sanitizeHeaderValue(payload.from||this.senderAddress),from=this.fromName?`"${this.sanitizeHeaderValue(this.fromName).replace(/"/g,"")}" <${address}>`:address,to=(Array.isArray(payload.to)?payload.to:[payload.to]).map((a)=>this.sanitizeHeaderValue(a)),result=await this.transporter.sendMail({from,to,subject:this.sanitizeHeaderValue(payload.subject),text:payload.text,html:payload.html,replyTo:payload.replyTo?this.sanitizeHeaderValue(payload.replyTo):void 0,attachments:payload.attachments?.map((att)=>({filename:att.filename,content:att.content,contentType:att.contentType}))}),rejected=(result.rejected??[]).filter(Boolean);if(rejected.length>0)return this.logger.warn("[SmtpEmailService] Some recipients were refused",{rejected,accepted:result.accepted?.length??0,subject:payload.subject}),{success:(result.accepted?.length??0)>0,rejected,error:`Bu adresler sunucu taraf\u0131ndan reddedildi: ${rejected.join(", ")}`};return this.logger.info("[SmtpEmailService] Email sent",{to:payload.to,subject:payload.subject,messageId:result.messageId}),{success:!0}}catch(error){let message=error instanceof Error?error.message:"Unknown error";return this.logger.error("[SmtpEmailService] Send failed",{error:message,to:payload.to}),{success:!1,error:explainSmtpFailure(message)}}}}});var init_Email=__esm(()=>{init_AzureEmailService();init_SmtpEmailService()});import fs2 from"fs";function loadGoogle(){if(!cachedGoogle)cachedGoogle=__require("googleapis").google;return cachedGoogle}class GmailService{gmail=null;fromEmail;fromName;logger;initialized=!1;credentialsFingerprint="";constructor(config,logger2){if(this.fromEmail=config.fromEmail,this.fromName=config.fromName||"Vorion",this.logger=logger2,config.enabled)this.initialize(config)}reconfigure(config){if(`${config.serviceAccountJson??""}|${config.jsonFilePath??""}|${config.fromEmail}`===this.credentialsFingerprint&&this.initialized)return;if(this.fromEmail=config.fromEmail||this.fromEmail,config.fromName)this.fromName=config.fromName;if(this.initialized=!1,this.gmail=null,config.enabled)this.initialize(config)}sanitizeHeaderValue(value){return value.replace(/[\r\n\u0000-\u001f\u007f]+/g," ").trim()}encodeHeaderValue(rawValue){let value=this.sanitizeHeaderValue(rawValue),hasNonAscii=!1;for(let i=0;i<value.length;i++)if(value.charCodeAt(i)>127){hasNonAscii=!0;break}if(!hasNonAscii)return value;return`=?UTF-8?B?${Buffer.from(value,"utf-8").toString("base64")}?=`}isAvailable(){return this.initialized&&this.gmail!==null}initialize(config){let{serviceAccountJson,jsonFilePath}=config;this.credentialsFingerprint=`${serviceAccountJson??""}|${jsonFilePath??""}|${config.fromEmail}`;try{let keyContent;if(serviceAccountJson?.trim())keyContent=serviceAccountJson;else if(jsonFilePath){if(!fs2.existsSync(jsonFilePath)){this.logger.error("[GmailService] JSON file not found",{path:jsonFilePath});return}keyContent=fs2.readFileSync(jsonFilePath,"utf8")}else{this.logger.warn("[GmailService] No service-account JSON or file path provided");return}let credentials;try{credentials=JSON.parse(keyContent)}catch{this.logger.error("[GmailService] Service-account JSON is not valid JSON",{source:serviceAccountJson?.trim()?"inline":"file"});return}if(!this.fromEmail){this.logger.warn("[GmailService] From email not configured");return}if(!credentials?.client_email||!credentials?.private_key){this.logger.error("[GmailService] Invalid credentials format");return}let privateKey=credentials.private_key;if(!privateKey.includes(`
34
34
  `)&&privateKey.includes("\\n"))privateKey=privateKey.replace(/\\n/g,`
35
- `);let auth=new google.auth.JWT({email:credentials.client_email,key:privateKey,scopes:["https://www.googleapis.com/auth/gmail.send"],subject:this.fromEmail});this.gmail=google.gmail({version:"v1",auth}),this.initialized=!0,this.logger.info("[GmailService] Initialized",{serviceAccount:credentials.client_email,fromEmail:this.fromEmail})}catch(error){this.logger.error("[GmailService] Initialization failed",{error})}}createEmailMessage(payload){let{to,subject,html,text,from,replyTo,attachments}=payload,toAddresses=this.sanitizeHeaderValue(Array.isArray(to)?to.map((address)=>this.sanitizeHeaderValue(address)).join(", "):to),fromAddress=this.sanitizeHeaderValue(from||`${this.fromName} <${this.fromEmail}>`),encodedSubject=this.encodeHeaderValue(subject);if(!attachments||attachments.length===0){let messageParts2=[`From: ${fromAddress}`,`To: ${toAddresses}`,`Subject: ${encodedSubject}`,"MIME-Version: 1.0","Content-Type: text/html; charset=utf-8"];if(replyTo)messageParts2.push(`Reply-To: ${this.sanitizeHeaderValue(replyTo)}`);return messageParts2.push("",html||text||""),Buffer.from(messageParts2.join(`
35
+ `);let google=loadGoogle(),auth=new google.auth.JWT({email:credentials.client_email,key:privateKey,scopes:["https://www.googleapis.com/auth/gmail.send"],subject:this.fromEmail});this.gmail=google.gmail({version:"v1",auth}),this.initialized=!0,this.logger.info("[GmailService] Initialized",{serviceAccount:credentials.client_email,fromEmail:this.fromEmail})}catch(error){this.logger.error("[GmailService] Initialization failed",{error})}}createEmailMessage(payload){let{to,subject,html,text,from,replyTo,attachments}=payload,toAddresses=this.sanitizeHeaderValue(Array.isArray(to)?to.map((address)=>this.sanitizeHeaderValue(address)).join(", "):to),fromAddress=this.sanitizeHeaderValue(from||`${this.fromName} <${this.fromEmail}>`),encodedSubject=this.encodeHeaderValue(subject);if(!attachments||attachments.length===0){let messageParts2=[`From: ${fromAddress}`,`To: ${toAddresses}`,`Subject: ${encodedSubject}`,"MIME-Version: 1.0","Content-Type: text/html; charset=utf-8"];if(replyTo)messageParts2.push(`Reply-To: ${this.sanitizeHeaderValue(replyTo)}`);return messageParts2.push("",html||text||""),Buffer.from(messageParts2.join(`
36
36
  `)).toString("base64").replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}let boundary=`----Nucleus_${Date.now()}_${Math.random().toString(36).substring(7)}`,messageParts=[`From: ${fromAddress}`,`To: ${toAddresses}`,`Subject: ${encodedSubject}`,"MIME-Version: 1.0",`Content-Type: multipart/mixed; boundary="${boundary}"`];if(replyTo)messageParts.push(`Reply-To: ${this.sanitizeHeaderValue(replyTo)}`);messageParts.push("",`--${boundary}`),messageParts.push("Content-Type: text/html; charset=utf-8","Content-Transfer-Encoding: base64","",Buffer.from(html||text||"").toString("base64"),"");for(let attachment of attachments)messageParts.push(`--${boundary}`,`Content-Type: ${attachment.contentType}`,`Content-Disposition: attachment; filename="${attachment.filename}"`,"Content-Transfer-Encoding: base64","",attachment.content.toString("base64"),"");return messageParts.push(`--${boundary}--`),Buffer.from(messageParts.join(`\r
37
- `)).toString("base64").replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}async sendEmail(payload){if(!this.gmail)return this.logger.error("[GmailService] Not initialized"),{success:!1,error:"Gmail service not initialized"};try{let encodedMessage=this.createEmailMessage(payload);return await this.gmail.users.messages.send({userId:"me",requestBody:{raw:encodedMessage}}),this.logger.info("[GmailService] Email sent",{to:payload.to,subject:payload.subject}),{success:!0}}catch(error){let message=error instanceof Error?error.message:"Unknown error";return this.logger.error("[GmailService] Send failed",{error:message,to:payload.to}),{success:!1,error:message}}}emailWrapper(content,appName){return`<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><meta name="color-scheme" content="light dark"><meta name="supported-color-schemes" content="light dark"><title>${appName}</title><style type="text/css">:root{color-scheme:light dark;supported-color-schemes:light dark;}@media(prefers-color-scheme:dark){.em-bg{background-color:#18181b!important;}.em-card{background-color:#27272a!important;border-color:#3f3f46!important;}.em-h{color:#fafafa!important;}.em-t{color:#d4d4d8!important;}.em-m{color:#a1a1aa!important;}.em-hr{background-color:#3f3f46!important;}.em-btn{background-color:#fafafa!important;}.em-btn-text{color:#18181b!important;}.em-link{color:#60a5fa!important;}.em-lb{background-color:#3f3f46!important;border-color:#52525b!important;}.em-lb-t{color:#a1a1aa!important;}.em-sn{background-color:#3f3f46!important;border-color:#52525b!important;color:#a1a1aa!important;}}</style></head><body style="margin:0;padding:0;background-color:#f4f4f5;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;-webkit-font-smoothing:antialiased;"><table role="presentation" class="em-bg" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f4f4f5;"><tr><td align="center" style="padding:40px 16px;"><table role="presentation" width="480" cellpadding="0" cellspacing="0" style="max-width:480px;width:100%;"><tr><td align="center" style="padding-bottom:24px;"><span class="em-m" style="font-size:14px;font-weight:600;color:#71717a;letter-spacing:-0.2px;">${appName}</span></td></tr><tr><td class="em-card" style="background-color:#ffffff;border:1px solid #e4e4e7;border-radius:12px;overflow:hidden;">${content}</td></tr><tr><td align="center" style="padding-top:24px;"><p class="em-m" style="margin:0;font-size:12px;color:#a1a1aa;line-height:1.6;">This email was sent by <strong>${appName}</strong>.<br>Please do not reply to this email.</p></td></tr></table></td></tr></table></body></html>`}emailButton(href,label){return`<table role="presentation" cellpadding="0" cellspacing="0" width="100%"><tr><td align="center" style="padding:28px 32px;"><table role="presentation" cellpadding="0" cellspacing="0"><tr><td class="em-btn" align="center" style="background-color:#18181b;border-radius:8px;"><a class="em-btn-text" href="${href}" target="_blank" style="display:inline-block;padding:12px 32px;font-size:14px;font-weight:600;color:#fafafa;text-decoration:none;letter-spacing:-0.1px;">${label}</a></td></tr></table></td></tr></table>`}emailDivider(){return'<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:0 32px;"><div class="em-hr" style="height:1px;background-color:#e4e4e7;"></div></td></tr></table>'}emailLinkFallback(link,notice){let html=`<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:4px 32px 32px 32px;"><p class="em-m" style="margin:0 0 8px 0;font-size:11px;font-weight:600;color:#a1a1aa;text-transform:uppercase;letter-spacing:0.5px;">Or copy this link</p><table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td class="em-lb" style="background-color:#f4f4f5;border:1px solid #e4e4e7;border-radius:8px;padding:12px 14px;"><a class="em-link" href="${link}" style="font-size:12px;color:#2563eb;word-break:break-all;line-height:1.5;text-decoration:none;">${link}</a></td></tr></table>`;if(notice)html+=`<p class="em-m" style="margin:16px 0 0 0;font-size:12px;color:#a1a1aa;">${notice}</p>`;return html+="</td></tr></table>",html}async sendWelcomeEmail(email,name,appName="Vorion"){let content=`<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:40px 32px 24px 32px;text-align:center;"><h1 class="em-h" style="margin:0;font-size:24px;font-weight:700;color:#18181b;letter-spacing:-0.5px;">Welcome to ${appName}</h1><p class="em-t" style="margin:8px 0 0 0;font-size:15px;color:#52525b;line-height:1.6;">Your account has been created successfully</p></td></tr></table>${this.emailDivider()}<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:24px 32px 32px 32px;"><p class="em-t" style="margin:0 0 16px 0;font-size:15px;color:#3f3f46;line-height:1.7;">Hello <strong class="em-h" style="color:#18181b;">${name}</strong>,</p><p class="em-t" style="margin:0;font-size:15px;color:#3f3f46;line-height:1.7;">Welcome aboard! You now have full access to <strong>${appName}</strong>. We're excited to have you.</p></td></tr></table>`;return this.sendEmail({to:email,subject:`Welcome to ${appName}`,html:this.emailWrapper(content,appName)})}async sendVerificationEmail(email,name,verificationLink,appName="Vorion"){let content=`<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:40px 32px 24px 32px;text-align:center;"><h1 class="em-h" style="margin:0;font-size:24px;font-weight:700;color:#18181b;letter-spacing:-0.5px;">Verify your email</h1><p class="em-t" style="margin:8px 0 0 0;font-size:15px;color:#52525b;line-height:1.6;">Confirm your address to get started</p></td></tr></table>${this.emailDivider()}<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:24px 32px 0 32px;"><p class="em-t" style="margin:0 0 4px 0;font-size:15px;color:#3f3f46;line-height:1.7;">Hello <strong class="em-h" style="color:#18181b;">${name}</strong>,</p><p class="em-t" style="margin:0;font-size:15px;color:#3f3f46;line-height:1.7;">Please verify your email address by clicking the button below.</p></td></tr><tr><td>${this.emailButton(verificationLink,"Verify Email Address")}</td></tr></table>${this.emailDivider()}${this.emailLinkFallback(verificationLink)}`;return this.sendEmail({to:email,subject:`Verify your email \u2014 ${appName}`,html:this.emailWrapper(content,appName)})}async sendPasswordResetEmail(email,name,resetLink,appName="Vorion"){let content=`<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:40px 32px 24px 32px;text-align:center;"><h1 class="em-h" style="margin:0;font-size:24px;font-weight:700;color:#18181b;letter-spacing:-0.5px;">Reset your password</h1><p class="em-t" style="margin:8px 0 0 0;font-size:15px;color:#52525b;line-height:1.6;">We received a request to reset your password</p></td></tr></table>${this.emailDivider()}<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:24px 32px 0 32px;"><p class="em-t" style="margin:0 0 4px 0;font-size:15px;color:#3f3f46;line-height:1.7;">Hello <strong class="em-h" style="color:#18181b;">${name}</strong>,</p><p class="em-t" style="margin:0;font-size:15px;color:#3f3f46;line-height:1.7;">Click the button below to choose a new password. If you didn't make this request, you can safely ignore this email.</p></td></tr><tr><td>${this.emailButton(resetLink,"Reset Password")}</td></tr></table>${this.emailDivider()}${this.emailLinkFallback(resetLink,"This link expires in 1 hour and can only be used once.")}`;return this.sendEmail({to:email,subject:`Reset your password \u2014 ${appName}`,html:this.emailWrapper(content,appName)})}async sendMagicLinkEmail(email,magicLink,appName="Vorion"){let content=`<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:40px 32px 24px 32px;text-align:center;"><h1 class="em-h" style="margin:0;font-size:24px;font-weight:700;color:#18181b;letter-spacing:-0.5px;">Sign in to ${appName}</h1><p class="em-t" style="margin:8px 0 0 0;font-size:15px;color:#52525b;line-height:1.6;">No password needed \u2014 just click to sign in</p></td></tr></table>${this.emailDivider()}<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:24px 32px 0 32px;"><p class="em-t" style="margin:0;font-size:15px;color:#3f3f46;line-height:1.7;">Click the button below to securely sign in to your <strong class="em-h" style="color:#18181b;">${appName}</strong> account.</p></td></tr><tr><td>${this.emailButton(magicLink,"Sign In")}</td></tr></table>${this.emailDivider()}${this.emailLinkFallback(magicLink,"This link expires in 15 minutes and can only be used once.")}`;return this.sendEmail({to:email,subject:`Sign in to ${appName}`,html:this.emailWrapper(content,appName)})}async sendAlertEmail(to,subject,html){return this.sendEmail({to,subject,html})}async sendInvitationEmail(email,inviteLink,appName="Vorion"){let content=`<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:40px 32px 24px 32px;text-align:center;"><h1 class="em-h" style="margin:0;font-size:24px;font-weight:700;color:#18181b;letter-spacing:-0.5px;">You're Invited</h1><p class="em-t" style="margin:8px 0 0 0;font-size:15px;color:#52525b;line-height:1.6;">Join <strong class="em-h" style="color:#18181b;">${appName}</strong> and start collaborating</p></td></tr></table>${this.emailDivider()}<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:0 32px 8px 32px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:8px 0;"><table role="presentation" cellpadding="0" cellspacing="0"><tr><td class="em-sn" style="width:28px;height:28px;background-color:#f4f4f5;border:1px solid #e4e4e7;border-radius:50%;text-align:center;line-height:28px;font-size:12px;font-weight:600;color:#71717a;">1</td><td class="em-t" style="padding-left:14px;font-size:14px;color:#52525b;">Click the button below</td></tr></table></td></tr><tr><td style="padding:8px 0;"><table role="presentation" cellpadding="0" cellspacing="0"><tr><td class="em-sn" style="width:28px;height:28px;background-color:#f4f4f5;border:1px solid #e4e4e7;border-radius:50%;text-align:center;line-height:28px;font-size:12px;font-weight:600;color:#71717a;">2</td><td class="em-t" style="padding-left:14px;font-size:14px;color:#52525b;">Choose a secure password</td></tr></table></td></tr><tr><td style="padding:8px 0;"><table role="presentation" cellpadding="0" cellspacing="0"><tr><td class="em-sn" style="width:28px;height:28px;background-color:#f4f4f5;border:1px solid #e4e4e7;border-radius:50%;text-align:center;line-height:28px;font-size:12px;font-weight:600;color:#71717a;">3</td><td class="em-t" style="padding-left:14px;font-size:14px;color:#52525b;">Start collaborating</td></tr></table></td></tr></table></td></tr></table>${this.emailButton(inviteLink,"Accept Invitation")}${this.emailDivider()}${this.emailLinkFallback(inviteLink,"This invitation expires in 7 days.")}`;return this.sendEmail({to:email,subject:`You're invited to join ${appName}`,html:this.emailWrapper(content,appName)})}}var init_Gmail=()=>{};async function deliver(service,payload,what){let recipients=list(payload.to);if(!service?.isAvailable())return{sent:!1,level:"info",message:`${what}: not sent \u2014 no email provider is configured or available`,rejected:[]};if(recipients.length===0)return{sent:!1,level:"warn",message:`${what}: not sent \u2014 no recipient`,rejected:[]};let result;try{result=await service.sendEmail(payload)}catch(error){return{sent:!1,level:"warn",message:`${what}: NOT sent \u2014 ${error instanceof Error?error.message:String(error)}`,rejected:[]}}let rejected=(result.rejected??[]).filter(Boolean);if(result.success===!1)return{sent:!1,level:"warn",message:`${what}: NOT sent \u2014 ${result.error??"the provider refused it"}`,rejected};if(rejected.length>0)return{sent:!0,level:"warn",message:`${what}: sent to ${recipients.length-rejected.length} of ${recipients.length} \u2014 refused: ${rejected.join(", ")}`,rejected};return{sent:!0,level:"info",message:`${what}: sent`,rejected:[]}}var list=(to)=>(Array.isArray(to)?to:[to]).filter(Boolean);class AlertService{logger;getEmailService;config;appId;state={lastAlertTime:{},activeAlerts:new Map};cooldownMs;constructor(deps){this.logger=deps.logger,this.getEmailService=deps.getEmailService,this.config=deps.config,this.appId=deps.appId,this.cooldownMs=this.parseTimeToMs(deps.config.alerts?.cooldown||"5m")}parseTimeToMs(time){let match=time.match(/^(\d+)(ms|s|m|h|d)$/);if(!match||!match[1]||!match[2])return 300000;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 300000}}async checkAndAlert(snapshot){if(!this.config.alerts?.enabled)return[];let alerts=[],thresholds=this.config.alerts.thresholds||{};if(snapshot.system){if(thresholds.cpuPercent&&snapshot.system.cpu.usage>thresholds.cpuPercent){let alert=this.createAlert("cpu_high",snapshot.system.cpu.usage,thresholds.cpuPercent,`CPU usage is ${snapshot.system.cpu.usage}% (threshold: ${thresholds.cpuPercent}%)`);if(alert)alerts.push(alert)}if(thresholds.memoryPercent&&snapshot.system.memory.usagePercent>thresholds.memoryPercent){let alert=this.createAlert("memory_high",snapshot.system.memory.usagePercent,thresholds.memoryPercent,`Memory usage is ${snapshot.system.memory.usagePercent}% (threshold: ${thresholds.memoryPercent}%)`);if(alert)alerts.push(alert)}if(thresholds.diskPercent&&snapshot.system.disk.usagePercent>thresholds.diskPercent){let alert=this.createAlert("disk_high",snapshot.system.disk.usagePercent,thresholds.diskPercent,`Disk usage is ${snapshot.system.disk.usagePercent}% (threshold: ${thresholds.diskPercent}%)`);if(alert)alerts.push(alert)}}if(snapshot.application){let enoughTraffic=snapshot.application.requests.perMinute>=MIN_REQUESTS_FOR_RATE_ALERT;if(enoughTraffic&&thresholds.errorRatePercent&&snapshot.application.errors.rate>thresholds.errorRatePercent){let alert=this.createAlert("error_rate_high",snapshot.application.errors.rate,thresholds.errorRatePercent,`Error rate is ${snapshot.application.errors.rate}% (threshold: ${thresholds.errorRatePercent}%)`);if(alert)alerts.push(alert)}if(enoughTraffic&&thresholds.responseTimeMs&&snapshot.application.responseTime.p95>thresholds.responseTimeMs){let alert=this.createAlert("response_time_high",snapshot.application.responseTime.p95,thresholds.responseTimeMs,`P95 response time is ${snapshot.application.responseTime.p95}ms (threshold: ${thresholds.responseTimeMs}ms)`);if(alert)alerts.push(alert)}if(thresholds.rateLimitBlocksPerMinute&&snapshot.application.rateLimits.blockedPerMinute>thresholds.rateLimitBlocksPerMinute){let alert=this.createAlert("rate_limit_blocks_high",snapshot.application.rateLimits.blockedPerMinute,thresholds.rateLimitBlocksPerMinute,`Rate limit blocks: ${snapshot.application.rateLimits.blockedPerMinute}/min (threshold: ${thresholds.rateLimitBlocksPerMinute}/min)`);if(alert)alerts.push(alert)}}if(alerts.length>0)await this.sendAlerts(alerts);return alerts}createAlert(type,value,threshold,message){let now=Date.now(),lastAlert=this.state.lastAlertTime[type]||0;if(now-lastAlert<this.cooldownMs)return null;let severity=this.getSeverity(type,value,threshold),alert={id:`${type}-${now}`,type,severity,message,value,threshold,timestamp:now,acknowledged:!1};return this.state.lastAlertTime[type]=now,this.state.activeAlerts.set(type,alert),this.logger.warn(`[Alert] ${severity.toUpperCase()}: ${message}`),alert}getSeverity(_type,value,threshold){return value/threshold>1.5?"critical":"warning"}async sendAlerts(alerts){if(!this.config.alerts?.email?.enabled)return;let emailService=this.getEmailService?.();if(!emailService||!emailService.isAvailable())return;let recipients=this.config.alerts.email.recipients||[];if(recipients.length===0)return;let subject=alerts.filter((a)=>a.severity==="critical").length>0?`\uD83D\uDEA8 CRITICAL Alert - ${this.appId}`:`\u26A0\uFE0F Warning Alert - ${this.appId}`,html=this.buildEmailHtml(alerts);for(let recipient of recipients){let report=await deliver(emailService,{to:recipient,subject,html},"[Alert]");if(report.level==="warn")this.logger.error(report.message);else this.logger.info(report.message)}}buildEmailHtml(alerts){let alertRows=alerts.map((alert)=>`
37
+ `)).toString("base64").replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}async sendEmail(payload){if(!this.gmail)return this.logger.error("[GmailService] Not initialized"),{success:!1,error:"Gmail service not initialized"};try{let encodedMessage=this.createEmailMessage(payload);return await this.gmail.users.messages.send({userId:"me",requestBody:{raw:encodedMessage}}),this.logger.info("[GmailService] Email sent",{to:payload.to,subject:payload.subject}),{success:!0}}catch(error){let message=error instanceof Error?error.message:"Unknown error";return this.logger.error("[GmailService] Send failed",{error:message,to:payload.to}),{success:!1,error:message}}}emailWrapper(content,appName){return`<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><meta name="color-scheme" content="light dark"><meta name="supported-color-schemes" content="light dark"><title>${appName}</title><style type="text/css">:root{color-scheme:light dark;supported-color-schemes:light dark;}@media(prefers-color-scheme:dark){.em-bg{background-color:#18181b!important;}.em-card{background-color:#27272a!important;border-color:#3f3f46!important;}.em-h{color:#fafafa!important;}.em-t{color:#d4d4d8!important;}.em-m{color:#a1a1aa!important;}.em-hr{background-color:#3f3f46!important;}.em-btn{background-color:#fafafa!important;}.em-btn-text{color:#18181b!important;}.em-link{color:#60a5fa!important;}.em-lb{background-color:#3f3f46!important;border-color:#52525b!important;}.em-lb-t{color:#a1a1aa!important;}.em-sn{background-color:#3f3f46!important;border-color:#52525b!important;color:#a1a1aa!important;}}</style></head><body style="margin:0;padding:0;background-color:#f4f4f5;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;-webkit-font-smoothing:antialiased;"><table role="presentation" class="em-bg" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f4f4f5;"><tr><td align="center" style="padding:40px 16px;"><table role="presentation" width="480" cellpadding="0" cellspacing="0" style="max-width:480px;width:100%;"><tr><td align="center" style="padding-bottom:24px;"><span class="em-m" style="font-size:14px;font-weight:600;color:#71717a;letter-spacing:-0.2px;">${appName}</span></td></tr><tr><td class="em-card" style="background-color:#ffffff;border:1px solid #e4e4e7;border-radius:12px;overflow:hidden;">${content}</td></tr><tr><td align="center" style="padding-top:24px;"><p class="em-m" style="margin:0;font-size:12px;color:#a1a1aa;line-height:1.6;">This email was sent by <strong>${appName}</strong>.<br>Please do not reply to this email.</p></td></tr></table></td></tr></table></body></html>`}emailButton(href,label){return`<table role="presentation" cellpadding="0" cellspacing="0" width="100%"><tr><td align="center" style="padding:28px 32px;"><table role="presentation" cellpadding="0" cellspacing="0"><tr><td class="em-btn" align="center" style="background-color:#18181b;border-radius:8px;"><a class="em-btn-text" href="${href}" target="_blank" style="display:inline-block;padding:12px 32px;font-size:14px;font-weight:600;color:#fafafa;text-decoration:none;letter-spacing:-0.1px;">${label}</a></td></tr></table></td></tr></table>`}emailDivider(){return'<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:0 32px;"><div class="em-hr" style="height:1px;background-color:#e4e4e7;"></div></td></tr></table>'}emailLinkFallback(link,notice){let html=`<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:4px 32px 32px 32px;"><p class="em-m" style="margin:0 0 8px 0;font-size:11px;font-weight:600;color:#a1a1aa;text-transform:uppercase;letter-spacing:0.5px;">Or copy this link</p><table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td class="em-lb" style="background-color:#f4f4f5;border:1px solid #e4e4e7;border-radius:8px;padding:12px 14px;"><a class="em-link" href="${link}" style="font-size:12px;color:#2563eb;word-break:break-all;line-height:1.5;text-decoration:none;">${link}</a></td></tr></table>`;if(notice)html+=`<p class="em-m" style="margin:16px 0 0 0;font-size:12px;color:#a1a1aa;">${notice}</p>`;return html+="</td></tr></table>",html}async sendWelcomeEmail(email,name,appName="Vorion"){let content=`<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:40px 32px 24px 32px;text-align:center;"><h1 class="em-h" style="margin:0;font-size:24px;font-weight:700;color:#18181b;letter-spacing:-0.5px;">Welcome to ${appName}</h1><p class="em-t" style="margin:8px 0 0 0;font-size:15px;color:#52525b;line-height:1.6;">Your account has been created successfully</p></td></tr></table>${this.emailDivider()}<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:24px 32px 32px 32px;"><p class="em-t" style="margin:0 0 16px 0;font-size:15px;color:#3f3f46;line-height:1.7;">Hello <strong class="em-h" style="color:#18181b;">${name}</strong>,</p><p class="em-t" style="margin:0;font-size:15px;color:#3f3f46;line-height:1.7;">Welcome aboard! You now have full access to <strong>${appName}</strong>. We're excited to have you.</p></td></tr></table>`;return this.sendEmail({to:email,subject:`Welcome to ${appName}`,html:this.emailWrapper(content,appName)})}async sendVerificationEmail(email,name,verificationLink,appName="Vorion"){let content=`<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:40px 32px 24px 32px;text-align:center;"><h1 class="em-h" style="margin:0;font-size:24px;font-weight:700;color:#18181b;letter-spacing:-0.5px;">Verify your email</h1><p class="em-t" style="margin:8px 0 0 0;font-size:15px;color:#52525b;line-height:1.6;">Confirm your address to get started</p></td></tr></table>${this.emailDivider()}<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:24px 32px 0 32px;"><p class="em-t" style="margin:0 0 4px 0;font-size:15px;color:#3f3f46;line-height:1.7;">Hello <strong class="em-h" style="color:#18181b;">${name}</strong>,</p><p class="em-t" style="margin:0;font-size:15px;color:#3f3f46;line-height:1.7;">Please verify your email address by clicking the button below.</p></td></tr><tr><td>${this.emailButton(verificationLink,"Verify Email Address")}</td></tr></table>${this.emailDivider()}${this.emailLinkFallback(verificationLink)}`;return this.sendEmail({to:email,subject:`Verify your email \u2014 ${appName}`,html:this.emailWrapper(content,appName)})}async sendPasswordResetEmail(email,name,resetLink,appName="Vorion"){let content=`<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:40px 32px 24px 32px;text-align:center;"><h1 class="em-h" style="margin:0;font-size:24px;font-weight:700;color:#18181b;letter-spacing:-0.5px;">Reset your password</h1><p class="em-t" style="margin:8px 0 0 0;font-size:15px;color:#52525b;line-height:1.6;">We received a request to reset your password</p></td></tr></table>${this.emailDivider()}<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:24px 32px 0 32px;"><p class="em-t" style="margin:0 0 4px 0;font-size:15px;color:#3f3f46;line-height:1.7;">Hello <strong class="em-h" style="color:#18181b;">${name}</strong>,</p><p class="em-t" style="margin:0;font-size:15px;color:#3f3f46;line-height:1.7;">Click the button below to choose a new password. If you didn't make this request, you can safely ignore this email.</p></td></tr><tr><td>${this.emailButton(resetLink,"Reset Password")}</td></tr></table>${this.emailDivider()}${this.emailLinkFallback(resetLink,"This link expires in 1 hour and can only be used once.")}`;return this.sendEmail({to:email,subject:`Reset your password \u2014 ${appName}`,html:this.emailWrapper(content,appName)})}async sendMagicLinkEmail(email,magicLink,appName="Vorion"){let content=`<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:40px 32px 24px 32px;text-align:center;"><h1 class="em-h" style="margin:0;font-size:24px;font-weight:700;color:#18181b;letter-spacing:-0.5px;">Sign in to ${appName}</h1><p class="em-t" style="margin:8px 0 0 0;font-size:15px;color:#52525b;line-height:1.6;">No password needed \u2014 just click to sign in</p></td></tr></table>${this.emailDivider()}<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:24px 32px 0 32px;"><p class="em-t" style="margin:0;font-size:15px;color:#3f3f46;line-height:1.7;">Click the button below to securely sign in to your <strong class="em-h" style="color:#18181b;">${appName}</strong> account.</p></td></tr><tr><td>${this.emailButton(magicLink,"Sign In")}</td></tr></table>${this.emailDivider()}${this.emailLinkFallback(magicLink,"This link expires in 15 minutes and can only be used once.")}`;return this.sendEmail({to:email,subject:`Sign in to ${appName}`,html:this.emailWrapper(content,appName)})}async sendAlertEmail(to,subject,html){return this.sendEmail({to,subject,html})}async sendInvitationEmail(email,inviteLink,appName="Vorion"){let content=`<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:40px 32px 24px 32px;text-align:center;"><h1 class="em-h" style="margin:0;font-size:24px;font-weight:700;color:#18181b;letter-spacing:-0.5px;">You're Invited</h1><p class="em-t" style="margin:8px 0 0 0;font-size:15px;color:#52525b;line-height:1.6;">Join <strong class="em-h" style="color:#18181b;">${appName}</strong> and start collaborating</p></td></tr></table>${this.emailDivider()}<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:0 32px 8px 32px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:8px 0;"><table role="presentation" cellpadding="0" cellspacing="0"><tr><td class="em-sn" style="width:28px;height:28px;background-color:#f4f4f5;border:1px solid #e4e4e7;border-radius:50%;text-align:center;line-height:28px;font-size:12px;font-weight:600;color:#71717a;">1</td><td class="em-t" style="padding-left:14px;font-size:14px;color:#52525b;">Click the button below</td></tr></table></td></tr><tr><td style="padding:8px 0;"><table role="presentation" cellpadding="0" cellspacing="0"><tr><td class="em-sn" style="width:28px;height:28px;background-color:#f4f4f5;border:1px solid #e4e4e7;border-radius:50%;text-align:center;line-height:28px;font-size:12px;font-weight:600;color:#71717a;">2</td><td class="em-t" style="padding-left:14px;font-size:14px;color:#52525b;">Choose a secure password</td></tr></table></td></tr><tr><td style="padding:8px 0;"><table role="presentation" cellpadding="0" cellspacing="0"><tr><td class="em-sn" style="width:28px;height:28px;background-color:#f4f4f5;border:1px solid #e4e4e7;border-radius:50%;text-align:center;line-height:28px;font-size:12px;font-weight:600;color:#71717a;">3</td><td class="em-t" style="padding-left:14px;font-size:14px;color:#52525b;">Start collaborating</td></tr></table></td></tr></table></td></tr></table>${this.emailButton(inviteLink,"Accept Invitation")}${this.emailDivider()}${this.emailLinkFallback(inviteLink,"This invitation expires in 7 days.")}`;return this.sendEmail({to:email,subject:`You're invited to join ${appName}`,html:this.emailWrapper(content,appName)})}}var cachedGoogle=null;var init_Gmail=()=>{};async function deliver(service,payload,what){let recipients=list(payload.to);if(!service?.isAvailable())return{sent:!1,level:"info",message:`${what}: not sent \u2014 no email provider is configured or available`,rejected:[]};if(recipients.length===0)return{sent:!1,level:"warn",message:`${what}: not sent \u2014 no recipient`,rejected:[]};let result;try{result=await service.sendEmail(payload)}catch(error){return{sent:!1,level:"warn",message:`${what}: NOT sent \u2014 ${error instanceof Error?error.message:String(error)}`,rejected:[]}}let rejected=(result.rejected??[]).filter(Boolean);if(result.success===!1)return{sent:!1,level:"warn",message:`${what}: NOT sent \u2014 ${result.error??"the provider refused it"}`,rejected};if(rejected.length>0)return{sent:!0,level:"warn",message:`${what}: sent to ${recipients.length-rejected.length} of ${recipients.length} \u2014 refused: ${rejected.join(", ")}`,rejected};return{sent:!0,level:"info",message:`${what}: sent`,rejected:[]}}var list=(to)=>(Array.isArray(to)?to:[to]).filter(Boolean);class AlertService{logger;getEmailService;config;appId;state={lastAlertTime:{},activeAlerts:new Map};cooldownMs;constructor(deps){this.logger=deps.logger,this.getEmailService=deps.getEmailService,this.config=deps.config,this.appId=deps.appId,this.cooldownMs=this.parseTimeToMs(deps.config.alerts?.cooldown||"5m")}parseTimeToMs(time){let match=time.match(/^(\d+)(ms|s|m|h|d)$/);if(!match||!match[1]||!match[2])return 300000;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 300000}}async checkAndAlert(snapshot){if(!this.config.alerts?.enabled)return[];let alerts=[],thresholds=this.config.alerts.thresholds||{};if(snapshot.system){if(thresholds.cpuPercent&&snapshot.system.cpu.usage>thresholds.cpuPercent){let alert=this.createAlert("cpu_high",snapshot.system.cpu.usage,thresholds.cpuPercent,`CPU usage is ${snapshot.system.cpu.usage}% (threshold: ${thresholds.cpuPercent}%)`);if(alert)alerts.push(alert)}if(thresholds.memoryPercent&&snapshot.system.memory.usagePercent>thresholds.memoryPercent){let alert=this.createAlert("memory_high",snapshot.system.memory.usagePercent,thresholds.memoryPercent,`Memory usage is ${snapshot.system.memory.usagePercent}% (threshold: ${thresholds.memoryPercent}%)`);if(alert)alerts.push(alert)}if(thresholds.diskPercent&&snapshot.system.disk.usagePercent>thresholds.diskPercent){let alert=this.createAlert("disk_high",snapshot.system.disk.usagePercent,thresholds.diskPercent,`Disk usage is ${snapshot.system.disk.usagePercent}% (threshold: ${thresholds.diskPercent}%)`);if(alert)alerts.push(alert)}}if(snapshot.application){let enoughTraffic=snapshot.application.requests.perMinute>=MIN_REQUESTS_FOR_RATE_ALERT;if(enoughTraffic&&thresholds.errorRatePercent&&snapshot.application.errors.rate>thresholds.errorRatePercent){let alert=this.createAlert("error_rate_high",snapshot.application.errors.rate,thresholds.errorRatePercent,`Error rate is ${snapshot.application.errors.rate}% (threshold: ${thresholds.errorRatePercent}%)`);if(alert)alerts.push(alert)}if(enoughTraffic&&thresholds.responseTimeMs&&snapshot.application.responseTime.p95>thresholds.responseTimeMs){let alert=this.createAlert("response_time_high",snapshot.application.responseTime.p95,thresholds.responseTimeMs,`P95 response time is ${snapshot.application.responseTime.p95}ms (threshold: ${thresholds.responseTimeMs}ms)`);if(alert)alerts.push(alert)}if(thresholds.rateLimitBlocksPerMinute&&snapshot.application.rateLimits.blockedPerMinute>thresholds.rateLimitBlocksPerMinute){let alert=this.createAlert("rate_limit_blocks_high",snapshot.application.rateLimits.blockedPerMinute,thresholds.rateLimitBlocksPerMinute,`Rate limit blocks: ${snapshot.application.rateLimits.blockedPerMinute}/min (threshold: ${thresholds.rateLimitBlocksPerMinute}/min)`);if(alert)alerts.push(alert)}}if(alerts.length>0)await this.sendAlerts(alerts);return alerts}createAlert(type,value,threshold,message){let now=Date.now(),lastAlert=this.state.lastAlertTime[type]||0;if(now-lastAlert<this.cooldownMs)return null;let severity=this.getSeverity(type,value,threshold),alert={id:`${type}-${now}`,type,severity,message,value,threshold,timestamp:now,acknowledged:!1};return this.state.lastAlertTime[type]=now,this.state.activeAlerts.set(type,alert),this.logger.warn(`[Alert] ${severity.toUpperCase()}: ${message}`),alert}getSeverity(_type,value,threshold){return value/threshold>1.5?"critical":"warning"}async sendAlerts(alerts){if(!this.config.alerts?.email?.enabled)return;let emailService=this.getEmailService?.();if(!emailService||!emailService.isAvailable())return;let recipients=this.config.alerts.email.recipients||[];if(recipients.length===0)return;let subject=alerts.filter((a)=>a.severity==="critical").length>0?`\uD83D\uDEA8 CRITICAL Alert - ${this.appId}`:`\u26A0\uFE0F Warning Alert - ${this.appId}`,html=this.buildEmailHtml(alerts);for(let recipient of recipients){let report=await deliver(emailService,{to:recipient,subject,html},"[Alert]");if(report.level==="warn")this.logger.error(report.message);else this.logger.info(report.message)}}buildEmailHtml(alerts){let alertRows=alerts.map((alert)=>`
38
38
  <tr style="background-color: ${alert.severity==="critical"?"#fee2e2":"#fef3c7"};">
39
39
  <td style="padding: 12px; border: 1px solid #e5e7eb;">${alert.severity.toUpperCase()}</td>
40
40
  <td style="padding: 12px; border: 1px solid #e5e7eb;">${alert.type}</td>
@@ -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&&registerConfig.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&&currentSessionId){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&&registerConfig.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&&currentSessionId){let{ne:ne3}=await import("drizzle-orm");query=db.update(sessionsTable2).set({isActive:!1,revokedAt:new Date,revokedReason:body.reason||"user_revoked"}).where(and20(eq56(col15("userId"),userId),eq56(col15("isActive"),!0),ne3(col15("id"),currentSessionId)))}await query,log4.info("All sessions revoked",{userId,excludeCurrent:body.excludeCurrent,reason:body.reason||"user_revoked"});let reqUrl=new URL(ctx.request.url);return logger2.audit({entityName:"user_sessions",operation:"SESSION_REVOKE_ALL",userId:userId||void 0,summary:body.excludeCurrent?"All other sessions revoked":"All sessions revoked",ipAddress:ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||ctx.request.headers.get("x-real-ip")?.trim()||"unknown",userAgent:ctx.request.headers.get("user-agent")||"unknown",path:reqUrl.pathname,query:reqUrl.search}),{success:!0,message:body.excludeCurrent?"All other sessions revoked successfully":"All sessions revoked successfully"}},{body:RevokeAllSessionsBodySchema,detail:{tags:["Authentication"],summary:"Revoke all sessions",description:"Revoke all sessions for the current user (optionally exclude current)"}}),sessionsRoutes.get(`${baseRoute}/stats`,async(ctx)=>{let{sessionsTable:sessionsTable2,col:col15}=getSessionsForRequest(ctx.request);if(!db)return{success:!1,message:"Database not configured"};let userId=ctx.request.headers.get("x-user-id");if(!userId)return{success:!1,message:"Authentication required"};let{count:count3,countDistinct}=await import("drizzle-orm"),activeSessions=await db.select({count:count3()}).from(sessionsTable2).where(and20(eq56(col15("userId"),userId),eq56(col15("isActive"),!0),isNull3(col15("revokedAt")))),uniqueDevices=await db.select({count:countDistinct(col15("deviceFingerprint"))}).from(sessionsTable2).where(and20(eq56(col15("userId"),userId),eq56(col15("isActive"),!0))),uniqueIps=await db.select({count:countDistinct(col15("ipAddress"))}).from(sessionsTable2).where(and20(eq56(col15("userId"),userId),eq56(col15("isActive"),!0)));return{success:!0,data:{activeSessions:activeSessions[0]?.count||0,uniqueDevices:uniqueDevices[0]?.count||0,uniqueIpAddresses:uniqueIps[0]?.count||0}}},{detail:{tags:["Authentication"],summary:"Session statistics",description:"Get session statistics for the current user"}});let APPROVAL_TOKEN_TTL_MS=86400000,buildResultHtml=(action,success,message)=>{let icon=success?action==="approve"?"\u2705":"\uD83D\uDEAB":"\u274C",title=success?action==="approve"?"Device Approved":"Device Rejected":"Action Failed",bName=sessionsConfig.brandName||"Nucleus";return`<!DOCTYPE html>
446
446
  <html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
447
447
  <title>${title} \u2014 ${bName}</title>
448
448
  <style>*{box-sizing:border-box}body{margin:0;padding:0;background-color:#f4f4f5;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;}</style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nucleus-core-ts",
3
- "version": "0.9.963",
3
+ "version": "0.9.965",
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",