nucleus-core-ts 0.9.899 → 0.9.900
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.build-ok +1 -1
- package/dist/dist/index.d.ts +2 -1
- package/dist/index.js +3 -3
- package/dist/src/Services/Gmail/index.d.ts +18 -0
- package/package.json +1 -1
package/dist/.build-ok
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.9.
|
|
1
|
+
0.9.900
|
package/dist/dist/index.d.ts
CHANGED
|
@@ -5971,7 +5971,8 @@ export class GmailService {
|
|
|
5971
5971
|
initialized: boolean;
|
|
5972
5972
|
credentialsFingerprint: string;
|
|
5973
5973
|
reconfigure(config: any): void;
|
|
5974
|
-
|
|
5974
|
+
sanitizeHeaderValue(value: any): any;
|
|
5975
|
+
encodeHeaderValue(rawValue: any): any;
|
|
5975
5976
|
isAvailable(): boolean;
|
|
5976
5977
|
initialize(config: any): void;
|
|
5977
5978
|
createEmailMessage(payload: any): string;
|
package/dist/index.js
CHANGED
|
@@ -30,10 +30,10 @@ and limitations under the License.
|
|
|
30
30
|
${textElements}
|
|
31
31
|
</g>
|
|
32
32
|
${overlayLines}
|
|
33
|
-
</svg>`}function generatePuzzleChallenge(difficulty){let pieceCount=DIFFICULTY_CONFIG[difficulty].puzzlePieces,pieces=[],correctOrder=[];for(let i=0;i<pieceCount;i++)pieces.push({id:i,x:i%3*100,y:Math.floor(i/3)*100}),correctOrder.push(i);let shuffledPieces=[...pieces];for(let i=shuffledPieces.length-1;i>0;i--){let j=getSecureRandomInt(0,i),itemI=shuffledPieces[i],itemJ=shuffledPieces[j];if(itemI&&itemJ)shuffledPieces[i]=itemJ,shuffledPieces[j]=itemI}return{question:"Arrange the pieces in correct order",answer:correctOrder.join(","),puzzleData:{pieces:shuffledPieces,correctOrder}}}class CaptchaService{redis;logger;config;constructor(deps){this.redis=deps.redis,this.logger=deps.logger,this.config={enabled:deps.config.enabled??!0,type:deps.config.type??"math",difficulty:deps.config.difficulty??"medium",expiresIn:deps.config.expiresIn??"5m",maxAttempts:deps.config.maxAttempts??3,caseSensitive:deps.config.caseSensitive??!1,rateLimit:{maxGeneratePerMinute:deps.config.rateLimit?.maxGeneratePerMinute??10,maxGeneratePerHour:deps.config.rateLimit?.maxGeneratePerHour??100}}}async generate(type,difficulty,ipAddress){if(ipAddress){if(await this.checkRateLimit(ipAddress))return this.logger.warn("[CAPTCHA] Rate limit exceeded",{ipAddress}),{success:!1,challengeId:"",type:type??this.config.type,question:"",expiresAt:0,rateLimited:!0,message:"Too many requests. Please try again later."};await this.incrementRateLimit(ipAddress)}let captchaType=type??this.config.type,captchaDifficulty=difficulty??this.config.difficulty,challengeId=generateSecureId(),expiresInSeconds=parseTimeToSeconds(this.config.expiresIn),expiresAt=Date.now()+expiresInSeconds*1000,question,answer,imageData,puzzleData;switch(captchaType){case"math":{let mathResult=generateMathChallenge(captchaDifficulty);question=mathResult.question,answer=mathResult.answer;break}case"text":{let textResult=generateTextChallenge(captchaDifficulty);question=textResult.question,answer=textResult.answer;break}case"image":{let imageResult=generateImageChallenge(captchaDifficulty);question=imageResult.question,answer=imageResult.answer,imageData=imageResult.imageData;break}case"puzzle":{let puzzleResult=generatePuzzleChallenge(captchaDifficulty);question=puzzleResult.question,answer=puzzleResult.answer,puzzleData=puzzleResult.puzzleData;break}default:{let defaultResult=generateMathChallenge(captchaDifficulty);question=defaultResult.question,answer=defaultResult.answer}}let normalizedAnswer=this.config.caseSensitive?answer:answer.toUpperCase(),answerHash=hashAnswer(normalizedAnswer),challenge={id:challengeId,type:captchaType,question,answer:answerHash,imageData,puzzleData,expiresAt,attempts:0,maxAttempts:this.config.maxAttempts,caseSensitive:this.config.caseSensitive};await this.redis.set(`captcha:${challengeId}`,JSON.stringify(challenge),{ex:expiresInSeconds}),this.logger.info("[CAPTCHA] Challenge generated",{challengeId,type:captchaType,difficulty:captchaDifficulty,expiresAt});let result={success:!0,challengeId,type:captchaType,question,expiresAt};if(imageData)result.imageData=imageData;if(puzzleData)result.puzzleData={pieces:puzzleData.pieces};return result}async validate(challengeId,userAnswer){let key=`captcha:${challengeId}`,stored=await this.redis.get(key);if(!stored)return this.logger.warn("[CAPTCHA] Challenge not found or expired",{challengeId}),{success:!1,valid:!1,message:"Challenge expired or not found"};let challenge=JSON.parse(stored);if(Date.now()>challenge.expiresAt)return await this.redis.del(key),this.logger.warn("[CAPTCHA] Challenge expired",{challengeId}),{success:!1,valid:!1,message:"Challenge expired"};let ttl=Math.max(1,Math.floor((challenge.expiresAt-Date.now())/1000)),attemptsKey=`${key}:att`,attempts;if(this.redis.incrementCounter)attempts=await this.redis.incrementCounter(attemptsKey,1,ttl);else challenge.attempts+=1,attempts=challenge.attempts,await this.redis.set(key,JSON.stringify(challenge),{ex:ttl});let clearChallenge=async()=>{if(await this.redis.del(key),this.redis.incrementCounter)await this.redis.del(attemptsKey)};if(attempts>challenge.maxAttempts)return await clearChallenge(),this.logger.warn("[CAPTCHA] Max attempts exceeded",{challengeId,attempts}),{success:!1,valid:!1,message:"Maximum attempts exceeded"};let normalizedUserAnswer=challenge.caseSensitive?userAnswer.trim():userAnswer.trim().toUpperCase(),userAnswerHash=hashAnswer(normalizedUserAnswer);if(timingSafeEqual(challenge.answer,userAnswerHash))return await clearChallenge(),this.logger.info("[CAPTCHA] Challenge validated successfully",{challengeId}),{success:!0,valid:!0,message:"Captcha validated successfully"};let remainingAttempts=challenge.maxAttempts-attempts;if(remainingAttempts<=0)await clearChallenge();return this.logger.warn("[CAPTCHA] Invalid answer",{challengeId,attemptsRemaining:remainingAttempts}),{success:!0,valid:!1,message:"Incorrect answer",attemptsRemaining:remainingAttempts}}async invalidate(challengeId){await this.redis.del(`captcha:${challengeId}`),this.logger.info("[CAPTCHA] Challenge invalidated",{challengeId})}isEnabled(){return this.config.enabled}async checkRateLimit(ipAddress){let minuteKey=`captcha_rate:${ipAddress}:minute`,hourKey=`captcha_rate:${ipAddress}:hour`,[minuteCount,hourCount]=await Promise.all([this.redis.get(minuteKey),this.redis.get(hourKey)]),minuteRequests=minuteCount?Number.parseInt(minuteCount,10):0,hourRequests=hourCount?Number.parseInt(hourCount,10):0,maxPerMinute=this.config.rateLimit.maxGeneratePerMinute??10,maxPerHour=this.config.rateLimit.maxGeneratePerHour??100;return minuteRequests>=maxPerMinute||hourRequests>=maxPerHour}async incrementRateLimit(ipAddress){let minuteKey=`captcha_rate:${ipAddress}:minute`,hourKey=`captcha_rate:${ipAddress}:hour`,[minuteCount,hourCount]=await Promise.all([this.redis.get(minuteKey),this.redis.get(hourKey)]),newMinuteCount=(minuteCount?Number.parseInt(minuteCount,10):0)+1,newHourCount=(hourCount?Number.parseInt(hourCount,10):0)+1;await Promise.all([this.redis.set(minuteKey,newMinuteCount.toString(),{ex:60}),this.redis.set(hourKey,newHourCount.toString(),{ex:3600})])}}var DIFFICULTY_CONFIG;var init_Captcha=__esm(()=>{DIFFICULTY_CONFIG={easy:{mathRange:{min:1,max:10},textLength:4,puzzlePieces:4},medium:{mathRange:{min:10,max:50},textLength:6,puzzlePieces:6},hard:{mathRange:{min:50,max:100},textLength:8,puzzlePieces:9}}});import{promises as dns}from"dns";var DEFAULT_TXT_PREFIX="_nucleus-challenge",DEFAULT_CHALLENGE_TTL=604800,DEFAULT_BASE_PATH="/domains",resolveEnvValue=(value)=>{if(value==null)return;let fromEnv=process.env[value];return fromEnv!=null&&fromEnv!==""?fromEnv:value},normalizeHostname=(raw)=>{let trimmed=(raw||"").trim();if(!trimmed)return"";let normalized=((trimmed.replace(/^https?:\/\//i,"").split("/")[0]?.split("?")[0]||"").split(":")[0]||"").toLowerCase().replace(/\.$/,"");try{normalized=new URL(`http://${normalized}`).hostname.replace(/\.$/,"")}catch{}return normalized},isValidHostname=(hostname)=>{if(!hostname||hostname.length>253)return!1;let labels=hostname.split(".");if(labels.length<2)return!1;return labels.every((label)=>/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/.test(label))},classifyHostname=(normalized,platformBaseDomain)=>{if(platformBaseDomain){let base=platformBaseDomain.toLowerCase().replace(/\.$/,"");if(normalized===base)return"apex";if(normalized.endsWith(`.${base}`)){let prefix=normalized.slice(0,-(base.length+1));if(prefix&&!prefix.includes("."))return"platform_subdomain"}}let labels=normalized.split(".");if(labels[0]==="www")return"www";if(labels.length<=2)return"apex";return"subdomain"},generateChallengeToken=()=>{let bytes=new Uint8Array(24);return crypto.getRandomValues(bytes),Array.from(bytes,(b)=>b.toString(16).padStart(2,"0")).join("")},resolveDomainsConfig=(raw)=>{let provider=raw?.provider??"manual_dns",cf=raw?.cloudflare,cloudflare=cf&&resolveEnvValue(cf.accountId)&&resolveEnvValue(cf.zoneId)&&resolveEnvValue(cf.apiToken)?{accountId:resolveEnvValue(cf.accountId),zoneId:resolveEnvValue(cf.zoneId),apiToken:resolveEnvValue(cf.apiToken),fallbackOrigin:resolveEnvValue(cf.fallbackOrigin)??null,cnameTarget:resolveEnvValue(cf.cnameTarget)??null}:null;return{enabled:raw?.enabled===!0,basePath:raw?.basePath??DEFAULT_BASE_PATH,provider,platformBaseDomain:raw?.platformBaseDomain?normalizeHostname(raw.platformBaseDomain):null,cnameTarget:resolveEnvValue(raw?.cnameTarget)??cloudflare?.cnameTarget??null,allowApexDomains:raw?.allowApexDomains!==!1,allowWwwDomains:raw?.allowWwwDomains!==!1,allowManagedRegistrations:raw?.allowManagedRegistrations===!0,reservedHostnames:(raw?.reservedHostnames??[]).map((h)=>normalizeHostname(h)),verification:{defaultMethod:raw?.verification?.defaultMethod??"txt",txtRecordPrefix:raw?.verification?.txtRecordPrefix??DEFAULT_TXT_PREFIX,challengeTtlSeconds:raw?.verification?.challengeTtlSeconds??DEFAULT_CHALLENGE_TTL},cloudflare,registrar:raw?.registrar?{provider:raw.registrar.provider??"cloudflare_registrar",defaultRegistrantOwnerType:raw.registrar.defaultRegistrantOwnerType??"customer"}:null}},str=(v)=>v==null?"":String(v),strOrNull=(v)=>v==null?null:String(v),parseMetadata=(v)=>{if(!v)return{};let obj=typeof v==="string"?safeJson(v):v;if(!obj||typeof obj!=="object")return{};let out={};for(let[k,val]of Object.entries(obj))if(typeof val==="string"||typeof val==="number"||typeof val==="boolean")out[k]=val;return out},safeJson=(v)=>{try{return JSON.parse(v)}catch{return{}}},rowToDomainHostname=(row)=>({id:str(row.id),ownerType:str(row.ownerType??row.owner_type)||"tenant",ownerId:str(row.ownerId??row.owner_id),tenantId:strOrNull(row.tenantId??row.tenant_id),schemaName:strOrNull(row.schemaName??row.schema_name),hostname:str(row.hostname),normalizedHostname:str(row.normalizedHostname??row.normalized_hostname),hostnameKind:str(row.hostnameKind??row.hostname_kind)||"apex",status:str(row.status),routingStatus:str(row.routingStatus??row.routing_status),certificateStatus:str(row.certificateStatus??row.certificate_status),verificationStatus:str(row.verificationStatus??row.verification_status),provider:str(row.provider)||"manual_dns",providerHostnameId:strOrNull(row.providerHostnameId??row.provider_hostname_id),dnsTarget:strOrNull(row.dnsTarget??row.dns_target),isPrimary:Boolean(row.isPrimary??row.is_primary),redirectToHostnameId:strOrNull(row.redirectToHostnameId??row.redirect_to_hostname_id),activatedAt:strOrNull(row.activatedAt??row.activated_at),disabledAt:strOrNull(row.disabledAt??row.disabled_at),failureReason:strOrNull(row.failureReason??row.failure_reason),metadata:parseMetadata(row.metadata)}),verifyTxtRecord=async(recordName,expectedValue)=>{try{let flat=(await dns.resolveTxt(recordName)).map((chunks)=>chunks.join("")),verified=flat.some((v)=>v===expectedValue);return{verified,observed:flat,reason:verified?void 0:"TXT value not found"}}catch(err){return{verified:!1,reason:err instanceof Error?err.message:"DNS lookup failed"}}},verifyCnameRecord=async(recordName,expectedTarget)=>{let target=expectedTarget.toLowerCase().replace(/\.$/,"");try{let flat=(await dns.resolveCname(recordName)).map((v)=>v.toLowerCase().replace(/\.$/,"")),verified=flat.includes(target);return{verified,observed:flat,reason:verified?void 0:"CNAME target mismatch"}}catch(err){return{verified:!1,reason:err instanceof Error?err.message:"DNS lookup failed"}}};var init_Domain_helpers=()=>{};import{eq as eq7}from"drizzle-orm";class DomainRegistrationService{getDb;getRegistry;logger;config;constructor(cfg){this.getDb=cfg.getDb,this.getRegistry=cfg.getTenantRegistry,this.logger=cfg.logger.scoped("domains.registration"),this.config=cfg.config}get enabled(){return this.config.allowManagedRegistrations}table(name){let registry=this.getRegistry();if(!registry)return null;return registry.getMainContext().schemaTables[name]??null}async event(registrationId,eventType,message){let db=this.getDb(),table=this.table("domainEvents");if(!db||!table)return;try{await db.insert(table).values({domainRegistrationId:registrationId,eventType,message:message??null,metadata:JSON.stringify({})})}catch(err){this.logger.warn("Failed to record registration event",{eventType,error:err instanceof Error?err.message:String(err)})}}async createRequest(input){if(!this.enabled)throw Error("Managed domain registration is not enabled");if(!input.termsAccepted)throw Error("Registration requires accepting the domain terms");let db=this.getDb(),table=this.table("domainRegistrations");if(!db||!table)throw Error("Domain registration service not ready");let normalized=normalizeHostname(input.requestedDomain),rows=await db.insert(table).values({ownerType:input.ownerType??"tenant",ownerId:input.ownerId,tenantId:input.tenantId??null,requestedDomain:normalized,provider:this.config.registrar?.provider??"cloudflare_registrar",registrantOwnerType:input.registrantOwnerType??this.config.registrar?.defaultRegistrantOwnerType??"customer",status:"awaiting_payment",autoRenew:input.autoRenew??!0,termsVersion:input.termsVersion,termsAcceptedAt:new Date,metadata:JSON.stringify(input.metadata??{})}).returning(),record=rowToRegistration(rows[0]);return await this.event(record.id,"registration_requested",`Requested ${normalized}`),record}async listRegistrations(filter){let db=this.getDb(),table=this.table("domainRegistrations");if(!db||!table)return[];let where=filter.tenantId?eq7(col2(table,"tenantId"),filter.tenantId):filter.ownerId?eq7(col2(table,"ownerId"),filter.ownerId):void 0;return(where?await db.select().from(table).where(where):await db.select().from(table)).map((r)=>rowToRegistration(r))}async getRegistration(id){let db=this.getDb(),table=this.table("domainRegistrations");if(!db||!table)return null;let rows=await db.select().from(table).where(eq7(col2(table,"id"),id)).limit(1);return rows[0]?rowToRegistration(rows[0]):null}async transition(id,status,patch){let db=this.getDb(),table=this.table("domainRegistrations");if(!db||!table)return null;return await db.update(table).set({status,registeredDomain:patch?.registeredDomain,providerRegistrationId:patch?.providerRegistrationId,priceAmount:patch?.priceAmount,currency:patch?.currency,renewalDate:patch?.renewalDate,failureReason:patch?.failureReason??null,updatedAt:new Date}).where(eq7(col2(table,"id"),id)),await this.event(id,`registration_${status}`),this.getRegistration(id)}async cancel(id){return this.transition(id,"cancelled")}async requestTransferOut(id){return this.transition(id,"transfer_requested")}}var col2=(table,name)=>table[name],rowToRegistration=(row)=>({id:String(row.id??""),ownerType:String(row.ownerType??row.owner_type??"tenant"),ownerId:String(row.ownerId??row.owner_id??""),tenantId:row.tenantId??row.tenant_id?String(row.tenantId??row.tenant_id):null,requestedDomain:String(row.requestedDomain??row.requested_domain??""),registeredDomain:row.registeredDomain??row.registered_domain?String(row.registeredDomain??row.registered_domain):null,provider:String(row.provider??"cloudflare_registrar"),status:String(row.status??"draft"),registrantOwnerType:String(row.registrantOwnerType??row.registrant_owner_type??"customer"),priceAmount:row.priceAmount??row.price_amount?String(row.priceAmount??row.price_amount):null,currency:row.currency?String(row.currency):null,autoRenew:Boolean(row.autoRenew??row.auto_renew??!0),termsVersion:row.termsVersion??row.terms_version?String(row.termsVersion??row.terms_version):null,termsAcceptedAt:row.termsAcceptedAt??row.terms_accepted_at?String(row.termsAcceptedAt??row.terms_accepted_at):null,failureReason:row.failureReason??row.failure_reason?String(row.failureReason??row.failure_reason):null});var init_DomainRegistrationService=__esm(()=>{init_Domain_helpers()});import{and as and3,eq as eq8}from"drizzle-orm";class DomainService{getDb;getRegistry;logger;config;provider;constructor(cfg){this.getDb=cfg.getDb,this.getRegistry=cfg.getTenantRegistry,this.logger=cfg.logger.scoped("domains.service"),this.config=cfg.config,this.provider=cfg.provider}table(name){let registry=this.getRegistry();if(!registry)return null;return registry.getMainContext().schemaTables[name]??null}toProviderInput(record){return{hostname:record.hostname,normalizedHostname:record.normalizedHostname,hostnameKind:record.hostnameKind}}isReserved(normalized){if(this.config.platformBaseDomain&&normalized===this.config.platformBaseDomain)return!0;return this.config.reservedHostnames.includes(normalized)}async createHostname(input){let db=this.getDb(),table=this.table("domainHostnames");if(!db||!table)throw Error("Domain service not ready");let normalized=normalizeHostname(input.hostname);if(!isValidHostname(normalized))throw Error(`Invalid hostname: ${input.hostname}`);if(this.isReserved(normalized))throw Error(`Hostname is reserved: ${normalized}`);let kind=classifyHostname(normalized,this.config.platformBaseDomain);if(kind==="apex"&&!this.config.allowApexDomains)throw Error("Apex domains are not allowed");if(kind==="www"&&!this.config.allowWwwDomains)throw Error("www domains are not allowed");if((await db.select().from(table).where(eq8(col3(table,"normalizedHostname"),normalized)).limit(1))[0])throw Error(`Hostname already registered: ${normalized}`);let providerInput={hostname:input.hostname,normalizedHostname:normalized,hostnameKind:kind},provisioned=await this.provider.createCustomHostname(providerInput),rows=await db.insert(table).values({ownerType:input.ownerType??"tenant",ownerId:input.ownerId,tenantId:input.tenantId??null,schemaName:input.schemaName??null,hostname:input.hostname,normalizedHostname:normalized,hostnameKind:kind,status:"pending_verification",routingStatus:provisioned.routingStatus,certificateStatus:provisioned.certificateStatus,verificationStatus:provisioned.verificationStatus,provider:this.provider.name,providerHostnameId:provisioned.providerHostnameId,dnsTarget:provisioned.dnsTarget,isPrimary:input.isPrimary??!1,metadata:JSON.stringify(input.metadata??{})}).returning(),record=rowToDomainHostname(rows[0]);return await this.persistChallenges(record.id,provisioned.challenges),await this.recordEvent({domainHostnameId:record.id,eventType:"hostname_requested",message:`Custom hostname requested: ${normalized}`,metadata:{provider:this.provider.name,kind}}),{hostname:record,instructions:provisioned.instructions}}async persistChallenges(hostnameId,challenges){let db=this.getDb(),table=this.table("domainVerificationChallenges");if(!db||!table||challenges.length===0)return;let expiresAt=new Date(Date.now()+this.config.verification.challengeTtlSeconds*1000);for(let c of challenges)await db.insert(table).values({domainHostnameId:hostnameId,challengeType:c.challengeType,recordType:c.recordType,recordName:c.recordName,recordValue:c.recordValue,expectedTarget:c.expectedTarget??null,status:"pending",expiresAt})}async listHostnames(filter){let db=this.getDb(),table=this.table("domainHostnames");if(!db||!table)return[];let where=filter.tenantId?eq8(col3(table,"tenantId"),filter.tenantId):filter.ownerId?eq8(col3(table,"ownerId"),filter.ownerId):void 0;return(where?await db.select().from(table).where(where):await db.select().from(table)).map((r)=>rowToDomainHostname(r))}async getHostname(id){let db=this.getDb(),table=this.table("domainHostnames");if(!db||!table)return null;let rows=await db.select().from(table).where(eq8(col3(table,"id"),id)).limit(1);return rows[0]?rowToDomainHostname(rows[0]):null}async getInstructions(id){let record=await this.getHostname(id);if(!record)return[];let db=this.getDb(),table=this.table("domainVerificationChallenges"),instructions=[];if(record.dnsTarget)instructions.push({recordType:record.hostnameKind==="apex"?"A":"CNAME",name:record.normalizedHostname,value:record.dnsTarget,note:record.hostnameKind==="apex"?"Apex domains require an A/ALIAS record at your DNS provider":"Point this hostname at the platform routing target"});if(db&&table){let rows=await db.select().from(table).where(eq8(col3(table,"domainHostnameId"),id));for(let r of rows)instructions.push({recordType:String(r.recordType??"TXT"),name:String(r.recordName??""),value:String(r.recordValue??""),note:String(r.challengeType??"")==="certificate_validation"?"SSL validation":"Ownership verification"})}return instructions}async loadPendingChallenge(hostnameId){let db=this.getDb(),table=this.table("domainVerificationChallenges");if(!db||!table)return null;let row=(await db.select().from(table).where(and3(eq8(col3(table,"domainHostnameId"),hostnameId),eq8(col3(table,"status"),"pending"))).limit(1))[0];if(!row)return null;return{challengeType:String(row.challengeType??"ownership_validation"),recordType:String(row.recordType??"TXT"),recordName:String(row.recordName??""),recordValue:String(row.recordValue??""),expectedTarget:row.expectedTarget!=null?String(row.expectedTarget):void 0}}async verifyHostname(id){let record=await this.getHostname(id);if(!record)return null;let providerInput=this.toProviderInput(record),routingStatus=record.routingStatus,certificateStatus=record.certificateStatus,verificationStatus=record.verificationStatus,failureReason=null;if(this.provider.name==="manual_dns"){let challenge=await this.loadPendingChallenge(id);if(challenge){let result=await this.provider.verifyOwnership(providerInput,challenge);if(verificationStatus=result.verified?"verified":"failed",failureReason=result.verified?null:result.reason??"Verification failed",result.verified)await this.markChallengesVerified(id)}let status2=await this.provider.getCustomHostnameStatus(record.providerHostnameId,providerInput);routingStatus=status2.routingStatus,certificateStatus=status2.certificateStatus}else{let status2=await this.provider.getCustomHostnameStatus(record.providerHostnameId,providerInput);if(routingStatus=status2.routingStatus,certificateStatus=status2.certificateStatus,verificationStatus=status2.verificationStatus,failureReason=status2.failureReason??null,verificationStatus==="verified")await this.markChallengesVerified(id)}let ready=verificationStatus==="verified"&&routingStatus==="active"&&(certificateStatus==="active"||certificateStatus==="not_required"),status=verificationStatus==="failed"?"failed":ready?"active":"verifying";if(await this.applyStatus(id,{status,routingStatus,certificateStatus,verificationStatus,failureReason,activate:ready}),ready)await this.syncRegistry(id);return await this.recordEvent({domainHostnameId:id,eventType:ready?"verification_succeeded":"verification_attempted",message:failureReason??`verification=${verificationStatus} routing=${routingStatus} ssl=${certificateStatus}`}),this.getHostname(id)}async activateHostname(id){let record=await this.getHostname(id);if(!record)return null;if(record.verificationStatus!=="verified")throw Error("Cannot activate a hostname before ownership is verified");return await this.applyStatus(id,{status:"active",routingStatus:"active",certificateStatus:record.certificateStatus==="failed"?"pending":record.certificateStatus,verificationStatus:"verified",failureReason:null,activate:!0}),await this.syncRegistry(id),await this.recordEvent({domainHostnameId:id,eventType:"routing_active",message:"Hostname activated"}),this.getHostname(id)}async disableHostname(id){let db=this.getDb(),table=this.table("domainHostnames"),record=await this.getHostname(id);if(!db||!table||!record)return null;return await this.provider.deleteCustomHostname(record.providerHostnameId),await db.update(table).set({status:"disabled",routingStatus:"inactive",disabledAt:new Date,updatedAt:new Date}).where(eq8(col3(table,"id"),id)),this.getRegistry()?.unregisterDomainHostname(record.normalizedHostname),await this.recordEvent({domainHostnameId:id,eventType:"hostname_disabled",message:"Hostname disabled"}),this.getHostname(id)}async makePrimary(id){let db=this.getDb(),table=this.table("domainHostnames"),record=await this.getHostname(id);if(!db||!table||!record)return null;let scope=record.tenantId?eq8(col3(table,"tenantId"),record.tenantId):eq8(col3(table,"ownerId"),record.ownerId);return await db.update(table).set({isPrimary:!1,updatedAt:new Date}).where(scope),await db.update(table).set({isPrimary:!0,updatedAt:new Date}).where(eq8(col3(table,"id"),id)),await this.recordEvent({domainHostnameId:id,eventType:"primary_changed",message:"Primary hostname set"}),this.getHostname(id)}async refreshStatus(id){return this.verifyHostname(id)}async resolveHostname(hostname){let db=this.getDb(),table=this.table("domainHostnames");if(!db||!table)return null;let normalized=normalizeHostname(hostname),row=(await db.select().from(table).where(and3(eq8(col3(table,"normalizedHostname"),normalized),eq8(col3(table,"status"),"active"))).limit(1))[0];if(!row)return null;let record=rowToDomainHostname(row),redirectToHostname=null;if(record.redirectToHostnameId)redirectToHostname=(await this.getHostname(record.redirectToHostnameId))?.normalizedHostname??null;return{hostnameId:record.id,tenantId:record.tenantId,schemaName:record.schemaName,isPrimary:record.isPrimary,redirectToHostname}}async recordEvent(input){let db=this.getDb(),table=this.table("domainEvents");if(!db||!table)return;try{await db.insert(table).values({domainHostnameId:input.domainHostnameId??null,domainRegistrationId:input.domainRegistrationId??null,eventType:input.eventType,actorType:input.actorType??null,actorId:input.actorId??null,message:input.message??null,ipAddress:input.ipAddress??null,metadata:JSON.stringify(input.metadata??{})})}catch(err){this.logger.warn("Failed to record domain event",{eventType:input.eventType,error:err instanceof Error?err.message:String(err)})}}async markChallengesVerified(hostnameId){let db=this.getDb(),table=this.table("domainVerificationChallenges");if(!db||!table)return;await db.update(table).set({status:"verified",verifiedAt:new Date,updatedAt:new Date}).where(eq8(col3(table,"domainHostnameId"),hostnameId))}async applyStatus(id,patch){let db=this.getDb(),table=this.table("domainHostnames");if(!db||!table)return;await db.update(table).set({status:patch.status,routingStatus:patch.routingStatus,certificateStatus:patch.certificateStatus,verificationStatus:patch.verificationStatus,failureReason:patch.failureReason,activatedAt:patch.activate?new Date:void 0,updatedAt:new Date}).where(eq8(col3(table,"id"),id))}async syncRegistry(id){let record=await this.getHostname(id),registry=this.getRegistry();if(!record||!registry)return;if(record.status==="active"&&record.tenantId)registry.registerDomainHostname(record.normalizedHostname,record.tenantId)}}var col3=(table,name)=>table[name];var init_DomainService=__esm(()=>{init_Domain_helpers()});class CloudflareForSaasProvider{name="cloudflare_for_saas";config;logger;zoneId;apiToken;constructor(config,logger2){if(!config.cloudflare)throw Error("[Domain] Cloudflare for SaaS provider requires domains.cloudflare config");this.config=config,this.logger=logger2.scoped("domains.cloudflare"),this.zoneId=config.cloudflare.zoneId,this.apiToken=config.cloudflare.apiToken}get cnameTarget(){return this.config.cloudflare?.cnameTarget??this.config.cnameTarget}async call(path2,init){let res=await fetch(`https://api.cloudflare.com/client/v4${path2}`,{...init,headers:{Authorization:`Bearer ${this.apiToken}`,"Content-Type":"application/json",...init.headers??{}}}),json=await res.json().catch(()=>({}));if(!res.ok||json.success===!1){let msg=json.errors?.map((e)=>e.message).join("; ")||`HTTP ${res.status}`;throw Error(`Cloudflare API error: ${msg}`)}return json}mapRouting(status){if(status==="active")return"active";if(status==="blocked"||status==="moved"||status==="deleted")return"failed";return"pending"}mapCertificate(sslStatus){if(sslStatus==="active")return"active";if(sslStatus==="validation_timed_out"||sslStatus==="deactivating"||sslStatus==="deleted")return"failed";return"pending"}mapVerification(result){if(result.status==="active")return"verified";if((result.ssl?.validation_errors?.length??0)>0)return"failed";return"pending"}buildChallenges(result){let challenges=[],ov=result.ownership_verification;if(ov?.name&&ov.value)challenges.push({challengeType:"hostname_validation",recordType:"TXT",recordName:ov.name,recordValue:ov.value});for(let vr of result.ssl?.validation_records??[])if(vr.txt_name&&vr.txt_value)challenges.push({challengeType:"certificate_validation",recordType:"TXT",recordName:vr.txt_name,recordValue:vr.txt_value});return challenges}buildInstructions(input,challenges){let target=this.cnameTarget,instructions=[];if(target)instructions.push({recordType:input.hostnameKind==="apex"?"A":"CNAME",name:input.normalizedHostname,value:target,note:input.hostnameKind==="apex"?"Apex domains require an A/ALIAS record pointing at the SaaS target":"Point this hostname at the Cloudflare for SaaS target"});for(let c of challenges)instructions.push({recordType:c.recordType,name:c.recordName,value:c.recordValue,note:c.challengeType==="certificate_validation"?"SSL validation":"Hostname validation"});return instructions}toResult(result,input){let challenges=this.buildChallenges(result);return{providerHostnameId:result.id??null,dnsTarget:this.cnameTarget,routingStatus:this.mapRouting(result.status),certificateStatus:this.mapCertificate(result.ssl?.status),verificationStatus:this.mapVerification(result),instructions:this.buildInstructions(input,challenges),challenges,raw:result}}async createCustomHostname(input){let body=JSON.stringify({hostname:input.normalizedHostname,ssl:{method:"txt",type:"dv",settings:{min_tls_version:"1.2"}}}),json=await this.call(`/zones/${this.zoneId}/custom_hostnames`,{method:"POST",body});return this.toResult(json.result??{},input)}async getCustomHostnameStatus(providerHostnameId,input){let result=await this.fetchHostname(providerHostnameId,input.normalizedHostname);if(!result)return{routingStatus:"failed",certificateStatus:"failed",verificationStatus:"failed",failureReason:"Custom hostname not found at provider"};return{routingStatus:this.mapRouting(result.status),certificateStatus:this.mapCertificate(result.ssl?.status),verificationStatus:this.mapVerification(result),providerStatus:`${result.status??"unknown"}/${result.ssl?.status??"unknown"}`,failureReason:result.ssl?.validation_errors?.map((e)=>e.message).join("; ")||void 0,raw:result}}async fetchHostname(providerHostnameId,normalizedHostname){if(providerHostnameId)return(await this.call(`/zones/${this.zoneId}/custom_hostnames/${providerHostnameId}`,{method:"GET"})).result??null;let list=(await this.call(`/zones/${this.zoneId}/custom_hostnames?hostname=${encodeURIComponent(normalizedHostname)}`,{method:"GET"})).result;return Array.isArray(list)?list[0]??null:null}async deleteCustomHostname(providerHostnameId){if(!providerHostnameId)return;try{await this.call(`/zones/${this.zoneId}/custom_hostnames/${providerHostnameId}`,{method:"DELETE"})}catch(err){this.logger.warn("Failed to delete Cloudflare custom hostname",{providerHostnameId,error:err instanceof Error?err.message:String(err)})}}async verifyOwnership(input,_challenge){let status=await this.getCustomHostnameStatus(null,input);return{verified:status.verificationStatus==="verified",reason:status.failureReason??status.providerStatus}}}class ManualDnsProvider{name="manual_dns";config;constructor(config){this.config=config}buildChallenge(input){let prefix=this.config.verification.txtRecordPrefix,token=`nucleus-domain-verification=${generateChallengeToken()}`;return{challengeType:"ownership_validation",recordType:"TXT",recordName:`${prefix}.${input.normalizedHostname}`,recordValue:token}}buildInstructions(input,challenge){let target=this.config.cnameTarget,instructions=[{recordType:"TXT",name:challenge.recordName,value:challenge.recordValue,note:"Ownership verification record"}];if(input.hostnameKind==="apex")instructions.push({recordType:"A",name:input.normalizedHostname,value:target??"<platform-ingress-ip>",note:"Apex domains require an A/ALIAS record at your DNS provider"});else if(target)instructions.push({recordType:"CNAME",name:input.normalizedHostname,value:target,note:"Point this hostname at the platform routing target"});return instructions}async createCustomHostname(input){let challenge=this.buildChallenge(input);return{providerHostnameId:null,dnsTarget:this.config.cnameTarget,routingStatus:"pending",certificateStatus:"not_required",verificationStatus:"pending",instructions:this.buildInstructions(input,challenge),challenges:[challenge]}}async getCustomHostnameStatus(_providerHostnameId,input){if(input.hostnameKind!=="apex"&&this.config.cnameTarget){let dns2=await verifyCnameRecord(input.normalizedHostname,this.config.cnameTarget);return{routingStatus:dns2.verified?"active":"pending",certificateStatus:"not_required",verificationStatus:"pending",providerStatus:dns2.verified?"cname_active":"cname_pending"}}return{routingStatus:"pending",certificateStatus:"not_required",verificationStatus:"pending",providerStatus:"manual"}}async deleteCustomHostname(_providerHostnameId){}async verifyOwnership(_input,challenge){if(challenge.recordType==="CNAME"&&challenge.expectedTarget)return verifyCnameRecord(challenge.recordName,challenge.expectedTarget);return verifyTxtRecord(challenge.recordName,challenge.recordValue)}}var init_ManualDnsProvider=__esm(()=>{init_Domain_helpers()});var createDomainProvider=(config,logger2)=>{if(config.provider==="cloudflare_for_saas"){if(!config.cloudflare)return logger2.warn("[Domain] cloudflare_for_saas selected but cloudflare config missing \u2014 using manual DNS"),new ManualDnsProvider(config);return new CloudflareForSaasProvider(config,logger2)}return new ManualDnsProvider(config)};var init_Providers=__esm(()=>{init_ManualDnsProvider();init_ManualDnsProvider()});var createDomainServices=(params)=>{let config=resolveDomainsConfig(params.options.domains);if(!config.enabled)return null;let provider=createDomainProvider(config,params.logger),serviceConfig={getDb:params.getDb,getTenantRegistry:params.getTenantRegistry,logger:params.logger,config,provider};return{domainService:new DomainService(serviceConfig),registrationService:new DomainRegistrationService(serviceConfig)}};var init_Domain=__esm(()=>{init_Domain_helpers();init_DomainRegistrationService();init_DomainService();init_Providers();init_Domain_helpers();init_DomainRegistrationService();init_DomainService();init_Providers()});class AzureEmailService{client=null;senderAddress;fromName;logger;initialized=!1;credentialsFingerprint="";constructor(config,logger2){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}}}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 init_Email=()=>{};import fs2 from"fs";import{google}from"googleapis";class GmailService{gmail=null;fromEmail;fromName;logger;initialized=!1;credentialsFingerprint="";constructor(config,logger2){if(this.fromEmail=config.fromEmail,this.fromName=config.fromName||"Vorion",this.logger=logger2,config.enabled)this.initialize(config)}reconfigure(config){if(`${config.serviceAccountJson??""}|${config.jsonFilePath??""}|${config.fromEmail}`===this.credentialsFingerprint&&this.initialized)return;if(this.fromEmail=config.fromEmail||this.fromEmail,config.fromName)this.fromName=config.fromName;if(this.initialized=!1,this.gmail=null,config.enabled)this.initialize(config)}encodeHeaderValue(value){let hasNonAscii=!1;for(let i=0;i<value.length;i++)if(value.charCodeAt(i)>127){hasNonAscii=!0;break}if(!hasNonAscii)return value;return`=?UTF-8?B?${Buffer.from(value,"utf-8").toString("base64")}?=`}isAvailable(){return this.initialized&&this.gmail!==null}initialize(config){let{serviceAccountJson,jsonFilePath}=config;this.credentialsFingerprint=`${serviceAccountJson??""}|${jsonFilePath??""}|${config.fromEmail}`;try{let keyContent;if(serviceAccountJson?.trim())keyContent=serviceAccountJson;else if(jsonFilePath){if(!fs2.existsSync(jsonFilePath)){this.logger.error("[GmailService] JSON file not found",{path:jsonFilePath});return}keyContent=fs2.readFileSync(jsonFilePath,"utf8")}else{this.logger.warn("[GmailService] No service-account JSON or file path provided");return}let credentials;try{credentials=JSON.parse(keyContent)}catch{this.logger.error("[GmailService] Service-account JSON is not valid JSON",{source:serviceAccountJson?.trim()?"inline":"file"});return}if(!this.fromEmail){this.logger.warn("[GmailService] From email not configured");return}if(!credentials?.client_email||!credentials?.private_key){this.logger.error("[GmailService] Invalid credentials format");return}let privateKey=credentials.private_key;if(!privateKey.includes(`
|
|
33
|
+
</svg>`}function generatePuzzleChallenge(difficulty){let pieceCount=DIFFICULTY_CONFIG[difficulty].puzzlePieces,pieces=[],correctOrder=[];for(let i=0;i<pieceCount;i++)pieces.push({id:i,x:i%3*100,y:Math.floor(i/3)*100}),correctOrder.push(i);let shuffledPieces=[...pieces];for(let i=shuffledPieces.length-1;i>0;i--){let j=getSecureRandomInt(0,i),itemI=shuffledPieces[i],itemJ=shuffledPieces[j];if(itemI&&itemJ)shuffledPieces[i]=itemJ,shuffledPieces[j]=itemI}return{question:"Arrange the pieces in correct order",answer:correctOrder.join(","),puzzleData:{pieces:shuffledPieces,correctOrder}}}class CaptchaService{redis;logger;config;constructor(deps){this.redis=deps.redis,this.logger=deps.logger,this.config={enabled:deps.config.enabled??!0,type:deps.config.type??"math",difficulty:deps.config.difficulty??"medium",expiresIn:deps.config.expiresIn??"5m",maxAttempts:deps.config.maxAttempts??3,caseSensitive:deps.config.caseSensitive??!1,rateLimit:{maxGeneratePerMinute:deps.config.rateLimit?.maxGeneratePerMinute??10,maxGeneratePerHour:deps.config.rateLimit?.maxGeneratePerHour??100}}}async generate(type,difficulty,ipAddress){if(ipAddress){if(await this.checkRateLimit(ipAddress))return this.logger.warn("[CAPTCHA] Rate limit exceeded",{ipAddress}),{success:!1,challengeId:"",type:type??this.config.type,question:"",expiresAt:0,rateLimited:!0,message:"Too many requests. Please try again later."};await this.incrementRateLimit(ipAddress)}let captchaType=type??this.config.type,captchaDifficulty=difficulty??this.config.difficulty,challengeId=generateSecureId(),expiresInSeconds=parseTimeToSeconds(this.config.expiresIn),expiresAt=Date.now()+expiresInSeconds*1000,question,answer,imageData,puzzleData;switch(captchaType){case"math":{let mathResult=generateMathChallenge(captchaDifficulty);question=mathResult.question,answer=mathResult.answer;break}case"text":{let textResult=generateTextChallenge(captchaDifficulty);question=textResult.question,answer=textResult.answer;break}case"image":{let imageResult=generateImageChallenge(captchaDifficulty);question=imageResult.question,answer=imageResult.answer,imageData=imageResult.imageData;break}case"puzzle":{let puzzleResult=generatePuzzleChallenge(captchaDifficulty);question=puzzleResult.question,answer=puzzleResult.answer,puzzleData=puzzleResult.puzzleData;break}default:{let defaultResult=generateMathChallenge(captchaDifficulty);question=defaultResult.question,answer=defaultResult.answer}}let normalizedAnswer=this.config.caseSensitive?answer:answer.toUpperCase(),answerHash=hashAnswer(normalizedAnswer),challenge={id:challengeId,type:captchaType,question,answer:answerHash,imageData,puzzleData,expiresAt,attempts:0,maxAttempts:this.config.maxAttempts,caseSensitive:this.config.caseSensitive};await this.redis.set(`captcha:${challengeId}`,JSON.stringify(challenge),{ex:expiresInSeconds}),this.logger.info("[CAPTCHA] Challenge generated",{challengeId,type:captchaType,difficulty:captchaDifficulty,expiresAt});let result={success:!0,challengeId,type:captchaType,question,expiresAt};if(imageData)result.imageData=imageData;if(puzzleData)result.puzzleData={pieces:puzzleData.pieces};return result}async validate(challengeId,userAnswer){let key=`captcha:${challengeId}`,stored=await this.redis.get(key);if(!stored)return this.logger.warn("[CAPTCHA] Challenge not found or expired",{challengeId}),{success:!1,valid:!1,message:"Challenge expired or not found"};let challenge=JSON.parse(stored);if(Date.now()>challenge.expiresAt)return await this.redis.del(key),this.logger.warn("[CAPTCHA] Challenge expired",{challengeId}),{success:!1,valid:!1,message:"Challenge expired"};let ttl=Math.max(1,Math.floor((challenge.expiresAt-Date.now())/1000)),attemptsKey=`${key}:att`,attempts;if(this.redis.incrementCounter)attempts=await this.redis.incrementCounter(attemptsKey,1,ttl);else challenge.attempts+=1,attempts=challenge.attempts,await this.redis.set(key,JSON.stringify(challenge),{ex:ttl});let clearChallenge=async()=>{if(await this.redis.del(key),this.redis.incrementCounter)await this.redis.del(attemptsKey)};if(attempts>challenge.maxAttempts)return await clearChallenge(),this.logger.warn("[CAPTCHA] Max attempts exceeded",{challengeId,attempts}),{success:!1,valid:!1,message:"Maximum attempts exceeded"};let normalizedUserAnswer=challenge.caseSensitive?userAnswer.trim():userAnswer.trim().toUpperCase(),userAnswerHash=hashAnswer(normalizedUserAnswer);if(timingSafeEqual(challenge.answer,userAnswerHash))return await clearChallenge(),this.logger.info("[CAPTCHA] Challenge validated successfully",{challengeId}),{success:!0,valid:!0,message:"Captcha validated successfully"};let remainingAttempts=challenge.maxAttempts-attempts;if(remainingAttempts<=0)await clearChallenge();return this.logger.warn("[CAPTCHA] Invalid answer",{challengeId,attemptsRemaining:remainingAttempts}),{success:!0,valid:!1,message:"Incorrect answer",attemptsRemaining:remainingAttempts}}async invalidate(challengeId){await this.redis.del(`captcha:${challengeId}`),this.logger.info("[CAPTCHA] Challenge invalidated",{challengeId})}isEnabled(){return this.config.enabled}async checkRateLimit(ipAddress){let minuteKey=`captcha_rate:${ipAddress}:minute`,hourKey=`captcha_rate:${ipAddress}:hour`,[minuteCount,hourCount]=await Promise.all([this.redis.get(minuteKey),this.redis.get(hourKey)]),minuteRequests=minuteCount?Number.parseInt(minuteCount,10):0,hourRequests=hourCount?Number.parseInt(hourCount,10):0,maxPerMinute=this.config.rateLimit.maxGeneratePerMinute??10,maxPerHour=this.config.rateLimit.maxGeneratePerHour??100;return minuteRequests>=maxPerMinute||hourRequests>=maxPerHour}async incrementRateLimit(ipAddress){let minuteKey=`captcha_rate:${ipAddress}:minute`,hourKey=`captcha_rate:${ipAddress}:hour`,[minuteCount,hourCount]=await Promise.all([this.redis.get(minuteKey),this.redis.get(hourKey)]),newMinuteCount=(minuteCount?Number.parseInt(minuteCount,10):0)+1,newHourCount=(hourCount?Number.parseInt(hourCount,10):0)+1;await Promise.all([this.redis.set(minuteKey,newMinuteCount.toString(),{ex:60}),this.redis.set(hourKey,newHourCount.toString(),{ex:3600})])}}var DIFFICULTY_CONFIG;var init_Captcha=__esm(()=>{DIFFICULTY_CONFIG={easy:{mathRange:{min:1,max:10},textLength:4,puzzlePieces:4},medium:{mathRange:{min:10,max:50},textLength:6,puzzlePieces:6},hard:{mathRange:{min:50,max:100},textLength:8,puzzlePieces:9}}});import{promises as dns}from"dns";var DEFAULT_TXT_PREFIX="_nucleus-challenge",DEFAULT_CHALLENGE_TTL=604800,DEFAULT_BASE_PATH="/domains",resolveEnvValue=(value)=>{if(value==null)return;let fromEnv=process.env[value];return fromEnv!=null&&fromEnv!==""?fromEnv:value},normalizeHostname=(raw)=>{let trimmed=(raw||"").trim();if(!trimmed)return"";let normalized=((trimmed.replace(/^https?:\/\//i,"").split("/")[0]?.split("?")[0]||"").split(":")[0]||"").toLowerCase().replace(/\.$/,"");try{normalized=new URL(`http://${normalized}`).hostname.replace(/\.$/,"")}catch{}return normalized},isValidHostname=(hostname)=>{if(!hostname||hostname.length>253)return!1;let labels=hostname.split(".");if(labels.length<2)return!1;return labels.every((label)=>/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/.test(label))},classifyHostname=(normalized,platformBaseDomain)=>{if(platformBaseDomain){let base=platformBaseDomain.toLowerCase().replace(/\.$/,"");if(normalized===base)return"apex";if(normalized.endsWith(`.${base}`)){let prefix=normalized.slice(0,-(base.length+1));if(prefix&&!prefix.includes("."))return"platform_subdomain"}}let labels=normalized.split(".");if(labels[0]==="www")return"www";if(labels.length<=2)return"apex";return"subdomain"},generateChallengeToken=()=>{let bytes=new Uint8Array(24);return crypto.getRandomValues(bytes),Array.from(bytes,(b)=>b.toString(16).padStart(2,"0")).join("")},resolveDomainsConfig=(raw)=>{let provider=raw?.provider??"manual_dns",cf=raw?.cloudflare,cloudflare=cf&&resolveEnvValue(cf.accountId)&&resolveEnvValue(cf.zoneId)&&resolveEnvValue(cf.apiToken)?{accountId:resolveEnvValue(cf.accountId),zoneId:resolveEnvValue(cf.zoneId),apiToken:resolveEnvValue(cf.apiToken),fallbackOrigin:resolveEnvValue(cf.fallbackOrigin)??null,cnameTarget:resolveEnvValue(cf.cnameTarget)??null}:null;return{enabled:raw?.enabled===!0,basePath:raw?.basePath??DEFAULT_BASE_PATH,provider,platformBaseDomain:raw?.platformBaseDomain?normalizeHostname(raw.platformBaseDomain):null,cnameTarget:resolveEnvValue(raw?.cnameTarget)??cloudflare?.cnameTarget??null,allowApexDomains:raw?.allowApexDomains!==!1,allowWwwDomains:raw?.allowWwwDomains!==!1,allowManagedRegistrations:raw?.allowManagedRegistrations===!0,reservedHostnames:(raw?.reservedHostnames??[]).map((h)=>normalizeHostname(h)),verification:{defaultMethod:raw?.verification?.defaultMethod??"txt",txtRecordPrefix:raw?.verification?.txtRecordPrefix??DEFAULT_TXT_PREFIX,challengeTtlSeconds:raw?.verification?.challengeTtlSeconds??DEFAULT_CHALLENGE_TTL},cloudflare,registrar:raw?.registrar?{provider:raw.registrar.provider??"cloudflare_registrar",defaultRegistrantOwnerType:raw.registrar.defaultRegistrantOwnerType??"customer"}:null}},str=(v)=>v==null?"":String(v),strOrNull=(v)=>v==null?null:String(v),parseMetadata=(v)=>{if(!v)return{};let obj=typeof v==="string"?safeJson(v):v;if(!obj||typeof obj!=="object")return{};let out={};for(let[k,val]of Object.entries(obj))if(typeof val==="string"||typeof val==="number"||typeof val==="boolean")out[k]=val;return out},safeJson=(v)=>{try{return JSON.parse(v)}catch{return{}}},rowToDomainHostname=(row)=>({id:str(row.id),ownerType:str(row.ownerType??row.owner_type)||"tenant",ownerId:str(row.ownerId??row.owner_id),tenantId:strOrNull(row.tenantId??row.tenant_id),schemaName:strOrNull(row.schemaName??row.schema_name),hostname:str(row.hostname),normalizedHostname:str(row.normalizedHostname??row.normalized_hostname),hostnameKind:str(row.hostnameKind??row.hostname_kind)||"apex",status:str(row.status),routingStatus:str(row.routingStatus??row.routing_status),certificateStatus:str(row.certificateStatus??row.certificate_status),verificationStatus:str(row.verificationStatus??row.verification_status),provider:str(row.provider)||"manual_dns",providerHostnameId:strOrNull(row.providerHostnameId??row.provider_hostname_id),dnsTarget:strOrNull(row.dnsTarget??row.dns_target),isPrimary:Boolean(row.isPrimary??row.is_primary),redirectToHostnameId:strOrNull(row.redirectToHostnameId??row.redirect_to_hostname_id),activatedAt:strOrNull(row.activatedAt??row.activated_at),disabledAt:strOrNull(row.disabledAt??row.disabled_at),failureReason:strOrNull(row.failureReason??row.failure_reason),metadata:parseMetadata(row.metadata)}),verifyTxtRecord=async(recordName,expectedValue)=>{try{let flat=(await dns.resolveTxt(recordName)).map((chunks)=>chunks.join("")),verified=flat.some((v)=>v===expectedValue);return{verified,observed:flat,reason:verified?void 0:"TXT value not found"}}catch(err){return{verified:!1,reason:err instanceof Error?err.message:"DNS lookup failed"}}},verifyCnameRecord=async(recordName,expectedTarget)=>{let target=expectedTarget.toLowerCase().replace(/\.$/,"");try{let flat=(await dns.resolveCname(recordName)).map((v)=>v.toLowerCase().replace(/\.$/,"")),verified=flat.includes(target);return{verified,observed:flat,reason:verified?void 0:"CNAME target mismatch"}}catch(err){return{verified:!1,reason:err instanceof Error?err.message:"DNS lookup failed"}}};var init_Domain_helpers=()=>{};import{eq as eq7}from"drizzle-orm";class DomainRegistrationService{getDb;getRegistry;logger;config;constructor(cfg){this.getDb=cfg.getDb,this.getRegistry=cfg.getTenantRegistry,this.logger=cfg.logger.scoped("domains.registration"),this.config=cfg.config}get enabled(){return this.config.allowManagedRegistrations}table(name){let registry=this.getRegistry();if(!registry)return null;return registry.getMainContext().schemaTables[name]??null}async event(registrationId,eventType,message){let db=this.getDb(),table=this.table("domainEvents");if(!db||!table)return;try{await db.insert(table).values({domainRegistrationId:registrationId,eventType,message:message??null,metadata:JSON.stringify({})})}catch(err){this.logger.warn("Failed to record registration event",{eventType,error:err instanceof Error?err.message:String(err)})}}async createRequest(input){if(!this.enabled)throw Error("Managed domain registration is not enabled");if(!input.termsAccepted)throw Error("Registration requires accepting the domain terms");let db=this.getDb(),table=this.table("domainRegistrations");if(!db||!table)throw Error("Domain registration service not ready");let normalized=normalizeHostname(input.requestedDomain),rows=await db.insert(table).values({ownerType:input.ownerType??"tenant",ownerId:input.ownerId,tenantId:input.tenantId??null,requestedDomain:normalized,provider:this.config.registrar?.provider??"cloudflare_registrar",registrantOwnerType:input.registrantOwnerType??this.config.registrar?.defaultRegistrantOwnerType??"customer",status:"awaiting_payment",autoRenew:input.autoRenew??!0,termsVersion:input.termsVersion,termsAcceptedAt:new Date,metadata:JSON.stringify(input.metadata??{})}).returning(),record=rowToRegistration(rows[0]);return await this.event(record.id,"registration_requested",`Requested ${normalized}`),record}async listRegistrations(filter){let db=this.getDb(),table=this.table("domainRegistrations");if(!db||!table)return[];let where=filter.tenantId?eq7(col2(table,"tenantId"),filter.tenantId):filter.ownerId?eq7(col2(table,"ownerId"),filter.ownerId):void 0;return(where?await db.select().from(table).where(where):await db.select().from(table)).map((r)=>rowToRegistration(r))}async getRegistration(id){let db=this.getDb(),table=this.table("domainRegistrations");if(!db||!table)return null;let rows=await db.select().from(table).where(eq7(col2(table,"id"),id)).limit(1);return rows[0]?rowToRegistration(rows[0]):null}async transition(id,status,patch){let db=this.getDb(),table=this.table("domainRegistrations");if(!db||!table)return null;return await db.update(table).set({status,registeredDomain:patch?.registeredDomain,providerRegistrationId:patch?.providerRegistrationId,priceAmount:patch?.priceAmount,currency:patch?.currency,renewalDate:patch?.renewalDate,failureReason:patch?.failureReason??null,updatedAt:new Date}).where(eq7(col2(table,"id"),id)),await this.event(id,`registration_${status}`),this.getRegistration(id)}async cancel(id){return this.transition(id,"cancelled")}async requestTransferOut(id){return this.transition(id,"transfer_requested")}}var col2=(table,name)=>table[name],rowToRegistration=(row)=>({id:String(row.id??""),ownerType:String(row.ownerType??row.owner_type??"tenant"),ownerId:String(row.ownerId??row.owner_id??""),tenantId:row.tenantId??row.tenant_id?String(row.tenantId??row.tenant_id):null,requestedDomain:String(row.requestedDomain??row.requested_domain??""),registeredDomain:row.registeredDomain??row.registered_domain?String(row.registeredDomain??row.registered_domain):null,provider:String(row.provider??"cloudflare_registrar"),status:String(row.status??"draft"),registrantOwnerType:String(row.registrantOwnerType??row.registrant_owner_type??"customer"),priceAmount:row.priceAmount??row.price_amount?String(row.priceAmount??row.price_amount):null,currency:row.currency?String(row.currency):null,autoRenew:Boolean(row.autoRenew??row.auto_renew??!0),termsVersion:row.termsVersion??row.terms_version?String(row.termsVersion??row.terms_version):null,termsAcceptedAt:row.termsAcceptedAt??row.terms_accepted_at?String(row.termsAcceptedAt??row.terms_accepted_at):null,failureReason:row.failureReason??row.failure_reason?String(row.failureReason??row.failure_reason):null});var init_DomainRegistrationService=__esm(()=>{init_Domain_helpers()});import{and as and3,eq as eq8}from"drizzle-orm";class DomainService{getDb;getRegistry;logger;config;provider;constructor(cfg){this.getDb=cfg.getDb,this.getRegistry=cfg.getTenantRegistry,this.logger=cfg.logger.scoped("domains.service"),this.config=cfg.config,this.provider=cfg.provider}table(name){let registry=this.getRegistry();if(!registry)return null;return registry.getMainContext().schemaTables[name]??null}toProviderInput(record){return{hostname:record.hostname,normalizedHostname:record.normalizedHostname,hostnameKind:record.hostnameKind}}isReserved(normalized){if(this.config.platformBaseDomain&&normalized===this.config.platformBaseDomain)return!0;return this.config.reservedHostnames.includes(normalized)}async createHostname(input){let db=this.getDb(),table=this.table("domainHostnames");if(!db||!table)throw Error("Domain service not ready");let normalized=normalizeHostname(input.hostname);if(!isValidHostname(normalized))throw Error(`Invalid hostname: ${input.hostname}`);if(this.isReserved(normalized))throw Error(`Hostname is reserved: ${normalized}`);let kind=classifyHostname(normalized,this.config.platformBaseDomain);if(kind==="apex"&&!this.config.allowApexDomains)throw Error("Apex domains are not allowed");if(kind==="www"&&!this.config.allowWwwDomains)throw Error("www domains are not allowed");if((await db.select().from(table).where(eq8(col3(table,"normalizedHostname"),normalized)).limit(1))[0])throw Error(`Hostname already registered: ${normalized}`);let providerInput={hostname:input.hostname,normalizedHostname:normalized,hostnameKind:kind},provisioned=await this.provider.createCustomHostname(providerInput),rows=await db.insert(table).values({ownerType:input.ownerType??"tenant",ownerId:input.ownerId,tenantId:input.tenantId??null,schemaName:input.schemaName??null,hostname:input.hostname,normalizedHostname:normalized,hostnameKind:kind,status:"pending_verification",routingStatus:provisioned.routingStatus,certificateStatus:provisioned.certificateStatus,verificationStatus:provisioned.verificationStatus,provider:this.provider.name,providerHostnameId:provisioned.providerHostnameId,dnsTarget:provisioned.dnsTarget,isPrimary:input.isPrimary??!1,metadata:JSON.stringify(input.metadata??{})}).returning(),record=rowToDomainHostname(rows[0]);return await this.persistChallenges(record.id,provisioned.challenges),await this.recordEvent({domainHostnameId:record.id,eventType:"hostname_requested",message:`Custom hostname requested: ${normalized}`,metadata:{provider:this.provider.name,kind}}),{hostname:record,instructions:provisioned.instructions}}async persistChallenges(hostnameId,challenges){let db=this.getDb(),table=this.table("domainVerificationChallenges");if(!db||!table||challenges.length===0)return;let expiresAt=new Date(Date.now()+this.config.verification.challengeTtlSeconds*1000);for(let c of challenges)await db.insert(table).values({domainHostnameId:hostnameId,challengeType:c.challengeType,recordType:c.recordType,recordName:c.recordName,recordValue:c.recordValue,expectedTarget:c.expectedTarget??null,status:"pending",expiresAt})}async listHostnames(filter){let db=this.getDb(),table=this.table("domainHostnames");if(!db||!table)return[];let where=filter.tenantId?eq8(col3(table,"tenantId"),filter.tenantId):filter.ownerId?eq8(col3(table,"ownerId"),filter.ownerId):void 0;return(where?await db.select().from(table).where(where):await db.select().from(table)).map((r)=>rowToDomainHostname(r))}async getHostname(id){let db=this.getDb(),table=this.table("domainHostnames");if(!db||!table)return null;let rows=await db.select().from(table).where(eq8(col3(table,"id"),id)).limit(1);return rows[0]?rowToDomainHostname(rows[0]):null}async getInstructions(id){let record=await this.getHostname(id);if(!record)return[];let db=this.getDb(),table=this.table("domainVerificationChallenges"),instructions=[];if(record.dnsTarget)instructions.push({recordType:record.hostnameKind==="apex"?"A":"CNAME",name:record.normalizedHostname,value:record.dnsTarget,note:record.hostnameKind==="apex"?"Apex domains require an A/ALIAS record at your DNS provider":"Point this hostname at the platform routing target"});if(db&&table){let rows=await db.select().from(table).where(eq8(col3(table,"domainHostnameId"),id));for(let r of rows)instructions.push({recordType:String(r.recordType??"TXT"),name:String(r.recordName??""),value:String(r.recordValue??""),note:String(r.challengeType??"")==="certificate_validation"?"SSL validation":"Ownership verification"})}return instructions}async loadPendingChallenge(hostnameId){let db=this.getDb(),table=this.table("domainVerificationChallenges");if(!db||!table)return null;let row=(await db.select().from(table).where(and3(eq8(col3(table,"domainHostnameId"),hostnameId),eq8(col3(table,"status"),"pending"))).limit(1))[0];if(!row)return null;return{challengeType:String(row.challengeType??"ownership_validation"),recordType:String(row.recordType??"TXT"),recordName:String(row.recordName??""),recordValue:String(row.recordValue??""),expectedTarget:row.expectedTarget!=null?String(row.expectedTarget):void 0}}async verifyHostname(id){let record=await this.getHostname(id);if(!record)return null;let providerInput=this.toProviderInput(record),routingStatus=record.routingStatus,certificateStatus=record.certificateStatus,verificationStatus=record.verificationStatus,failureReason=null;if(this.provider.name==="manual_dns"){let challenge=await this.loadPendingChallenge(id);if(challenge){let result=await this.provider.verifyOwnership(providerInput,challenge);if(verificationStatus=result.verified?"verified":"failed",failureReason=result.verified?null:result.reason??"Verification failed",result.verified)await this.markChallengesVerified(id)}let status2=await this.provider.getCustomHostnameStatus(record.providerHostnameId,providerInput);routingStatus=status2.routingStatus,certificateStatus=status2.certificateStatus}else{let status2=await this.provider.getCustomHostnameStatus(record.providerHostnameId,providerInput);if(routingStatus=status2.routingStatus,certificateStatus=status2.certificateStatus,verificationStatus=status2.verificationStatus,failureReason=status2.failureReason??null,verificationStatus==="verified")await this.markChallengesVerified(id)}let ready=verificationStatus==="verified"&&routingStatus==="active"&&(certificateStatus==="active"||certificateStatus==="not_required"),status=verificationStatus==="failed"?"failed":ready?"active":"verifying";if(await this.applyStatus(id,{status,routingStatus,certificateStatus,verificationStatus,failureReason,activate:ready}),ready)await this.syncRegistry(id);return await this.recordEvent({domainHostnameId:id,eventType:ready?"verification_succeeded":"verification_attempted",message:failureReason??`verification=${verificationStatus} routing=${routingStatus} ssl=${certificateStatus}`}),this.getHostname(id)}async activateHostname(id){let record=await this.getHostname(id);if(!record)return null;if(record.verificationStatus!=="verified")throw Error("Cannot activate a hostname before ownership is verified");return await this.applyStatus(id,{status:"active",routingStatus:"active",certificateStatus:record.certificateStatus==="failed"?"pending":record.certificateStatus,verificationStatus:"verified",failureReason:null,activate:!0}),await this.syncRegistry(id),await this.recordEvent({domainHostnameId:id,eventType:"routing_active",message:"Hostname activated"}),this.getHostname(id)}async disableHostname(id){let db=this.getDb(),table=this.table("domainHostnames"),record=await this.getHostname(id);if(!db||!table||!record)return null;return await this.provider.deleteCustomHostname(record.providerHostnameId),await db.update(table).set({status:"disabled",routingStatus:"inactive",disabledAt:new Date,updatedAt:new Date}).where(eq8(col3(table,"id"),id)),this.getRegistry()?.unregisterDomainHostname(record.normalizedHostname),await this.recordEvent({domainHostnameId:id,eventType:"hostname_disabled",message:"Hostname disabled"}),this.getHostname(id)}async makePrimary(id){let db=this.getDb(),table=this.table("domainHostnames"),record=await this.getHostname(id);if(!db||!table||!record)return null;let scope=record.tenantId?eq8(col3(table,"tenantId"),record.tenantId):eq8(col3(table,"ownerId"),record.ownerId);return await db.update(table).set({isPrimary:!1,updatedAt:new Date}).where(scope),await db.update(table).set({isPrimary:!0,updatedAt:new Date}).where(eq8(col3(table,"id"),id)),await this.recordEvent({domainHostnameId:id,eventType:"primary_changed",message:"Primary hostname set"}),this.getHostname(id)}async refreshStatus(id){return this.verifyHostname(id)}async resolveHostname(hostname){let db=this.getDb(),table=this.table("domainHostnames");if(!db||!table)return null;let normalized=normalizeHostname(hostname),row=(await db.select().from(table).where(and3(eq8(col3(table,"normalizedHostname"),normalized),eq8(col3(table,"status"),"active"))).limit(1))[0];if(!row)return null;let record=rowToDomainHostname(row),redirectToHostname=null;if(record.redirectToHostnameId)redirectToHostname=(await this.getHostname(record.redirectToHostnameId))?.normalizedHostname??null;return{hostnameId:record.id,tenantId:record.tenantId,schemaName:record.schemaName,isPrimary:record.isPrimary,redirectToHostname}}async recordEvent(input){let db=this.getDb(),table=this.table("domainEvents");if(!db||!table)return;try{await db.insert(table).values({domainHostnameId:input.domainHostnameId??null,domainRegistrationId:input.domainRegistrationId??null,eventType:input.eventType,actorType:input.actorType??null,actorId:input.actorId??null,message:input.message??null,ipAddress:input.ipAddress??null,metadata:JSON.stringify(input.metadata??{})})}catch(err){this.logger.warn("Failed to record domain event",{eventType:input.eventType,error:err instanceof Error?err.message:String(err)})}}async markChallengesVerified(hostnameId){let db=this.getDb(),table=this.table("domainVerificationChallenges");if(!db||!table)return;await db.update(table).set({status:"verified",verifiedAt:new Date,updatedAt:new Date}).where(eq8(col3(table,"domainHostnameId"),hostnameId))}async applyStatus(id,patch){let db=this.getDb(),table=this.table("domainHostnames");if(!db||!table)return;await db.update(table).set({status:patch.status,routingStatus:patch.routingStatus,certificateStatus:patch.certificateStatus,verificationStatus:patch.verificationStatus,failureReason:patch.failureReason,activatedAt:patch.activate?new Date:void 0,updatedAt:new Date}).where(eq8(col3(table,"id"),id))}async syncRegistry(id){let record=await this.getHostname(id),registry=this.getRegistry();if(!record||!registry)return;if(record.status==="active"&&record.tenantId)registry.registerDomainHostname(record.normalizedHostname,record.tenantId)}}var col3=(table,name)=>table[name];var init_DomainService=__esm(()=>{init_Domain_helpers()});class CloudflareForSaasProvider{name="cloudflare_for_saas";config;logger;zoneId;apiToken;constructor(config,logger2){if(!config.cloudflare)throw Error("[Domain] Cloudflare for SaaS provider requires domains.cloudflare config");this.config=config,this.logger=logger2.scoped("domains.cloudflare"),this.zoneId=config.cloudflare.zoneId,this.apiToken=config.cloudflare.apiToken}get cnameTarget(){return this.config.cloudflare?.cnameTarget??this.config.cnameTarget}async call(path2,init){let res=await fetch(`https://api.cloudflare.com/client/v4${path2}`,{...init,headers:{Authorization:`Bearer ${this.apiToken}`,"Content-Type":"application/json",...init.headers??{}}}),json=await res.json().catch(()=>({}));if(!res.ok||json.success===!1){let msg=json.errors?.map((e)=>e.message).join("; ")||`HTTP ${res.status}`;throw Error(`Cloudflare API error: ${msg}`)}return json}mapRouting(status){if(status==="active")return"active";if(status==="blocked"||status==="moved"||status==="deleted")return"failed";return"pending"}mapCertificate(sslStatus){if(sslStatus==="active")return"active";if(sslStatus==="validation_timed_out"||sslStatus==="deactivating"||sslStatus==="deleted")return"failed";return"pending"}mapVerification(result){if(result.status==="active")return"verified";if((result.ssl?.validation_errors?.length??0)>0)return"failed";return"pending"}buildChallenges(result){let challenges=[],ov=result.ownership_verification;if(ov?.name&&ov.value)challenges.push({challengeType:"hostname_validation",recordType:"TXT",recordName:ov.name,recordValue:ov.value});for(let vr of result.ssl?.validation_records??[])if(vr.txt_name&&vr.txt_value)challenges.push({challengeType:"certificate_validation",recordType:"TXT",recordName:vr.txt_name,recordValue:vr.txt_value});return challenges}buildInstructions(input,challenges){let target=this.cnameTarget,instructions=[];if(target)instructions.push({recordType:input.hostnameKind==="apex"?"A":"CNAME",name:input.normalizedHostname,value:target,note:input.hostnameKind==="apex"?"Apex domains require an A/ALIAS record pointing at the SaaS target":"Point this hostname at the Cloudflare for SaaS target"});for(let c of challenges)instructions.push({recordType:c.recordType,name:c.recordName,value:c.recordValue,note:c.challengeType==="certificate_validation"?"SSL validation":"Hostname validation"});return instructions}toResult(result,input){let challenges=this.buildChallenges(result);return{providerHostnameId:result.id??null,dnsTarget:this.cnameTarget,routingStatus:this.mapRouting(result.status),certificateStatus:this.mapCertificate(result.ssl?.status),verificationStatus:this.mapVerification(result),instructions:this.buildInstructions(input,challenges),challenges,raw:result}}async createCustomHostname(input){let body=JSON.stringify({hostname:input.normalizedHostname,ssl:{method:"txt",type:"dv",settings:{min_tls_version:"1.2"}}}),json=await this.call(`/zones/${this.zoneId}/custom_hostnames`,{method:"POST",body});return this.toResult(json.result??{},input)}async getCustomHostnameStatus(providerHostnameId,input){let result=await this.fetchHostname(providerHostnameId,input.normalizedHostname);if(!result)return{routingStatus:"failed",certificateStatus:"failed",verificationStatus:"failed",failureReason:"Custom hostname not found at provider"};return{routingStatus:this.mapRouting(result.status),certificateStatus:this.mapCertificate(result.ssl?.status),verificationStatus:this.mapVerification(result),providerStatus:`${result.status??"unknown"}/${result.ssl?.status??"unknown"}`,failureReason:result.ssl?.validation_errors?.map((e)=>e.message).join("; ")||void 0,raw:result}}async fetchHostname(providerHostnameId,normalizedHostname){if(providerHostnameId)return(await this.call(`/zones/${this.zoneId}/custom_hostnames/${providerHostnameId}`,{method:"GET"})).result??null;let list=(await this.call(`/zones/${this.zoneId}/custom_hostnames?hostname=${encodeURIComponent(normalizedHostname)}`,{method:"GET"})).result;return Array.isArray(list)?list[0]??null:null}async deleteCustomHostname(providerHostnameId){if(!providerHostnameId)return;try{await this.call(`/zones/${this.zoneId}/custom_hostnames/${providerHostnameId}`,{method:"DELETE"})}catch(err){this.logger.warn("Failed to delete Cloudflare custom hostname",{providerHostnameId,error:err instanceof Error?err.message:String(err)})}}async verifyOwnership(input,_challenge){let status=await this.getCustomHostnameStatus(null,input);return{verified:status.verificationStatus==="verified",reason:status.failureReason??status.providerStatus}}}class ManualDnsProvider{name="manual_dns";config;constructor(config){this.config=config}buildChallenge(input){let prefix=this.config.verification.txtRecordPrefix,token=`nucleus-domain-verification=${generateChallengeToken()}`;return{challengeType:"ownership_validation",recordType:"TXT",recordName:`${prefix}.${input.normalizedHostname}`,recordValue:token}}buildInstructions(input,challenge){let target=this.config.cnameTarget,instructions=[{recordType:"TXT",name:challenge.recordName,value:challenge.recordValue,note:"Ownership verification record"}];if(input.hostnameKind==="apex")instructions.push({recordType:"A",name:input.normalizedHostname,value:target??"<platform-ingress-ip>",note:"Apex domains require an A/ALIAS record at your DNS provider"});else if(target)instructions.push({recordType:"CNAME",name:input.normalizedHostname,value:target,note:"Point this hostname at the platform routing target"});return instructions}async createCustomHostname(input){let challenge=this.buildChallenge(input);return{providerHostnameId:null,dnsTarget:this.config.cnameTarget,routingStatus:"pending",certificateStatus:"not_required",verificationStatus:"pending",instructions:this.buildInstructions(input,challenge),challenges:[challenge]}}async getCustomHostnameStatus(_providerHostnameId,input){if(input.hostnameKind!=="apex"&&this.config.cnameTarget){let dns2=await verifyCnameRecord(input.normalizedHostname,this.config.cnameTarget);return{routingStatus:dns2.verified?"active":"pending",certificateStatus:"not_required",verificationStatus:"pending",providerStatus:dns2.verified?"cname_active":"cname_pending"}}return{routingStatus:"pending",certificateStatus:"not_required",verificationStatus:"pending",providerStatus:"manual"}}async deleteCustomHostname(_providerHostnameId){}async verifyOwnership(_input,challenge){if(challenge.recordType==="CNAME"&&challenge.expectedTarget)return verifyCnameRecord(challenge.recordName,challenge.expectedTarget);return verifyTxtRecord(challenge.recordName,challenge.recordValue)}}var init_ManualDnsProvider=__esm(()=>{init_Domain_helpers()});var createDomainProvider=(config,logger2)=>{if(config.provider==="cloudflare_for_saas"){if(!config.cloudflare)return logger2.warn("[Domain] cloudflare_for_saas selected but cloudflare config missing \u2014 using manual DNS"),new ManualDnsProvider(config);return new CloudflareForSaasProvider(config,logger2)}return new ManualDnsProvider(config)};var init_Providers=__esm(()=>{init_ManualDnsProvider();init_ManualDnsProvider()});var createDomainServices=(params)=>{let config=resolveDomainsConfig(params.options.domains);if(!config.enabled)return null;let provider=createDomainProvider(config,params.logger),serviceConfig={getDb:params.getDb,getTenantRegistry:params.getTenantRegistry,logger:params.logger,config,provider};return{domainService:new DomainService(serviceConfig),registrationService:new DomainRegistrationService(serviceConfig)}};var init_Domain=__esm(()=>{init_Domain_helpers();init_DomainRegistrationService();init_DomainService();init_Providers();init_Domain_helpers();init_DomainRegistrationService();init_DomainService();init_Providers()});class AzureEmailService{client=null;senderAddress;fromName;logger;initialized=!1;credentialsFingerprint="";constructor(config,logger2){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}}}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 init_Email=()=>{};import fs2 from"fs";import{google}from"googleapis";class GmailService{gmail=null;fromEmail;fromName;logger;initialized=!1;credentialsFingerprint="";constructor(config,logger2){if(this.fromEmail=config.fromEmail,this.fromName=config.fromName||"Vorion",this.logger=logger2,config.enabled)this.initialize(config)}reconfigure(config){if(`${config.serviceAccountJson??""}|${config.jsonFilePath??""}|${config.fromEmail}`===this.credentialsFingerprint&&this.initialized)return;if(this.fromEmail=config.fromEmail||this.fromEmail,config.fromName)this.fromName=config.fromName;if(this.initialized=!1,this.gmail=null,config.enabled)this.initialize(config)}sanitizeHeaderValue(value){return value.replace(/[\r\n\u0000-\u001f\u007f]+/g," ").trim()}encodeHeaderValue(rawValue){let value=this.sanitizeHeaderValue(rawValue),hasNonAscii=!1;for(let i=0;i<value.length;i++)if(value.charCodeAt(i)>127){hasNonAscii=!0;break}if(!hasNonAscii)return value;return`=?UTF-8?B?${Buffer.from(value,"utf-8").toString("base64")}?=`}isAvailable(){return this.initialized&&this.gmail!==null}initialize(config){let{serviceAccountJson,jsonFilePath}=config;this.credentialsFingerprint=`${serviceAccountJson??""}|${jsonFilePath??""}|${config.fromEmail}`;try{let keyContent;if(serviceAccountJson?.trim())keyContent=serviceAccountJson;else if(jsonFilePath){if(!fs2.existsSync(jsonFilePath)){this.logger.error("[GmailService] JSON file not found",{path:jsonFilePath});return}keyContent=fs2.readFileSync(jsonFilePath,"utf8")}else{this.logger.warn("[GmailService] No service-account JSON or file path provided");return}let credentials;try{credentials=JSON.parse(keyContent)}catch{this.logger.error("[GmailService] Service-account JSON is not valid JSON",{source:serviceAccountJson?.trim()?"inline":"file"});return}if(!this.fromEmail){this.logger.warn("[GmailService] From email not configured");return}if(!credentials?.client_email||!credentials?.private_key){this.logger.error("[GmailService] Invalid credentials format");return}let privateKey=credentials.private_key;if(!privateKey.includes(`
|
|
34
34
|
`)&&privateKey.includes("\\n"))privateKey=privateKey.replace(/\\n/g,`
|
|
35
|
-
`);let auth=new google.auth.JWT({email:credentials.client_email,key:privateKey,scopes:["https://www.googleapis.com/auth/gmail.send"],subject:this.fromEmail});this.gmail=google.gmail({version:"v1",auth}),this.initialized=!0,this.logger.info("[GmailService] Initialized",{serviceAccount:credentials.client_email,fromEmail:this.fromEmail})}catch(error){this.logger.error("[GmailService] Initialization failed",{error})}}createEmailMessage(payload){let{to,subject,html,text,from,replyTo,attachments}=payload,toAddresses=Array.isArray(to)?to.join(", "):to,fromAddress=from||`${this.fromName} <${this.fromEmail}
|
|
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: ${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
|
|
35
|
+
`);let auth=new google.auth.JWT({email:credentials.client_email,key:privateKey,scopes:["https://www.googleapis.com/auth/gmail.send"],subject:this.fromEmail});this.gmail=google.gmail({version:"v1",auth}),this.initialized=!0,this.logger.info("[GmailService] Initialized",{serviceAccount:credentials.client_email,fromEmail:this.fromEmail})}catch(error){this.logger.error("[GmailService] Initialization failed",{error})}}createEmailMessage(payload){let{to,subject,html,text,from,replyTo,attachments}=payload,toAddresses=this.sanitizeHeaderValue(Array.isArray(to)?to.map((address)=>this.sanitizeHeaderValue(address)).join(", "):to),fromAddress=this.sanitizeHeaderValue(from||`${this.fromName} <${this.fromEmail}>`),encodedSubject=this.encodeHeaderValue(subject);if(!attachments||attachments.length===0){let messageParts2=[`From: ${fromAddress}`,`To: ${toAddresses}`,`Subject: ${encodedSubject}`,"MIME-Version: 1.0","Content-Type: text/html; charset=utf-8"];if(replyTo)messageParts2.push(`Reply-To: ${this.sanitizeHeaderValue(replyTo)}`);return messageParts2.push("",html||text||""),Buffer.from(messageParts2.join(`
|
|
36
|
+
`)).toString("base64").replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}let boundary=`----Nucleus_${Date.now()}_${Math.random().toString(36).substring(7)}`,messageParts=[`From: ${fromAddress}`,`To: ${toAddresses}`,`Subject: ${encodedSubject}`,"MIME-Version: 1.0",`Content-Type: multipart/mixed; boundary="${boundary}"`];if(replyTo)messageParts.push(`Reply-To: ${this.sanitizeHeaderValue(replyTo)}`);messageParts.push("",`--${boundary}`),messageParts.push("Content-Type: text/html; charset=utf-8","Content-Transfer-Encoding: base64","",Buffer.from(html||text||"").toString("base64"),"");for(let attachment of attachments)messageParts.push(`--${boundary}`,`Content-Type: ${attachment.contentType}`,`Content-Disposition: attachment; filename="${attachment.filename}"`,"Content-Transfer-Encoding: base64","",attachment.content.toString("base64"),"");return messageParts.push(`--${boundary}--`),Buffer.from(messageParts.join(`\r
|
|
37
37
|
`)).toString("base64").replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}async sendEmail(payload){if(!this.gmail)return this.logger.error("[GmailService] Not initialized"),{success:!1,error:"Gmail service not initialized"};try{let encodedMessage=this.createEmailMessage(payload);return await this.gmail.users.messages.send({userId:"me",requestBody:{raw:encodedMessage}}),this.logger.info("[GmailService] Email sent",{to:payload.to,subject:payload.subject}),{success:!0}}catch(error){let message=error instanceof Error?error.message:"Unknown error";return this.logger.error("[GmailService] Send failed",{error:message,to:payload.to}),{success:!1,error:message}}}emailWrapper(content,appName){return`<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><meta name="color-scheme" content="light dark"><meta name="supported-color-schemes" content="light dark"><title>${appName}</title><style type="text/css">:root{color-scheme:light dark;supported-color-schemes:light dark;}@media(prefers-color-scheme:dark){.em-bg{background-color:#18181b!important;}.em-card{background-color:#27272a!important;border-color:#3f3f46!important;}.em-h{color:#fafafa!important;}.em-t{color:#d4d4d8!important;}.em-m{color:#a1a1aa!important;}.em-hr{background-color:#3f3f46!important;}.em-btn{background-color:#fafafa!important;}.em-btn-text{color:#18181b!important;}.em-link{color:#60a5fa!important;}.em-lb{background-color:#3f3f46!important;border-color:#52525b!important;}.em-lb-t{color:#a1a1aa!important;}.em-sn{background-color:#3f3f46!important;border-color:#52525b!important;color:#a1a1aa!important;}}</style></head><body style="margin:0;padding:0;background-color:#f4f4f5;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;-webkit-font-smoothing:antialiased;"><table role="presentation" class="em-bg" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f4f4f5;"><tr><td align="center" style="padding:40px 16px;"><table role="presentation" width="480" cellpadding="0" cellspacing="0" style="max-width:480px;width:100%;"><tr><td align="center" style="padding-bottom:24px;"><span class="em-m" style="font-size:14px;font-weight:600;color:#71717a;letter-spacing:-0.2px;">${appName}</span></td></tr><tr><td class="em-card" style="background-color:#ffffff;border:1px solid #e4e4e7;border-radius:12px;overflow:hidden;">${content}</td></tr><tr><td align="center" style="padding-top:24px;"><p class="em-m" style="margin:0;font-size:12px;color:#a1a1aa;line-height:1.6;">This email was sent by <strong>${appName}</strong>.<br>Please do not reply to this email.</p></td></tr></table></td></tr></table></body></html>`}emailButton(href,label){return`<table role="presentation" cellpadding="0" cellspacing="0" width="100%"><tr><td align="center" style="padding:28px 32px;"><table role="presentation" cellpadding="0" cellspacing="0"><tr><td class="em-btn" align="center" style="background-color:#18181b;border-radius:8px;"><a class="em-btn-text" href="${href}" target="_blank" style="display:inline-block;padding:12px 32px;font-size:14px;font-weight:600;color:#fafafa;text-decoration:none;letter-spacing:-0.1px;">${label}</a></td></tr></table></td></tr></table>`}emailDivider(){return'<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:0 32px;"><div class="em-hr" style="height:1px;background-color:#e4e4e7;"></div></td></tr></table>'}emailLinkFallback(link,notice){let html=`<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:4px 32px 32px 32px;"><p class="em-m" style="margin:0 0 8px 0;font-size:11px;font-weight:600;color:#a1a1aa;text-transform:uppercase;letter-spacing:0.5px;">Or copy this link</p><table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td class="em-lb" style="background-color:#f4f4f5;border:1px solid #e4e4e7;border-radius:8px;padding:12px 14px;"><a class="em-link" href="${link}" style="font-size:12px;color:#2563eb;word-break:break-all;line-height:1.5;text-decoration:none;">${link}</a></td></tr></table>`;if(notice)html+=`<p class="em-m" style="margin:16px 0 0 0;font-size:12px;color:#a1a1aa;">${notice}</p>`;return html+="</td></tr></table>",html}async sendWelcomeEmail(email,name,appName="Vorion"){let content=`<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:40px 32px 24px 32px;text-align:center;"><h1 class="em-h" style="margin:0;font-size:24px;font-weight:700;color:#18181b;letter-spacing:-0.5px;">Welcome to ${appName}</h1><p class="em-t" style="margin:8px 0 0 0;font-size:15px;color:#52525b;line-height:1.6;">Your account has been created successfully</p></td></tr></table>${this.emailDivider()}<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:24px 32px 32px 32px;"><p class="em-t" style="margin:0 0 16px 0;font-size:15px;color:#3f3f46;line-height:1.7;">Hello <strong class="em-h" style="color:#18181b;">${name}</strong>,</p><p class="em-t" style="margin:0;font-size:15px;color:#3f3f46;line-height:1.7;">Welcome aboard! You now have full access to <strong>${appName}</strong>. We're excited to have you.</p></td></tr></table>`;return this.sendEmail({to:email,subject:`Welcome to ${appName}`,html:this.emailWrapper(content,appName)})}async sendVerificationEmail(email,name,verificationLink,appName="Vorion"){let content=`<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:40px 32px 24px 32px;text-align:center;"><h1 class="em-h" style="margin:0;font-size:24px;font-weight:700;color:#18181b;letter-spacing:-0.5px;">Verify your email</h1><p class="em-t" style="margin:8px 0 0 0;font-size:15px;color:#52525b;line-height:1.6;">Confirm your address to get started</p></td></tr></table>${this.emailDivider()}<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:24px 32px 0 32px;"><p class="em-t" style="margin:0 0 4px 0;font-size:15px;color:#3f3f46;line-height:1.7;">Hello <strong class="em-h" style="color:#18181b;">${name}</strong>,</p><p class="em-t" style="margin:0;font-size:15px;color:#3f3f46;line-height:1.7;">Please verify your email address by clicking the button below.</p></td></tr><tr><td>${this.emailButton(verificationLink,"Verify Email Address")}</td></tr></table>${this.emailDivider()}${this.emailLinkFallback(verificationLink)}`;return this.sendEmail({to:email,subject:`Verify your email \u2014 ${appName}`,html:this.emailWrapper(content,appName)})}async sendPasswordResetEmail(email,name,resetLink,appName="Vorion"){let content=`<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:40px 32px 24px 32px;text-align:center;"><h1 class="em-h" style="margin:0;font-size:24px;font-weight:700;color:#18181b;letter-spacing:-0.5px;">Reset your password</h1><p class="em-t" style="margin:8px 0 0 0;font-size:15px;color:#52525b;line-height:1.6;">We received a request to reset your password</p></td></tr></table>${this.emailDivider()}<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:24px 32px 0 32px;"><p class="em-t" style="margin:0 0 4px 0;font-size:15px;color:#3f3f46;line-height:1.7;">Hello <strong class="em-h" style="color:#18181b;">${name}</strong>,</p><p class="em-t" style="margin:0;font-size:15px;color:#3f3f46;line-height:1.7;">Click the button below to choose a new password. If you didn't make this request, you can safely ignore this email.</p></td></tr><tr><td>${this.emailButton(resetLink,"Reset Password")}</td></tr></table>${this.emailDivider()}${this.emailLinkFallback(resetLink,"This link expires in 1 hour and can only be used once.")}`;return this.sendEmail({to:email,subject:`Reset your password \u2014 ${appName}`,html:this.emailWrapper(content,appName)})}async sendMagicLinkEmail(email,magicLink,appName="Vorion"){let content=`<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:40px 32px 24px 32px;text-align:center;"><h1 class="em-h" style="margin:0;font-size:24px;font-weight:700;color:#18181b;letter-spacing:-0.5px;">Sign in to ${appName}</h1><p class="em-t" style="margin:8px 0 0 0;font-size:15px;color:#52525b;line-height:1.6;">No password needed \u2014 just click to sign in</p></td></tr></table>${this.emailDivider()}<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:24px 32px 0 32px;"><p class="em-t" style="margin:0;font-size:15px;color:#3f3f46;line-height:1.7;">Click the button below to securely sign in to your <strong class="em-h" style="color:#18181b;">${appName}</strong> account.</p></td></tr><tr><td>${this.emailButton(magicLink,"Sign In")}</td></tr></table>${this.emailDivider()}${this.emailLinkFallback(magicLink,"This link expires in 15 minutes and can only be used once.")}`;return this.sendEmail({to:email,subject:`Sign in to ${appName}`,html:this.emailWrapper(content,appName)})}async sendAlertEmail(to,subject,html){return this.sendEmail({to,subject,html})}async sendInvitationEmail(email,inviteLink,appName="Vorion"){let content=`<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:40px 32px 24px 32px;text-align:center;"><h1 class="em-h" style="margin:0;font-size:24px;font-weight:700;color:#18181b;letter-spacing:-0.5px;">You're Invited</h1><p class="em-t" style="margin:8px 0 0 0;font-size:15px;color:#52525b;line-height:1.6;">Join <strong class="em-h" style="color:#18181b;">${appName}</strong> and start collaborating</p></td></tr></table>${this.emailDivider()}<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:0 32px 8px 32px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:8px 0;"><table role="presentation" cellpadding="0" cellspacing="0"><tr><td class="em-sn" style="width:28px;height:28px;background-color:#f4f4f5;border:1px solid #e4e4e7;border-radius:50%;text-align:center;line-height:28px;font-size:12px;font-weight:600;color:#71717a;">1</td><td class="em-t" style="padding-left:14px;font-size:14px;color:#52525b;">Click the button below</td></tr></table></td></tr><tr><td style="padding:8px 0;"><table role="presentation" cellpadding="0" cellspacing="0"><tr><td class="em-sn" style="width:28px;height:28px;background-color:#f4f4f5;border:1px solid #e4e4e7;border-radius:50%;text-align:center;line-height:28px;font-size:12px;font-weight:600;color:#71717a;">2</td><td class="em-t" style="padding-left:14px;font-size:14px;color:#52525b;">Choose a secure password</td></tr></table></td></tr><tr><td style="padding:8px 0;"><table role="presentation" cellpadding="0" cellspacing="0"><tr><td class="em-sn" style="width:28px;height:28px;background-color:#f4f4f5;border:1px solid #e4e4e7;border-radius:50%;text-align:center;line-height:28px;font-size:12px;font-weight:600;color:#71717a;">3</td><td class="em-t" style="padding-left:14px;font-size:14px;color:#52525b;">Start collaborating</td></tr></table></td></tr></table></td></tr></table>${this.emailButton(inviteLink,"Accept Invitation")}${this.emailDivider()}${this.emailLinkFallback(inviteLink,"This invitation expires in 7 days.")}`;return this.sendEmail({to:email,subject:`You're invited to join ${appName}`,html:this.emailWrapper(content,appName)})}}var init_Gmail=()=>{};class AlertService{logger;getEmailService;config;appId;state={lastAlertTime:{},activeAlerts:new Map};cooldownMs;constructor(deps){this.logger=deps.logger,this.getEmailService=deps.getEmailService,this.config=deps.config,this.appId=deps.appId,this.cooldownMs=this.parseTimeToMs(deps.config.alerts?.cooldown||"5m")}parseTimeToMs(time){let match=time.match(/^(\d+)(ms|s|m|h|d)$/);if(!match||!match[1]||!match[2])return 300000;let value=parseInt(match[1],10);switch(match[2]){case"ms":return value;case"s":return value*1000;case"m":return value*60*1000;case"h":return value*60*60*1000;case"d":return value*24*60*60*1000;default:return 300000}}async checkAndAlert(snapshot){if(!this.config.alerts?.enabled)return[];let alerts=[],thresholds=this.config.alerts.thresholds||{};if(snapshot.system){if(thresholds.cpuPercent&&snapshot.system.cpu.usage>thresholds.cpuPercent){let alert=this.createAlert("cpu_high",snapshot.system.cpu.usage,thresholds.cpuPercent,`CPU usage is ${snapshot.system.cpu.usage}% (threshold: ${thresholds.cpuPercent}%)`);if(alert)alerts.push(alert)}if(thresholds.memoryPercent&&snapshot.system.memory.usagePercent>thresholds.memoryPercent){let alert=this.createAlert("memory_high",snapshot.system.memory.usagePercent,thresholds.memoryPercent,`Memory usage is ${snapshot.system.memory.usagePercent}% (threshold: ${thresholds.memoryPercent}%)`);if(alert)alerts.push(alert)}if(thresholds.diskPercent&&snapshot.system.disk.usagePercent>thresholds.diskPercent){let alert=this.createAlert("disk_high",snapshot.system.disk.usagePercent,thresholds.diskPercent,`Disk usage is ${snapshot.system.disk.usagePercent}% (threshold: ${thresholds.diskPercent}%)`);if(alert)alerts.push(alert)}}if(snapshot.application){if(thresholds.errorRatePercent&&snapshot.application.errors.rate>thresholds.errorRatePercent){let alert=this.createAlert("error_rate_high",snapshot.application.errors.rate,thresholds.errorRatePercent,`Error rate is ${snapshot.application.errors.rate}% (threshold: ${thresholds.errorRatePercent}%)`);if(alert)alerts.push(alert)}if(thresholds.responseTimeMs&&snapshot.application.responseTime.p95>thresholds.responseTimeMs){let alert=this.createAlert("response_time_high",snapshot.application.responseTime.p95,thresholds.responseTimeMs,`P95 response time is ${snapshot.application.responseTime.p95}ms (threshold: ${thresholds.responseTimeMs}ms)`);if(alert)alerts.push(alert)}if(thresholds.rateLimitBlocksPerMinute&&snapshot.application.rateLimits.blockedPerMinute>thresholds.rateLimitBlocksPerMinute){let alert=this.createAlert("rate_limit_blocks_high",snapshot.application.rateLimits.blockedPerMinute,thresholds.rateLimitBlocksPerMinute,`Rate limit blocks: ${snapshot.application.rateLimits.blockedPerMinute}/min (threshold: ${thresholds.rateLimitBlocksPerMinute}/min)`);if(alert)alerts.push(alert)}}if(alerts.length>0)await this.sendAlerts(alerts);return alerts}createAlert(type,value,threshold,message){let now=Date.now(),lastAlert=this.state.lastAlertTime[type]||0;if(now-lastAlert<this.cooldownMs)return null;let severity=this.getSeverity(type,value,threshold),alert={id:`${type}-${now}`,type,severity,message,value,threshold,timestamp:now,acknowledged:!1};return this.state.lastAlertTime[type]=now,this.state.activeAlerts.set(type,alert),this.logger.warn(`[Alert] ${severity.toUpperCase()}: ${message}`),alert}getSeverity(_type,value,threshold){return value/threshold>1.5?"critical":"warning"}async sendAlerts(alerts){if(!this.config.alerts?.email?.enabled)return;let emailService=this.getEmailService?.();if(!emailService||!emailService.isAvailable())return;let recipients=this.config.alerts.email.recipients||[];if(recipients.length===0)return;let subject=alerts.filter((a)=>a.severity==="critical").length>0?`\uD83D\uDEA8 CRITICAL Alert - ${this.appId}`:`\u26A0\uFE0F Warning Alert - ${this.appId}`,html=this.buildEmailHtml(alerts);for(let recipient of recipients)try{await emailService.sendAlertEmail(recipient,subject,html),this.logger.info(`[Alert] Email sent to ${recipient}`)}catch(error){this.logger.error(`[Alert] Failed to send email to ${recipient}: ${error}`)}}buildEmailHtml(alerts){let alertRows=alerts.map((alert)=>`
|
|
38
38
|
<tr style="background-color: ${alert.severity==="critical"?"#fee2e2":"#fef3c7"};">
|
|
39
39
|
<td style="padding: 12px; border: 1px solid #e5e7eb;">${alert.severity.toUpperCase()}</td>
|
|
@@ -27,6 +27,24 @@ export declare class GmailService implements EmailService {
|
|
|
27
27
|
* effect on the very next email rather than at the next restart.
|
|
28
28
|
*/
|
|
29
29
|
reconfigure(config: GmailServiceConfig): void;
|
|
30
|
+
/**
|
|
31
|
+
* A header value that cannot become a header of its own.
|
|
32
|
+
*
|
|
33
|
+
* `createEmailMessage` writes these straight into raw MIME, and a header is
|
|
34
|
+
* terminated by a line break — so a value carrying CR or LF ends the header
|
|
35
|
+
* it was meant to be and starts whatever the text says next. Anyone able to
|
|
36
|
+
* set a subject could therefore add `Bcc:` and copy every portal e-mail to an
|
|
37
|
+
* address of their choosing, or `Reply-To:` and collect the replies.
|
|
38
|
+
*
|
|
39
|
+
* That is reachable from ordinary product surfaces: an admin types the
|
|
40
|
+
* subject of the news broadcast, and the portal signs and sends the result.
|
|
41
|
+
*
|
|
42
|
+
* RFC 5322 has no way to escape a line break inside a value — the only legal
|
|
43
|
+
* fold is CRLF followed by whitespace, which means the same thing as a space.
|
|
44
|
+
* So breaks and the other C0 controls are collapsed to spaces rather than
|
|
45
|
+
* encoded, and nothing about the visible subject changes.
|
|
46
|
+
*/
|
|
47
|
+
private sanitizeHeaderValue;
|
|
30
48
|
private encodeHeaderValue;
|
|
31
49
|
isAvailable(): boolean;
|
|
32
50
|
private initialize;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nucleus-core-ts",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.900",
|
|
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",
|