nucleus-core-ts 0.9.978 → 0.9.979

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.978
1
+ 0.9.979
package/dist/index.js CHANGED
@@ -30,7 +30,7 @@ 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_KEY_PREFIX}${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_KEY_PREFIX}${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=parseStoredJson(stored);if(!challenge||typeof challenge.expiresAt!=="number")return await this.redis.del(key),this.logger.warn("[CAPTCHA] Stored challenge unreadable, discarded",{challengeId,storedType:typeof stored}),{success:!1,valid:!1,message:"Challenge expired or not found"};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_KEY_PREFIX}${challengeId}`),this.logger.info("[CAPTCHA] Challenge invalidated",{challengeId})}isEnabled(){return this.config.enabled}async checkRateLimit(ipAddress){let minuteKey=`${RATE_LIMIT_KEY_PREFIX}${ipAddress}:minute`,hourKey=`${RATE_LIMIT_KEY_PREFIX}${ipAddress}:hour`,[minuteCount,hourCount]=await Promise.all([this.redis.get(minuteKey),this.redis.get(hourKey)]),minuteRequests=storedCount(minuteCount),hourRequests=storedCount(hourCount),maxPerMinute=this.config.rateLimit.maxGeneratePerMinute??10,maxPerHour=this.config.rateLimit.maxGeneratePerHour??100;return minuteRequests>=maxPerMinute||hourRequests>=maxPerHour}async incrementRateLimit(ipAddress){let minuteKey=`${RATE_LIMIT_KEY_PREFIX}${ipAddress}:minute`,hourKey=`${RATE_LIMIT_KEY_PREFIX}${ipAddress}:hour`,[minuteCount,hourCount]=await Promise.all([this.redis.get(minuteKey),this.redis.get(hourKey)]),newMinuteCount=storedCount(minuteCount)+1,newHourCount=storedCount(hourCount)+1;await Promise.all([this.redis.set(minuteKey,newMinuteCount.toString(),{ex:60}),this.redis.set(hourKey,newHourCount.toString(),{ex:3600})])}}var CAPTCHA_KEY_PREFIX="captcha:",RATE_LIMIT_KEY_PREFIX="captcha_rate:",storedCount=(stored)=>{let value=parseStoredJson(stored),parsed=typeof value==="number"?value:Number.parseInt(String(value??""),10);return Number.isFinite(parsed)?parsed:0},DIFFICULTY_CONFIG,CHARACTERS="ABCDEFGHJKLMNPQRSTUVWXYZ23456789";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(`
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_KEY_PREFIX}${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_KEY_PREFIX}${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=parseStoredJson(stored);if(!challenge||typeof challenge.expiresAt!=="number")return await this.redis.del(key),this.logger.warn("[CAPTCHA] Stored challenge unreadable, discarded",{challengeId,storedType:typeof stored}),{success:!1,valid:!1,message:"Challenge expired or not found"};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_KEY_PREFIX}${challengeId}`),this.logger.info("[CAPTCHA] Challenge invalidated",{challengeId})}isEnabled(){return this.config.enabled}async checkRateLimit(ipAddress){let minuteKey=`${RATE_LIMIT_KEY_PREFIX}${ipAddress}:minute`,hourKey=`${RATE_LIMIT_KEY_PREFIX}${ipAddress}:hour`,[minuteCount,hourCount]=await Promise.all([this.redis.get(minuteKey),this.redis.get(hourKey)]),minuteRequests=storedCount(minuteCount),hourRequests=storedCount(hourCount),maxPerMinute=this.config.rateLimit.maxGeneratePerMinute??10,maxPerHour=this.config.rateLimit.maxGeneratePerHour??100;return minuteRequests>=maxPerMinute||hourRequests>=maxPerHour}async incrementRateLimit(ipAddress){let minuteKey=`${RATE_LIMIT_KEY_PREFIX}${ipAddress}:minute`,hourKey=`${RATE_LIMIT_KEY_PREFIX}${ipAddress}:hour`,[minuteCount,hourCount]=await Promise.all([this.redis.get(minuteKey),this.redis.get(hourKey)]),newMinuteCount=storedCount(minuteCount)+1,newHourCount=storedCount(hourCount)+1;await Promise.all([this.redis.set(minuteKey,newMinuteCount.toString(),{ex:60}),this.redis.set(hourKey,newHourCount.toString(),{ex:3600})])}}var CAPTCHA_KEY_PREFIX="captcha:",RATE_LIMIT_KEY_PREFIX="captcha_rate:",storedCount=(stored)=>{let value=parseStoredJson(stored),parsed=typeof value==="number"?value:Number.parseInt(String(value??""),10);return Number.isFinite(parsed)?parsed:0},DIFFICULTY_CONFIG,CHARACTERS="ABCDEFGHJKLMNPQRSTUVWXYZ23456789";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}))},...payload.replyTo?{replyTo:[{address:payload.replyTo}]}:{},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
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
@@ -0,0 +1,79 @@
1
+ /**
2
+ * What a process leaves behind when it does not get to say goodbye.
3
+ *
4
+ * Measured on a live install: the API had restarted 9 times in four days, the
5
+ * last one 128 minutes earlier, and the container status said `OOMKilled`,
6
+ * `exitCode: 137`. The application's own log ended with
7
+ * `← GET /branding 200 (4ms)` — an ordinary, healthy line. Nothing else.
8
+ *
9
+ * That is not a gap in the logging. A SIGKILL runs no handler: there is no
10
+ * `uncaughtException`, no `SIGTERM`, no `beforeExit`, no flush. Anything the
11
+ * process was holding — including the live monitoring service's entire memory
12
+ * series, which lives in a plain array — dies with it. And Kubernetes' own
13
+ * record of the reason is on the pod, which is replaced.
14
+ *
15
+ * So the evidence has to be written BEFORE the death, and OUTSIDE the process.
16
+ * Redis is both: it is already a dependency, and on that install it had been up
17
+ * for 79 days while the API died nine times.
18
+ */
19
+ /** Written repeatedly while the process is alive; the last one is the evidence. */
20
+ export interface LifecycleHeartbeat {
21
+ /** New for every boot, so two runs are never confused. */
22
+ bootId: string;
23
+ /** Epoch ms. */
24
+ startedAt: number;
25
+ /** Epoch ms of the most recent beat — how far the process got. */
26
+ lastSeenAt: number;
27
+ /** Set only by a graceful shutdown. Its ABSENCE is what names a crash. */
28
+ stoppedAt?: number;
29
+ /** Why it stopped, when it had the chance to say — 'SIGTERM', 'onStop'. */
30
+ stopReason?: string;
31
+ /**
32
+ * Whatever the process managed to say about its own death: an uncaught
33
+ * exception or an unhandled rejection. Absent for a kill, which is exactly
34
+ * the case the memory numbers below have to explain instead.
35
+ */
36
+ fatal?: {
37
+ name: string;
38
+ message: string;
39
+ stack?: string;
40
+ at: number;
41
+ };
42
+ /** The last memory reading, which is what makes an OOM legible after the fact. */
43
+ memory?: {
44
+ rss: number;
45
+ heapUsed: number;
46
+ external: number;
47
+ /** The cgroup ceiling, so "980 of 1024 MB" can be read without a cluster. */
48
+ total: number;
49
+ usagePercent: number;
50
+ };
51
+ /** Free-form, so an install can add what it needs (pod name, image tag). */
52
+ labels?: Record<string, string>;
53
+ }
54
+ /** One death, written by the run that came AFTER it. */
55
+ export interface LifecycleIncident {
56
+ /** The boot that died. */
57
+ bootId: string;
58
+ startedAt: number;
59
+ /** The last beat before it went — an upper bound on when it died. */
60
+ lastSeenAt: number;
61
+ /** `lastSeenAt - startedAt`: how long it survived. */
62
+ uptimeMs: number;
63
+ /** How long the evidence had gone stale, i.e. at most how long it was down. */
64
+ detectedAfterMs: number;
65
+ /**
66
+ * The best available account, in order of confidence:
67
+ * - `fatal` — the process told us, with an error
68
+ * - `killed` — no goodbye and no error; memory at the ceiling says why
69
+ * - `vanished` — no goodbye, no error, and memory was NOT near the ceiling
70
+ */
71
+ verdict: 'fatal' | 'killed' | 'vanished';
72
+ fatal?: LifecycleHeartbeat['fatal'];
73
+ memory?: LifecycleHeartbeat['memory'];
74
+ labels?: Record<string, string>;
75
+ /** A sentence a person can read without knowing any of the above. */
76
+ summary: string;
77
+ /** When the NEXT boot noticed. */
78
+ recordedAt: number;
79
+ }
@@ -0,0 +1,19 @@
1
+ import type { LifecycleHeartbeat, LifecycleIncident } from './types';
2
+ /**
3
+ * Turning a stale heartbeat into a sentence somebody can act on.
4
+ *
5
+ * Kept apart from the storage so the JUDGEMENT — which is the whole value — can
6
+ * be tested without Redis, a clock, or a process that dies.
7
+ */
8
+ /**
9
+ * How close to the ceiling counts as "this is why it died".
10
+ *
11
+ * Not 100%: the kernel kills on the way up, and the last beat lands somewhere
12
+ * below the peak. On the install this was written for, the limit was 1 GiB and
13
+ * the ratchet climbed ~45 MB/h — so the final beat can easily be tens of
14
+ * megabytes short of the line it crossed a moment later.
15
+ */
16
+ export declare const NEAR_CEILING_PERCENT = 85;
17
+ export declare function judge(previous: LifecycleHeartbeat, now: number): Omit<LifecycleIncident, 'recordedAt'>;
18
+ /** Turkish-free, because this string goes to logs and English tooling alike. */
19
+ export declare function duration(ms: number): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nucleus-core-ts",
3
- "version": "0.9.978",
3
+ "version": "0.9.979",
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",