nucleus-core-ts 0.9.990 → 0.9.991
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/index.js +2 -2
- package/dist/src/ElysiaPlugin/runtime.d.ts +8 -0
- package/dist/src/Services/Email/AzureEmailService.d.ts +2 -0
- package/dist/src/Services/Email/SmtpEmailService.d.ts +2 -0
- package/dist/src/Services/Email/types.d.ts +28 -0
- package/package.json +1 -1
package/dist/.build-ok
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.9.
|
|
1
|
+
0.9.991
|
package/dist/index.js
CHANGED
|
@@ -30,7 +30,7 @@ and limitations under the License.
|
|
|
30
30
|
${textElements}
|
|
31
31
|
</g>
|
|
32
32
|
${overlayLines}
|
|
33
|
-
</svg>`}function generatePuzzleChallenge(difficulty){let pieceCount=DIFFICULTY_CONFIG[difficulty].puzzlePieces,pieces=[],correctOrder=[];for(let i=0;i<pieceCount;i++)pieces.push({id:i,x:i%3*100,y:Math.floor(i/3)*100}),correctOrder.push(i);let shuffledPieces=[...pieces];for(let i=shuffledPieces.length-1;i>0;i--){let j=getSecureRandomInt(0,i),itemI=shuffledPieces[i],itemJ=shuffledPieces[j];if(itemI&&itemJ)shuffledPieces[i]=itemJ,shuffledPieces[j]=itemI}return{question:"Arrange the pieces in correct order",answer:correctOrder.join(","),puzzleData:{pieces:shuffledPieces,correctOrder}}}class CaptchaService{redis;logger;config;constructor(deps){this.redis=deps.redis,this.logger=deps.logger,this.config={enabled:deps.config.enabled??!0,type:deps.config.type??"math",difficulty:deps.config.difficulty??"medium",expiresIn:deps.config.expiresIn??"5m",maxAttempts:deps.config.maxAttempts??3,caseSensitive:deps.config.caseSensitive??!1,rateLimit:{maxGeneratePerMinute:deps.config.rateLimit?.maxGeneratePerMinute??10,maxGeneratePerHour:deps.config.rateLimit?.maxGeneratePerHour??100}}}async generate(type,difficulty,ipAddress){if(ipAddress){if(await this.checkRateLimit(ipAddress))return this.logger.warn("[CAPTCHA] Rate limit exceeded",{ipAddress}),{success:!1,challengeId:"",type:type??this.config.type,question:"",expiresAt:0,rateLimited:!0,message:"Too many requests. Please try again later."};await this.incrementRateLimit(ipAddress)}let captchaType=type??this.config.type,captchaDifficulty=difficulty??this.config.difficulty,challengeId=generateSecureId(),expiresInSeconds=parseTimeToSeconds(this.config.expiresIn),expiresAt=Date.now()+expiresInSeconds*1000,question,answer,imageData,puzzleData;switch(captchaType){case"math":{let mathResult=generateMathChallenge(captchaDifficulty);question=mathResult.question,answer=mathResult.answer;break}case"text":{let textResult=generateTextChallenge(captchaDifficulty);question=textResult.question,answer=textResult.answer;break}case"image":{let imageResult=generateImageChallenge(captchaDifficulty);question=imageResult.question,answer=imageResult.answer,imageData=imageResult.imageData;break}case"puzzle":{let puzzleResult=generatePuzzleChallenge(captchaDifficulty);question=puzzleResult.question,answer=puzzleResult.answer,puzzleData=puzzleResult.puzzleData;break}default:{let defaultResult=generateMathChallenge(captchaDifficulty);question=defaultResult.question,answer=defaultResult.answer}}let normalizedAnswer=this.config.caseSensitive?answer:answer.toUpperCase(),answerHash=hashAnswer(normalizedAnswer),challenge={id:challengeId,type:captchaType,question,answer:answerHash,imageData,puzzleData,expiresAt,attempts:0,maxAttempts:this.config.maxAttempts,caseSensitive:this.config.caseSensitive};await this.redis.set(`${CAPTCHA_KEY_PREFIX}${challengeId}`,JSON.stringify(challenge),{ex:expiresInSeconds}),this.logger.info("[CAPTCHA] Challenge generated",{challengeId,type:captchaType,difficulty:captchaDifficulty,expiresAt});let result={success:!0,challengeId,type:captchaType,question,expiresAt};if(imageData)result.imageData=imageData;if(puzzleData)result.puzzleData={pieces:puzzleData.pieces};return result}async validate(challengeId,userAnswer){let key=`${CAPTCHA_KEY_PREFIX}${challengeId}`,stored=await this.redis.get(key);if(!stored)return this.logger.warn("[CAPTCHA] Challenge not found or expired",{challengeId}),{success:!1,valid:!1,message:"Challenge expired or not found"};let challenge=parseStoredJson(stored);if(!challenge||typeof challenge.expiresAt!=="number")return await this.redis.del(key),this.logger.warn("[CAPTCHA] Stored challenge unreadable, discarded",{challengeId,storedType:typeof stored}),{success:!1,valid:!1,message:"Challenge expired or not found"};if(Date.now()>challenge.expiresAt)return await this.redis.del(key),this.logger.warn("[CAPTCHA] Challenge expired",{challengeId}),{success:!1,valid:!1,message:"Challenge expired"};let ttl=Math.max(1,Math.floor((challenge.expiresAt-Date.now())/1000)),attemptsKey=`${key}:att`,attempts;if(this.redis.incrementCounter)attempts=await this.redis.incrementCounter(attemptsKey,1,ttl);else challenge.attempts+=1,attempts=challenge.attempts,await this.redis.set(key,JSON.stringify(challenge),{ex:ttl});let clearChallenge=async()=>{if(await this.redis.del(key),this.redis.incrementCounter)await this.redis.del(attemptsKey)};if(attempts>challenge.maxAttempts)return await clearChallenge(),this.logger.warn("[CAPTCHA] Max attempts exceeded",{challengeId,attempts}),{success:!1,valid:!1,message:"Maximum attempts exceeded"};let normalizedUserAnswer=challenge.caseSensitive?userAnswer.trim():userAnswer.trim().toUpperCase(),userAnswerHash=hashAnswer(normalizedUserAnswer);if(timingSafeEqual(challenge.answer,userAnswerHash))return await clearChallenge(),this.logger.info("[CAPTCHA] Challenge validated successfully",{challengeId}),{success:!0,valid:!0,message:"Captcha validated successfully"};let remainingAttempts=challenge.maxAttempts-attempts;if(remainingAttempts<=0)await clearChallenge();return this.logger.warn("[CAPTCHA] Invalid answer",{challengeId,attemptsRemaining:remainingAttempts}),{success:!0,valid:!1,message:"Incorrect answer",attemptsRemaining:remainingAttempts}}async invalidate(challengeId){await this.redis.del(`${CAPTCHA_KEY_PREFIX}${challengeId}`),this.logger.info("[CAPTCHA] Challenge invalidated",{challengeId})}isEnabled(){return this.config.enabled}async checkRateLimit(ipAddress){let minuteKey=`${RATE_LIMIT_KEY_PREFIX}${ipAddress}:minute`,hourKey=`${RATE_LIMIT_KEY_PREFIX}${ipAddress}:hour`,[minuteCount,hourCount]=await Promise.all([this.redis.get(minuteKey),this.redis.get(hourKey)]),minuteRequests=storedCount(minuteCount),hourRequests=storedCount(hourCount),maxPerMinute=this.config.rateLimit.maxGeneratePerMinute??10,maxPerHour=this.config.rateLimit.maxGeneratePerHour??100;return minuteRequests>=maxPerMinute||hourRequests>=maxPerHour}async incrementRateLimit(ipAddress){let minuteKey=`${RATE_LIMIT_KEY_PREFIX}${ipAddress}:minute`,hourKey=`${RATE_LIMIT_KEY_PREFIX}${ipAddress}:hour`,[minuteCount,hourCount]=await Promise.all([this.redis.get(minuteKey),this.redis.get(hourKey)]),newMinuteCount=storedCount(minuteCount)+1,newHourCount=storedCount(hourCount)+1;await Promise.all([this.redis.set(minuteKey,newMinuteCount.toString(),{ex:60}),this.redis.set(hourKey,newHourCount.toString(),{ex:3600})])}}var CAPTCHA_KEY_PREFIX="captcha:",RATE_LIMIT_KEY_PREFIX="captcha_rate:",storedCount=(stored)=>{let value=parseStoredJson(stored),parsed=typeof value==="number"?value:Number.parseInt(String(value??""),10);return Number.isFinite(parsed)?parsed:0},DIFFICULTY_CONFIG,CHARACTERS="ABCDEFGHJKLMNPQRSTUVWXYZ23456789";var init_Captcha=__esm(()=>{DIFFICULTY_CONFIG={easy:{mathRange:{min:1,max:10},textLength:4,puzzlePieces:4},medium:{mathRange:{min:10,max:50},textLength:6,puzzlePieces:6},hard:{mathRange:{min:50,max:100},textLength:8,puzzlePieces:9}}});import{promises as dns}from"dns";var DEFAULT_TXT_PREFIX="_nucleus-challenge",DEFAULT_CHALLENGE_TTL=604800,DEFAULT_BASE_PATH="/domains",resolveEnvValue=(value)=>{if(value==null)return;let fromEnv=process.env[value];return fromEnv!=null&&fromEnv!==""?fromEnv:value},normalizeHostname=(raw)=>{let trimmed=(raw||"").trim();if(!trimmed)return"";let normalized=((trimmed.replace(/^https?:\/\//i,"").split("/")[0]?.split("?")[0]||"").split(":")[0]||"").toLowerCase().replace(/\.$/,"");try{normalized=new URL(`http://${normalized}`).hostname.replace(/\.$/,"")}catch{}return normalized},isValidHostname=(hostname)=>{if(!hostname||hostname.length>253)return!1;let labels=hostname.split(".");if(labels.length<2)return!1;return labels.every((label)=>/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/.test(label))},classifyHostname=(normalized,platformBaseDomain)=>{if(platformBaseDomain){let base=platformBaseDomain.toLowerCase().replace(/\.$/,"");if(normalized===base)return"apex";if(normalized.endsWith(`.${base}`)){let prefix=normalized.slice(0,-(base.length+1));if(prefix&&!prefix.includes("."))return"platform_subdomain"}}let labels=normalized.split(".");if(labels[0]==="www")return"www";if(labels.length<=2)return"apex";return"subdomain"},generateChallengeToken=()=>{let bytes=new Uint8Array(24);return crypto.getRandomValues(bytes),Array.from(bytes,(b)=>b.toString(16).padStart(2,"0")).join("")},resolveDomainsConfig=(raw)=>{let provider=raw?.provider??"manual_dns",cf=raw?.cloudflare,cloudflare=cf&&resolveEnvValue(cf.accountId)&&resolveEnvValue(cf.zoneId)&&resolveEnvValue(cf.apiToken)?{accountId:resolveEnvValue(cf.accountId),zoneId:resolveEnvValue(cf.zoneId),apiToken:resolveEnvValue(cf.apiToken),fallbackOrigin:resolveEnvValue(cf.fallbackOrigin)??null,cnameTarget:resolveEnvValue(cf.cnameTarget)??null}:null;return{enabled:raw?.enabled===!0,basePath:raw?.basePath??DEFAULT_BASE_PATH,provider,platformBaseDomain:raw?.platformBaseDomain?normalizeHostname(raw.platformBaseDomain):null,cnameTarget:resolveEnvValue(raw?.cnameTarget)??cloudflare?.cnameTarget??null,allowApexDomains:raw?.allowApexDomains!==!1,allowWwwDomains:raw?.allowWwwDomains!==!1,allowManagedRegistrations:raw?.allowManagedRegistrations===!0,reservedHostnames:(raw?.reservedHostnames??[]).map((h)=>normalizeHostname(h)),verification:{defaultMethod:raw?.verification?.defaultMethod??"txt",txtRecordPrefix:raw?.verification?.txtRecordPrefix??DEFAULT_TXT_PREFIX,challengeTtlSeconds:raw?.verification?.challengeTtlSeconds??DEFAULT_CHALLENGE_TTL},cloudflare,registrar:raw?.registrar?{provider:raw.registrar.provider??"cloudflare_registrar",defaultRegistrantOwnerType:raw.registrar.defaultRegistrantOwnerType??"customer"}:null}},str=(v)=>v==null?"":String(v),strOrNull=(v)=>v==null?null:String(v),parseMetadata=(v)=>{if(!v)return{};let obj=typeof v==="string"?safeJson(v):v;if(!obj||typeof obj!=="object")return{};let out={};for(let[k,val]of Object.entries(obj))if(typeof val==="string"||typeof val==="number"||typeof val==="boolean")out[k]=val;return out},safeJson=(v)=>{try{return JSON.parse(v)}catch{return{}}},rowToDomainHostname=(row)=>({id:str(row.id),ownerType:str(row.ownerType??row.owner_type)||"tenant",ownerId:str(row.ownerId??row.owner_id),tenantId:strOrNull(row.tenantId??row.tenant_id),schemaName:strOrNull(row.schemaName??row.schema_name),hostname:str(row.hostname),normalizedHostname:str(row.normalizedHostname??row.normalized_hostname),hostnameKind:str(row.hostnameKind??row.hostname_kind)||"apex",status:str(row.status),routingStatus:str(row.routingStatus??row.routing_status),certificateStatus:str(row.certificateStatus??row.certificate_status),verificationStatus:str(row.verificationStatus??row.verification_status),provider:str(row.provider)||"manual_dns",providerHostnameId:strOrNull(row.providerHostnameId??row.provider_hostname_id),dnsTarget:strOrNull(row.dnsTarget??row.dns_target),isPrimary:Boolean(row.isPrimary??row.is_primary),redirectToHostnameId:strOrNull(row.redirectToHostnameId??row.redirect_to_hostname_id),activatedAt:strOrNull(row.activatedAt??row.activated_at),disabledAt:strOrNull(row.disabledAt??row.disabled_at),failureReason:strOrNull(row.failureReason??row.failure_reason),metadata:parseMetadata(row.metadata)}),verifyTxtRecord=async(recordName,expectedValue)=>{try{let flat=(await dns.resolveTxt(recordName)).map((chunks)=>chunks.join("")),verified=flat.some((v)=>v===expectedValue);return{verified,observed:flat,reason:verified?void 0:"TXT value not found"}}catch(err){return{verified:!1,reason:err instanceof Error?err.message:"DNS lookup failed"}}},verifyCnameRecord=async(recordName,expectedTarget)=>{let target=expectedTarget.toLowerCase().replace(/\.$/,"");try{let flat=(await dns.resolveCname(recordName)).map((v)=>v.toLowerCase().replace(/\.$/,"")),verified=flat.includes(target);return{verified,observed:flat,reason:verified?void 0:"CNAME target mismatch"}}catch(err){return{verified:!1,reason:err instanceof Error?err.message:"DNS lookup failed"}}};var init_Domain_helpers=()=>{};import{eq as eq7}from"drizzle-orm";class DomainRegistrationService{getDb;getRegistry;logger;config;constructor(cfg){this.getDb=cfg.getDb,this.getRegistry=cfg.getTenantRegistry,this.logger=cfg.logger.scoped("domains.registration"),this.config=cfg.config}get enabled(){return this.config.allowManagedRegistrations}table(name){let registry=this.getRegistry();if(!registry)return null;return registry.getMainContext().schemaTables[name]??null}async event(registrationId,eventType,message){let db=this.getDb(),table=this.table("domainEvents");if(!db||!table)return;try{await db.insert(table).values({domainRegistrationId:registrationId,eventType,message:message??null,metadata:JSON.stringify({})})}catch(err){this.logger.warn("Failed to record registration event",{eventType,error:err instanceof Error?err.message:String(err)})}}async createRequest(input){if(!this.enabled)throw Error("Managed domain registration is not enabled");if(!input.termsAccepted)throw Error("Registration requires accepting the domain terms");let db=this.getDb(),table=this.table("domainRegistrations");if(!db||!table)throw Error("Domain registration service not ready");let normalized=normalizeHostname(input.requestedDomain),rows=await db.insert(table).values({ownerType:input.ownerType??"tenant",ownerId:input.ownerId,tenantId:input.tenantId??null,requestedDomain:normalized,provider:this.config.registrar?.provider??"cloudflare_registrar",registrantOwnerType:input.registrantOwnerType??this.config.registrar?.defaultRegistrantOwnerType??"customer",status:"awaiting_payment",autoRenew:input.autoRenew??!0,termsVersion:input.termsVersion,termsAcceptedAt:new Date,metadata:JSON.stringify(input.metadata??{})}).returning(),record=rowToRegistration(rows[0]);return await this.event(record.id,"registration_requested",`Requested ${normalized}`),record}async listRegistrations(filter){let db=this.getDb(),table=this.table("domainRegistrations");if(!db||!table)return[];let where=filter.tenantId?eq7(col2(table,"tenantId"),filter.tenantId):filter.ownerId?eq7(col2(table,"ownerId"),filter.ownerId):void 0;return(where?await db.select().from(table).where(where):await db.select().from(table)).map((r)=>rowToRegistration(r))}async getRegistration(id){let db=this.getDb(),table=this.table("domainRegistrations");if(!db||!table)return null;let rows=await db.select().from(table).where(eq7(col2(table,"id"),id)).limit(1);return rows[0]?rowToRegistration(rows[0]):null}async transition(id,status,patch){let db=this.getDb(),table=this.table("domainRegistrations");if(!db||!table)return null;return await db.update(table).set({status,registeredDomain:patch?.registeredDomain,providerRegistrationId:patch?.providerRegistrationId,priceAmount:patch?.priceAmount,currency:patch?.currency,renewalDate:patch?.renewalDate,failureReason:patch?.failureReason??null,updatedAt:new Date}).where(eq7(col2(table,"id"),id)),await this.event(id,`registration_${status}`),this.getRegistration(id)}async cancel(id){return this.transition(id,"cancelled")}async requestTransferOut(id){return this.transition(id,"transfer_requested")}}var col2=(table,name)=>table[name],rowToRegistration=(row)=>({id:String(row.id??""),ownerType:String(row.ownerType??row.owner_type??"tenant"),ownerId:String(row.ownerId??row.owner_id??""),tenantId:row.tenantId??row.tenant_id?String(row.tenantId??row.tenant_id):null,requestedDomain:String(row.requestedDomain??row.requested_domain??""),registeredDomain:row.registeredDomain??row.registered_domain?String(row.registeredDomain??row.registered_domain):null,provider:String(row.provider??"cloudflare_registrar"),status:String(row.status??"draft"),registrantOwnerType:String(row.registrantOwnerType??row.registrant_owner_type??"customer"),priceAmount:row.priceAmount??row.price_amount?String(row.priceAmount??row.price_amount):null,currency:row.currency?String(row.currency):null,autoRenew:Boolean(row.autoRenew??row.auto_renew??!0),termsVersion:row.termsVersion??row.terms_version?String(row.termsVersion??row.terms_version):null,termsAcceptedAt:row.termsAcceptedAt??row.terms_accepted_at?String(row.termsAcceptedAt??row.terms_accepted_at):null,failureReason:row.failureReason??row.failure_reason?String(row.failureReason??row.failure_reason):null});var init_DomainRegistrationService=__esm(()=>{init_Domain_helpers()});import{and as and3,eq as eq8}from"drizzle-orm";class DomainService{getDb;getRegistry;logger;config;provider;constructor(cfg){this.getDb=cfg.getDb,this.getRegistry=cfg.getTenantRegistry,this.logger=cfg.logger.scoped("domains.service"),this.config=cfg.config,this.provider=cfg.provider}table(name){let registry=this.getRegistry();if(!registry)return null;return registry.getMainContext().schemaTables[name]??null}toProviderInput(record){return{hostname:record.hostname,normalizedHostname:record.normalizedHostname,hostnameKind:record.hostnameKind}}isReserved(normalized){if(this.config.platformBaseDomain&&normalized===this.config.platformBaseDomain)return!0;return this.config.reservedHostnames.includes(normalized)}async createHostname(input){let db=this.getDb(),table=this.table("domainHostnames");if(!db||!table)throw Error("Domain service not ready");let normalized=normalizeHostname(input.hostname);if(!isValidHostname(normalized))throw Error(`Invalid hostname: ${input.hostname}`);if(this.isReserved(normalized))throw Error(`Hostname is reserved: ${normalized}`);let kind=classifyHostname(normalized,this.config.platformBaseDomain);if(kind==="apex"&&!this.config.allowApexDomains)throw Error("Apex domains are not allowed");if(kind==="www"&&!this.config.allowWwwDomains)throw Error("www domains are not allowed");if((await db.select().from(table).where(eq8(col3(table,"normalizedHostname"),normalized)).limit(1))[0])throw Error(`Hostname already registered: ${normalized}`);let providerInput={hostname:input.hostname,normalizedHostname:normalized,hostnameKind:kind},provisioned=await this.provider.createCustomHostname(providerInput),rows=await db.insert(table).values({ownerType:input.ownerType??"tenant",ownerId:input.ownerId,tenantId:input.tenantId??null,schemaName:input.schemaName??null,hostname:input.hostname,normalizedHostname:normalized,hostnameKind:kind,status:"pending_verification",routingStatus:provisioned.routingStatus,certificateStatus:provisioned.certificateStatus,verificationStatus:provisioned.verificationStatus,provider:this.provider.name,providerHostnameId:provisioned.providerHostnameId,dnsTarget:provisioned.dnsTarget,isPrimary:input.isPrimary??!1,metadata:JSON.stringify(input.metadata??{})}).returning(),record=rowToDomainHostname(rows[0]);return await this.persistChallenges(record.id,provisioned.challenges),await this.recordEvent({domainHostnameId:record.id,eventType:"hostname_requested",message:`Custom hostname requested: ${normalized}`,metadata:{provider:this.provider.name,kind}}),{hostname:record,instructions:provisioned.instructions}}async persistChallenges(hostnameId,challenges){let db=this.getDb(),table=this.table("domainVerificationChallenges");if(!db||!table||challenges.length===0)return;let expiresAt=new Date(Date.now()+this.config.verification.challengeTtlSeconds*1000);for(let c of challenges)await db.insert(table).values({domainHostnameId:hostnameId,challengeType:c.challengeType,recordType:c.recordType,recordName:c.recordName,recordValue:c.recordValue,expectedTarget:c.expectedTarget??null,status:"pending",expiresAt})}async listHostnames(filter){let db=this.getDb(),table=this.table("domainHostnames");if(!db||!table)return[];let where=filter.tenantId?eq8(col3(table,"tenantId"),filter.tenantId):filter.ownerId?eq8(col3(table,"ownerId"),filter.ownerId):void 0;return(where?await db.select().from(table).where(where):await db.select().from(table)).map((r)=>rowToDomainHostname(r))}async getHostname(id){let db=this.getDb(),table=this.table("domainHostnames");if(!db||!table)return null;let rows=await db.select().from(table).where(eq8(col3(table,"id"),id)).limit(1);return rows[0]?rowToDomainHostname(rows[0]):null}async getInstructions(id){let record=await this.getHostname(id);if(!record)return[];let db=this.getDb(),table=this.table("domainVerificationChallenges"),instructions=[];if(record.dnsTarget)instructions.push({recordType:record.hostnameKind==="apex"?"A":"CNAME",name:record.normalizedHostname,value:record.dnsTarget,note:record.hostnameKind==="apex"?"Apex domains require an A/ALIAS record at your DNS provider":"Point this hostname at the platform routing target"});if(db&&table){let rows=await db.select().from(table).where(eq8(col3(table,"domainHostnameId"),id));for(let r of rows)instructions.push({recordType:String(r.recordType??"TXT"),name:String(r.recordName??""),value:String(r.recordValue??""),note:String(r.challengeType??"")==="certificate_validation"?"SSL validation":"Ownership verification"})}return instructions}async loadPendingChallenge(hostnameId){let db=this.getDb(),table=this.table("domainVerificationChallenges");if(!db||!table)return null;let row=(await db.select().from(table).where(and3(eq8(col3(table,"domainHostnameId"),hostnameId),eq8(col3(table,"status"),"pending"))).limit(1))[0];if(!row)return null;return{challengeType:String(row.challengeType??"ownership_validation"),recordType:String(row.recordType??"TXT"),recordName:String(row.recordName??""),recordValue:String(row.recordValue??""),expectedTarget:row.expectedTarget!=null?String(row.expectedTarget):void 0}}async verifyHostname(id){let record=await this.getHostname(id);if(!record)return null;let providerInput=this.toProviderInput(record),routingStatus=record.routingStatus,certificateStatus=record.certificateStatus,verificationStatus=record.verificationStatus,failureReason=null;if(this.provider.name==="manual_dns"){let challenge=await this.loadPendingChallenge(id);if(challenge){let result=await this.provider.verifyOwnership(providerInput,challenge);if(verificationStatus=result.verified?"verified":"failed",failureReason=result.verified?null:result.reason??"Verification failed",result.verified)await this.markChallengesVerified(id)}let status2=await this.provider.getCustomHostnameStatus(record.providerHostnameId,providerInput);routingStatus=status2.routingStatus,certificateStatus=status2.certificateStatus}else{let status2=await this.provider.getCustomHostnameStatus(record.providerHostnameId,providerInput);if(routingStatus=status2.routingStatus,certificateStatus=status2.certificateStatus,verificationStatus=status2.verificationStatus,failureReason=status2.failureReason??null,verificationStatus==="verified")await this.markChallengesVerified(id)}let ready=verificationStatus==="verified"&&routingStatus==="active"&&(certificateStatus==="active"||certificateStatus==="not_required"),status=verificationStatus==="failed"?"failed":ready?"active":"verifying";if(await this.applyStatus(id,{status,routingStatus,certificateStatus,verificationStatus,failureReason,activate:ready}),ready)await this.syncRegistry(id);return await this.recordEvent({domainHostnameId:id,eventType:ready?"verification_succeeded":"verification_attempted",message:failureReason??`verification=${verificationStatus} routing=${routingStatus} ssl=${certificateStatus}`}),this.getHostname(id)}async activateHostname(id){let record=await this.getHostname(id);if(!record)return null;if(record.verificationStatus!=="verified")throw Error("Cannot activate a hostname before ownership is verified");return await this.applyStatus(id,{status:"active",routingStatus:"active",certificateStatus:record.certificateStatus==="failed"?"pending":record.certificateStatus,verificationStatus:"verified",failureReason:null,activate:!0}),await this.syncRegistry(id),await this.recordEvent({domainHostnameId:id,eventType:"routing_active",message:"Hostname activated"}),this.getHostname(id)}async disableHostname(id){let db=this.getDb(),table=this.table("domainHostnames"),record=await this.getHostname(id);if(!db||!table||!record)return null;return await this.provider.deleteCustomHostname(record.providerHostnameId),await db.update(table).set({status:"disabled",routingStatus:"inactive",disabledAt:new Date,updatedAt:new Date}).where(eq8(col3(table,"id"),id)),this.getRegistry()?.unregisterDomainHostname(record.normalizedHostname),await this.recordEvent({domainHostnameId:id,eventType:"hostname_disabled",message:"Hostname disabled"}),this.getHostname(id)}async makePrimary(id){let db=this.getDb(),table=this.table("domainHostnames"),record=await this.getHostname(id);if(!db||!table||!record)return null;let scope=record.tenantId?eq8(col3(table,"tenantId"),record.tenantId):eq8(col3(table,"ownerId"),record.ownerId);return await db.update(table).set({isPrimary:!1,updatedAt:new Date}).where(scope),await db.update(table).set({isPrimary:!0,updatedAt:new Date}).where(eq8(col3(table,"id"),id)),await this.recordEvent({domainHostnameId:id,eventType:"primary_changed",message:"Primary hostname set"}),this.getHostname(id)}async refreshStatus(id){return this.verifyHostname(id)}async resolveHostname(hostname){let db=this.getDb(),table=this.table("domainHostnames");if(!db||!table)return null;let normalized=normalizeHostname(hostname),row=(await db.select().from(table).where(and3(eq8(col3(table,"normalizedHostname"),normalized),eq8(col3(table,"status"),"active"))).limit(1))[0];if(!row)return null;let record=rowToDomainHostname(row),redirectToHostname=null;if(record.redirectToHostnameId)redirectToHostname=(await this.getHostname(record.redirectToHostnameId))?.normalizedHostname??null;return{hostnameId:record.id,tenantId:record.tenantId,schemaName:record.schemaName,isPrimary:record.isPrimary,redirectToHostname}}async recordEvent(input){let db=this.getDb(),table=this.table("domainEvents");if(!db||!table)return;try{await db.insert(table).values({domainHostnameId:input.domainHostnameId??null,domainRegistrationId:input.domainRegistrationId??null,eventType:input.eventType,actorType:input.actorType??null,actorId:input.actorId??null,message:input.message??null,ipAddress:input.ipAddress??null,metadata:JSON.stringify(input.metadata??{})})}catch(err){this.logger.warn("Failed to record domain event",{eventType:input.eventType,error:err instanceof Error?err.message:String(err)})}}async markChallengesVerified(hostnameId){let db=this.getDb(),table=this.table("domainVerificationChallenges");if(!db||!table)return;await db.update(table).set({status:"verified",verifiedAt:new Date,updatedAt:new Date}).where(eq8(col3(table,"domainHostnameId"),hostnameId))}async applyStatus(id,patch){let db=this.getDb(),table=this.table("domainHostnames");if(!db||!table)return;await db.update(table).set({status:patch.status,routingStatus:patch.routingStatus,certificateStatus:patch.certificateStatus,verificationStatus:patch.verificationStatus,failureReason:patch.failureReason,activatedAt:patch.activate?new Date:void 0,updatedAt:new Date}).where(eq8(col3(table,"id"),id))}async syncRegistry(id){let record=await this.getHostname(id),registry=this.getRegistry();if(!record||!registry)return;if(record.status==="active"&&record.tenantId)registry.registerDomainHostname(record.normalizedHostname,record.tenantId)}}var col3=(table,name)=>table[name];var init_DomainService=__esm(()=>{init_Domain_helpers()});class CloudflareForSaasProvider{name="cloudflare_for_saas";config;logger;zoneId;apiToken;constructor(config,logger2){if(!config.cloudflare)throw Error("[Domain] Cloudflare for SaaS provider requires domains.cloudflare config");this.config=config,this.logger=logger2.scoped("domains.cloudflare"),this.zoneId=config.cloudflare.zoneId,this.apiToken=config.cloudflare.apiToken}get cnameTarget(){return this.config.cloudflare?.cnameTarget??this.config.cnameTarget}async call(path2,init){let res=await fetch(`https://api.cloudflare.com/client/v4${path2}`,{...init,headers:{Authorization:`Bearer ${this.apiToken}`,"Content-Type":"application/json",...init.headers??{}}}),json=await res.json().catch(()=>({}));if(!res.ok||json.success===!1){let msg=json.errors?.map((e)=>e.message).join("; ")||`HTTP ${res.status}`;throw Error(`Cloudflare API error: ${msg}`)}return json}mapRouting(status){if(status==="active")return"active";if(status==="blocked"||status==="moved"||status==="deleted")return"failed";return"pending"}mapCertificate(sslStatus){if(sslStatus==="active")return"active";if(sslStatus==="validation_timed_out"||sslStatus==="deactivating"||sslStatus==="deleted")return"failed";return"pending"}mapVerification(result){if(result.status==="active")return"verified";if((result.ssl?.validation_errors?.length??0)>0)return"failed";return"pending"}buildChallenges(result){let challenges=[],ov=result.ownership_verification;if(ov?.name&&ov.value)challenges.push({challengeType:"hostname_validation",recordType:"TXT",recordName:ov.name,recordValue:ov.value});for(let vr of result.ssl?.validation_records??[])if(vr.txt_name&&vr.txt_value)challenges.push({challengeType:"certificate_validation",recordType:"TXT",recordName:vr.txt_name,recordValue:vr.txt_value});return challenges}buildInstructions(input,challenges){let target=this.cnameTarget,instructions=[];if(target)instructions.push({recordType:input.hostnameKind==="apex"?"A":"CNAME",name:input.normalizedHostname,value:target,note:input.hostnameKind==="apex"?"Apex domains require an A/ALIAS record pointing at the SaaS target":"Point this hostname at the Cloudflare for SaaS target"});for(let c of challenges)instructions.push({recordType:c.recordType,name:c.recordName,value:c.recordValue,note:c.challengeType==="certificate_validation"?"SSL validation":"Hostname validation"});return instructions}toResult(result,input){let challenges=this.buildChallenges(result);return{providerHostnameId:result.id??null,dnsTarget:this.cnameTarget,routingStatus:this.mapRouting(result.status),certificateStatus:this.mapCertificate(result.ssl?.status),verificationStatus:this.mapVerification(result),instructions:this.buildInstructions(input,challenges),challenges,raw:result}}async createCustomHostname(input){let body=JSON.stringify({hostname:input.normalizedHostname,ssl:{method:"txt",type:"dv",settings:{min_tls_version:"1.2"}}}),json=await this.call(`/zones/${this.zoneId}/custom_hostnames`,{method:"POST",body});return this.toResult(json.result??{},input)}async getCustomHostnameStatus(providerHostnameId,input){let result=await this.fetchHostname(providerHostnameId,input.normalizedHostname);if(!result)return{routingStatus:"failed",certificateStatus:"failed",verificationStatus:"failed",failureReason:"Custom hostname not found at provider"};return{routingStatus:this.mapRouting(result.status),certificateStatus:this.mapCertificate(result.ssl?.status),verificationStatus:this.mapVerification(result),providerStatus:`${result.status??"unknown"}/${result.ssl?.status??"unknown"}`,failureReason:result.ssl?.validation_errors?.map((e)=>e.message).join("; ")||void 0,raw:result}}async fetchHostname(providerHostnameId,normalizedHostname){if(providerHostnameId)return(await this.call(`/zones/${this.zoneId}/custom_hostnames/${providerHostnameId}`,{method:"GET"})).result??null;let list=(await this.call(`/zones/${this.zoneId}/custom_hostnames?hostname=${encodeURIComponent(normalizedHostname)}`,{method:"GET"})).result;return Array.isArray(list)?list[0]??null:null}async deleteCustomHostname(providerHostnameId){if(!providerHostnameId)return;try{await this.call(`/zones/${this.zoneId}/custom_hostnames/${providerHostnameId}`,{method:"DELETE"})}catch(err){this.logger.warn("Failed to delete Cloudflare custom hostname",{providerHostnameId,error:err instanceof Error?err.message:String(err)})}}async verifyOwnership(input,_challenge){let status=await this.getCustomHostnameStatus(null,input);return{verified:status.verificationStatus==="verified",reason:status.failureReason??status.providerStatus}}}class ManualDnsProvider{name="manual_dns";config;constructor(config){this.config=config}buildChallenge(input){let prefix=this.config.verification.txtRecordPrefix,token=`nucleus-domain-verification=${generateChallengeToken()}`;return{challengeType:"ownership_validation",recordType:"TXT",recordName:`${prefix}.${input.normalizedHostname}`,recordValue:token}}buildInstructions(input,challenge){let target=this.config.cnameTarget,instructions=[{recordType:"TXT",name:challenge.recordName,value:challenge.recordValue,note:"Ownership verification record"}];if(input.hostnameKind==="apex")instructions.push({recordType:"A",name:input.normalizedHostname,value:target??"<platform-ingress-ip>",note:"Apex domains require an A/ALIAS record at your DNS provider"});else if(target)instructions.push({recordType:"CNAME",name:input.normalizedHostname,value:target,note:"Point this hostname at the platform routing target"});return instructions}async createCustomHostname(input){let challenge=this.buildChallenge(input);return{providerHostnameId:null,dnsTarget:this.config.cnameTarget,routingStatus:"pending",certificateStatus:"not_required",verificationStatus:"pending",instructions:this.buildInstructions(input,challenge),challenges:[challenge]}}async getCustomHostnameStatus(_providerHostnameId,input){if(input.hostnameKind!=="apex"&&this.config.cnameTarget){let dns2=await verifyCnameRecord(input.normalizedHostname,this.config.cnameTarget);return{routingStatus:dns2.verified?"active":"pending",certificateStatus:"not_required",verificationStatus:"pending",providerStatus:dns2.verified?"cname_active":"cname_pending"}}return{routingStatus:"pending",certificateStatus:"not_required",verificationStatus:"pending",providerStatus:"manual"}}async deleteCustomHostname(_providerHostnameId){}async verifyOwnership(_input,challenge){if(challenge.recordType==="CNAME"&&challenge.expectedTarget)return verifyCnameRecord(challenge.recordName,challenge.expectedTarget);return verifyTxtRecord(challenge.recordName,challenge.recordValue)}}var init_ManualDnsProvider=__esm(()=>{init_Domain_helpers()});var createDomainProvider=(config,logger2)=>{if(config.provider==="cloudflare_for_saas"){if(!config.cloudflare)return logger2.warn("[Domain] cloudflare_for_saas selected but cloudflare config missing \u2014 using manual DNS"),new ManualDnsProvider(config);return new CloudflareForSaasProvider(config,logger2)}return new ManualDnsProvider(config)};var init_Providers=__esm(()=>{init_ManualDnsProvider();init_ManualDnsProvider()});var createDomainServices=(params)=>{let config=resolveDomainsConfig(params.options.domains);if(!config.enabled)return null;let provider=createDomainProvider(config,params.logger),serviceConfig={getDb:params.getDb,getTenantRegistry:params.getTenantRegistry,logger:params.logger,config,provider};return{domainService:new DomainService(serviceConfig),registrationService:new DomainRegistrationService(serviceConfig)}};var init_Domain=__esm(()=>{init_Domain_helpers();init_DomainRegistrationService();init_DomainService();init_Providers();init_Domain_helpers();init_DomainRegistrationService();init_DomainService();init_Providers()});class TemplatedEmailService{emailWrapper(content,appName){return`<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><meta name="color-scheme" content="light dark"><meta name="supported-color-schemes" content="light dark"><title>${appName}</title><style type="text/css">:root{color-scheme:light dark;supported-color-schemes:light dark;}@media(prefers-color-scheme:dark){.em-bg{background-color:#18181b!important;}.em-card{background-color:#27272a!important;border-color:#3f3f46!important;}.em-h{color:#fafafa!important;}.em-t{color:#d4d4d8!important;}.em-m{color:#a1a1aa!important;}.em-hr{background-color:#3f3f46!important;}.em-btn{background-color:#fafafa!important;}.em-btn-text{color:#18181b!important;}.em-link{color:#60a5fa!important;}.em-lb{background-color:#3f3f46!important;border-color:#52525b!important;}.em-lb-t{color:#a1a1aa!important;}.em-sn{background-color:#3f3f46!important;border-color:#52525b!important;color:#a1a1aa!important;}}</style></head><body style="margin:0;padding:0;background-color:#f4f4f5;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;-webkit-font-smoothing:antialiased;"><table role="presentation" class="em-bg" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f4f4f5;"><tr><td align="center" style="padding:40px 16px;"><table role="presentation" width="480" cellpadding="0" cellspacing="0" style="max-width:480px;width:100%;"><tr><td align="center" style="padding-bottom:24px;"><span class="em-m" style="font-size:14px;font-weight:600;color:#71717a;letter-spacing:-0.2px;">${appName}</span></td></tr><tr><td class="em-card" style="background-color:#ffffff;border:1px solid #e4e4e7;border-radius:12px;overflow:hidden;">${content}</td></tr><tr><td align="center" style="padding-top:24px;"><p class="em-m" style="margin:0;font-size:12px;color:#a1a1aa;line-height:1.6;">This email was sent by <strong>${appName}</strong>.<br>Please do not reply to this email.</p></td></tr></table></td></tr></table></body></html>`}emailButton(href,label){return`<table role="presentation" cellpadding="0" cellspacing="0" width="100%"><tr><td align="center" style="padding:28px 32px;"><table role="presentation" cellpadding="0" cellspacing="0"><tr><td class="em-btn" align="center" style="background-color:#18181b;border-radius:8px;"><a class="em-btn-text" href="${href}" target="_blank" style="display:inline-block;padding:12px 32px;font-size:14px;font-weight:600;color:#fafafa;text-decoration:none;letter-spacing:-0.1px;">${label}</a></td></tr></table></td></tr></table>`}emailDivider(){return'<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:0 32px;"><div class="em-hr" style="height:1px;background-color:#e4e4e7;"></div></td></tr></table>'}emailLinkFallback(link,notice){let html=`<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:4px 32px 32px 32px;"><p class="em-m" style="margin:0 0 8px 0;font-size:11px;font-weight:600;color:#a1a1aa;text-transform:uppercase;letter-spacing:0.5px;">Or copy this link</p><table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td class="em-lb" style="background-color:#f4f4f5;border:1px solid #e4e4e7;border-radius:8px;padding:12px 14px;"><a class="em-link" href="${link}" style="font-size:12px;color:#2563eb;word-break:break-all;line-height:1.5;text-decoration:none;">${link}</a></td></tr></table>`;if(notice)html+=`<p class="em-m" style="margin:16px 0 0 0;font-size:12px;color:#a1a1aa;">${notice}</p>`;return html+="</td></tr></table>",html}async sendWelcomeEmail(email,name,appName="Nucleus"){let content=`<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:40px 32px 24px 32px;text-align:center;"><h1 class="em-h" style="margin:0;font-size:24px;font-weight:700;color:#18181b;letter-spacing:-0.5px;">Welcome to ${appName}</h1><p class="em-t" style="margin:8px 0 0 0;font-size:15px;color:#52525b;line-height:1.6;">Your account has been created successfully</p></td></tr></table>${this.emailDivider()}<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:24px 32px 32px 32px;"><p class="em-t" style="margin:0 0 16px 0;font-size:15px;color:#3f3f46;line-height:1.7;">Hello <strong class="em-h" style="color:#18181b;">${name}</strong>,</p><p class="em-t" style="margin:0;font-size:15px;color:#3f3f46;line-height:1.7;">Welcome aboard! You now have full access to <strong>${appName}</strong>. We're excited to have you.</p></td></tr></table>`;return this.sendEmail({to:email,subject:`Welcome to ${appName}`,html:this.emailWrapper(content,appName)})}async sendVerificationEmail(email,name,verificationLink,appName="Nucleus"){let content=`<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:40px 32px 24px 32px;text-align:center;"><h1 class="em-h" style="margin:0;font-size:24px;font-weight:700;color:#18181b;letter-spacing:-0.5px;">Verify your email</h1><p class="em-t" style="margin:8px 0 0 0;font-size:15px;color:#52525b;line-height:1.6;">Confirm your address to get started</p></td></tr></table>${this.emailDivider()}<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:24px 32px 0 32px;"><p class="em-t" style="margin:0 0 4px 0;font-size:15px;color:#3f3f46;line-height:1.7;">Hello <strong class="em-h" style="color:#18181b;">${name}</strong>,</p><p class="em-t" style="margin:0;font-size:15px;color:#3f3f46;line-height:1.7;">Please verify your email address by clicking the button below.</p></td></tr><tr><td>${this.emailButton(verificationLink,"Verify Email Address")}</td></tr></table>${this.emailDivider()}${this.emailLinkFallback(verificationLink)}`;return this.sendEmail({to:email,subject:`Verify your email \u2014 ${appName}`,html:this.emailWrapper(content,appName)})}async sendPasswordResetEmail(email,name,resetLink,appName="Nucleus"){let content=`<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:40px 32px 24px 32px;text-align:center;"><h1 class="em-h" style="margin:0;font-size:24px;font-weight:700;color:#18181b;letter-spacing:-0.5px;">Reset your password</h1><p class="em-t" style="margin:8px 0 0 0;font-size:15px;color:#52525b;line-height:1.6;">We received a request to reset your password</p></td></tr></table>${this.emailDivider()}<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:24px 32px 0 32px;"><p class="em-t" style="margin:0 0 4px 0;font-size:15px;color:#3f3f46;line-height:1.7;">Hello <strong class="em-h" style="color:#18181b;">${name}</strong>,</p><p class="em-t" style="margin:0;font-size:15px;color:#3f3f46;line-height:1.7;">Click the button below to choose a new password. If you didn't make this request, you can safely ignore this email.</p></td></tr><tr><td>${this.emailButton(resetLink,"Reset Password")}</td></tr></table>${this.emailDivider()}${this.emailLinkFallback(resetLink,"This link expires in 1 hour and can only be used once.")}`;return this.sendEmail({to:email,subject:`Reset your password \u2014 ${appName}`,html:this.emailWrapper(content,appName)})}async sendMagicLinkEmail(email,magicLink,appName="Nucleus"){let content=`<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:40px 32px 24px 32px;text-align:center;"><h1 class="em-h" style="margin:0;font-size:24px;font-weight:700;color:#18181b;letter-spacing:-0.5px;">Sign in to ${appName}</h1><p class="em-t" style="margin:8px 0 0 0;font-size:15px;color:#52525b;line-height:1.6;">No password needed \u2014 just click to sign in</p></td></tr></table>${this.emailDivider()}<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:24px 32px 0 32px;"><p class="em-t" style="margin:0;font-size:15px;color:#3f3f46;line-height:1.7;">Click the button below to securely sign in to your <strong class="em-h" style="color:#18181b;">${appName}</strong> account.</p></td></tr><tr><td>${this.emailButton(magicLink,"Sign In")}</td></tr></table>${this.emailDivider()}${this.emailLinkFallback(magicLink,"This link expires in 15 minutes and can only be used once.")}`;return this.sendEmail({to:email,subject:`Sign in to ${appName}`,html:this.emailWrapper(content,appName)})}async sendAlertEmail(to,subject,html){return this.sendEmail({to,subject,html})}async sendInvitationEmail(email,inviteLink,appName="Nucleus"){let content=`<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:40px 32px 24px 32px;text-align:center;"><h1 class="em-h" style="margin:0;font-size:24px;font-weight:700;color:#18181b;letter-spacing:-0.5px;">You're Invited</h1><p class="em-t" style="margin:8px 0 0 0;font-size:15px;color:#52525b;line-height:1.6;">Join <strong class="em-h" style="color:#18181b;">${appName}</strong> and start collaborating</p></td></tr></table>${this.emailDivider()}<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:0 32px 8px 32px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:8px 0;"><table role="presentation" cellpadding="0" cellspacing="0"><tr><td class="em-sn" style="width:28px;height:28px;background-color:#f4f4f5;border:1px solid #e4e4e7;border-radius:50%;text-align:center;line-height:28px;font-size:12px;font-weight:600;color:#71717a;">1</td><td class="em-t" style="padding-left:14px;font-size:14px;color:#52525b;">Click the button below</td></tr></table></td></tr><tr><td style="padding:8px 0;"><table role="presentation" cellpadding="0" cellspacing="0"><tr><td class="em-sn" style="width:28px;height:28px;background-color:#f4f4f5;border:1px solid #e4e4e7;border-radius:50%;text-align:center;line-height:28px;font-size:12px;font-weight:600;color:#71717a;">2</td><td class="em-t" style="padding-left:14px;font-size:14px;color:#52525b;">Choose a secure password</td></tr></table></td></tr><tr><td style="padding:8px 0;"><table role="presentation" cellpadding="0" cellspacing="0"><tr><td class="em-sn" style="width:28px;height:28px;background-color:#f4f4f5;border:1px solid #e4e4e7;border-radius:50%;text-align:center;line-height:28px;font-size:12px;font-weight:600;color:#71717a;">3</td><td class="em-t" style="padding-left:14px;font-size:14px;color:#52525b;">Start collaborating</td></tr></table></td></tr></table></td></tr></table>${this.emailButton(inviteLink,"Accept Invitation")}${this.emailDivider()}${this.emailLinkFallback(inviteLink,"This invitation expires in 7 days.")}`;return this.sendEmail({to:email,subject:`You're invited to join ${appName}`,html:this.emailWrapper(content,appName)})}}var AzureEmailService;var init_AzureEmailService=__esm(()=>{AzureEmailService=class AzureEmailService extends TemplatedEmailService{client=null;senderAddress;fromName;logger;initialized=!1;credentialsFingerprint="";constructor(config,logger2){super();if(this.senderAddress=config.senderAddress,this.fromName=config.fromName||"Nucleus",this.logger=logger2,config.enabled)this.credentialsFingerprint=`${config.connectionString??""}|${config.senderAddress}`,this.initialize(config.connectionString)}reconfigure(config){let fingerprint=`${config.connectionString??""}|${config.senderAddress}`;if(fingerprint===this.credentialsFingerprint&&this.initialized)return;if(this.credentialsFingerprint=fingerprint,this.senderAddress=config.senderAddress||this.senderAddress,config.fromName)this.fromName=config.fromName;if(this.initialized=!1,this.client=null,config.enabled)this.initialize(config.connectionString)}isAvailable(){return this.initialized&&this.client!==null}initialize(connectionString){try{if(!connectionString){this.logger.warn("[AzureEmailService] No connection string provided");return}if(!this.senderAddress){this.logger.warn("[AzureEmailService] No sender address configured");return}let{EmailClient}=__require("@azure/communication-email");this.client=new EmailClient(connectionString),this.initialized=!0,this.logger.info("[AzureEmailService] Initialized",{senderAddress:this.senderAddress})}catch(error){let message=error instanceof Error?error.message:String(error);this.logger.error("[AzureEmailService] Initialization failed",{error:message})}}async sendEmail(payload){if(!this.client)return this.logger.error("[AzureEmailService] Not initialized"),{success:!1,error:"Azure Email service not initialized"};try{let toAddresses=Array.isArray(payload.to)?payload.to:[payload.to],result=await(await this.client.beginSend({senderAddress:payload.from||this.senderAddress,content:{subject:payload.subject,plainText:payload.text,html:payload.html},recipients:{to:toAddresses.map((addr)=>({address:addr}))},...payload.replyTo?{replyTo:[{address:payload.replyTo}]}:{},attachments:payload.attachments?.map((att)=>({name:att.filename,contentType:att.contentType,contentInBase64:att.content.toString("base64")}))})).pollUntilDone();if(result.status==="Succeeded")return this.logger.info("[AzureEmailService] Email sent",{to:payload.to,subject:payload.subject,operationId:result.id}),{success:!0};let errorMsg=result.error?.message||`Send failed with status: ${result.status}`;return this.logger.error("[AzureEmailService] Send failed",{status:result.status,error:errorMsg,to:payload.to}),{success:!1,error:errorMsg}}catch(error){let message=error instanceof Error?error.message:"Unknown error";return this.logger.error("[AzureEmailService] Send failed",{error:message,to:payload.to}),{success:!1,error:message}}}}});import{createRequire}from"module";function stripAppPasswordGroups(password2){return APP_PASSWORD_SHAPE.test(password2)?password2.replace(/\s+/g,""):password2.trim()}function explainSmtpFailure(message){let text=message.toLowerCase();if(text.includes("5.7.3")||text.includes("authentication unsuccessful"))return"Sunucu kimlik do\u011Frulamay\u0131 reddetti. En s\u0131k sebebi parola de\u011Fil: Exchange Online'da "+"SMTP AUTH kirac\u0131 d\xFCzeyinde varsay\u0131lan olarak KAPALIDIR ve do\u011Fru parolay\u0131 da reddeder. "+"IT'den o posta kutusu i\xE7in \u201CAuthenticated SMTP\u201Dyi a\xE7mas\u0131n\u0131 isteyin. Di\u011Fer ihtimal, "+"uygulama parolas\u0131n\u0131n \u201CPosta kutusu\u201D alan\u0131na yaz\u0131lan adresten ba\u015Fka bir hesaba ait olmas\u0131. "+`Sunucunun kendi yan\u0131t\u0131: ${message}`;if(text.includes("5.7.57")||text.includes("must issue a starttls"))return`Sunucu \u015Fifreli ba\u011Flant\u0131 istiyor. Portu 587 (STARTTLS) veya 465 olarak deneyin. Sunucunun yan\u0131t\u0131: ${message}`;if(text.includes("enotfound")||text.includes("eai_again"))return`Sunucu ad\u0131 \xE7\xF6z\xFClemedi \u2014 \u201CSunucu\u201D alan\u0131n\u0131 kontrol edin (Microsoft 365 i\xE7in smtp.office365.com). Ayr\u0131nt\u0131: ${message}`;if(text.includes("etimedout")||text.includes("econnrefused"))return`Sunucuya ba\u011Flan\u0131lamad\u0131. A\u011F bu porta \xE7\u0131k\u0131\u015Fa izin vermiyor olabilir. Ayr\u0131nt\u0131: ${message}`;if(text.includes("5.7.60")||text.includes("send as"))return`G\xF6nderen adresi bu posta kutusuna ait de\u011Fil. Bo\u015F b\u0131rak\u0131n ya da Exchange'de \u201CSend As\u201D yetkisi verdirin. Sunucunun yan\u0131t\u0131: ${message}`;return message}var APP_PASSWORD_SHAPE,IMPLICIT_TLS_PORT=465,DEFAULT_PORT=587,SmtpEmailService;var init_SmtpEmailService=__esm(()=>{APP_PASSWORD_SHAPE=/^\s*(?:[A-Za-z0-9]{4}\s+){3}[A-Za-z0-9]{4}\s*$/;SmtpEmailService=class SmtpEmailService extends TemplatedEmailService{transporter=null;senderAddress;fromName;logger;initialized=!1;credentialsFingerprint="";constructor(config,logger2){super();this.senderAddress=config.senderAddress,this.fromName=config.fromName||"Nucleus",this.logger=logger2,this.applyConfig(config)}applyConfig(config){let fingerprint=[config.host??"",String(config.port??""),String(config.secure??""),config.user??"",config.password??"",config.senderAddress??"",config.fromName??"",String(config.enabled)].join("|");if(fingerprint===this.credentialsFingerprint&&this.initialized)return;if(this.credentialsFingerprint=fingerprint,this.senderAddress=config.senderAddress||this.senderAddress,config.fromName)this.fromName=config.fromName;if(this.initialized=!1,this.transporter=null,config.enabled)this.initialize(config)}isAvailable(){return this.initialized&&this.transporter!==null}initialize(config){try{let host=config.host?.trim();if(!host){this.logger.warn("[SmtpEmailService] No host configured");return}let user=config.user?.trim();if(!user||!config.password){this.logger.warn("[SmtpEmailService] No credentials configured");return}if(!this.senderAddress)this.senderAddress=user;let port=config.port??DEFAULT_PORT,nodemailer=createRequire(import.meta.url)("nodemailer"),secure=config.secure??port===IMPLICIT_TLS_PORT,pass=stripAppPasswordGroups(config.password);this.transporter=nodemailer.createTransport({host,port,secure,requireTLS:!secure,tls:{minVersion:"TLSv1.2"},auth:{user,pass},pool:!1,connectionTimeout:20000,greetingTimeout:20000,socketTimeout:60000}),this.initialized=!0,this.logger.info("[SmtpEmailService] Initialized",{host,port,senderAddress:this.senderAddress})}catch(error){let message=error instanceof Error?error.message:String(error);this.logger.error("[SmtpEmailService] Initialization failed \u2014 is `nodemailer` installed in this app?",{error:message,host:config.host,port:config.port??DEFAULT_PORT})}}sanitizeHeaderValue(value){return value.replace(/[\r\n\u0000-\u001f\u007f]+/g," ").trim()}async sendEmail(payload){if(!this.transporter)return this.logger.error("[SmtpEmailService] Not initialized"),{success:!1,error:"SMTP email service not initialized"};try{let address=this.sanitizeHeaderValue(payload.from||this.senderAddress),from=this.fromName?`"${this.sanitizeHeaderValue(this.fromName).replace(/"/g,"")}" <${address}>`:address,to=(Array.isArray(payload.to)?payload.to:[payload.to]).map((a)=>this.sanitizeHeaderValue(a)),result=await this.transporter.sendMail({from,to,subject:this.sanitizeHeaderValue(payload.subject),text:payload.text,html:payload.html,replyTo:payload.replyTo?this.sanitizeHeaderValue(payload.replyTo):void 0,attachments:payload.attachments?.map((att)=>({filename:att.filename,content:att.content,contentType:att.contentType}))}),rejected=(result.rejected??[]).filter(Boolean);if(rejected.length>0)return this.logger.warn("[SmtpEmailService] Some recipients were refused",{rejected,accepted:result.accepted?.length??0,subject:payload.subject}),{success:(result.accepted?.length??0)>0,rejected,error:`Bu adresler sunucu taraf\u0131ndan reddedildi: ${rejected.join(", ")}`};return this.logger.info("[SmtpEmailService] Email sent",{to:payload.to,subject:payload.subject,messageId:result.messageId}),{success:!0}}catch(error){let message=error instanceof Error?error.message:"Unknown error";return this.logger.error("[SmtpEmailService] Send failed",{error:message,to:payload.to}),{success:!1,error:explainSmtpFailure(message)}}}}});var init_Email=__esm(()=>{init_AzureEmailService();init_SmtpEmailService()});import fs2 from"fs";function loadGoogle(){if(!cachedGoogle)try{cachedGoogle=__require("googleapis").google}catch(error){throw Error("Gmail e-posta sa\u011Flay\u0131c\u0131s\u0131 i\xE7in `googleapis` paketi gerekiyor. "+"nucleus-core onu iste\u011Fe ba\u011Fl\u0131 tutuyor (206 MB, yaln\u0131z Gmail i\xE7in). "+"Kurulumun package.json dosyas\u0131na `googleapis` ekleyip yeniden kurun. "+`As\u0131l hata: ${error instanceof Error?error.message:String(error)}`)}return cachedGoogle}class GmailService{gmail=null;fromEmail;fromName;logger;initialized=!1;credentialsFingerprint="";constructor(config,logger2){if(this.fromEmail=config.fromEmail,this.fromName=config.fromName||"Vorion",this.logger=logger2,config.enabled)this.initialize(config)}reconfigure(config){if(`${config.serviceAccountJson??""}|${config.jsonFilePath??""}|${config.fromEmail}`===this.credentialsFingerprint&&this.initialized)return;if(this.fromEmail=config.fromEmail||this.fromEmail,config.fromName)this.fromName=config.fromName;if(this.initialized=!1,this.gmail=null,config.enabled)this.initialize(config)}sanitizeHeaderValue(value){return value.replace(/[\r\n\u0000-\u001f\u007f]+/g," ").trim()}encodeHeaderValue(rawValue){let value=this.sanitizeHeaderValue(rawValue),hasNonAscii=!1;for(let i=0;i<value.length;i++)if(value.charCodeAt(i)>127){hasNonAscii=!0;break}if(!hasNonAscii)return value;return`=?UTF-8?B?${Buffer.from(value,"utf-8").toString("base64")}?=`}isAvailable(){return this.initialized&&this.gmail!==null}initialize(config){let{serviceAccountJson,jsonFilePath}=config;this.credentialsFingerprint=`${serviceAccountJson??""}|${jsonFilePath??""}|${config.fromEmail}`;try{let keyContent;if(serviceAccountJson?.trim())keyContent=serviceAccountJson;else if(jsonFilePath){if(!fs2.existsSync(jsonFilePath)){this.logger.error("[GmailService] JSON file not found",{path:jsonFilePath});return}keyContent=fs2.readFileSync(jsonFilePath,"utf8")}else{this.logger.warn("[GmailService] No service-account JSON or file path provided");return}let credentials;try{credentials=JSON.parse(keyContent)}catch{this.logger.error("[GmailService] Service-account JSON is not valid JSON",{source:serviceAccountJson?.trim()?"inline":"file"});return}if(!this.fromEmail){this.logger.warn("[GmailService] From email not configured");return}if(!credentials?.client_email||!credentials?.private_key){this.logger.error("[GmailService] Invalid credentials format");return}let privateKey=credentials.private_key;if(!privateKey.includes(`
|
|
33
|
+
</svg>`}function generatePuzzleChallenge(difficulty){let pieceCount=DIFFICULTY_CONFIG[difficulty].puzzlePieces,pieces=[],correctOrder=[];for(let i=0;i<pieceCount;i++)pieces.push({id:i,x:i%3*100,y:Math.floor(i/3)*100}),correctOrder.push(i);let shuffledPieces=[...pieces];for(let i=shuffledPieces.length-1;i>0;i--){let j=getSecureRandomInt(0,i),itemI=shuffledPieces[i],itemJ=shuffledPieces[j];if(itemI&&itemJ)shuffledPieces[i]=itemJ,shuffledPieces[j]=itemI}return{question:"Arrange the pieces in correct order",answer:correctOrder.join(","),puzzleData:{pieces:shuffledPieces,correctOrder}}}class CaptchaService{redis;logger;config;constructor(deps){this.redis=deps.redis,this.logger=deps.logger,this.config={enabled:deps.config.enabled??!0,type:deps.config.type??"math",difficulty:deps.config.difficulty??"medium",expiresIn:deps.config.expiresIn??"5m",maxAttempts:deps.config.maxAttempts??3,caseSensitive:deps.config.caseSensitive??!1,rateLimit:{maxGeneratePerMinute:deps.config.rateLimit?.maxGeneratePerMinute??10,maxGeneratePerHour:deps.config.rateLimit?.maxGeneratePerHour??100}}}async generate(type,difficulty,ipAddress){if(ipAddress){if(await this.checkRateLimit(ipAddress))return this.logger.warn("[CAPTCHA] Rate limit exceeded",{ipAddress}),{success:!1,challengeId:"",type:type??this.config.type,question:"",expiresAt:0,rateLimited:!0,message:"Too many requests. Please try again later."};await this.incrementRateLimit(ipAddress)}let captchaType=type??this.config.type,captchaDifficulty=difficulty??this.config.difficulty,challengeId=generateSecureId(),expiresInSeconds=parseTimeToSeconds(this.config.expiresIn),expiresAt=Date.now()+expiresInSeconds*1000,question,answer,imageData,puzzleData;switch(captchaType){case"math":{let mathResult=generateMathChallenge(captchaDifficulty);question=mathResult.question,answer=mathResult.answer;break}case"text":{let textResult=generateTextChallenge(captchaDifficulty);question=textResult.question,answer=textResult.answer;break}case"image":{let imageResult=generateImageChallenge(captchaDifficulty);question=imageResult.question,answer=imageResult.answer,imageData=imageResult.imageData;break}case"puzzle":{let puzzleResult=generatePuzzleChallenge(captchaDifficulty);question=puzzleResult.question,answer=puzzleResult.answer,puzzleData=puzzleResult.puzzleData;break}default:{let defaultResult=generateMathChallenge(captchaDifficulty);question=defaultResult.question,answer=defaultResult.answer}}let normalizedAnswer=this.config.caseSensitive?answer:answer.toUpperCase(),answerHash=hashAnswer(normalizedAnswer),challenge={id:challengeId,type:captchaType,question,answer:answerHash,imageData,puzzleData,expiresAt,attempts:0,maxAttempts:this.config.maxAttempts,caseSensitive:this.config.caseSensitive};await this.redis.set(`${CAPTCHA_KEY_PREFIX}${challengeId}`,JSON.stringify(challenge),{ex:expiresInSeconds}),this.logger.info("[CAPTCHA] Challenge generated",{challengeId,type:captchaType,difficulty:captchaDifficulty,expiresAt});let result={success:!0,challengeId,type:captchaType,question,expiresAt};if(imageData)result.imageData=imageData;if(puzzleData)result.puzzleData={pieces:puzzleData.pieces};return result}async validate(challengeId,userAnswer){let key=`${CAPTCHA_KEY_PREFIX}${challengeId}`,stored=await this.redis.get(key);if(!stored)return this.logger.warn("[CAPTCHA] Challenge not found or expired",{challengeId}),{success:!1,valid:!1,message:"Challenge expired or not found"};let challenge=parseStoredJson(stored);if(!challenge||typeof challenge.expiresAt!=="number")return await this.redis.del(key),this.logger.warn("[CAPTCHA] Stored challenge unreadable, discarded",{challengeId,storedType:typeof stored}),{success:!1,valid:!1,message:"Challenge expired or not found"};if(Date.now()>challenge.expiresAt)return await this.redis.del(key),this.logger.warn("[CAPTCHA] Challenge expired",{challengeId}),{success:!1,valid:!1,message:"Challenge expired"};let ttl=Math.max(1,Math.floor((challenge.expiresAt-Date.now())/1000)),attemptsKey=`${key}:att`,attempts;if(this.redis.incrementCounter)attempts=await this.redis.incrementCounter(attemptsKey,1,ttl);else challenge.attempts+=1,attempts=challenge.attempts,await this.redis.set(key,JSON.stringify(challenge),{ex:ttl});let clearChallenge=async()=>{if(await this.redis.del(key),this.redis.incrementCounter)await this.redis.del(attemptsKey)};if(attempts>challenge.maxAttempts)return await clearChallenge(),this.logger.warn("[CAPTCHA] Max attempts exceeded",{challengeId,attempts}),{success:!1,valid:!1,message:"Maximum attempts exceeded"};let normalizedUserAnswer=challenge.caseSensitive?userAnswer.trim():userAnswer.trim().toUpperCase(),userAnswerHash=hashAnswer(normalizedUserAnswer);if(timingSafeEqual(challenge.answer,userAnswerHash))return await clearChallenge(),this.logger.info("[CAPTCHA] Challenge validated successfully",{challengeId}),{success:!0,valid:!0,message:"Captcha validated successfully"};let remainingAttempts=challenge.maxAttempts-attempts;if(remainingAttempts<=0)await clearChallenge();return this.logger.warn("[CAPTCHA] Invalid answer",{challengeId,attemptsRemaining:remainingAttempts}),{success:!0,valid:!1,message:"Incorrect answer",attemptsRemaining:remainingAttempts}}async invalidate(challengeId){await this.redis.del(`${CAPTCHA_KEY_PREFIX}${challengeId}`),this.logger.info("[CAPTCHA] Challenge invalidated",{challengeId})}isEnabled(){return this.config.enabled}async checkRateLimit(ipAddress){let minuteKey=`${RATE_LIMIT_KEY_PREFIX}${ipAddress}:minute`,hourKey=`${RATE_LIMIT_KEY_PREFIX}${ipAddress}:hour`,[minuteCount,hourCount]=await Promise.all([this.redis.get(minuteKey),this.redis.get(hourKey)]),minuteRequests=storedCount(minuteCount),hourRequests=storedCount(hourCount),maxPerMinute=this.config.rateLimit.maxGeneratePerMinute??10,maxPerHour=this.config.rateLimit.maxGeneratePerHour??100;return minuteRequests>=maxPerMinute||hourRequests>=maxPerHour}async incrementRateLimit(ipAddress){let minuteKey=`${RATE_LIMIT_KEY_PREFIX}${ipAddress}:minute`,hourKey=`${RATE_LIMIT_KEY_PREFIX}${ipAddress}:hour`,[minuteCount,hourCount]=await Promise.all([this.redis.get(minuteKey),this.redis.get(hourKey)]),newMinuteCount=storedCount(minuteCount)+1,newHourCount=storedCount(hourCount)+1;await Promise.all([this.redis.set(minuteKey,newMinuteCount.toString(),{ex:60}),this.redis.set(hourKey,newHourCount.toString(),{ex:3600})])}}var CAPTCHA_KEY_PREFIX="captcha:",RATE_LIMIT_KEY_PREFIX="captcha_rate:",storedCount=(stored)=>{let value=parseStoredJson(stored),parsed=typeof value==="number"?value:Number.parseInt(String(value??""),10);return Number.isFinite(parsed)?parsed:0},DIFFICULTY_CONFIG,CHARACTERS="ABCDEFGHJKLMNPQRSTUVWXYZ23456789";var init_Captcha=__esm(()=>{DIFFICULTY_CONFIG={easy:{mathRange:{min:1,max:10},textLength:4,puzzlePieces:4},medium:{mathRange:{min:10,max:50},textLength:6,puzzlePieces:6},hard:{mathRange:{min:50,max:100},textLength:8,puzzlePieces:9}}});import{promises as dns}from"dns";var DEFAULT_TXT_PREFIX="_nucleus-challenge",DEFAULT_CHALLENGE_TTL=604800,DEFAULT_BASE_PATH="/domains",resolveEnvValue=(value)=>{if(value==null)return;let fromEnv=process.env[value];return fromEnv!=null&&fromEnv!==""?fromEnv:value},normalizeHostname=(raw)=>{let trimmed=(raw||"").trim();if(!trimmed)return"";let normalized=((trimmed.replace(/^https?:\/\//i,"").split("/")[0]?.split("?")[0]||"").split(":")[0]||"").toLowerCase().replace(/\.$/,"");try{normalized=new URL(`http://${normalized}`).hostname.replace(/\.$/,"")}catch{}return normalized},isValidHostname=(hostname)=>{if(!hostname||hostname.length>253)return!1;let labels=hostname.split(".");if(labels.length<2)return!1;return labels.every((label)=>/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/.test(label))},classifyHostname=(normalized,platformBaseDomain)=>{if(platformBaseDomain){let base=platformBaseDomain.toLowerCase().replace(/\.$/,"");if(normalized===base)return"apex";if(normalized.endsWith(`.${base}`)){let prefix=normalized.slice(0,-(base.length+1));if(prefix&&!prefix.includes("."))return"platform_subdomain"}}let labels=normalized.split(".");if(labels[0]==="www")return"www";if(labels.length<=2)return"apex";return"subdomain"},generateChallengeToken=()=>{let bytes=new Uint8Array(24);return crypto.getRandomValues(bytes),Array.from(bytes,(b)=>b.toString(16).padStart(2,"0")).join("")},resolveDomainsConfig=(raw)=>{let provider=raw?.provider??"manual_dns",cf=raw?.cloudflare,cloudflare=cf&&resolveEnvValue(cf.accountId)&&resolveEnvValue(cf.zoneId)&&resolveEnvValue(cf.apiToken)?{accountId:resolveEnvValue(cf.accountId),zoneId:resolveEnvValue(cf.zoneId),apiToken:resolveEnvValue(cf.apiToken),fallbackOrigin:resolveEnvValue(cf.fallbackOrigin)??null,cnameTarget:resolveEnvValue(cf.cnameTarget)??null}:null;return{enabled:raw?.enabled===!0,basePath:raw?.basePath??DEFAULT_BASE_PATH,provider,platformBaseDomain:raw?.platformBaseDomain?normalizeHostname(raw.platformBaseDomain):null,cnameTarget:resolveEnvValue(raw?.cnameTarget)??cloudflare?.cnameTarget??null,allowApexDomains:raw?.allowApexDomains!==!1,allowWwwDomains:raw?.allowWwwDomains!==!1,allowManagedRegistrations:raw?.allowManagedRegistrations===!0,reservedHostnames:(raw?.reservedHostnames??[]).map((h)=>normalizeHostname(h)),verification:{defaultMethod:raw?.verification?.defaultMethod??"txt",txtRecordPrefix:raw?.verification?.txtRecordPrefix??DEFAULT_TXT_PREFIX,challengeTtlSeconds:raw?.verification?.challengeTtlSeconds??DEFAULT_CHALLENGE_TTL},cloudflare,registrar:raw?.registrar?{provider:raw.registrar.provider??"cloudflare_registrar",defaultRegistrantOwnerType:raw.registrar.defaultRegistrantOwnerType??"customer"}:null}},str=(v)=>v==null?"":String(v),strOrNull=(v)=>v==null?null:String(v),parseMetadata=(v)=>{if(!v)return{};let obj=typeof v==="string"?safeJson(v):v;if(!obj||typeof obj!=="object")return{};let out={};for(let[k,val]of Object.entries(obj))if(typeof val==="string"||typeof val==="number"||typeof val==="boolean")out[k]=val;return out},safeJson=(v)=>{try{return JSON.parse(v)}catch{return{}}},rowToDomainHostname=(row)=>({id:str(row.id),ownerType:str(row.ownerType??row.owner_type)||"tenant",ownerId:str(row.ownerId??row.owner_id),tenantId:strOrNull(row.tenantId??row.tenant_id),schemaName:strOrNull(row.schemaName??row.schema_name),hostname:str(row.hostname),normalizedHostname:str(row.normalizedHostname??row.normalized_hostname),hostnameKind:str(row.hostnameKind??row.hostname_kind)||"apex",status:str(row.status),routingStatus:str(row.routingStatus??row.routing_status),certificateStatus:str(row.certificateStatus??row.certificate_status),verificationStatus:str(row.verificationStatus??row.verification_status),provider:str(row.provider)||"manual_dns",providerHostnameId:strOrNull(row.providerHostnameId??row.provider_hostname_id),dnsTarget:strOrNull(row.dnsTarget??row.dns_target),isPrimary:Boolean(row.isPrimary??row.is_primary),redirectToHostnameId:strOrNull(row.redirectToHostnameId??row.redirect_to_hostname_id),activatedAt:strOrNull(row.activatedAt??row.activated_at),disabledAt:strOrNull(row.disabledAt??row.disabled_at),failureReason:strOrNull(row.failureReason??row.failure_reason),metadata:parseMetadata(row.metadata)}),verifyTxtRecord=async(recordName,expectedValue)=>{try{let flat=(await dns.resolveTxt(recordName)).map((chunks)=>chunks.join("")),verified=flat.some((v)=>v===expectedValue);return{verified,observed:flat,reason:verified?void 0:"TXT value not found"}}catch(err){return{verified:!1,reason:err instanceof Error?err.message:"DNS lookup failed"}}},verifyCnameRecord=async(recordName,expectedTarget)=>{let target=expectedTarget.toLowerCase().replace(/\.$/,"");try{let flat=(await dns.resolveCname(recordName)).map((v)=>v.toLowerCase().replace(/\.$/,"")),verified=flat.includes(target);return{verified,observed:flat,reason:verified?void 0:"CNAME target mismatch"}}catch(err){return{verified:!1,reason:err instanceof Error?err.message:"DNS lookup failed"}}};var init_Domain_helpers=()=>{};import{eq as eq7}from"drizzle-orm";class DomainRegistrationService{getDb;getRegistry;logger;config;constructor(cfg){this.getDb=cfg.getDb,this.getRegistry=cfg.getTenantRegistry,this.logger=cfg.logger.scoped("domains.registration"),this.config=cfg.config}get enabled(){return this.config.allowManagedRegistrations}table(name){let registry=this.getRegistry();if(!registry)return null;return registry.getMainContext().schemaTables[name]??null}async event(registrationId,eventType,message){let db=this.getDb(),table=this.table("domainEvents");if(!db||!table)return;try{await db.insert(table).values({domainRegistrationId:registrationId,eventType,message:message??null,metadata:JSON.stringify({})})}catch(err){this.logger.warn("Failed to record registration event",{eventType,error:err instanceof Error?err.message:String(err)})}}async createRequest(input){if(!this.enabled)throw Error("Managed domain registration is not enabled");if(!input.termsAccepted)throw Error("Registration requires accepting the domain terms");let db=this.getDb(),table=this.table("domainRegistrations");if(!db||!table)throw Error("Domain registration service not ready");let normalized=normalizeHostname(input.requestedDomain),rows=await db.insert(table).values({ownerType:input.ownerType??"tenant",ownerId:input.ownerId,tenantId:input.tenantId??null,requestedDomain:normalized,provider:this.config.registrar?.provider??"cloudflare_registrar",registrantOwnerType:input.registrantOwnerType??this.config.registrar?.defaultRegistrantOwnerType??"customer",status:"awaiting_payment",autoRenew:input.autoRenew??!0,termsVersion:input.termsVersion,termsAcceptedAt:new Date,metadata:JSON.stringify(input.metadata??{})}).returning(),record=rowToRegistration(rows[0]);return await this.event(record.id,"registration_requested",`Requested ${normalized}`),record}async listRegistrations(filter){let db=this.getDb(),table=this.table("domainRegistrations");if(!db||!table)return[];let where=filter.tenantId?eq7(col2(table,"tenantId"),filter.tenantId):filter.ownerId?eq7(col2(table,"ownerId"),filter.ownerId):void 0;return(where?await db.select().from(table).where(where):await db.select().from(table)).map((r)=>rowToRegistration(r))}async getRegistration(id){let db=this.getDb(),table=this.table("domainRegistrations");if(!db||!table)return null;let rows=await db.select().from(table).where(eq7(col2(table,"id"),id)).limit(1);return rows[0]?rowToRegistration(rows[0]):null}async transition(id,status,patch){let db=this.getDb(),table=this.table("domainRegistrations");if(!db||!table)return null;return await db.update(table).set({status,registeredDomain:patch?.registeredDomain,providerRegistrationId:patch?.providerRegistrationId,priceAmount:patch?.priceAmount,currency:patch?.currency,renewalDate:patch?.renewalDate,failureReason:patch?.failureReason??null,updatedAt:new Date}).where(eq7(col2(table,"id"),id)),await this.event(id,`registration_${status}`),this.getRegistration(id)}async cancel(id){return this.transition(id,"cancelled")}async requestTransferOut(id){return this.transition(id,"transfer_requested")}}var col2=(table,name)=>table[name],rowToRegistration=(row)=>({id:String(row.id??""),ownerType:String(row.ownerType??row.owner_type??"tenant"),ownerId:String(row.ownerId??row.owner_id??""),tenantId:row.tenantId??row.tenant_id?String(row.tenantId??row.tenant_id):null,requestedDomain:String(row.requestedDomain??row.requested_domain??""),registeredDomain:row.registeredDomain??row.registered_domain?String(row.registeredDomain??row.registered_domain):null,provider:String(row.provider??"cloudflare_registrar"),status:String(row.status??"draft"),registrantOwnerType:String(row.registrantOwnerType??row.registrant_owner_type??"customer"),priceAmount:row.priceAmount??row.price_amount?String(row.priceAmount??row.price_amount):null,currency:row.currency?String(row.currency):null,autoRenew:Boolean(row.autoRenew??row.auto_renew??!0),termsVersion:row.termsVersion??row.terms_version?String(row.termsVersion??row.terms_version):null,termsAcceptedAt:row.termsAcceptedAt??row.terms_accepted_at?String(row.termsAcceptedAt??row.terms_accepted_at):null,failureReason:row.failureReason??row.failure_reason?String(row.failureReason??row.failure_reason):null});var init_DomainRegistrationService=__esm(()=>{init_Domain_helpers()});import{and as and3,eq as eq8}from"drizzle-orm";class DomainService{getDb;getRegistry;logger;config;provider;constructor(cfg){this.getDb=cfg.getDb,this.getRegistry=cfg.getTenantRegistry,this.logger=cfg.logger.scoped("domains.service"),this.config=cfg.config,this.provider=cfg.provider}table(name){let registry=this.getRegistry();if(!registry)return null;return registry.getMainContext().schemaTables[name]??null}toProviderInput(record){return{hostname:record.hostname,normalizedHostname:record.normalizedHostname,hostnameKind:record.hostnameKind}}isReserved(normalized){if(this.config.platformBaseDomain&&normalized===this.config.platformBaseDomain)return!0;return this.config.reservedHostnames.includes(normalized)}async createHostname(input){let db=this.getDb(),table=this.table("domainHostnames");if(!db||!table)throw Error("Domain service not ready");let normalized=normalizeHostname(input.hostname);if(!isValidHostname(normalized))throw Error(`Invalid hostname: ${input.hostname}`);if(this.isReserved(normalized))throw Error(`Hostname is reserved: ${normalized}`);let kind=classifyHostname(normalized,this.config.platformBaseDomain);if(kind==="apex"&&!this.config.allowApexDomains)throw Error("Apex domains are not allowed");if(kind==="www"&&!this.config.allowWwwDomains)throw Error("www domains are not allowed");if((await db.select().from(table).where(eq8(col3(table,"normalizedHostname"),normalized)).limit(1))[0])throw Error(`Hostname already registered: ${normalized}`);let providerInput={hostname:input.hostname,normalizedHostname:normalized,hostnameKind:kind},provisioned=await this.provider.createCustomHostname(providerInput),rows=await db.insert(table).values({ownerType:input.ownerType??"tenant",ownerId:input.ownerId,tenantId:input.tenantId??null,schemaName:input.schemaName??null,hostname:input.hostname,normalizedHostname:normalized,hostnameKind:kind,status:"pending_verification",routingStatus:provisioned.routingStatus,certificateStatus:provisioned.certificateStatus,verificationStatus:provisioned.verificationStatus,provider:this.provider.name,providerHostnameId:provisioned.providerHostnameId,dnsTarget:provisioned.dnsTarget,isPrimary:input.isPrimary??!1,metadata:JSON.stringify(input.metadata??{})}).returning(),record=rowToDomainHostname(rows[0]);return await this.persistChallenges(record.id,provisioned.challenges),await this.recordEvent({domainHostnameId:record.id,eventType:"hostname_requested",message:`Custom hostname requested: ${normalized}`,metadata:{provider:this.provider.name,kind}}),{hostname:record,instructions:provisioned.instructions}}async persistChallenges(hostnameId,challenges){let db=this.getDb(),table=this.table("domainVerificationChallenges");if(!db||!table||challenges.length===0)return;let expiresAt=new Date(Date.now()+this.config.verification.challengeTtlSeconds*1000);for(let c of challenges)await db.insert(table).values({domainHostnameId:hostnameId,challengeType:c.challengeType,recordType:c.recordType,recordName:c.recordName,recordValue:c.recordValue,expectedTarget:c.expectedTarget??null,status:"pending",expiresAt})}async listHostnames(filter){let db=this.getDb(),table=this.table("domainHostnames");if(!db||!table)return[];let where=filter.tenantId?eq8(col3(table,"tenantId"),filter.tenantId):filter.ownerId?eq8(col3(table,"ownerId"),filter.ownerId):void 0;return(where?await db.select().from(table).where(where):await db.select().from(table)).map((r)=>rowToDomainHostname(r))}async getHostname(id){let db=this.getDb(),table=this.table("domainHostnames");if(!db||!table)return null;let rows=await db.select().from(table).where(eq8(col3(table,"id"),id)).limit(1);return rows[0]?rowToDomainHostname(rows[0]):null}async getInstructions(id){let record=await this.getHostname(id);if(!record)return[];let db=this.getDb(),table=this.table("domainVerificationChallenges"),instructions=[];if(record.dnsTarget)instructions.push({recordType:record.hostnameKind==="apex"?"A":"CNAME",name:record.normalizedHostname,value:record.dnsTarget,note:record.hostnameKind==="apex"?"Apex domains require an A/ALIAS record at your DNS provider":"Point this hostname at the platform routing target"});if(db&&table){let rows=await db.select().from(table).where(eq8(col3(table,"domainHostnameId"),id));for(let r of rows)instructions.push({recordType:String(r.recordType??"TXT"),name:String(r.recordName??""),value:String(r.recordValue??""),note:String(r.challengeType??"")==="certificate_validation"?"SSL validation":"Ownership verification"})}return instructions}async loadPendingChallenge(hostnameId){let db=this.getDb(),table=this.table("domainVerificationChallenges");if(!db||!table)return null;let row=(await db.select().from(table).where(and3(eq8(col3(table,"domainHostnameId"),hostnameId),eq8(col3(table,"status"),"pending"))).limit(1))[0];if(!row)return null;return{challengeType:String(row.challengeType??"ownership_validation"),recordType:String(row.recordType??"TXT"),recordName:String(row.recordName??""),recordValue:String(row.recordValue??""),expectedTarget:row.expectedTarget!=null?String(row.expectedTarget):void 0}}async verifyHostname(id){let record=await this.getHostname(id);if(!record)return null;let providerInput=this.toProviderInput(record),routingStatus=record.routingStatus,certificateStatus=record.certificateStatus,verificationStatus=record.verificationStatus,failureReason=null;if(this.provider.name==="manual_dns"){let challenge=await this.loadPendingChallenge(id);if(challenge){let result=await this.provider.verifyOwnership(providerInput,challenge);if(verificationStatus=result.verified?"verified":"failed",failureReason=result.verified?null:result.reason??"Verification failed",result.verified)await this.markChallengesVerified(id)}let status2=await this.provider.getCustomHostnameStatus(record.providerHostnameId,providerInput);routingStatus=status2.routingStatus,certificateStatus=status2.certificateStatus}else{let status2=await this.provider.getCustomHostnameStatus(record.providerHostnameId,providerInput);if(routingStatus=status2.routingStatus,certificateStatus=status2.certificateStatus,verificationStatus=status2.verificationStatus,failureReason=status2.failureReason??null,verificationStatus==="verified")await this.markChallengesVerified(id)}let ready=verificationStatus==="verified"&&routingStatus==="active"&&(certificateStatus==="active"||certificateStatus==="not_required"),status=verificationStatus==="failed"?"failed":ready?"active":"verifying";if(await this.applyStatus(id,{status,routingStatus,certificateStatus,verificationStatus,failureReason,activate:ready}),ready)await this.syncRegistry(id);return await this.recordEvent({domainHostnameId:id,eventType:ready?"verification_succeeded":"verification_attempted",message:failureReason??`verification=${verificationStatus} routing=${routingStatus} ssl=${certificateStatus}`}),this.getHostname(id)}async activateHostname(id){let record=await this.getHostname(id);if(!record)return null;if(record.verificationStatus!=="verified")throw Error("Cannot activate a hostname before ownership is verified");return await this.applyStatus(id,{status:"active",routingStatus:"active",certificateStatus:record.certificateStatus==="failed"?"pending":record.certificateStatus,verificationStatus:"verified",failureReason:null,activate:!0}),await this.syncRegistry(id),await this.recordEvent({domainHostnameId:id,eventType:"routing_active",message:"Hostname activated"}),this.getHostname(id)}async disableHostname(id){let db=this.getDb(),table=this.table("domainHostnames"),record=await this.getHostname(id);if(!db||!table||!record)return null;return await this.provider.deleteCustomHostname(record.providerHostnameId),await db.update(table).set({status:"disabled",routingStatus:"inactive",disabledAt:new Date,updatedAt:new Date}).where(eq8(col3(table,"id"),id)),this.getRegistry()?.unregisterDomainHostname(record.normalizedHostname),await this.recordEvent({domainHostnameId:id,eventType:"hostname_disabled",message:"Hostname disabled"}),this.getHostname(id)}async makePrimary(id){let db=this.getDb(),table=this.table("domainHostnames"),record=await this.getHostname(id);if(!db||!table||!record)return null;let scope=record.tenantId?eq8(col3(table,"tenantId"),record.tenantId):eq8(col3(table,"ownerId"),record.ownerId);return await db.update(table).set({isPrimary:!1,updatedAt:new Date}).where(scope),await db.update(table).set({isPrimary:!0,updatedAt:new Date}).where(eq8(col3(table,"id"),id)),await this.recordEvent({domainHostnameId:id,eventType:"primary_changed",message:"Primary hostname set"}),this.getHostname(id)}async refreshStatus(id){return this.verifyHostname(id)}async resolveHostname(hostname){let db=this.getDb(),table=this.table("domainHostnames");if(!db||!table)return null;let normalized=normalizeHostname(hostname),row=(await db.select().from(table).where(and3(eq8(col3(table,"normalizedHostname"),normalized),eq8(col3(table,"status"),"active"))).limit(1))[0];if(!row)return null;let record=rowToDomainHostname(row),redirectToHostname=null;if(record.redirectToHostnameId)redirectToHostname=(await this.getHostname(record.redirectToHostnameId))?.normalizedHostname??null;return{hostnameId:record.id,tenantId:record.tenantId,schemaName:record.schemaName,isPrimary:record.isPrimary,redirectToHostname}}async recordEvent(input){let db=this.getDb(),table=this.table("domainEvents");if(!db||!table)return;try{await db.insert(table).values({domainHostnameId:input.domainHostnameId??null,domainRegistrationId:input.domainRegistrationId??null,eventType:input.eventType,actorType:input.actorType??null,actorId:input.actorId??null,message:input.message??null,ipAddress:input.ipAddress??null,metadata:JSON.stringify(input.metadata??{})})}catch(err){this.logger.warn("Failed to record domain event",{eventType:input.eventType,error:err instanceof Error?err.message:String(err)})}}async markChallengesVerified(hostnameId){let db=this.getDb(),table=this.table("domainVerificationChallenges");if(!db||!table)return;await db.update(table).set({status:"verified",verifiedAt:new Date,updatedAt:new Date}).where(eq8(col3(table,"domainHostnameId"),hostnameId))}async applyStatus(id,patch){let db=this.getDb(),table=this.table("domainHostnames");if(!db||!table)return;await db.update(table).set({status:patch.status,routingStatus:patch.routingStatus,certificateStatus:patch.certificateStatus,verificationStatus:patch.verificationStatus,failureReason:patch.failureReason,activatedAt:patch.activate?new Date:void 0,updatedAt:new Date}).where(eq8(col3(table,"id"),id))}async syncRegistry(id){let record=await this.getHostname(id),registry=this.getRegistry();if(!record||!registry)return;if(record.status==="active"&&record.tenantId)registry.registerDomainHostname(record.normalizedHostname,record.tenantId)}}var col3=(table,name)=>table[name];var init_DomainService=__esm(()=>{init_Domain_helpers()});class CloudflareForSaasProvider{name="cloudflare_for_saas";config;logger;zoneId;apiToken;constructor(config,logger2){if(!config.cloudflare)throw Error("[Domain] Cloudflare for SaaS provider requires domains.cloudflare config");this.config=config,this.logger=logger2.scoped("domains.cloudflare"),this.zoneId=config.cloudflare.zoneId,this.apiToken=config.cloudflare.apiToken}get cnameTarget(){return this.config.cloudflare?.cnameTarget??this.config.cnameTarget}async call(path2,init){let res=await fetch(`https://api.cloudflare.com/client/v4${path2}`,{...init,headers:{Authorization:`Bearer ${this.apiToken}`,"Content-Type":"application/json",...init.headers??{}}}),json=await res.json().catch(()=>({}));if(!res.ok||json.success===!1){let msg=json.errors?.map((e)=>e.message).join("; ")||`HTTP ${res.status}`;throw Error(`Cloudflare API error: ${msg}`)}return json}mapRouting(status){if(status==="active")return"active";if(status==="blocked"||status==="moved"||status==="deleted")return"failed";return"pending"}mapCertificate(sslStatus){if(sslStatus==="active")return"active";if(sslStatus==="validation_timed_out"||sslStatus==="deactivating"||sslStatus==="deleted")return"failed";return"pending"}mapVerification(result){if(result.status==="active")return"verified";if((result.ssl?.validation_errors?.length??0)>0)return"failed";return"pending"}buildChallenges(result){let challenges=[],ov=result.ownership_verification;if(ov?.name&&ov.value)challenges.push({challengeType:"hostname_validation",recordType:"TXT",recordName:ov.name,recordValue:ov.value});for(let vr of result.ssl?.validation_records??[])if(vr.txt_name&&vr.txt_value)challenges.push({challengeType:"certificate_validation",recordType:"TXT",recordName:vr.txt_name,recordValue:vr.txt_value});return challenges}buildInstructions(input,challenges){let target=this.cnameTarget,instructions=[];if(target)instructions.push({recordType:input.hostnameKind==="apex"?"A":"CNAME",name:input.normalizedHostname,value:target,note:input.hostnameKind==="apex"?"Apex domains require an A/ALIAS record pointing at the SaaS target":"Point this hostname at the Cloudflare for SaaS target"});for(let c of challenges)instructions.push({recordType:c.recordType,name:c.recordName,value:c.recordValue,note:c.challengeType==="certificate_validation"?"SSL validation":"Hostname validation"});return instructions}toResult(result,input){let challenges=this.buildChallenges(result);return{providerHostnameId:result.id??null,dnsTarget:this.cnameTarget,routingStatus:this.mapRouting(result.status),certificateStatus:this.mapCertificate(result.ssl?.status),verificationStatus:this.mapVerification(result),instructions:this.buildInstructions(input,challenges),challenges,raw:result}}async createCustomHostname(input){let body=JSON.stringify({hostname:input.normalizedHostname,ssl:{method:"txt",type:"dv",settings:{min_tls_version:"1.2"}}}),json=await this.call(`/zones/${this.zoneId}/custom_hostnames`,{method:"POST",body});return this.toResult(json.result??{},input)}async getCustomHostnameStatus(providerHostnameId,input){let result=await this.fetchHostname(providerHostnameId,input.normalizedHostname);if(!result)return{routingStatus:"failed",certificateStatus:"failed",verificationStatus:"failed",failureReason:"Custom hostname not found at provider"};return{routingStatus:this.mapRouting(result.status),certificateStatus:this.mapCertificate(result.ssl?.status),verificationStatus:this.mapVerification(result),providerStatus:`${result.status??"unknown"}/${result.ssl?.status??"unknown"}`,failureReason:result.ssl?.validation_errors?.map((e)=>e.message).join("; ")||void 0,raw:result}}async fetchHostname(providerHostnameId,normalizedHostname){if(providerHostnameId)return(await this.call(`/zones/${this.zoneId}/custom_hostnames/${providerHostnameId}`,{method:"GET"})).result??null;let list=(await this.call(`/zones/${this.zoneId}/custom_hostnames?hostname=${encodeURIComponent(normalizedHostname)}`,{method:"GET"})).result;return Array.isArray(list)?list[0]??null:null}async deleteCustomHostname(providerHostnameId){if(!providerHostnameId)return;try{await this.call(`/zones/${this.zoneId}/custom_hostnames/${providerHostnameId}`,{method:"DELETE"})}catch(err){this.logger.warn("Failed to delete Cloudflare custom hostname",{providerHostnameId,error:err instanceof Error?err.message:String(err)})}}async verifyOwnership(input,_challenge){let status=await this.getCustomHostnameStatus(null,input);return{verified:status.verificationStatus==="verified",reason:status.failureReason??status.providerStatus}}}class ManualDnsProvider{name="manual_dns";config;constructor(config){this.config=config}buildChallenge(input){let prefix=this.config.verification.txtRecordPrefix,token=`nucleus-domain-verification=${generateChallengeToken()}`;return{challengeType:"ownership_validation",recordType:"TXT",recordName:`${prefix}.${input.normalizedHostname}`,recordValue:token}}buildInstructions(input,challenge){let target=this.config.cnameTarget,instructions=[{recordType:"TXT",name:challenge.recordName,value:challenge.recordValue,note:"Ownership verification record"}];if(input.hostnameKind==="apex")instructions.push({recordType:"A",name:input.normalizedHostname,value:target??"<platform-ingress-ip>",note:"Apex domains require an A/ALIAS record at your DNS provider"});else if(target)instructions.push({recordType:"CNAME",name:input.normalizedHostname,value:target,note:"Point this hostname at the platform routing target"});return instructions}async createCustomHostname(input){let challenge=this.buildChallenge(input);return{providerHostnameId:null,dnsTarget:this.config.cnameTarget,routingStatus:"pending",certificateStatus:"not_required",verificationStatus:"pending",instructions:this.buildInstructions(input,challenge),challenges:[challenge]}}async getCustomHostnameStatus(_providerHostnameId,input){if(input.hostnameKind!=="apex"&&this.config.cnameTarget){let dns2=await verifyCnameRecord(input.normalizedHostname,this.config.cnameTarget);return{routingStatus:dns2.verified?"active":"pending",certificateStatus:"not_required",verificationStatus:"pending",providerStatus:dns2.verified?"cname_active":"cname_pending"}}return{routingStatus:"pending",certificateStatus:"not_required",verificationStatus:"pending",providerStatus:"manual"}}async deleteCustomHostname(_providerHostnameId){}async verifyOwnership(_input,challenge){if(challenge.recordType==="CNAME"&&challenge.expectedTarget)return verifyCnameRecord(challenge.recordName,challenge.expectedTarget);return verifyTxtRecord(challenge.recordName,challenge.recordValue)}}var init_ManualDnsProvider=__esm(()=>{init_Domain_helpers()});var createDomainProvider=(config,logger2)=>{if(config.provider==="cloudflare_for_saas"){if(!config.cloudflare)return logger2.warn("[Domain] cloudflare_for_saas selected but cloudflare config missing \u2014 using manual DNS"),new ManualDnsProvider(config);return new CloudflareForSaasProvider(config,logger2)}return new ManualDnsProvider(config)};var init_Providers=__esm(()=>{init_ManualDnsProvider();init_ManualDnsProvider()});var createDomainServices=(params)=>{let config=resolveDomainsConfig(params.options.domains);if(!config.enabled)return null;let provider=createDomainProvider(config,params.logger),serviceConfig={getDb:params.getDb,getTenantRegistry:params.getTenantRegistry,logger:params.logger,config,provider};return{domainService:new DomainService(serviceConfig),registrationService:new DomainRegistrationService(serviceConfig)}};var init_Domain=__esm(()=>{init_Domain_helpers();init_DomainRegistrationService();init_DomainService();init_Providers();init_Domain_helpers();init_DomainRegistrationService();init_DomainService();init_Providers()});class TemplatedEmailService{emailWrapper(content,appName){return`<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><meta name="color-scheme" content="light dark"><meta name="supported-color-schemes" content="light dark"><title>${appName}</title><style type="text/css">:root{color-scheme:light dark;supported-color-schemes:light dark;}@media(prefers-color-scheme:dark){.em-bg{background-color:#18181b!important;}.em-card{background-color:#27272a!important;border-color:#3f3f46!important;}.em-h{color:#fafafa!important;}.em-t{color:#d4d4d8!important;}.em-m{color:#a1a1aa!important;}.em-hr{background-color:#3f3f46!important;}.em-btn{background-color:#fafafa!important;}.em-btn-text{color:#18181b!important;}.em-link{color:#60a5fa!important;}.em-lb{background-color:#3f3f46!important;border-color:#52525b!important;}.em-lb-t{color:#a1a1aa!important;}.em-sn{background-color:#3f3f46!important;border-color:#52525b!important;color:#a1a1aa!important;}}</style></head><body style="margin:0;padding:0;background-color:#f4f4f5;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;-webkit-font-smoothing:antialiased;"><table role="presentation" class="em-bg" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f4f4f5;"><tr><td align="center" style="padding:40px 16px;"><table role="presentation" width="480" cellpadding="0" cellspacing="0" style="max-width:480px;width:100%;"><tr><td align="center" style="padding-bottom:24px;"><span class="em-m" style="font-size:14px;font-weight:600;color:#71717a;letter-spacing:-0.2px;">${appName}</span></td></tr><tr><td class="em-card" style="background-color:#ffffff;border:1px solid #e4e4e7;border-radius:12px;overflow:hidden;">${content}</td></tr><tr><td align="center" style="padding-top:24px;"><p class="em-m" style="margin:0;font-size:12px;color:#a1a1aa;line-height:1.6;">This email was sent by <strong>${appName}</strong>.<br>Please do not reply to this email.</p></td></tr></table></td></tr></table></body></html>`}emailButton(href,label){return`<table role="presentation" cellpadding="0" cellspacing="0" width="100%"><tr><td align="center" style="padding:28px 32px;"><table role="presentation" cellpadding="0" cellspacing="0"><tr><td class="em-btn" align="center" style="background-color:#18181b;border-radius:8px;"><a class="em-btn-text" href="${href}" target="_blank" style="display:inline-block;padding:12px 32px;font-size:14px;font-weight:600;color:#fafafa;text-decoration:none;letter-spacing:-0.1px;">${label}</a></td></tr></table></td></tr></table>`}emailDivider(){return'<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:0 32px;"><div class="em-hr" style="height:1px;background-color:#e4e4e7;"></div></td></tr></table>'}emailLinkFallback(link,notice){let html=`<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:4px 32px 32px 32px;"><p class="em-m" style="margin:0 0 8px 0;font-size:11px;font-weight:600;color:#a1a1aa;text-transform:uppercase;letter-spacing:0.5px;">Or copy this link</p><table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td class="em-lb" style="background-color:#f4f4f5;border:1px solid #e4e4e7;border-radius:8px;padding:12px 14px;"><a class="em-link" href="${link}" style="font-size:12px;color:#2563eb;word-break:break-all;line-height:1.5;text-decoration:none;">${link}</a></td></tr></table>`;if(notice)html+=`<p class="em-m" style="margin:16px 0 0 0;font-size:12px;color:#a1a1aa;">${notice}</p>`;return html+="</td></tr></table>",html}async sendWelcomeEmail(email,name,appName="Nucleus"){let content=`<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:40px 32px 24px 32px;text-align:center;"><h1 class="em-h" style="margin:0;font-size:24px;font-weight:700;color:#18181b;letter-spacing:-0.5px;">Welcome to ${appName}</h1><p class="em-t" style="margin:8px 0 0 0;font-size:15px;color:#52525b;line-height:1.6;">Your account has been created successfully</p></td></tr></table>${this.emailDivider()}<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:24px 32px 32px 32px;"><p class="em-t" style="margin:0 0 16px 0;font-size:15px;color:#3f3f46;line-height:1.7;">Hello <strong class="em-h" style="color:#18181b;">${name}</strong>,</p><p class="em-t" style="margin:0;font-size:15px;color:#3f3f46;line-height:1.7;">Welcome aboard! You now have full access to <strong>${appName}</strong>. We're excited to have you.</p></td></tr></table>`;return this.sendEmail({to:email,subject:`Welcome to ${appName}`,html:this.emailWrapper(content,appName)})}async sendVerificationEmail(email,name,verificationLink,appName="Nucleus"){let content=`<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:40px 32px 24px 32px;text-align:center;"><h1 class="em-h" style="margin:0;font-size:24px;font-weight:700;color:#18181b;letter-spacing:-0.5px;">Verify your email</h1><p class="em-t" style="margin:8px 0 0 0;font-size:15px;color:#52525b;line-height:1.6;">Confirm your address to get started</p></td></tr></table>${this.emailDivider()}<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:24px 32px 0 32px;"><p class="em-t" style="margin:0 0 4px 0;font-size:15px;color:#3f3f46;line-height:1.7;">Hello <strong class="em-h" style="color:#18181b;">${name}</strong>,</p><p class="em-t" style="margin:0;font-size:15px;color:#3f3f46;line-height:1.7;">Please verify your email address by clicking the button below.</p></td></tr><tr><td>${this.emailButton(verificationLink,"Verify Email Address")}</td></tr></table>${this.emailDivider()}${this.emailLinkFallback(verificationLink)}`;return this.sendEmail({to:email,subject:`Verify your email \u2014 ${appName}`,html:this.emailWrapper(content,appName)})}async sendPasswordResetEmail(email,name,resetLink,appName="Nucleus"){let content=`<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:40px 32px 24px 32px;text-align:center;"><h1 class="em-h" style="margin:0;font-size:24px;font-weight:700;color:#18181b;letter-spacing:-0.5px;">Reset your password</h1><p class="em-t" style="margin:8px 0 0 0;font-size:15px;color:#52525b;line-height:1.6;">We received a request to reset your password</p></td></tr></table>${this.emailDivider()}<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:24px 32px 0 32px;"><p class="em-t" style="margin:0 0 4px 0;font-size:15px;color:#3f3f46;line-height:1.7;">Hello <strong class="em-h" style="color:#18181b;">${name}</strong>,</p><p class="em-t" style="margin:0;font-size:15px;color:#3f3f46;line-height:1.7;">Click the button below to choose a new password. If you didn't make this request, you can safely ignore this email.</p></td></tr><tr><td>${this.emailButton(resetLink,"Reset Password")}</td></tr></table>${this.emailDivider()}${this.emailLinkFallback(resetLink,"This link expires in 1 hour and can only be used once.")}`;return this.sendEmail({to:email,subject:`Reset your password \u2014 ${appName}`,html:this.emailWrapper(content,appName)})}async sendMagicLinkEmail(email,magicLink,appName="Nucleus"){let content=`<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:40px 32px 24px 32px;text-align:center;"><h1 class="em-h" style="margin:0;font-size:24px;font-weight:700;color:#18181b;letter-spacing:-0.5px;">Sign in to ${appName}</h1><p class="em-t" style="margin:8px 0 0 0;font-size:15px;color:#52525b;line-height:1.6;">No password needed \u2014 just click to sign in</p></td></tr></table>${this.emailDivider()}<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:24px 32px 0 32px;"><p class="em-t" style="margin:0;font-size:15px;color:#3f3f46;line-height:1.7;">Click the button below to securely sign in to your <strong class="em-h" style="color:#18181b;">${appName}</strong> account.</p></td></tr><tr><td>${this.emailButton(magicLink,"Sign In")}</td></tr></table>${this.emailDivider()}${this.emailLinkFallback(magicLink,"This link expires in 15 minutes and can only be used once.")}`;return this.sendEmail({to:email,subject:`Sign in to ${appName}`,html:this.emailWrapper(content,appName)})}async sendAlertEmail(to,subject,html){return this.sendEmail({to,subject,html})}async sendInvitationEmail(email,inviteLink,appName="Nucleus"){let content=`<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:40px 32px 24px 32px;text-align:center;"><h1 class="em-h" style="margin:0;font-size:24px;font-weight:700;color:#18181b;letter-spacing:-0.5px;">You're Invited</h1><p class="em-t" style="margin:8px 0 0 0;font-size:15px;color:#52525b;line-height:1.6;">Join <strong class="em-h" style="color:#18181b;">${appName}</strong> and start collaborating</p></td></tr></table>${this.emailDivider()}<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:0 32px 8px 32px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:8px 0;"><table role="presentation" cellpadding="0" cellspacing="0"><tr><td class="em-sn" style="width:28px;height:28px;background-color:#f4f4f5;border:1px solid #e4e4e7;border-radius:50%;text-align:center;line-height:28px;font-size:12px;font-weight:600;color:#71717a;">1</td><td class="em-t" style="padding-left:14px;font-size:14px;color:#52525b;">Click the button below</td></tr></table></td></tr><tr><td style="padding:8px 0;"><table role="presentation" cellpadding="0" cellspacing="0"><tr><td class="em-sn" style="width:28px;height:28px;background-color:#f4f4f5;border:1px solid #e4e4e7;border-radius:50%;text-align:center;line-height:28px;font-size:12px;font-weight:600;color:#71717a;">2</td><td class="em-t" style="padding-left:14px;font-size:14px;color:#52525b;">Choose a secure password</td></tr></table></td></tr><tr><td style="padding:8px 0;"><table role="presentation" cellpadding="0" cellspacing="0"><tr><td class="em-sn" style="width:28px;height:28px;background-color:#f4f4f5;border:1px solid #e4e4e7;border-radius:50%;text-align:center;line-height:28px;font-size:12px;font-weight:600;color:#71717a;">3</td><td class="em-t" style="padding-left:14px;font-size:14px;color:#52525b;">Start collaborating</td></tr></table></td></tr></table></td></tr></table>${this.emailButton(inviteLink,"Accept Invitation")}${this.emailDivider()}${this.emailLinkFallback(inviteLink,"This invitation expires in 7 days.")}`;return this.sendEmail({to:email,subject:`You're invited to join ${appName}`,html:this.emailWrapper(content,appName)})}}var AzureEmailService;var init_AzureEmailService=__esm(()=>{AzureEmailService=class AzureEmailService extends TemplatedEmailService{client=null;senderAddress;fromName;logger;initialized=!1;credentialsFingerprint="";constructor(config,logger2){super();if(this.senderAddress=config.senderAddress,this.fromName=config.fromName||"Nucleus",this.logger=logger2,config.enabled)this.credentialsFingerprint=`${config.connectionString??""}|${config.senderAddress}`,this.initialize(config.connectionString)}reconfigure(config){let fingerprint=`${config.connectionString??""}|${config.senderAddress}`;if(fingerprint===this.credentialsFingerprint&&this.initialized)return;if(this.credentialsFingerprint=fingerprint,this.senderAddress=config.senderAddress||this.senderAddress,config.fromName)this.fromName=config.fromName;if(this.initialized=!1,this.client=null,config.enabled)this.initialize(config.connectionString)}isAvailable(){return this.initialized&&this.client!==null}initialize(connectionString){try{if(!connectionString){this.logger.warn("[AzureEmailService] No connection string provided");return}if(!this.senderAddress){this.logger.warn("[AzureEmailService] No sender address configured");return}let{EmailClient}=__require("@azure/communication-email");this.client=new EmailClient(connectionString),this.initialized=!0,this.logger.info("[AzureEmailService] Initialized",{senderAddress:this.senderAddress})}catch(error){let message=error instanceof Error?error.message:String(error);this.logger.error("[AzureEmailService] Initialization failed",{error:message})}}getSenderAddress(){return this.senderAddress}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],bccAddresses=payload.bcc?Array.isArray(payload.bcc)?payload.bcc:[payload.bcc]:[],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})),...bccAddresses.length?{bcc:bccAddresses.map((addr)=>({address:addr}))}:{}},...payload.replyTo?{replyTo:[{address:payload.replyTo}]}:{},attachments:payload.attachments?.map((att)=>({name:att.filename,contentType:att.contentType,contentInBase64:att.content.toString("base64")}))})).pollUntilDone();if(result.status==="Succeeded")return this.logger.info("[AzureEmailService] Email sent",{to:payload.to,subject:payload.subject,operationId:result.id}),{success:!0};let errorMsg=result.error?.message||`Send failed with status: ${result.status}`;return this.logger.error("[AzureEmailService] Send failed",{status:result.status,error:errorMsg,to:payload.to}),{success:!1,error:errorMsg}}catch(error){let message=error instanceof Error?error.message:"Unknown error";return this.logger.error("[AzureEmailService] Send failed",{error:message,to:payload.to}),{success:!1,error:message}}}}});import{createRequire}from"module";function stripAppPasswordGroups(password2){return APP_PASSWORD_SHAPE.test(password2)?password2.replace(/\s+/g,""):password2.trim()}function explainSmtpFailure(message){let text=message.toLowerCase();if(text.includes("5.7.3")||text.includes("authentication unsuccessful"))return"Sunucu kimlik do\u011Frulamay\u0131 reddetti. En s\u0131k sebebi parola de\u011Fil: Exchange Online'da "+"SMTP AUTH kirac\u0131 d\xFCzeyinde varsay\u0131lan olarak KAPALIDIR ve do\u011Fru parolay\u0131 da reddeder. "+"IT'den o posta kutusu i\xE7in \u201CAuthenticated SMTP\u201Dyi a\xE7mas\u0131n\u0131 isteyin. Di\u011Fer ihtimal, "+"uygulama parolas\u0131n\u0131n \u201CPosta kutusu\u201D alan\u0131na yaz\u0131lan adresten ba\u015Fka bir hesaba ait olmas\u0131. "+`Sunucunun kendi yan\u0131t\u0131: ${message}`;if(text.includes("5.7.57")||text.includes("must issue a starttls"))return`Sunucu \u015Fifreli ba\u011Flant\u0131 istiyor. Portu 587 (STARTTLS) veya 465 olarak deneyin. Sunucunun yan\u0131t\u0131: ${message}`;if(text.includes("enotfound")||text.includes("eai_again"))return`Sunucu ad\u0131 \xE7\xF6z\xFClemedi \u2014 \u201CSunucu\u201D alan\u0131n\u0131 kontrol edin (Microsoft 365 i\xE7in smtp.office365.com). Ayr\u0131nt\u0131: ${message}`;if(text.includes("etimedout")||text.includes("econnrefused"))return`Sunucuya ba\u011Flan\u0131lamad\u0131. A\u011F bu porta \xE7\u0131k\u0131\u015Fa izin vermiyor olabilir. Ayr\u0131nt\u0131: ${message}`;if(text.includes("5.7.60")||text.includes("send as"))return`G\xF6nderen adresi bu posta kutusuna ait de\u011Fil. Bo\u015F b\u0131rak\u0131n ya da Exchange'de \u201CSend As\u201D yetkisi verdirin. Sunucunun yan\u0131t\u0131: ${message}`;return message}var APP_PASSWORD_SHAPE,IMPLICIT_TLS_PORT=465,DEFAULT_PORT=587,SmtpEmailService;var init_SmtpEmailService=__esm(()=>{APP_PASSWORD_SHAPE=/^\s*(?:[A-Za-z0-9]{4}\s+){3}[A-Za-z0-9]{4}\s*$/;SmtpEmailService=class SmtpEmailService extends TemplatedEmailService{transporter=null;senderAddress;fromName;logger;initialized=!1;credentialsFingerprint="";constructor(config,logger2){super();this.senderAddress=config.senderAddress,this.fromName=config.fromName||"Nucleus",this.logger=logger2,this.applyConfig(config)}applyConfig(config){let fingerprint=[config.host??"",String(config.port??""),String(config.secure??""),config.user??"",config.password??"",config.senderAddress??"",config.fromName??"",String(config.enabled)].join("|");if(fingerprint===this.credentialsFingerprint&&this.initialized)return;if(this.credentialsFingerprint=fingerprint,this.senderAddress=config.senderAddress||this.senderAddress,config.fromName)this.fromName=config.fromName;if(this.initialized=!1,this.transporter=null,config.enabled)this.initialize(config)}isAvailable(){return this.initialized&&this.transporter!==null}initialize(config){try{let host=config.host?.trim();if(!host){this.logger.warn("[SmtpEmailService] No host configured");return}let user=config.user?.trim();if(!user||!config.password){this.logger.warn("[SmtpEmailService] No credentials configured");return}if(!this.senderAddress)this.senderAddress=user;let port=config.port??DEFAULT_PORT,nodemailer=createRequire(import.meta.url)("nodemailer"),secure=config.secure??port===IMPLICIT_TLS_PORT,pass=stripAppPasswordGroups(config.password);this.transporter=nodemailer.createTransport({host,port,secure,requireTLS:!secure,tls:{minVersion:"TLSv1.2"},auth:{user,pass},pool:!1,connectionTimeout:20000,greetingTimeout:20000,socketTimeout:60000}),this.initialized=!0,this.logger.info("[SmtpEmailService] Initialized",{host,port,senderAddress:this.senderAddress})}catch(error){let message=error instanceof Error?error.message:String(error);this.logger.error("[SmtpEmailService] Initialization failed \u2014 is `nodemailer` installed in this app?",{error:message,host:config.host,port:config.port??DEFAULT_PORT})}}sanitizeHeaderValue(value){return value.replace(/[\r\n\u0000-\u001f\u007f]+/g," ").trim()}getSenderAddress(){return this.senderAddress}async sendEmail(payload){if(!this.transporter)return this.logger.error("[SmtpEmailService] Not initialized"),{success:!1,error:"SMTP email service not initialized"};try{let address=this.sanitizeHeaderValue(payload.from||this.senderAddress),from=this.fromName?`"${this.sanitizeHeaderValue(this.fromName).replace(/"/g,"")}" <${address}>`:address,to=(Array.isArray(payload.to)?payload.to:[payload.to]).map((a)=>this.sanitizeHeaderValue(a)),bcc=payload.bcc?(Array.isArray(payload.bcc)?payload.bcc:[payload.bcc]).map((a)=>this.sanitizeHeaderValue(a)):void 0,result=await this.transporter.sendMail({from,to,...bcc?.length?{bcc}:{},subject:this.sanitizeHeaderValue(payload.subject),text:payload.text,html:payload.html,replyTo:payload.replyTo?this.sanitizeHeaderValue(payload.replyTo):void 0,attachments:payload.attachments?.map((att)=>({filename:att.filename,content:att.content,contentType:att.contentType}))}),rejected=(result.rejected??[]).filter(Boolean);if(rejected.length>0)return this.logger.warn("[SmtpEmailService] Some recipients were refused",{rejected,accepted:result.accepted?.length??0,subject:payload.subject}),{success:(result.accepted?.length??0)>0,rejected,error:`Bu adresler sunucu taraf\u0131ndan reddedildi: ${rejected.join(", ")}`};return this.logger.info("[SmtpEmailService] Email sent",{to:payload.to,subject:payload.subject,messageId:result.messageId}),{success:!0}}catch(error){let message=error instanceof Error?error.message:"Unknown error";return this.logger.error("[SmtpEmailService] Send failed",{error:message,to:payload.to}),{success:!1,error:explainSmtpFailure(message)}}}}});var init_Email=__esm(()=>{init_AzureEmailService();init_SmtpEmailService()});import fs2 from"fs";function loadGoogle(){if(!cachedGoogle)try{cachedGoogle=__require("googleapis").google}catch(error){throw Error("Gmail e-posta sa\u011Flay\u0131c\u0131s\u0131 i\xE7in `googleapis` paketi gerekiyor. "+"nucleus-core onu iste\u011Fe ba\u011Fl\u0131 tutuyor (206 MB, yaln\u0131z Gmail i\xE7in). "+"Kurulumun package.json dosyas\u0131na `googleapis` ekleyip yeniden kurun. "+`As\u0131l hata: ${error instanceof Error?error.message:String(error)}`)}return cachedGoogle}class GmailService{gmail=null;fromEmail;fromName;logger;initialized=!1;credentialsFingerprint="";constructor(config,logger2){if(this.fromEmail=config.fromEmail,this.fromName=config.fromName||"Vorion",this.logger=logger2,config.enabled)this.initialize(config)}reconfigure(config){if(`${config.serviceAccountJson??""}|${config.jsonFilePath??""}|${config.fromEmail}`===this.credentialsFingerprint&&this.initialized)return;if(this.fromEmail=config.fromEmail||this.fromEmail,config.fromName)this.fromName=config.fromName;if(this.initialized=!1,this.gmail=null,config.enabled)this.initialize(config)}sanitizeHeaderValue(value){return value.replace(/[\r\n\u0000-\u001f\u007f]+/g," ").trim()}encodeHeaderValue(rawValue){let value=this.sanitizeHeaderValue(rawValue),hasNonAscii=!1;for(let i=0;i<value.length;i++)if(value.charCodeAt(i)>127){hasNonAscii=!0;break}if(!hasNonAscii)return value;return`=?UTF-8?B?${Buffer.from(value,"utf-8").toString("base64")}?=`}isAvailable(){return this.initialized&&this.gmail!==null}initialize(config){let{serviceAccountJson,jsonFilePath}=config;this.credentialsFingerprint=`${serviceAccountJson??""}|${jsonFilePath??""}|${config.fromEmail}`;try{let keyContent;if(serviceAccountJson?.trim())keyContent=serviceAccountJson;else if(jsonFilePath){if(!fs2.existsSync(jsonFilePath)){this.logger.error("[GmailService] JSON file not found",{path:jsonFilePath});return}keyContent=fs2.readFileSync(jsonFilePath,"utf8")}else{this.logger.warn("[GmailService] No service-account JSON or file path provided");return}let credentials;try{credentials=JSON.parse(keyContent)}catch{this.logger.error("[GmailService] Service-account JSON is not valid JSON",{source:serviceAccountJson?.trim()?"inline":"file"});return}if(!this.fromEmail){this.logger.warn("[GmailService] From email not configured");return}if(!credentials?.client_email||!credentials?.private_key){this.logger.error("[GmailService] Invalid credentials format");return}let privateKey=credentials.private_key;if(!privateKey.includes(`
|
|
34
34
|
`)&&privateKey.includes("\\n"))privateKey=privateKey.replace(/\\n/g,`
|
|
35
35
|
`);let google=loadGoogle(),auth=new google.auth.JWT({email:credentials.client_email,key:privateKey,scopes:["https://www.googleapis.com/auth/gmail.send"],subject:this.fromEmail});this.gmail=google.gmail({version:"v1",auth}),this.initialized=!0,this.logger.info("[GmailService] Initialized",{serviceAccount:credentials.client_email,fromEmail:this.fromEmail})}catch(error){this.logger.error("[GmailService] Initialization failed",{error})}}createEmailMessage(payload){let{to,subject,html,text,from,replyTo,attachments}=payload,toAddresses=this.sanitizeHeaderValue(Array.isArray(to)?to.map((address)=>this.sanitizeHeaderValue(address)).join(", "):to),fromAddress=this.sanitizeHeaderValue(from||`${this.fromName} <${this.fromEmail}>`),encodedSubject=this.encodeHeaderValue(subject);if(!attachments||attachments.length===0){let messageParts2=[`From: ${fromAddress}`,`To: ${toAddresses}`,`Subject: ${encodedSubject}`,"MIME-Version: 1.0","Content-Type: text/html; charset=utf-8"];if(replyTo)messageParts2.push(`Reply-To: ${this.sanitizeHeaderValue(replyTo)}`);return messageParts2.push("",html||text||""),Buffer.from(messageParts2.join(`
|
|
36
36
|
`)).toString("base64").replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}let boundary=`----Nucleus_${Date.now()}_${Math.random().toString(36).substring(7)}`,messageParts=[`From: ${fromAddress}`,`To: ${toAddresses}`,`Subject: ${encodedSubject}`,"MIME-Version: 1.0",`Content-Type: multipart/mixed; boundary="${boundary}"`];if(replyTo)messageParts.push(`Reply-To: ${this.sanitizeHeaderValue(replyTo)}`);messageParts.push("",`--${boundary}`),messageParts.push("Content-Type: text/html; charset=utf-8","Content-Transfer-Encoding: base64","",Buffer.from(html||text||"").toString("base64"),"");for(let attachment of attachments)messageParts.push(`--${boundary}`,`Content-Type: ${attachment.contentType}`,`Content-Disposition: attachment; filename="${attachment.filename}"`,"Content-Transfer-Encoding: base64","",attachment.content.toString("base64"),"");return messageParts.push(`--${boundary}--`),Buffer.from(messageParts.join(`\r
|
|
@@ -550,7 +550,7 @@ data: ${payload}
|
|
|
550
550
|
`)};function createLiveMonitoringRoutes(config){let{getService,getLifecycleRecorder,logger:logger2,basePath,streamInterval,db,schemaTables}=config,plugin=new Elysia15({prefix:basePath});plugin.onBeforeHandle(createGodminGuard({db:db??null,schemaTables:schemaTables??{},logger:logger2},"live monitoring"));let requireService=()=>{let svc=getService();if(!svc)throw Error("Live monitoring service not initialized");return svc};return plugin.get("/health",()=>{let svc=getService();return{status:svc?"ok":"initializing",timestamp:Date.now(),monitoring:svc?.isEnabled()??!1}},{detail:{tags:["Live Monitoring"],summary:"Health check for live monitoring"}}),plugin.get("/settings",()=>{return requireService().getSettings()},{detail:{tags:["Live Monitoring"],summary:"Get live monitoring settings"}}),plugin.patch("/settings",({body})=>{return requireService().changeSettings(body)},{body:t13.Partial(t13.Object({logMemory:t13.Boolean(),logCpu:t13.Boolean(),logDapr:t13.Boolean(),logWebSocket:t13.Boolean(),memoryLogInterval:t13.Number(),cpuLogInterval:t13.Number(),memoryLogLimit:t13.Number(),cpuLogLimit:t13.Number(),daprLogLimit:t13.Number(),wsLogLimit:t13.Number(),requestLogLimit:t13.Number()})),detail:{tags:["Live Monitoring"],summary:"Change live monitoring settings"}}),plugin.get("/incidents",async({query})=>{let raw=Number(query?.limit),limit=Number.isFinite(raw)&&raw>0?Math.min(raw,50):20,recorder=getLifecycleRecorder?.();if(!recorder)return{available:!1,incidents:[]};return{available:!0,incidents:await recorder.incidents(limit)}},{query:t13.Optional(t13.Object({limit:t13.Optional(t13.String())})),detail:{tags:["Live Monitoring"],summary:"Why previous runs of this process ended"}}),plugin.get("/realtime",({query})=>{let raw=Number(query?.windowMinutes),minutes=Number.isFinite(raw)&&raw>0?Math.min(raw,1440):void 0;return requireService().getRealtimeHealth(minutes?minutes*60000:void 0)},{query:t13.Optional(t13.Object({windowMinutes:t13.Optional(t13.String())})),detail:{tags:["Live Monitoring"],summary:"Realtime (WebSocket) health digest"}}),plugin.get("/logs",()=>{return requireService().getLogs()},{detail:{tags:["Live Monitoring"],summary:"Get all live monitoring logs"}}),plugin.get("/logs/stream",({request})=>{let abortSignal=request.signal,cleanup,svc=requireService(),snapshot=svc.getSnapshot(),lastTimestamps={memory:snapshot.memory.length?snapshot.memory[snapshot.memory.length-1]?.timestamp??0:0,cpu:snapshot.cpu.length?snapshot.cpu[snapshot.cpu.length-1]?.timestamp??0:0,request:snapshot.requests.length?snapshot.requests[snapshot.requests.length-1]?.timestamp??0:0,dapr:snapshot.dapr.length?snapshot.dapr[snapshot.dapr.length-1]?.timestamp??0:0,ws:snapshot.ws.length?snapshot.ws[snapshot.ws.length-1]?.timestamp??0:0},stream=new ReadableStream({start(controller){let closed=!1;controller.enqueue(encodeEvent("snapshot",snapshot));let interval=setInterval(()=>{if(closed)return;let update=svc.getUpdatesSince(lastTimestamps);if(!update){controller.enqueue(encodeEvent("heartbeat",{timestamp:Date.now()}));return}if(update.memory.length)lastTimestamps.memory=update.memory[update.memory.length-1]?.timestamp??lastTimestamps.memory;if(update.cpu.length)lastTimestamps.cpu=update.cpu[update.cpu.length-1]?.timestamp??lastTimestamps.cpu;if(update.requests.length)lastTimestamps.request=update.requests[update.requests.length-1]?.timestamp??lastTimestamps.request;if(update.dapr.length)lastTimestamps.dapr=update.dapr[update.dapr.length-1]?.timestamp??lastTimestamps.dapr;if(update.ws.length)lastTimestamps.ws=update.ws[update.ws.length-1]?.timestamp??lastTimestamps.ws;controller.enqueue(encodeEvent("update",update))},streamInterval),cleanupHandler=()=>{if(closed)return;closed=!0,clearInterval(interval),abortSignal?.removeEventListener("abort",cleanupHandler);try{controller.close()}catch{}};cleanup=cleanupHandler,abortSignal?.addEventListener("abort",cleanupHandler)},cancel(){cleanup?.()}});return new Response(stream,{headers:STREAM_HEADERS})},{detail:{tags:["Live Monitoring"],summary:"Stream real-time live monitoring data via SSE"}}),logger2.info(`[LiveMonitoring] Routes enabled at ${basePath} (stream interval: ${streamInterval}ms)`),plugin}init_adminGuard();import Elysia16,{t as t14}from"elysia";var STREAM_HEADERS2={"Content-Type":"text/event-stream; charset=utf-8","Cache-Control":"no-cache, no-transform",Connection:"keep-alive"},textEncoder2=new TextEncoder,encodeEvent2=(event,data)=>textEncoder2.encode(`event: ${event}
|
|
551
551
|
data: ${JSON.stringify(data)}
|
|
552
552
|
|
|
553
|
-
`),LOG_LEVELS=["debug","info","warn","error","fatal"],SOURCES=["logger","stdout","stderr"];function parseLevels(raw){if(!raw)return;let parsed=raw.split(",").map((value)=>value.trim().toLowerCase()).filter((value)=>LOG_LEVELS.includes(value));return parsed.length>0?parsed:void 0}function parseSource(raw){if(!raw)return;let value=raw.trim().toLowerCase();return SOURCES.includes(value)?value:void 0}function parseMinLevel(raw){if(!raw)return;let value=raw.trim().toLowerCase();return LOG_LEVELS.includes(value)?value:void 0}function parseCursor(raw){if(raw===void 0||raw===null||raw==="")return;let value=Number(raw);return Number.isFinite(value)&&value>=0?value:void 0}function buildQuery(query){return{levels:parseLevels(query.levels),minLevel:parseMinLevel(query.minLevel),scope:query.scope?.trim()||void 0,search:query.search||void 0,source:parseSource(query.source),after:parseCursor(query.after),before:parseCursor(query.before),limit:parseCursor(query.limit)}}var STREAM_KEEPALIVE_MS=25000;function createServerLogRoutes(config){let{buffer,logger:logger2,basePath,path:path8,allowRoles,streamEnabled=!0,clearEnabled=!0,db,schemaTables}=config,plugin=new Elysia16({prefix:basePath});if(plugin.onBeforeHandle(createPrivilegedGuard({db:db??null,schemaTables:schemaTables??{},logger:logger2},"server logs",{allowRoles})),plugin.get(path8,({query})=>{return{isSuccess:!0,message:"Server logs",data:{...buffer.query(buildQuery(query)),scopes:buffer.scopes(),levelCounts:buffer.levelCounts()}}},{query:t14.Object({levels:t14.Optional(t14.String()),minLevel:t14.Optional(t14.String()),scope:t14.Optional(t14.String()),search:t14.Optional(t14.String()),source:t14.Optional(t14.String()),after:t14.Optional(t14.Numeric()),before:t14.Optional(t14.Numeric()),limit:t14.Optional(t14.Numeric())}),detail:{tags:["Monitoring"],summary:"Read buffered server logs",description:"Filtered, cursor-paged view of the in-memory server log buffer. Use `after` to tail forward and `before` to page backwards."}}),streamEnabled)plugin.get(`${path8}/stream`,({request,query})=>{let tailQuery=buildQuery(query),{after,before:_before,limit:_limit,...filter}=tailQuery,abortSignal=request.signal,cleanup,stream=new ReadableStream({start(controller){let closed=!1,backlog=buffer.query({...filter,...after!==void 0?{after}:{},limit:200});controller.enqueue(encodeEvent2("backlog",backlog));let unsubscribe=buffer.subscribe((record)=>{if(closed)return;if(!buffer.query({...filter,after:record.seq-1,limit:1}).records.some((r)=>r.seq===record.seq))return;try{controller.enqueue(encodeEvent2("logs",[record]))}catch{cleanup?.()}}),heartbeat=setInterval(()=>{if(closed)return;try{controller.enqueue(encodeEvent2("ping",{timestamp:Date.now()}))}catch{cleanup?.()}},STREAM_KEEPALIVE_MS),cleanupHandler=()=>{if(closed)return;closed=!0,clearInterval(heartbeat),unsubscribe(),abortSignal?.removeEventListener("abort",cleanupHandler);try{controller.close()}catch{}};cleanup=cleanupHandler,abortSignal?.addEventListener("abort",cleanupHandler)},cancel(){cleanup?.()}});return new Response(stream,{headers:STREAM_HEADERS2})},{query:t14.Object({levels:t14.Optional(t14.String()),minLevel:t14.Optional(t14.String()),scope:t14.Optional(t14.String()),search:t14.Optional(t14.String()),source:t14.Optional(t14.String()),after:t14.Optional(t14.Numeric())}),detail:{tags:["Monitoring"],summary:"Tail server logs",description:"Server-Sent Events tail of the server log buffer, filtered server-side."}});if(clearEnabled)plugin.delete(path8,()=>{return buffer.clear(),logger2.info("[ServerLogs] Buffer cleared from the admin panel"),{isSuccess:!0,message:"Server log buffer cleared",data:null}},{detail:{tags:["Monitoring"],summary:"Clear the server log buffer"}});return logger2.info(`[ServerLogs] Routes enabled at ${basePath}${path8} (capacity: ${buffer.getCapacity()}, stream: ${streamEnabled})`),plugin}init_Notification();init_adminGuard();import{Elysia as Elysia17,t as t15}from"elysia";var CHANNELS=["portal","email","telegram","webhook"];function createNotificationRoutes(routeConfig){let{db,schemaTables,config,logger:logger2,tenantRegistry,getEmailService}=routeConfig,basePath=routeConfig.basePath||config.endpoints?.basePath||"/notifications",buildService=(tables)=>new NotificationService({db,schemaTables:tables,config,logger:logger2,getEmailService}),defaultService=buildService(schemaTables),serviceFor=(request)=>{if(!tenantRegistry)return defaultService;let schemaName=request.headers.get("x-tenant-schema");if(!schemaName)return defaultService;let ctx=tenantRegistry.getSchemaContext(schemaName);return ctx?buildService(ctx.schemaTables):defaultService},allowedSendRoles=new Set((config.endpoints?.sendRoles??[]).map((role)=>role.toLowerCase())),canSend=async(request)=>{if(allowedSendRoles.size>0){if(decodeHeaderList(request.headers.get("x-user-roles")).some((role)=>allowedSendRoles.has(role.toLowerCase())))return!0}return isGodminRequest(request,{db,schemaTables,logger:logger2})},routes=new Elysia17({prefix:basePath}),requireUser=(request,set)=>{let userId=request.headers.get("x-user-id");if(!userId)return set.status=401,null;return userId};return routes.get("/",async({request,query,set})=>{let userId=requireUser(request,set);if(!userId)return{success:!1,message:"User ID required"};return{success:!0,data:await serviceFor(request).getNotifications(userId,{limit:query.limit?Number(query.limit):void 0,offset:query.offset?Number(query.offset):void 0,type:query.type||void 0})}},{query:t15.Object({limit:t15.Optional(t15.String()),offset:t15.Optional(t15.String()),type:t15.Optional(t15.String())}),detail:{tags:["Notifications"],summary:"List the caller\u2019s notifications"}}),routes.get("/unseen-count",async({request,set})=>{let userId=requireUser(request,set);if(!userId)return{success:!1,message:"User ID required"};return{success:!0,data:{count:await serviceFor(request).getUnseenCount(userId)}}},{detail:{tags:["Notifications"],summary:"Unseen notification count"}}),routes.post("/:notification_id/seen",async({params,request,set})=>{let userId=requireUser(request,set);if(!userId)return{success:!1,message:"User ID required"};let marked=await serviceFor(request).markAsSeen(params.notification_id,userId);return{success:marked,message:marked?"Marked as seen":"Failed"}},{params:t15.Object({notification_id:t15.String()}),detail:{tags:["Notifications"],summary:"Mark one notification as seen"}}),routes.post("/seen-all",async({request,set})=>{let userId=requireUser(request,set);if(!userId)return{success:!1,message:"User ID required"};return{success:!0,data:{count:await serviceFor(request).markAllAsSeen(userId)}}},{detail:{tags:["Notifications"],summary:"Mark every notification as seen"}}),routes.post("/send",async({body,request,set})=>{if(!requireUser(request,set))return{success:!1,message:"Authentication required"};if(!await canSend(request))return set.status=403,{success:!1,message:"Sending notifications requires godmin or an allowed role"};let recipients=body.userIds??(body.userId?[body.userId]:[]);if(recipients.length===0)return set.status=400,{success:!1,message:"userId or userIds is required"};let channels=body.channels??[config.defaultChannel??"portal"],service=serviceFor(request),failed=(await Promise.allSettled(recipients.map((user_id)=>service.send({user_id,title:body.title,body:body.body,entity_name:body.entityName,entity_id:body.entityId,type:body.type??"custom",source:body.source,channels})))).map((result,index)=>({result,userId:recipients[index]})).filter((entry)=>entry.result.status==="rejected").map((entry)=>entry.userId);return{success:failed.length===0,message:failed.length===0?`Sent to ${recipients.length} recipient(s)`:`Sent to ${recipients.length-failed.length}, failed for ${failed.length}`,data:{sent:recipients.length-failed.length,failed}}},{body:t15.Object({userId:t15.Optional(t15.String()),userIds:t15.Optional(t15.Array(t15.String())),title:t15.String(),body:t15.Optional(t15.String()),entityName:t15.Optional(t15.String()),entityId:t15.Optional(t15.String()),type:t15.Optional(t15.Union([t15.Literal("verification"),t15.Literal("system"),t15.Literal("custom")])),source:t15.Optional(t15.String()),channels:t15.Optional(t15.Array(t15.Union(CHANNELS.map((c)=>t15.Literal(c)))))}),detail:{tags:["Notifications"],summary:"Send a notification to one or more users",description:"Privileged. Delivers over the requested channels (portal, email, telegram, webhook). Email uses the configured provider \u2014 credentials resolve through the secrets store, so a rotation applies immediately."}}),routes.post("/email",async({body,request,set})=>{if(!requireUser(request,set))return{success:!1,message:"Authentication required"};if(!await canSend(request))return set.status=403,{success:!1,message:"Sending email requires godmin or an allowed role"};let emailService=getEmailService?.()??null;if(!emailService?.isAvailable())return set.status=503,{success:!1,message:"No email provider is configured or available"};let result=await emailService.sendEmail({to:body.to,subject:body.subject,html:body.html,text:body.text,replyTo:body.replyTo});if(!result.success)return set.status=502,{success:!1,message:result.error??"Email delivery failed"};return logger2.info("[Notification] Transactional email sent",{recipients:Array.isArray(body.to)?body.to.length:1,subject:body.subject}),{success:!0,message:"Email sent",data:null}},{body:t15.Object({to:t15.Union([t15.String(),t15.Array(t15.String())]),subject:t15.String(),html:t15.Optional(t15.String()),text:t15.Optional(t15.String()),replyTo:t15.Optional(t15.String())}),detail:{tags:["Notifications"],summary:"Send a transactional email to explicit recipients",description:"Backend-to-backend send through the configured provider \u2014 an app never needs an email SDK or the provider credentials of its own. Privileged, same gate as /send."}}),{routes,notificationService:defaultService}}init_ack_manager();import{timingSafeEqual as timingSafeEqual3}from"crypto";import Elysia18 from"elysia";init_ack_manager();var clients=new Map,presenceBroadcastDebounce=new Map,DEFAULT_MAX_TOPICS_PER_CLIENT=512;function createClientManager(config){let{redis,logger:logger2}=config,ackTtl=config.ack.ttlSeconds,ackEnabled=config.ack.enabled,maxRetries=config.ack.maxRetries,presenceEnabled=config.presence.enabled,presenceDebounceMs=config.presence.debounceMs,maxClientsPerUser=config.maxClientsPerUser,maxTopicsPerClient=config.maxTopicsPerClient??DEFAULT_MAX_TOPICS_PER_CLIENT,MAX_TOPIC_LENGTH=256;function generateClientId(){return`client_${Date.now()}_${Math.random().toString(36).substring(2,9)}`}function capTopics(topics){let accepted=[],dropped=[],seen=new Set;for(let t16 of topics){if(typeof t16!=="string")continue;let trimmed=t16.length>256?t16.slice(0,256):t16;if(seen.has(trimmed))continue;if(seen.add(trimmed),accepted.length>=maxTopicsPerClient){dropped.push(trimmed);continue}accepted.push(trimmed)}return{accepted,dropped}}function reportDropped(context,clientId,userId,dropped){if(dropped.length===0)return;logger2.warn("[PubSub] Topic subscription capped \u2014 events for these will NOT be delivered",{context,clientId,userId,cap:maxTopicsPerClient,droppedCount:dropped.length,dropped})}function registerClient(clientId,ws,userId,topics){let existingClients=getClientsByUser(userId);if(existingClients.length>=maxClientsPerUser){let oldestClientId=existingClients[0];if(oldestClientId)logger2.info("[PubSub] Max clients reached, closing oldest",{userId,oldestClientId}),unregisterClient(oldestClientId)}let{accepted,dropped}=capTopics(topics);return clients.set(clientId,{ws,userId,subscribedTopics:new Set(accepted),connectedAt:Date.now(),pendingAcks:new Map}),logger2.info("[PubSub] Client registered",{clientId,userId,topics:accepted,requestedCount:topics.length,subscribedCount:accepted.length,totalClients:clients.size}),reportDropped("registerClient",clientId,userId,dropped),{accepted,dropped}}function unregisterClient(clientId){let client=clients.get(clientId);if(!client)return;let userId=client.userId;if(clients.delete(clientId),logger2.info("[PubSub] Client unregistered",{clientId,userId,totalClients:clients.size}),presenceEnabled&&userId){if(getClientsByUser(userId).length===0)broadcastPresenceToOthers(userId,"offline")}}function updateClientTopics(clientId,topics){let client=clients.get(clientId);if(!client)return{accepted:[],dropped:[]};let{accepted,dropped}=capTopics(topics);return client.subscribedTopics=new Set(accepted),reportDropped("updateClientTopics",clientId,client.userId,dropped),{accepted,dropped}}function getClientsByUser(userId){let result=[];for(let[clientId,client]of clients)if(client.userId===userId)result.push(clientId);return result}function getConnectedUserIds(){let userIds=new Set;for(let[,client]of clients)if(client.userId)userIds.add(client.userId);return userIds}function getClientCount(){return clients.size}function sendToClient(clientId,message){let client=clients.get(clientId);if(!client)return!1;try{return client.ws.send(JSON.stringify(message)),!0}catch{return clients.delete(clientId),!1}}function broadcastEvent(topic,event){let messageId=generateMessageId(),timestamp=Date.now(),payload=JSON.stringify({type:"event",messageId:ackEnabled?messageId:void 0,topic,data:event,timestamp}),sentCount=0;for(let[clientId,client]of clients)if(client.subscribedTopics.has("*")||client.subscribedTopics.has(topic))try{if(client.ws.send(payload),sentCount++,ackEnabled){if(client.pendingAcks.set(messageId,{sentAt:timestamp,retryCount:0}),recordSent(),client.userId)storePendingMessage(redis,{messageId,topic,clientId,userId:client.userId,data:event,sentAt:timestamp,retryCount:0,maxRetries},ackTtl).catch(()=>{})}}catch{clients.delete(clientId)}logger2.info("[PubSub] Broadcasted event",{topic,messageId,sentCount})}function broadcastToUser(userId,topic,event){let messageId=generateMessageId(),timestamp=Date.now(),payload=JSON.stringify({type:"event",messageId:ackEnabled?messageId:void 0,topic,data:event,timestamp}),sentCount=0,sentToAnyClient=!1;for(let[clientId,client]of clients){if(client.userId!==userId)continue;if(!client.subscribedTopics.has("*")&&!client.subscribedTopics.has(topic))continue;try{if(client.ws.send(payload),sentCount++,sentToAnyClient=!0,ackEnabled)client.pendingAcks.set(messageId,{sentAt:timestamp,retryCount:0}),recordSent()}catch{clients.delete(clientId)}}if(ackEnabled)storePendingMessage(redis,{messageId,topic,clientId:sentToAnyClient?"delivered":"pending",userId,data:event,sentAt:timestamp,retryCount:0,maxRetries},ackTtl).catch(()=>{});logger2.info("[PubSub] Broadcasted to user",{userId,topic,messageId,sentCount,storedForOffline:!sentToAnyClient})}function broadcastToUserNoAck(userId,topic,event){let timestamp=Date.now(),payload=JSON.stringify({type:"event",topic,data:event,timestamp});for(let[clientId,client]of clients){if(client.userId!==userId)continue;if(!client.subscribedTopics.has("*")&&!client.subscribedTopics.has(topic))continue;try{client.ws.send(payload)}catch{clients.delete(clientId)}}}function broadcastPresenceToOthers(userId,status){let debounceKey=`${userId}:${status}`,now=Date.now(),lastBroadcast=presenceBroadcastDebounce.get(debounceKey);if(lastBroadcast&&now-lastBroadcast<presenceDebounceMs)return;if(presenceBroadcastDebounce.set(debounceKey,now),presenceBroadcastDebounce.size>1000){let cutoff=now-presenceDebounceMs*2;for(let[key2,timestamp]of presenceBroadcastDebounce)if(timestamp<cutoff)presenceBroadcastDebounce.delete(key2)}let connectedUserIds=getConnectedUserIds();for(let targetUserId of connectedUserIds)if(targetUserId!==userId)broadcastToUserNoAck(targetUserId,"user-presence",{type:"user-presence",data:{userId,status}})}async function handleClientAck(clientId,messageId){if(!ackEnabled)return!0;let client=clients.get(clientId);if(!client)return!1;if(client.pendingAcks.get(messageId))client.pendingAcks.delete(messageId);if(client.userId)return acknowledgeMessage(redis,client.userId,messageId,ackTtl);return!0}async function deliverPendingMessages(userId,clientId){if(!ackEnabled)return 0;let client=clients.get(clientId);if(!client||client.userId!==userId)return 0;let pendingMessages=await getPendingMessagesForUser(redis,userId);if(pendingMessages.length===0)return 0;logger2.info("[PubSub] Delivering pending messages",{userId,count:pendingMessages.length});let deliveredCount=0;for(let message of pendingMessages){if(!client.subscribedTopics.has("*")&&!client.subscribedTopics.has(message.topic))continue;let payload=JSON.stringify({type:"event",messageId:message.messageId,topic:message.topic,data:message.data,timestamp:message.sentAt,isRedelivery:!0});try{client.ws.send(payload),client.pendingAcks.set(message.messageId,{sentAt:Date.now(),retryCount:message.retryCount+1}),await incrementRetryCount(redis,userId,message.messageId,ackTtl,maxRetries),deliveredCount++}catch{break}}return deliveredCount}return{generateClientId,registerClient,unregisterClient,updateClientTopics,getClientsByUser,getConnectedUserIds,getClientCount,sendToClient,broadcastEvent,broadcastToUser,broadcastToUserNoAck,broadcastPresenceToOthers,handleClientAck,deliverPendingMessages,getPendingMessageCount:(userId)=>getPendingMessageCount(redis,userId)}}var OPAQUE_RUN=/[A-Za-z0-9_-]{24,}(?:\.[A-Za-z0-9_-]{8,}){0,2}/g;function previewOf2(frame){return frame.slice(0,120).replace(OPAQUE_RUN,(match)=>`[redacted ${match.length} chars]`)}function classifyClientFrame(message){if(typeof message==="string"){if(message.trim()==="")return{kind:"ignore"};let parsed;try{parsed=JSON.parse(message)}catch{return{kind:"malformed",preview:previewOf2(message)}}return asMessage(parsed,message.length)}if(message&&typeof message==="object")return asMessage(message,approximateSize(message));return{kind:"ignore"}}function approximateSize(value){if(ArrayBuffer.isView(value))return value.byteLength;if(value instanceof ArrayBuffer)return value.byteLength;try{return JSON.stringify(value)?.length??0}catch{return 0}}function asMessage(value,size){if(!value||typeof value!=="object"||Array.isArray(value))return{kind:"ignore"};let record=value,type=record.type;if(typeof type!=="string"||type==="")return{kind:"ignore"};return{kind:"message",parsed:record,type,size}}function tokensMatch(a,b){let ba=Buffer.from(a),bb=Buffer.from(b);if(ba.length!==bb.length)return!1;return timingSafeEqual3(ba,bb)}var HEARTBEAT_WS_TYPES=new Set(["ping","pong","ack"]);function buildHandshakeHeaders(rawHeaders,queryToken){let headers=new Headers;if(rawHeaders&&typeof rawHeaders==="object"){for(let[key2,value]of Object.entries(rawHeaders))if(typeof value==="string")headers.set(key2,value)}if(queryToken)headers.set("authorization",`Bearer ${queryToken}`),headers.delete("cookie");return headers}function createPubSubRoutes(config){let{logger:logger2,getLiveMonitoringService}=config,clientManager=createClientManager(config);if(config.ack.enabled)initAckCleanup();let cleanupTimer=null,daprApiToken=config.daprApiToken||process.env.APP_API_TOKEN||process.env.DAPR_API_TOKEN,warnedNoDaprToken=!1,plugin=new Elysia18;return plugin.onStart(()=>{if(config.cleanupIntervalMs>0)cleanupTimer=setInterval(()=>{logger2.info("[PubSub] Periodic cleanup running",{totalClients:clientManager.getClientCount()})},config.cleanupIntervalMs);logger2.info("[PubSub] Routes initialized",{basePath:config.basePath,wsPath:config.wsPath,ackEnabled:config.ack.enabled,presenceEnabled:config.presence.enabled})}),plugin.onStop(()=>{if(cleanupTimer)clearInterval(cleanupTimer),cleanupTimer=null}),plugin.post(`${config.basePath}/:topic`,async({params,body,request,set})=>{let topic=params.topic;if(daprApiToken){let presented=request.headers.get("dapr-api-token")??"";if(!tokensMatch(presented,daprApiToken))return logger2.warn("[PubSub] Rejected Dapr event: missing/invalid dapr-api-token",{topic}),set.status=403,{status:"DROP"}}else if(!warnedNoDaprToken)warnedNoDaprToken=!0,logger2.warn("[PubSub] Dapr subscription endpoint is UNAUTHENTICATED \u2014 set config.pubsub.daprApiToken or the APP_API_TOKEN/DAPR_API_TOKEN env so only the Dapr sidecar can inject events");let eventData;try{if(!body){let text2=await request.text();eventData=JSON.parse(text2)}else eventData=body}catch{return set.status=200,{status:"DROP"}}logger2.info("[PubSub] Dapr event received",{topic,eventId:eventData.id,source:eventData.source});let userId=eventData.data?.user_id;if(userId)clientManager.broadcastToUser(userId,topic,eventData);else clientManager.broadcastEvent(topic,eventData);return getLiveMonitoringService?.()?.recordDaprEvent("pubsub_subscribe",{topic,success:!0,metadata:{eventId:eventData.id,source:eventData.source,userId:userId||null}}),set.status=200,{status:"SUCCESS"}}),plugin.ws(config.wsPath,{idleTimeout:config.wsIdleTimeout,open(ws){let data=ws.data||{},query=data.query||{},userId;if(config.authenticate){let handshakeHeaders=buildHandshakeHeaders(data.headers,query.token),authResult=config.authenticate(handshakeHeaders);if(!authResult||!("userId"in authResult)){let reason=authResult?.reason??"unspecified";ws.send(JSON.stringify({type:"error",error:"Unauthorized: a valid session is required for this connection",reason,timestamp:Date.now()})),ws.close(4001,`Unauthorized: ${reason}`),getLiveMonitoringService?.()?.recordWsEvent("error",{success:!1,error:`Unauthorized WebSocket handshake: ${reason}`,metadata:{reason}});return}userId=authResult.userId}else userId=query.userId;if(!userId){ws.send(JSON.stringify({type:"error",error:"userId is required for WebSocket connection",timestamp:Date.now()})),ws.close(4001,"userId is required"),getLiveMonitoringService?.()?.recordWsEvent("error",{success:!1,error:"userId is required"});return}let clientId=clientManager.generateClientId(),topics=query.topics?.split(",").map((t16)=>t16.trim())||["*"];ws.data.clientId=clientId,ws.data.openedAt=Date.now();let registration=clientManager.registerClient(clientId,ws,userId,topics);if(ws.send(JSON.stringify({type:"connected",clientId,subscribedTopics:registration.accepted,...registration.dropped.length>0?{droppedTopics:registration.dropped,topicCap:config.maxTopicsPerClient}:{},timestamp:Date.now()})),getLiveMonitoringService?.()?.recordWsEvent("connection",{clientId,userId,topic:topics.join(","),success:!0}),config.presence.enabled)clientManager.broadcastPresenceToOthers(userId,"online");let connectedUserId=userId;if(config.ack.enabled&&connectedUserId)clientManager.getPendingMessageCount(connectedUserId).then((count2)=>{if(count2>0)logger2.info("[PubSub] Delivering pending messages",{userId:connectedUserId,count:count2}),clientManager.deliverPendingMessages(connectedUserId,clientId).catch(()=>{})}).catch(()=>{})},message(ws,message){let clientId=ws.data.clientId,frame=classifyClientFrame(message);if(frame.kind==="ignore")return;if(frame.kind==="malformed"){logger2.debug("[PubSub] Failed to parse client message",{clientId,frame:frame.preview});return}let{parsed}=frame;try{if(!HEARTBEAT_WS_TYPES.has(frame.type))getLiveMonitoringService?.()?.recordWsEvent("message",{clientId,messageType:frame.type,success:!0,dataSize:frame.size});switch(frame.type){case"subscribe":if(Array.isArray(parsed.topics)){let update=clientManager.updateClientTopics(clientId,parsed.topics);clientManager.sendToClient(clientId,{type:"subscribed",topics:update.accepted,...update.dropped.length>0?{droppedTopics:update.dropped,topicCap:config.maxTopicsPerClient}:{},timestamp:Date.now()})}break;case"unsubscribe":logger2.info("[PubSub] Unsubscribe request",{clientId,topics:parsed.topics});break;case"ping":clientManager.sendToClient(clientId,{type:"pong",timestamp:Date.now()});break;case"ack":if(parsed.messageId)clientManager.handleClientAck(clientId,parsed.messageId).catch(()=>{});break}}catch(error){logger2.error("[PubSub] Client message handler failed",error,{clientId,messageType:frame.type})}},close(ws,code,reason){let state=ws.data,clientId=state.clientId,openedAt=state.openedAt,livedMs=typeof openedAt==="number"?Date.now()-openedAt:null;if(!clientId)return;clientManager.unregisterClient(clientId),getLiveMonitoringService?.()?.recordWsEvent("close",{clientId,success:!0,metadata:{code,reason:reason||"",livedMs,shortLived:livedMs!==null&&livedMs<60000}}),logger2.info("[PubSub] Client disconnected",{clientId,code,reason,livedMs})}}),{plugin,clientManager}}function classifyRefusal(error){if(!error)return"unspecified";switch(error){case"Token expired":return"expired";case"Invalid signature":return"invalid-signature";case"Invalid token format: expected 3 parts":case"Invalid token format: missing parts":case"Invalid header: failed to decode":case"Invalid header: typ must be JWT":case"Invalid payload: failed to decode":return"malformed";case"Invalid issuer":case"Invalid audience":return"wrong-audience";case"Token issued in the future":return"not-yet-valid";default:if(error.startsWith("Unsupported algorithm:"))return"malformed";return"unspecified"}}import Elysia19,{t as t16}from"elysia";init_Secrets();var timed=async(fn)=>{let started=performance.now();return{result:await fn(),durationMs:Math.round(performance.now()-started)}},postForm=async(url,body)=>{let response=await fetch(url,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",Accept:"application/json"},body:new URLSearchParams(body).toString(),signal:AbortSignal.timeout(12000)}),text2=await response.text(),json={};try{json=JSON.parse(text2)}catch{json=Object.fromEntries(new URLSearchParams(text2))}return{status:response.status,json,text:text2}},describeError=(error)=>{if(error instanceof Error){if(error.name==="TimeoutError")return"Provider did not respond within 12s";return error.message}return String(error)},MICROSOFT_TOKEN_URL=(tenant)=>`https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`,DEFAULT_TOKEN_URLS={google:"https://oauth2.googleapis.com/token",github:"https://github.com/login/oauth/access_token"},describeRedirectUri=(value)=>{let trimmed=value?.trim();if(!trimmed)return"Y\xF6nlendirme adresi (Redirect URI) bo\u015F \u2014 giri\u015F tamamlanamaz";if(/^[A-Z][A-Z0-9_]{2,}$/.test(trimmed))return`Y\xF6nlendirme adresi bir ortam de\u011Fi\u015Fkeni ad\u0131 olarak duruyor ("${trimmed}") ve o de\u011Fi\u015Fken tan\u0131ml\u0131 de\u011Fil`;let parsed;try{parsed=new URL(trimmed)}catch{return`Y\xF6nlendirme adresi ge\xE7erli bir URL de\u011Fil: "${trimmed}"`}if(parsed.protocol!=="https:"&&parsed.hostname!=="localhost")return`Y\xF6nlendirme adresi https olmal\u0131 (\u015Fu an "${parsed.protocol}//")`;return null};async function testOAuthProvider(provider,config){if(!config.clientId||!config.clientSecret)return{ok:!1,status:"failed",message:"Client ID ve Client Secret dolu olmal\u0131",durationMs:0};let redirectProblem=describeRedirectUri(config.redirectUri);if(redirectProblem)return{ok:!1,status:"failed",message:redirectProblem,durationMs:0};try{if(provider==="microsoft"){let tenant=config.tenantId||"common";if(tenant==="common"||tenant==="organizations"||tenant==="consumers")return await probeWithBogusCode(MICROSOFT_TOKEN_URL(tenant),config);let{result,durationMs}=await timed(()=>postForm(MICROSOFT_TOKEN_URL(tenant),{grant_type:"client_credentials",client_id:config.clientId,client_secret:config.clientSecret,scope:"https://graph.microsoft.com/.default"}));if(result.status===200&&result.json.access_token)return{ok:!0,status:"ok",message:`Entra kimlik bilgileri do\u011Fruland\u0131 (tenant ${tenant})`,durationMs};let code=String(result.json.error??result.status);return{ok:!1,status:"failed",message:String(result.json.error_description??result.text).slice(0,400),code,durationMs}}let tokenUrl=config.tokenUrl||DEFAULT_TOKEN_URLS[provider];if(!tokenUrl)return{ok:!1,status:"unknown",message:`"${provider}" i\xE7in token adresi tan\u0131ml\u0131 de\u011Fil \u2014 tokenUrl girin`,durationMs:0};return await probeWithBogusCode(tokenUrl,config)}catch(error){return{ok:!1,status:"failed",message:describeError(error),durationMs:0}}}async function probeWithBogusCode(tokenUrl,config){let{result,durationMs}=await timed(()=>postForm(tokenUrl,{grant_type:"authorization_code",code:"nucleus-credential-probe",client_id:config.clientId,client_secret:config.clientSecret,redirect_uri:config.redirectUri||"https://localhost/callback"})),code=String(result.json.error??""),description=String(result.json.error_description??result.text).slice(0,400);if(code==="invalid_grant"||code==="bad_verification_code")return{ok:!0,status:"ok",message:"Kimlik bilgileri sa\u011Flay\u0131c\u0131 taraf\u0131ndan kabul edildi",code,durationMs};if(code==="invalid_client"||code==="unauthorized_client"||result.status===401)return{ok:!1,status:"failed",message:description||"Sa\u011Flay\u0131c\u0131 Client ID/Secret \xE7iftini reddetti",code:code||"invalid_client",durationMs};if(description.includes("AADSTS7000215")||description.includes("AADSTS700016"))return{ok:!1,status:"failed",message:description,code,durationMs};return{ok:!1,status:"unknown",message:description||"Sa\u011Flay\u0131c\u0131 beklenmeyen bir yan\u0131t d\xF6nd\xFC",code:code||String(result.status),durationMs}}async function testStorageProvider(provider){if(!provider)return{ok:!1,status:"failed",message:"Depolama sa\u011Flay\u0131c\u0131 yap\u0131land\u0131r\u0131lmam\u0131\u015F",durationMs:0};try{let{result,durationMs}=await timed(()=>provider.isAvailable());return result?{ok:!0,status:"ok",message:`Ba\u011Flant\u0131 do\u011Fruland\u0131: ${provider.describe}`,durationMs}:{ok:!1,status:"failed",message:`Eri\u015Filemedi: ${provider.describe} \u2014 sunucu, payla\u015F\u0131m ad\u0131 ve kimlik bilgilerini kontrol edin`,durationMs}}catch(error){return{ok:!1,status:"failed",message:describeError(error),durationMs:0}}}async function testEmailProvider(service,recipient){if(!service)return{ok:!1,status:"failed",message:"E-posta sa\u011Flay\u0131c\u0131 yap\u0131land\u0131r\u0131lmam\u0131\u015F",durationMs:0};if(!service.isAvailable())return{ok:!1,status:"failed",message:"Sa\u011Flay\u0131c\u0131 kimlik bilgileriyle ba\u015Flat\u0131lamad\u0131",durationMs:0};if(!recipient)return{ok:!0,status:"ok",message:"Sa\u011Flay\u0131c\u0131 ba\u015Flat\u0131ld\u0131 \u2014 ger\xE7ek g\xF6nderim i\xE7in bir al\u0131c\u0131 adresi girin",durationMs:0};try{let{result,durationMs}=await timed(()=>service.sendEmail({to:recipient,subject:"Nucleus ba\u011Flant\u0131 testi",html:"<p>Bu e-posta, y\xF6netim panelinden yap\u0131lan ba\u011Flant\u0131 testiyle g\xF6nderildi.</p>"}));return result.success?{ok:!0,status:"ok",message:`Test e-postas\u0131 ${recipient} adresine g\xF6nderildi`,durationMs}:{ok:!1,status:"failed",message:result.error??"G\xF6nderim ba\u015Far\u0131s\u0131z",durationMs}}catch(error){return{ok:!1,status:"failed",message:describeError(error),durationMs:0}}}init_godminGuard();var SECRET_KINDS=["text","password","json","connection_string","pem","boolean","list"];var SCOPE_PATTERN=/^[A-Za-z0-9_.-]{1,200}$/,KEY_PATTERN=/^[A-Za-z0-9_-]{1,120}$/,fail5=(set,status,message)=>{return set.status=status,{isSuccess:!1,message,data:null}};function createSecretsRoutes(routeConfig){let{logger:logger2,basePath,db,schemaTables,getService,resolvedOptions,getEmailService,getStorageProvider:getStorageProvider2}=routeConfig,plugin=new Elysia19({prefix:basePath});plugin.onBeforeHandle(createGodminGuard2({logger:logger2,db,schemaTables,area:"secrets management",allowedRoles:routeConfig.adminRoles}));let requireService=(set)=>{let service=getService();if(!service)return set.status=503,null;return service},actorOf=(request)=>request.headers.get("x-user-id")??void 0;return plugin.get("/slots",async({set})=>{let service=requireService(set);if(!service)return{isSuccess:!1,message:"Secrets management is disabled \u2014 set secrets.enabled and provide an encryption key",data:null};let slots=await service.describe(resolvedOptions),groups=[...new Set(slots.map((s)=>s.group))].sort();return{isSuccess:!0,message:"Credential slots",data:{slots,groups,counts:{total:slots.length,fromDb:slots.filter((s)=>s.source==="db").length,fromEnv:slots.filter((s)=>s.source==="env").length,missing:slots.filter((s)=>s.source==="missing").length,missingRequired:slots.filter((s)=>s.source==="missing"&&s.required).length}}}},{detail:{tags:["Secrets"],summary:"List credential slots discovered from the running config",description:"Slots are derived by walking the config tree, so a new integration appears without a code change. Values are never returned \u2014 only a masked preview and the resolution source."}}),plugin.get("/",async({set})=>{let service=requireService(set);if(!service)return{isSuccess:!1,message:"Secrets management is disabled",data:null};return{isSuccess:!0,message:"Stored credentials",data:{secrets:await service.list()}}},{detail:{tags:["Secrets"],summary:"List stored credentials",description:"Metadata only \u2014 the encrypted value is never returned."}}),plugin.put("/:scope/:key",async({params,body,set,request})=>{let service=requireService(set);if(!service)return{isSuccess:!1,message:"Secrets management is disabled",data:null};let{scope,key:key2}=params;if(!SCOPE_PATTERN.test(scope))return fail5(set,400,"Invalid scope");if(!KEY_PATTERN.test(key2))return fail5(set,400,"Invalid key");let value=body.value;if(typeof value!=="string"||value.length===0)return fail5(set,400,"value is required");if(value.length>65536)return fail5(set,400,"value exceeds 64KB");if(body.kind==="json")try{JSON.parse(value)}catch{return fail5(set,400,"value is not valid JSON")}let saved=await service.set({scope,key:key2,value,kind:body.kind,description:body.description,actorId:actorOf(request)});if(!saved)return fail5(set,500,"Could not store the credential");return{isSuccess:!0,message:"Credential stored",data:{secret:saved}}},{params:t16.Object({scope:t16.String(),key:t16.String()}),body:t16.Object({value:t16.String(),kind:t16.Optional(t16.Union(SECRET_KINDS.map((kind)=>t16.Literal(kind)))),description:t16.Optional(t16.String())}),detail:{tags:["Secrets"],summary:"Set or rotate a credential",description:"Stores the value encrypted (AES-256-GCM). Rotating retains the previous value for a one-step rollback. Takes effect within seconds on every pod \u2014 no restart."}}),plugin.post("/:scope/:key/rollback",async({params,set,request})=>{let service=requireService(set);if(!service)return{isSuccess:!1,message:"Secrets management is disabled",data:null};let restored=await service.rollback(params.scope,params.key,actorOf(request));if(!restored)return fail5(set,404,"No previous value to roll back to");return{isSuccess:!0,message:"Previous credential restored",data:{secret:restored}}},{params:t16.Object({scope:t16.String(),key:t16.String()}),detail:{tags:["Secrets"],summary:"Roll back to the value replaced by the last rotation"}}),plugin.delete("/:scope/:key",async({params,set,request})=>{let service=requireService(set);if(!service)return{isSuccess:!1,message:"Secrets management is disabled",data:null};if(!await service.remove(params.scope,params.key,actorOf(request)))return fail5(set,404,"Credential not found");return{isSuccess:!0,message:"Credential removed \u2014 the config falls back to env or literal",data:null}},{params:t16.Object({scope:t16.String(),key:t16.String()}),detail:{tags:["Secrets"],summary:"Delete a stored credential",description:"Resolution falls back to the env var or literal declared in config."}}),plugin.post("/test",async({body,set})=>{let service=requireService(set);if(!service)return{isSuccess:!1,message:"Secrets management is disabled",data:null};let{target:target2}=body,result;if(target2==="email")result=await testEmailProvider(getEmailService?.()??null,body.recipient);else if(target2==="oauth"){let provider=body.provider;if(!provider)return fail5(set,400,"provider is required for an oauth test");let scope=`authentication.oauth.providers.${provider}`,providerConfig=(resolvedOptions.authentication?.oauth?.providers??{})[provider]??{},declaredString=(key2)=>{let raw=providerConfig[key2];return typeof raw==="string"?raw:void 0};result=await testOAuthProvider(provider,{clientId:service.value(scope,"clientId",declaredString("clientId")),clientSecret:service.value(scope,"clientSecret",declaredString("clientSecret")),tenantId:service.value(scope,"tenantId",declaredString("tenantId")),tokenUrl:declaredString("tokenUrl"),redirectUri:service.value(scope,"redirectUri",declaredString("redirectUri"))})}else if(target2==="storage")result=await testStorageProvider(getStorageProvider2?.()??null);else return fail5(set,400,`Unsupported test target "${target2}"`);return logger2.info("[Secrets] Connection test",{target:target2,provider:body.provider,status:result.status,durationMs:result.durationMs}),{isSuccess:result.ok,message:result.message,data:{result}}},{body:t16.Object({target:t16.Union([t16.Literal("oauth"),t16.Literal("email"),t16.Literal("storage")]),provider:t16.Optional(t16.String()),recipient:t16.Optional(t16.String())}),detail:{tags:["Secrets"],summary:"Test a credential against the real provider",description:"Backend-to-backend only. OAuth uses client_credentials where the provider supports it and a client-authentication probe otherwise; email optionally sends a real test message."}}),plugin.post("/reencrypt",async({body,set})=>{let service=requireService(set);if(!service)return{isSuccess:!1,message:"Secrets management is disabled",data:null};if(!body.newMasterKey||body.newMasterKey.length<16)return fail5(set,400,"newMasterKey must be at least 16 characters");let result=await service.reencryptAll(body.newMasterKey),uncovered="Credentials stored outside this store \u2014 an integration connection\u2019s token, client secret and service password \u2014 are NOT re-encrypted by this. Re-enter them from the integrations panel after the new key is deployed.";return{isSuccess:result.failed.length===0,message:result.failed.length===0?`Re-encrypted ${result.migrated} credentials \u2014 deploy the new key before the next restart. ${uncovered}`:`Nothing was re-encrypted: ${result.failed.length} credential(s) could not be read with the current key, so none were rewritten.`,data:{...result,uncovered}}},{body:t16.Object({newMasterKey:t16.String()}),detail:{tags:["Secrets"],summary:"Re-encrypt every credential under a new master key",description:"Rotates the key protecting the store. The new key must be deployed to the environment before the next restart, or nothing will decrypt."}}),plugin.get("/discover",()=>({isSuccess:!0,message:"Slots discovered from the running config",data:{slots:discoverSlots(resolvedOptions).map(({configValue:_configValue,...slot})=>slot)}}),{detail:{tags:["Secrets"],summary:"Show which credential slots the config declares",description:"Diagnostics \u2014 resolution state is not consulted and no value is read."}}),plugin.post("/storage/migrate",async(ctx)=>{let body=ctx.body,build=routeConfig.buildStorageProvider;if(!build)return fail5(ctx.set,503,"Depolama yap\u0131land\u0131r\u0131lmam\u0131\u015F");let destination=build(body.to),source=build(body.to==="smb"?"local":"smb");if(!source||!destination)return fail5(ctx.set,503,"Depolama yap\u0131land\u0131r\u0131lmam\u0131\u015F");if(!await destination.isAvailable())return fail5(ctx.set,422,`Hedef depo eri\u015Filebilir de\u011Fil (${destination.describe}) \u2014 \xF6nce ba\u011Flant\u0131y\u0131 test edin`);if(body.dryRun){let preview=await migrateStorage({from:source,to:Object.assign(Object.create(Object.getPrototypeOf(destination)),destination,{exists:async()=>!1,write:async()=>{},ensureDir:async()=>{}}),maxFiles:1e5},{logger:logger2});return{isSuccess:!0,message:`${preview.copied} dosya ta\u015F\u0131nacak`,data:{preview}}}let report=await migrateStorage({from:source,to:destination},{logger:logger2}),clean=report.failed===0&&!report.truncated;return logger2.warn("[Storage] Migration run from the admin panel",{to:body.to,copied:report.copied,skipped:report.skipped,failed:report.failed,truncated:report.truncated}),{isSuccess:clean,message:clean?`${report.copied} dosya ta\u015F\u0131nd\u0131, ${report.skipped} zaten mevcuttu`:`${report.copied} ta\u015F\u0131nd\u0131, ${report.failed} ba\u015Far\u0131s\u0131z${report.truncated?", liste kesildi":""}`,data:{report}}},{body:t16.Object({to:t16.Union([t16.Literal("local"),t16.Literal("smb")]),dryRun:t16.Optional(t16.Boolean())}),detail:{tags:["Secrets"],summary:"Copy every stored file to the other backend",description:"Copy-only: the source is never deleted, and a file already on the target is skipped so a re-run resumes. Run BEFORE switching storage.provider."}}),plugin}var snakeToCamel4=(s)=>s.replace(/_([a-z])/g,(_,l)=>l.toUpperCase());function warnOnEntityBasePathCollision(feature,basePath,entities,logger2){let segment=basePath.replace(/^\/+|\/+$/g,"");if(!segment||segment.includes("/"))return!1;let clash=(entities??[]).find((entity)=>snakeToCamel4(entity.table_name)===segment);if(!clash)return!1;return logger2.error(`[${feature}] basePath "${basePath}" is also the generated entity route for "${clash.table_name}". Both are registered and one will shadow the other. Move the feature (e.g. "/nucleus${basePath}") or rename the entity.`),!0}function requiredBodyCeiling(input){if(!input.storageEnabled)return null;let perFile=input.maxFileSizeBytes;if(!perFile||!Number.isFinite(perFile)||perFile<=0)return null;let files=Math.max(1,Math.floor(input.maxFiles??1));return perFile*files+8388608}function bodyCeilingComplaint(input){if(input.required===null)return null;if((input.configured??134217728)>=input.required)return null;let mb2=(bytes)=>`${Math.round(bytes/1048576)} MB`,source=input.configured==null?`the host passed no \`serve.maxRequestBodySize\`, so Bun's default of ${mb2(134217728)} applies`:`the host passed ${mb2(input.configured)}`;return`Uploads are configured up to ${mb2(input.required)}, but ${source}. `+"Anything larger is refused by the server before this application sees it \u2014 "+`the upload appears to run and then fails with nothing to show. Fix: new Elysia({ serve: { maxRequestBodySize: ${input.required} } }).`}var APP_BASELINE_BYTES=268435456,COPIES_IN_FLIGHT=2;function memoryCeilingComplaint(input){if(input.required===null||input.cgroupLimit===null||input.cgroupLimit<=0)return null;let needed=input.required*COPIES_IN_FLIGHT+APP_BASELINE_BYTES;if(input.cgroupLimit>=needed)return null;let mb2=(bytes)=>`${Math.round(bytes/1048576)} MB`,affordable=Math.max(0,Math.floor((input.cgroupLimit-APP_BASELINE_BYTES)/COPIES_IN_FLIGHT));return`Uploads are configured up to ${mb2(input.required)}, but this container is capped at ${mb2(input.cgroupLimit)} and an upload is held in memory twice on top of the ~${mb2(APP_BASELINE_BYTES)} `+`the app already holds \u2014 it needs ~${mb2(needed)}. The kernel will kill the process mid-upload. Fix: raise the container's memory limit to at least ${mb2(needed)}, or lower storage.maxFileSizeBytes to about ${mb2(affordable)}.`}var ipMatches=(ip,entry)=>entry.includes("/")?isIpInCidr(ip,entry):ip===entry;function resolveClientIp(socketIp,forwardedFor,trustedProxies=[]){let peer=socketIp?.trim()||"",chain=(forwardedFor||"").split(",").map((s)=>s.trim()).filter(Boolean);if(!peer)return chain[0]||"unknown";if(trustedProxies.length===0)return peer;let trusted=(ip)=>trustedProxies.some((p)=>ipMatches(ip,p));if(!trusted(peer))return peer;for(let i=chain.length-1;i>=0;i--){let hop=chain[i];if(hop&&!trusted(hop))return hop}return chain[0]||peer}var DEFAULTS={cookieName:"csrf_token",headerName:"x-csrf-token"},resolveCsrfConfig=(raw)=>{if(raw===!0)return{enabled:!0,...DEFAULTS};if(!raw)return{enabled:!1,...DEFAULTS};return{enabled:raw.enabled!==!1,cookieName:raw.cookieName||DEFAULTS.cookieName,headerName:(raw.headerName||DEFAULTS.headerName).toLowerCase()}},parseCookies=(cookieHeader)=>{let out={};if(!cookieHeader)return out;for(let part of cookieHeader.split(";")){let trimmed=part.trim(),eq32=trimmed.indexOf("=");if(eq32>0)out[trimmed.slice(0,eq32)]=trimmed.slice(eq32+1)}return out},SAFE_METHODS=new Set(["GET","HEAD","OPTIONS"]),isStateChanging=(method)=>!SAFE_METHODS.has(method.toUpperCase()),requiresCsrf=(params)=>{if(!isStateChanging(params.method))return!1;if(params.hasAuthorizationHeader)return!1;return params.authCookieNames.some((n)=>!!params.cookies[n])},csrfTokenMatches=(csrfCookie,csrfHeader)=>{if(!csrfCookie||!csrfHeader)return!1;return csrfCookie===csrfHeader};var SENSITIVE_HEADER_NAMES=new Set(["authorization","proxy-authorization","cookie","set-cookie","x-access-token","x-refresh-token","x-session-id","x-user-id","x-user-roles","x-user-claims","x-user-email","x-api-key","x-api-key-id","x-api-key-owner-type","x-auth-type"]);function redactSensitiveHeaders(headers){let out={};return headers.forEach((value,key2)=>{out[key2]=SENSITIVE_HEADER_NAMES.has(key2.toLowerCase())?"[REDACTED]":value}),out}var exceedsBodyLimit=(contentLengthHeader,maxBytes)=>{if(!maxBytes||maxBytes<=0)return!1;if(!contentLengthHeader)return!1;let len=Number(contentLengthHeader);if(!Number.isFinite(len)||len<0)return!1;return len>maxBytes};init_sensitiveColumns();var TENANT_CLAIM="tenant",verifyTenantBinding=(tokenTenant,resolvedSchema,mode="lenient")=>{if(mode==="off")return{ok:!0};if(!resolvedSchema)return{ok:!0};let claim=typeof tokenTenant==="string"&&tokenTenant.length>0?tokenTenant:void 0;if(!claim)return mode==="strict"?{ok:!1,reason:"token is not bound to a tenant"}:{ok:!0};return claim===resolvedSchema?{ok:!0}:{ok:!1,reason:"token tenant does not match the requested tenant"}};init_storage2();import{eq as eq32}from"drizzle-orm";import{Elysia as Elysia20,t as t17}from"elysia";var RESERVED_SUBDOMAINS=["www","api","app","admin","dashboard","mail","ftp","cdn","static","assets","docs","help","support","status","blog","dev","staging","test","demo","localhost"],DEFAULT_SELF_SIGNUP_LIMITS={maxSignupsPerIpPerWindow:3,ipWindowMs:3600000,maxTenantsPerEmail:1};var col9=(table,name2)=>table[name2];function createCheckSubdomainRoute(config){let checkRoute=new Elysia20;return checkRoute.get("/tenants/check-subdomain/:subdomain",async(ctx)=>{let{subdomain}=ctx.params,normalized=subdomain.toLowerCase().trim();if(!/^[a-z][a-z0-9-]*$/.test(normalized)||normalized.length<2||normalized.length>63)return ctx.set.status=400,{isSuccess:!1,message:"Invalid subdomain format. Must start with a letter, contain only lowercase letters, numbers, and hyphens, and be 2-63 characters.",data:{available:!1,subdomain:normalized}};if(RESERVED_SUBDOMAINS.includes(normalized))return{isSuccess:!0,message:"Subdomain is reserved",data:{available:!1,subdomain:normalized}};let tenantRegistry=config.getTenantRegistry(),db=config.getDb();if(tenantRegistry){if(tenantRegistry.getActiveTenants().find((t18)=>t18.subdomain===normalized))return{isSuccess:!0,message:"Subdomain is already taken",data:{available:!1,subdomain:normalized}}}if(db&&tenantRegistry){let tenantsTable=tenantRegistry.getMainContext().schemaTables.tenants;if(tenantsTable)try{if((await db.select().from(tenantsTable).where(eq32(col9(tenantsTable,"subdomain"),normalized)).limit(1)).length>0)return{isSuccess:!0,message:"Subdomain is already taken",data:{available:!1,subdomain:normalized}}}catch{}}return{isSuccess:!0,message:"Subdomain is available",data:{available:!0,subdomain:normalized}}},{params:t17.Object({subdomain:t17.String({minLength:2,maxLength:63})}),detail:{tags:["Tenants"],summary:"Check subdomain availability",description:"Public endpoint to check if a subdomain is available for registration. No authentication required."}}),checkRoute}init_Logger2();import{eq as eq33}from"drizzle-orm";import{Elysia as Elysia21,t as t18}from"elysia";var col10=(table,name2)=>table[name2];function createManageRoutes(config){let log4=config.logger.scoped(TENANT_SUSPEND),manageRoutes=new Elysia21;return manageRoutes.get("/tenants",async(ctx)=>{let db=config.getDb(),tenantRegistry=config.getTenantRegistry();if(!db||!tenantRegistry)return ctx.set.status=503,{success:!1,message:"Tenant management not available",data:null};let requestingUserId=ctx.request.headers.get("x-user-id");if(!requestingUserId)return ctx.set.status=401,{success:!1,message:"Authentication required",data:null};if(!await verifyGodmin(db,tenantRegistry,requestingUserId))return ctx.set.status=403,{success:!1,message:"Godmin privileges required",data:null};let allTenants=[...tenantRegistry.getActiveTenants(),...Array.from(getAllTenants(tenantRegistry)).filter((t19)=>t19.status!=="active")];return{success:!0,message:`Found ${allTenants.length} tenants`,data:{items:allTenants.map((t19)=>({id:t19.id,subdomain:t19.subdomain,schemaName:t19.schemaName,companyId:t19.companyId,companyName:t19.companyName,godAdminEmail:t19.godAdminEmail,status:t19.status,plan:t19.plan,domain:t19.domain,maxUsers:t19.maxUsers,provisionedAt:t19.provisionedAt,suspendedAt:t19.suspendedAt})),totalCount:allTenants.length}}},{detail:{tags:["Tenants"],summary:"List tenants",description:"List all tenants in the platform. Requires godmin privileges."}}),manageRoutes.get("/tenants/:id",async(ctx)=>{let db=config.getDb(),tenantRegistry=config.getTenantRegistry();if(!db||!tenantRegistry)return ctx.set.status=503,{success:!1,message:"Tenant management not available",data:null};let requestingUserId=ctx.request.headers.get("x-user-id");if(!requestingUserId)return ctx.set.status=401,{success:!1,message:"Authentication required",data:null};if(!await verifyGodmin(db,tenantRegistry,requestingUserId))return ctx.set.status=403,{success:!1,message:"Godmin privileges required",data:null};let{id}=ctx.params,tenant=tenantRegistry.getTenantById(id);if(!tenant)return ctx.set.status=404,{success:!1,message:"Tenant not found",data:null};let features=tenantRegistry.getTenantFeatures(id);return{success:!0,message:"Tenant found",data:{...tenant,features:features.map((f)=>({featureName:f.featureName,enabled:f.enabled}))}}},{detail:{tags:["Tenants"],summary:"Get tenant details",description:"Get detailed information about a specific tenant. Requires godmin privileges."}}),manageRoutes.post("/tenants/:id/suspend",async(ctx)=>{let db=config.getDb(),tenantRegistry=config.getTenantRegistry();if(!db||!tenantRegistry)return ctx.set.status=503,{success:!1,message:"Tenant management not available"};let requestingUserId=ctx.request.headers.get("x-user-id");if(!requestingUserId)return ctx.set.status=401,{success:!1,message:"Authentication required"};if(!await verifyGodmin(db,tenantRegistry,requestingUserId))return ctx.set.status=403,{success:!1,message:"Godmin privileges required"};let{id}=ctx.params,body=ctx.body,tenant=tenantRegistry.getTenantById(id);if(!tenant)return ctx.set.status=404,{success:!1,message:"Tenant not found"};if(tenant.status==="suspended")return ctx.set.status=400,{success:!1,message:"Tenant is already suspended"};let mainContext=tenantRegistry.getMainContext(),tenantsTable=mainContext.schemaTables.tenants;if(!tenantsTable)return ctx.set.status=503,{success:!1,message:"Tenants table not available"};let now=new Date;await db.update(tenantsTable).set({status:"suspended",suspendedAt:now,suspendedReason:body.reason||"Suspended by godmin",updatedAt:now}).where(eq33(col10(tenantsTable,"id"),id));let tenantEventsTable=mainContext.schemaTables.tenantEvents;if(tenantEventsTable)try{await db.insert(tenantEventsTable).values({id:crypto.randomUUID(),tenantId:id,eventType:"suspended",eventData:JSON.stringify({reason:body.reason||"Suspended by godmin"}),performedBy:requestingUserId,ipAddress:ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||"unknown",createdAt:now,updatedAt:now})}catch{log4.warn("Failed to record suspend event")}return await tenantRegistry.refreshTenant(id),log4.info("Tenant suspended",{tenantId:id,reason:body.reason}),{success:!0,message:`Tenant "${tenant.subdomain}" suspended`}},{body:t18.Object({reason:t18.Optional(t18.String())}),detail:{tags:["Tenants"],summary:"Suspend tenant",description:"Suspend a tenant, preventing all access. Requires godmin privileges."}}),manageRoutes.post("/tenants/:id/reactivate",async(ctx)=>{let db=config.getDb(),tenantRegistry=config.getTenantRegistry();if(!db||!tenantRegistry)return ctx.set.status=503,{success:!1,message:"Tenant management not available"};let requestingUserId=ctx.request.headers.get("x-user-id");if(!requestingUserId)return ctx.set.status=401,{success:!1,message:"Authentication required"};if(!await verifyGodmin(db,tenantRegistry,requestingUserId))return ctx.set.status=403,{success:!1,message:"Godmin privileges required"};let{id}=ctx.params,tenant=tenantRegistry.getTenantById(id);if(!tenant)return ctx.set.status=404,{success:!1,message:"Tenant not found"};if(tenant.status!=="suspended")return ctx.set.status=400,{success:!1,message:`Tenant is not suspended (current: ${tenant.status})`};let mainContext=tenantRegistry.getMainContext(),tenantsTable=mainContext.schemaTables.tenants;if(!tenantsTable)return ctx.set.status=503,{success:!1,message:"Tenants table not available"};let now=new Date;await db.update(tenantsTable).set({status:"active",suspendedAt:null,suspendedReason:null,updatedAt:now}).where(eq33(col10(tenantsTable,"id"),id));let tenantEventsTable=mainContext.schemaTables.tenantEvents;if(tenantEventsTable)try{await db.insert(tenantEventsTable).values({id:crypto.randomUUID(),tenantId:id,eventType:"reactivated",eventData:JSON.stringify({previousReason:tenant.suspendedReason}),performedBy:requestingUserId,ipAddress:ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||"unknown",createdAt:now,updatedAt:now})}catch{log4.warn("Failed to record reactivate event")}return await tenantRegistry.refreshTenant(id),log4.info("Tenant reactivated",{tenantId:id}),{success:!0,message:`Tenant "${tenant.subdomain}" reactivated`}},{detail:{tags:["Tenants"],summary:"Reactivate tenant",description:"Reactivate a suspended tenant. Requires godmin privileges."}}),manageRoutes}var verifyGodmin=async(db,tenantRegistry,userId)=>{let usersTable=tenantRegistry.getMainContext().schemaTables.users;if(!usersTable)return!1;return(await db.select().from(usersTable).where(eq33(col10(usersTable,"id"),userId)).limit(1))[0]?.isGod===!0},getAllTenants=(registry2)=>{let schemaNames=registry2.getAllSchemaNames(),tenants=[];for(let name2 of schemaNames){let ctx=registry2.getSchemaContext(name2);if(ctx?.tenant)tenants.push(ctx.tenant)}return tenants};init_Logger2();init_utils6();import{eq as eq34}from"drizzle-orm";import{Elysia as Elysia22,t as t19}from"elysia";var col11=(table,name2)=>table[name2],ProvisionBodySchema=t19.Object({subdomain:t19.String({minLength:2,maxLength:63,pattern:"^[a-z][a-z0-9-]*$"}),companyId:t19.String({minLength:1}),companyName:t19.Optional(t19.String()),godAdminEmail:t19.String({format:"email"}),plan:t19.Optional(t19.String()),domain:t19.Optional(t19.String())});function createProvisionRoute(config){let log4=config.logger.scoped(TENANT_PROVISION),provisionRoutes=new Elysia22;return provisionRoutes.post("/tenants/provision",async(ctx)=>{let db=config.getDb(),tenantRegistry=config.getTenantRegistry();if(!db||!tenantRegistry)return ctx.set.status=503,{success:!1,message:"Tenant provisioning not available"};let requestingUserId=ctx.request.headers.get("x-user-id");if(!requestingUserId)return ctx.set.status=401,{success:!1,message:"Authentication required"};let mainContext=tenantRegistry.getMainContext(),usersTable=mainContext.schemaTables.users;if(!usersTable)return ctx.set.status=503,{success:!1,message:"Users table not available"};let requestingUser=(await db.select().from(usersTable).where(eq34(col11(usersTable,"id"),requestingUserId)).limit(1))[0];if(!requestingUser||!requestingUser.isGod)return ctx.set.status=403,{success:!1,message:"Godmin privileges required"};let body=ctx.body,tenantSchemaName=`tenant_${body.subdomain.replace(/-/g,"_")}`;if(tenantRegistry.getActiveTenants().find((t20)=>t20.subdomain===body.subdomain))return ctx.set.status=409,{success:!1,message:`Subdomain "${body.subdomain}" is already taken`};let tenantsTable=mainContext.schemaTables.tenants;if(!tenantsTable)return ctx.set.status=503,{success:!1,message:"Tenants table not available in main schema"};let tenantId=crypto.randomUUID(),now=new Date;try{await db.insert(tenantsTable).values({id:tenantId,subdomain:body.subdomain,schemaName:tenantSchemaName,companyId:body.companyId,companyName:body.companyName||null,godAdminEmail:body.godAdminEmail,status:"provisioning",plan:body.plan||null,domain:body.domain||null,createdAt:now,updatedAt:now}),log4.info("Tenant record created",{tenantId,subdomain:body.subdomain,schemaName:tenantSchemaName})}catch(err){let msg=err instanceof Error?err.message:String(err);return log4.error("Failed to insert tenant record",{error:msg}),ctx.set.status=500,{success:!1,message:`Failed to create tenant record: ${msg}`}}try{let tenantRecord={id:tenantId,subdomain:body.subdomain,schemaName:tenantSchemaName,companyId:body.companyId,companyName:body.companyName||null,godAdminEmail:body.godAdminEmail,status:"provisioning",plan:body.plan||null,domain:body.domain||null,settings:{},trustedSources:[],maxUsers:null,provisionedAt:null,suspendedAt:null,suspendedReason:null},tenantContext=await tenantRegistry.provisionTenant(tenantRecord);log4.info("Schema provisioned",{tenantId,schemaName:tenantSchemaName,tableCount:Object.keys(tenantContext.schemaTables).length});let tenantUsersTable=tenantContext.schemaTables.users;if(tenantUsersTable){let godminId=crypto.randomUUID(),tempPassword=crypto.randomUUID().slice(0,16),hashedPassword=await hashPassword(tempPassword);await db.insert(tenantUsersTable).values({id:godminId,email:body.godAdminEmail,password:hashedPassword,isGod:!0,isActive:!0,createdAt:now,updatedAt:now}),log4.info("Godmin user seeded",{tenantId,godminEmail:body.godAdminEmail,godminId})}await db.update(tenantsTable).set({status:"active",provisionedAt:now,updatedAt:now}).where(eq34(col11(tenantsTable,"id"),tenantId));let tenantEventsTable=mainContext.schemaTables.tenantEvents;if(tenantEventsTable)try{await db.insert(tenantEventsTable).values({id:crypto.randomUUID(),tenantId,eventType:"provisioned",eventData:JSON.stringify({schemaName:tenantSchemaName,godAdminEmail:body.godAdminEmail,plan:body.plan||null}),performedBy:requestingUserId,ipAddress:ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||"unknown",createdAt:now,updatedAt:now})}catch{log4.warn("Failed to record tenant_event, continuing")}return await tenantRegistry.refreshTenant(tenantId),log4.info("Provisioning complete",{tenantId,subdomain:body.subdomain,schemaName:tenantSchemaName}),await config.logger.audit({entityName:"tenants",entityId:tenantId,operation:"PROVISION",userId:requestingUserId,summary:`Tenant "${body.subdomain}" provisioned by godmin`,ipAddress:ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||"unknown",userAgent:ctx.request.headers.get("user-agent")||"unknown",path:new URL(ctx.request.url).pathname,query:""}),{success:!0,message:"Tenant provisioned successfully",data:{tenantId,schemaName:tenantSchemaName,subdomain:body.subdomain,godAdminEmail:body.godAdminEmail,status:"active"}}}catch(err){let msg=err instanceof Error?err.message:String(err);log4.error("Provisioning failed",{error:msg,tenantId});try{await db.update(tenantsTable).set({status:"provisioning",updatedAt:now}).where(eq34(col11(tenantsTable,"id"),tenantId))}catch{}return ctx.set.status=500,{success:!1,message:`Provisioning failed: ${msg}`}}},{body:ProvisionBodySchema,detail:{tags:["Tenants"],summary:"Provision new tenant",description:"Create a new tenant with its own schema, tables, and godmin user. Requires godmin privileges."}}),provisionRoutes}init_Authorization();init_Logger2();init_utils6();import{eq as eq35}from"drizzle-orm";import{Elysia as Elysia23,t as t20}from"elysia";var col12=(table,name2)=>table[name2],ipRateMap=new Map,lastCleanup=Date.now();function cleanupRateMap(windowMs){let now=Date.now();if(now-lastCleanup<600000)return;lastCleanup=now;for(let[key2,entry]of ipRateMap.entries())if(now-entry.windowStart>windowMs)ipRateMap.delete(key2)}function isIpRateLimited(ip,maxPerWindow,windowMs){let now=Date.now(),entry=ipRateMap.get(ip);if(!entry||now-entry.windowStart>windowMs)return ipRateMap.set(ip,{count:1,windowStart:now}),!1;return entry.count++,entry.count>maxPerWindow}var SelfSignupBodySchema=t20.Object({email:t20.String({format:"email"}),password:t20.String({minLength:8}),subdomain:t20.String({minLength:2,maxLength:63,pattern:"^[a-z][a-z0-9-]*$"}),plan:t20.Optional(t20.String()),companyName:t20.Optional(t20.String())});function createSelfSignupRoute(config){let log4=config.logger.scoped(TENANT_SELF_SIGNUP),limits={...DEFAULT_SELF_SIGNUP_LIMITS,...config.selfSignupLimits},selfSignupRoutes=new Elysia23;return selfSignupRoutes.post("/tenants/self-signup",async(ctx)=>{cleanupRateMap(limits.ipWindowMs);let db=config.getDb(),tenantRegistry=config.getTenantRegistry();if(!db||!tenantRegistry)return ctx.set.status=503,{success:!1,message:"Tenant provisioning not available"};let ip=ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||ctx.request.headers.get("x-real-ip")?.trim()||"unknown";if(isIpRateLimited(ip,limits.maxSignupsPerIpPerWindow,limits.ipWindowMs))return log4.warn("Self-signup rate limited",{ip}),ctx.set.status=429,{success:!1,message:"Too many signup attempts. Please try again later."};let body=ctx.body,email=body.email.toLowerCase().trim(),subdomain=body.subdomain.toLowerCase().trim();if(RESERVED_SUBDOMAINS.includes(subdomain))return ctx.set.status=400,{success:!1,message:"This subdomain is reserved."};let mainContext=tenantRegistry.getMainContext(),usersTable=mainContext.schemaTables.users,tenantsTable=mainContext.schemaTables.tenants;if(!usersTable||!tenantsTable)return ctx.set.status=503,{success:!1,message:"Required tables not available."};if(tenantRegistry.getActiveTenants().find((t21)=>t21.subdomain===subdomain))return ctx.set.status=409,{success:!1,message:"This subdomain is already taken."};try{if((await db.select().from(tenantsTable).where(eq35(col12(tenantsTable,"subdomain"),subdomain)).limit(1)).length>0)return ctx.set.status=409,{success:!1,message:"This subdomain is already taken."}}catch{}try{if((await db.select().from(tenantsTable).where(eq35(col12(tenantsTable,"godAdminEmail"),email))).length>=limits.maxTenantsPerEmail)return ctx.set.status=409,{success:!1,message:`Tenant limit reached. Maximum ${limits.maxTenantsPerEmail} tenant(s) per account.`}}catch{}let userId,hashedPassword=await hashPassword(body.password),now=new Date,existingUsers=await db.select().from(usersTable).where(eq35(col12(usersTable,"email"),email)).limit(1);if(existingUsers.length>0)userId=existingUsers[0].id,log4.info("Self-signup: existing user resolved",{userId,email});else{userId=crypto.randomUUID();try{await db.insert(usersTable).values({id:userId,email,password:hashedPassword,isActive:!0,createdAt:now,updatedAt:now}),log4.info("Self-signup: user created",{userId,email})}catch(err){let msg=err instanceof Error?err.message:String(err);return log4.error("Self-signup: user creation failed",{error:msg}),ctx.set.status=500,{success:!1,message:"Failed to create account."}}}let tenantId=crypto.randomUUID(),tenantSchemaName=`tenant_${subdomain.replace(/-/g,"_")}`;try{await db.insert(tenantsTable).values({id:tenantId,subdomain,schemaName:tenantSchemaName,companyId:userId,companyName:body.companyName||subdomain,godAdminEmail:email,status:"provisioning",plan:body.plan||null,createdAt:now,updatedAt:now}),log4.info("Tenant record created",{tenantId,subdomain})}catch(err){let msg=err instanceof Error?err.message:String(err);return log4.error("Tenant record creation failed",{error:msg}),ctx.set.status=500,{success:!1,message:"Failed to create tenant."}}try{let tenantRecord={id:tenantId,subdomain,schemaName:tenantSchemaName,companyId:userId,companyName:body.companyName||subdomain,godAdminEmail:email,status:"provisioning",plan:body.plan||null,domain:null,settings:{},trustedSources:[],maxUsers:null,provisionedAt:null,suspendedAt:null,suspendedReason:null},tenantContext=await tenantRegistry.provisionTenant(tenantRecord);log4.info("Schema provisioned",{tenantId,schemaName:tenantSchemaName,tableCount:Object.keys(tenantContext.schemaTables).length});let tenantUsersTable=tenantContext.schemaTables.users;if(tenantUsersTable)await db.insert(tenantUsersTable).values({id:userId,email,password:hashedPassword,isGod:!0,isActive:!0,createdAt:now,updatedAt:now}),log4.info("Godmin seeded in tenant schema",{tenantId,email,userId});let tenantRolesTable=tenantContext.schemaTables.roles,tenantUserRolesTable=tenantContext.schemaTables.userRoles;if(tenantRolesTable&&tenantUserRolesTable)try{let godminRoles=await db.select().from(tenantRolesTable).where(eq35(tenantRolesTable.name,GODMIN_ROLE_NAME)).limit(1),godminRoleId;if(godminRoles.length>0)godminRoleId=godminRoles[0].id;else{let[newRole]=await db.insert(tenantRolesTable).values({name:GODMIN_ROLE_NAME,description:"God mode administrator - bypasses all authorization checks"}).returning();godminRoleId=newRole.id,log4.info("Created godmin role in tenant schema",{tenantId,roleId:godminRoleId})}if(godminRoleId)await db.insert(tenantUserRolesTable).values({userId,roleId:godminRoleId}),log4.info("Assigned godmin role to signup user",{tenantId,userId,roleId:godminRoleId})}catch(roleErr){log4.warn("Failed to assign godmin role to signup user",{tenantId,userId,error:roleErr instanceof Error?roleErr.message:String(roleErr)})}await db.update(tenantsTable).set({status:"active",provisionedAt:now,updatedAt:now}).where(eq35(col12(tenantsTable,"id"),tenantId));let tenantEventsTable=mainContext.schemaTables.tenantEvents;if(tenantEventsTable)try{await db.insert(tenantEventsTable).values({id:crypto.randomUUID(),tenantId,eventType:"self_signup",eventData:JSON.stringify({email,subdomain,plan:body.plan||null}),performedBy:userId,ipAddress:ip,createdAt:now,updatedAt:now})}catch{}return await tenantRegistry.refreshTenant(tenantId),log4.info("Self-signup complete",{tenantId,subdomain,email}),await config.logger.audit({entityName:"tenants",entityId:tenantId,operation:"SELF_SIGNUP",userId,summary:`Self-signup: tenant "${subdomain}" provisioned by ${email}`,ipAddress:ip,userAgent:ctx.request.headers.get("user-agent")||"unknown",path:new URL(ctx.request.url).pathname,query:""}),{success:!0,message:"Tenant created successfully",data:{tenantId,subdomain,schemaName:tenantSchemaName,userId,email,status:"active"}}}catch(err){let msg=err instanceof Error?err.message:String(err);log4.error("Self-signup provisioning failed",{error:msg,tenantId});try{await db.update(tenantsTable).set({status:"provisioning",updatedAt:now}).where(eq35(col12(tenantsTable,"id"),tenantId))}catch{}return ctx.set.status=500,{success:!1,message:"Provisioning failed. Please try again."}}},{body:SelfSignupBodySchema,detail:{tags:["Tenants"],summary:"Self-service tenant signup",description:"Public endpoint for self-service signup. Creates user account, provisions tenant schema, and seeds godmin. Rate-limited by IP with configurable max tenants per email."}}),selfSignupRoutes}function createTenantRoutes(app,config){return app.use(createCheckSubdomainRoute(config)),app.use(createProvisionRoute(config)),app.use(createSelfSignupRoute(config)),app.use(createManageRoutes(config)),app}init_Notification();init_Verification();init_adminGuard();import{Elysia as Elysia24,t as t21}from"elysia";function createVerificationRoutes(routeConfig){let{db,schemaTables,config,logger:logger2,tenantRegistry}=routeConfig,defaultVerificationService=new VerificationService({db,schemaTables,config,logger:logger2}),defaultNotificationService=new NotificationService({db,schemaTables,config:routeConfig.notificationConfig||{enabled:!1},logger:logger2,getEmailService:routeConfig.getEmailService}),wireNotificationHandler=(verSvc,notifSvc)=>{verSvc.setNotificationHandler(async(params)=>{if(!routeConfig.notificationConfig?.enabled)return;await notifSvc.triggerNotifications({trigger:params.trigger,flow_id:params.flow_id,entity_name:params.entity_name,entity_id:params.entity_id,node_id:params.node_id,verifier_id:params.verifier_id,decision:params.decision,context:params.context})})};wireNotificationHandler(defaultVerificationService,defaultNotificationService);let getServicesForRequest=(request)=>{if(!tenantRegistry)return{verificationService:defaultVerificationService,notificationService:defaultNotificationService};let schemaName=request.headers.get("x-tenant-schema");if(!schemaName)return{verificationService:defaultVerificationService,notificationService:defaultNotificationService};let ctx=tenantRegistry.getSchemaContext(schemaName);if(!ctx)return{verificationService:defaultVerificationService,notificationService:defaultNotificationService};let reqVerSvc=new VerificationService({db,schemaTables:ctx.schemaTables,config,logger:logger2}),reqNotifSvc=new NotificationService({db,schemaTables:ctx.schemaTables,config:routeConfig.notificationConfig||{enabled:!1},logger:logger2,getEmailService:routeConfig.getEmailService});return wireNotificationHandler(reqVerSvc,reqNotifSvc),{verificationService:reqVerSvc,notificationService:reqNotifSvc}},basePath=config.endpoints?.basePath||"/verifications",flowBasePath=config.flowEndpoints?.basePath||"/verification-flows",routes=new Elysia24,verificationRoutes=new Elysia24({prefix:basePath});if(verificationRoutes.onBeforeHandle(({request,set})=>{if(!request.headers.get("x-user-id"))return set.status=401,{success:!1,message:"Authentication required"}}),verificationRoutes.get("/status/:entity_name/:entity_id",async({params,request,set})=>{let{verificationService}=getServicesForRequest(request),status=await verificationService.getStatus(params.entity_name,params.entity_id),startedBy=status.instance?.started_by;if(!hasGodminRole(request)&&startedBy!=null&&String(startedBy)!==String(request.headers.get("x-user-id")))return set.status=403,{success:!1,message:"Forbidden"};return{success:!0,data:status}},{params:t21.Object({entity_name:t21.String(),entity_id:t21.String()})}),verificationRoutes.post("/:entity_name/:entity_id/decide",async({params,body,request})=>{let{verificationService}=getServicesForRequest(request),userId=request.headers.get("x-user-id");if(!userId)return{success:!1,message:"User ID required",status:401};return await verificationService.decide({entity_name:params.entity_name,entity_id:params.entity_id,user_id:userId,decision:body.decision,reason:body.reason,signature_id:body.signature_id,diff:body.diff})},{params:t21.Object({entity_name:t21.String(),entity_id:t21.String()}),body:t21.Object({decision:t21.Union([t21.Literal("approved"),t21.Literal("rejected")]),reason:t21.Optional(t21.String()),signature_id:t21.Optional(t21.String()),diff:t21.Optional(t21.Record(t21.String(),t21.Unknown()))})}),verificationRoutes.get("/pending",async({request})=>{let{verificationService}=getServicesForRequest(request),userId=request.headers.get("x-user-id");if(!userId)return{success:!1,message:"User ID required",status:401};return{success:!0,data:await verificationService.getPending(userId)}}),verificationRoutes.get("/history/:entity_name/:entity_id",async({params,request,set})=>{let{verificationService}=getServicesForRequest(request),status=await verificationService.getStatus(params.entity_name,params.entity_id),startedBy=status.instance?.started_by;if(!hasGodminRole(request)&&startedBy!=null&&String(startedBy)!==String(request.headers.get("x-user-id")))return set.status=403,{success:!1,message:"Forbidden"};return{success:!0,data:{verifications:status.verifications,current_step:status.current_step,total_steps:status.total_steps,is_completed:status.is_completed,is_rejected:status.is_rejected}}},{params:t21.Object({entity_name:t21.String(),entity_id:t21.String()})}),verificationRoutes.post("/start",async({body,request})=>{let{verificationService}=getServicesForRequest(request),userId=request.headers.get("x-user-id");return await verificationService.startFlow({flow_id:body.flow_id,entity_name:body.entity_name,entity_id:body.entity_id,started_by:userId||void 0})},{body:t21.Object({flow_id:t21.String(),entity_name:t21.String(),entity_id:t21.String()})}),verificationRoutes.post("/start-for-entity",async({body,request})=>{let{verificationService}=getServicesForRequest(request),userId=request.headers.get("x-user-id");return await verificationService.startFlowForEntity({entity_name:body.entity_name,entity_id:body.entity_id,started_by:userId||void 0})},{body:t21.Object({entity_name:t21.String(),entity_id:t21.String()})}),verificationRoutes.get("/statuses/:entity_name",async({params,query,request,set})=>{if(!hasGodminRole(request))return set.status=403,{success:!1,message:"Forbidden: operator privileges required"};let{verificationService}=getServicesForRequest(request);return{success:!0,data:await verificationService.listEntityStatuses({entity_name:params.entity_name,status:query.status||void 0,page:query.page?Number(query.page):void 0,limit:query.limit?Number(query.limit):void 0})}},{params:t21.Object({entity_name:t21.String()}),query:t21.Object({status:t21.Optional(t21.String()),page:t21.Optional(t21.String()),limit:t21.Optional(t21.String())})}),routes.use(verificationRoutes),logger2.info(`[Verification] Routes registered at ${basePath}`),config.flowEndpoints?.enabled!==!1){let flowRoutes=new Elysia24({prefix:flowBasePath});flowRoutes.onBeforeHandle(createGodminGuard({db,schemaTables,logger:logger2},"verification flow management")),flowRoutes.get("/",async({query,request})=>{let{verificationService:vs}=getServicesForRequest(request);return{success:!0,data:await vs.listFlows(query.entity_name||void 0)}},{query:t21.Object({entity_name:t21.Optional(t21.String())})}),flowRoutes.get("/:flow_id",async({params,request})=>{let{verificationService:vs}=getServicesForRequest(request),result=await vs.getFlow(params.flow_id);if(!result)return{success:!1,message:"Flow not found"};return{success:!0,data:result}},{params:t21.Object({flow_id:t21.String()})}),flowRoutes.post("/",async({body,request})=>{let{verificationService:vs}=getServicesForRequest(request);return await vs.saveFlow(body)},{body:t21.Object({flow_id:t21.String(),entity_name:t21.String(),name:t21.String(),description:t21.Optional(t21.String()),trigger_on:t21.Union([t21.Literal("create"),t21.Literal("update"),t21.Literal("delete"),t21.Literal("manual")]),trigger_fields:t21.Optional(t21.Array(t21.String())),is_draft:t21.Boolean(),viewport:t21.Optional(t21.Object({x:t21.Number(),y:t21.Number(),zoom:t21.Number()})),graph:t21.Object({steps:t21.Array(t21.Any()),edges:t21.Array(t21.Any()),verifier_configs:t21.Array(t21.Any()),notification_rules:t21.Array(t21.Any()),notification_recipients:t21.Array(t21.Any()),notification_channels:t21.Array(t21.Any())})})}),flowRoutes.post("/:flow_id/publish",async({params,request})=>{let{verificationService:vs}=getServicesForRequest(request);return await vs.publishFlow(params.flow_id)},{params:t21.Object({flow_id:t21.String()})}),flowRoutes.delete("/:flow_id",async({params,request})=>{let{verificationService:vs}=getServicesForRequest(request);return await vs.deleteFlow(params.flow_id)},{params:t21.Object({flow_id:t21.String()})}),routes.use(flowRoutes),logger2.info(`[Verification] Flow routes registered at ${flowBasePath}`)}return{routes,verificationService:defaultVerificationService,notificationService:defaultNotificationService}}function createNucleusRuntime(deps){return{sendEmail:async(payload)=>{let mailer=deps.getEmailService();if(!mailer?.isAvailable())return{success:!1,error:"No email provider is configured or available"};return mailer.sendEmail(payload)},emailProvider:()=>deps.activeEmailProvider(),isEmailAvailable:()=>!!deps.getEmailService()?.isAvailable()}}import{Elysia as Elysia25}from"elysia";var a=`/* basic theme */
|
|
553
|
+
`),LOG_LEVELS=["debug","info","warn","error","fatal"],SOURCES=["logger","stdout","stderr"];function parseLevels(raw){if(!raw)return;let parsed=raw.split(",").map((value)=>value.trim().toLowerCase()).filter((value)=>LOG_LEVELS.includes(value));return parsed.length>0?parsed:void 0}function parseSource(raw){if(!raw)return;let value=raw.trim().toLowerCase();return SOURCES.includes(value)?value:void 0}function parseMinLevel(raw){if(!raw)return;let value=raw.trim().toLowerCase();return LOG_LEVELS.includes(value)?value:void 0}function parseCursor(raw){if(raw===void 0||raw===null||raw==="")return;let value=Number(raw);return Number.isFinite(value)&&value>=0?value:void 0}function buildQuery(query){return{levels:parseLevels(query.levels),minLevel:parseMinLevel(query.minLevel),scope:query.scope?.trim()||void 0,search:query.search||void 0,source:parseSource(query.source),after:parseCursor(query.after),before:parseCursor(query.before),limit:parseCursor(query.limit)}}var STREAM_KEEPALIVE_MS=25000;function createServerLogRoutes(config){let{buffer,logger:logger2,basePath,path:path8,allowRoles,streamEnabled=!0,clearEnabled=!0,db,schemaTables}=config,plugin=new Elysia16({prefix:basePath});if(plugin.onBeforeHandle(createPrivilegedGuard({db:db??null,schemaTables:schemaTables??{},logger:logger2},"server logs",{allowRoles})),plugin.get(path8,({query})=>{return{isSuccess:!0,message:"Server logs",data:{...buffer.query(buildQuery(query)),scopes:buffer.scopes(),levelCounts:buffer.levelCounts()}}},{query:t14.Object({levels:t14.Optional(t14.String()),minLevel:t14.Optional(t14.String()),scope:t14.Optional(t14.String()),search:t14.Optional(t14.String()),source:t14.Optional(t14.String()),after:t14.Optional(t14.Numeric()),before:t14.Optional(t14.Numeric()),limit:t14.Optional(t14.Numeric())}),detail:{tags:["Monitoring"],summary:"Read buffered server logs",description:"Filtered, cursor-paged view of the in-memory server log buffer. Use `after` to tail forward and `before` to page backwards."}}),streamEnabled)plugin.get(`${path8}/stream`,({request,query})=>{let tailQuery=buildQuery(query),{after,before:_before,limit:_limit,...filter}=tailQuery,abortSignal=request.signal,cleanup,stream=new ReadableStream({start(controller){let closed=!1,backlog=buffer.query({...filter,...after!==void 0?{after}:{},limit:200});controller.enqueue(encodeEvent2("backlog",backlog));let unsubscribe=buffer.subscribe((record)=>{if(closed)return;if(!buffer.query({...filter,after:record.seq-1,limit:1}).records.some((r)=>r.seq===record.seq))return;try{controller.enqueue(encodeEvent2("logs",[record]))}catch{cleanup?.()}}),heartbeat=setInterval(()=>{if(closed)return;try{controller.enqueue(encodeEvent2("ping",{timestamp:Date.now()}))}catch{cleanup?.()}},STREAM_KEEPALIVE_MS),cleanupHandler=()=>{if(closed)return;closed=!0,clearInterval(heartbeat),unsubscribe(),abortSignal?.removeEventListener("abort",cleanupHandler);try{controller.close()}catch{}};cleanup=cleanupHandler,abortSignal?.addEventListener("abort",cleanupHandler)},cancel(){cleanup?.()}});return new Response(stream,{headers:STREAM_HEADERS2})},{query:t14.Object({levels:t14.Optional(t14.String()),minLevel:t14.Optional(t14.String()),scope:t14.Optional(t14.String()),search:t14.Optional(t14.String()),source:t14.Optional(t14.String()),after:t14.Optional(t14.Numeric())}),detail:{tags:["Monitoring"],summary:"Tail server logs",description:"Server-Sent Events tail of the server log buffer, filtered server-side."}});if(clearEnabled)plugin.delete(path8,()=>{return buffer.clear(),logger2.info("[ServerLogs] Buffer cleared from the admin panel"),{isSuccess:!0,message:"Server log buffer cleared",data:null}},{detail:{tags:["Monitoring"],summary:"Clear the server log buffer"}});return logger2.info(`[ServerLogs] Routes enabled at ${basePath}${path8} (capacity: ${buffer.getCapacity()}, stream: ${streamEnabled})`),plugin}init_Notification();init_adminGuard();import{Elysia as Elysia17,t as t15}from"elysia";var CHANNELS=["portal","email","telegram","webhook"];function createNotificationRoutes(routeConfig){let{db,schemaTables,config,logger:logger2,tenantRegistry,getEmailService}=routeConfig,basePath=routeConfig.basePath||config.endpoints?.basePath||"/notifications",buildService=(tables)=>new NotificationService({db,schemaTables:tables,config,logger:logger2,getEmailService}),defaultService=buildService(schemaTables),serviceFor=(request)=>{if(!tenantRegistry)return defaultService;let schemaName=request.headers.get("x-tenant-schema");if(!schemaName)return defaultService;let ctx=tenantRegistry.getSchemaContext(schemaName);return ctx?buildService(ctx.schemaTables):defaultService},allowedSendRoles=new Set((config.endpoints?.sendRoles??[]).map((role)=>role.toLowerCase())),canSend=async(request)=>{if(allowedSendRoles.size>0){if(decodeHeaderList(request.headers.get("x-user-roles")).some((role)=>allowedSendRoles.has(role.toLowerCase())))return!0}return isGodminRequest(request,{db,schemaTables,logger:logger2})},routes=new Elysia17({prefix:basePath}),requireUser=(request,set)=>{let userId=request.headers.get("x-user-id");if(!userId)return set.status=401,null;return userId};return routes.get("/",async({request,query,set})=>{let userId=requireUser(request,set);if(!userId)return{success:!1,message:"User ID required"};return{success:!0,data:await serviceFor(request).getNotifications(userId,{limit:query.limit?Number(query.limit):void 0,offset:query.offset?Number(query.offset):void 0,type:query.type||void 0})}},{query:t15.Object({limit:t15.Optional(t15.String()),offset:t15.Optional(t15.String()),type:t15.Optional(t15.String())}),detail:{tags:["Notifications"],summary:"List the caller\u2019s notifications"}}),routes.get("/unseen-count",async({request,set})=>{let userId=requireUser(request,set);if(!userId)return{success:!1,message:"User ID required"};return{success:!0,data:{count:await serviceFor(request).getUnseenCount(userId)}}},{detail:{tags:["Notifications"],summary:"Unseen notification count"}}),routes.post("/:notification_id/seen",async({params,request,set})=>{let userId=requireUser(request,set);if(!userId)return{success:!1,message:"User ID required"};let marked=await serviceFor(request).markAsSeen(params.notification_id,userId);return{success:marked,message:marked?"Marked as seen":"Failed"}},{params:t15.Object({notification_id:t15.String()}),detail:{tags:["Notifications"],summary:"Mark one notification as seen"}}),routes.post("/seen-all",async({request,set})=>{let userId=requireUser(request,set);if(!userId)return{success:!1,message:"User ID required"};return{success:!0,data:{count:await serviceFor(request).markAllAsSeen(userId)}}},{detail:{tags:["Notifications"],summary:"Mark every notification as seen"}}),routes.post("/send",async({body,request,set})=>{if(!requireUser(request,set))return{success:!1,message:"Authentication required"};if(!await canSend(request))return set.status=403,{success:!1,message:"Sending notifications requires godmin or an allowed role"};let recipients=body.userIds??(body.userId?[body.userId]:[]);if(recipients.length===0)return set.status=400,{success:!1,message:"userId or userIds is required"};let channels=body.channels??[config.defaultChannel??"portal"],service=serviceFor(request),failed=(await Promise.allSettled(recipients.map((user_id)=>service.send({user_id,title:body.title,body:body.body,entity_name:body.entityName,entity_id:body.entityId,type:body.type??"custom",source:body.source,channels})))).map((result,index)=>({result,userId:recipients[index]})).filter((entry)=>entry.result.status==="rejected").map((entry)=>entry.userId);return{success:failed.length===0,message:failed.length===0?`Sent to ${recipients.length} recipient(s)`:`Sent to ${recipients.length-failed.length}, failed for ${failed.length}`,data:{sent:recipients.length-failed.length,failed}}},{body:t15.Object({userId:t15.Optional(t15.String()),userIds:t15.Optional(t15.Array(t15.String())),title:t15.String(),body:t15.Optional(t15.String()),entityName:t15.Optional(t15.String()),entityId:t15.Optional(t15.String()),type:t15.Optional(t15.Union([t15.Literal("verification"),t15.Literal("system"),t15.Literal("custom")])),source:t15.Optional(t15.String()),channels:t15.Optional(t15.Array(t15.Union(CHANNELS.map((c)=>t15.Literal(c)))))}),detail:{tags:["Notifications"],summary:"Send a notification to one or more users",description:"Privileged. Delivers over the requested channels (portal, email, telegram, webhook). Email uses the configured provider \u2014 credentials resolve through the secrets store, so a rotation applies immediately."}}),routes.post("/email",async({body,request,set})=>{if(!requireUser(request,set))return{success:!1,message:"Authentication required"};if(!await canSend(request))return set.status=403,{success:!1,message:"Sending email requires godmin or an allowed role"};let emailService=getEmailService?.()??null;if(!emailService?.isAvailable())return set.status=503,{success:!1,message:"No email provider is configured or available"};let result=await emailService.sendEmail({to:body.to,subject:body.subject,html:body.html,text:body.text,replyTo:body.replyTo});if(!result.success)return set.status=502,{success:!1,message:result.error??"Email delivery failed"};return logger2.info("[Notification] Transactional email sent",{recipients:Array.isArray(body.to)?body.to.length:1,subject:body.subject}),{success:!0,message:"Email sent",data:null}},{body:t15.Object({to:t15.Union([t15.String(),t15.Array(t15.String())]),subject:t15.String(),html:t15.Optional(t15.String()),text:t15.Optional(t15.String()),replyTo:t15.Optional(t15.String())}),detail:{tags:["Notifications"],summary:"Send a transactional email to explicit recipients",description:"Backend-to-backend send through the configured provider \u2014 an app never needs an email SDK or the provider credentials of its own. Privileged, same gate as /send."}}),{routes,notificationService:defaultService}}init_ack_manager();import{timingSafeEqual as timingSafeEqual3}from"crypto";import Elysia18 from"elysia";init_ack_manager();var clients=new Map,presenceBroadcastDebounce=new Map,DEFAULT_MAX_TOPICS_PER_CLIENT=512;function createClientManager(config){let{redis,logger:logger2}=config,ackTtl=config.ack.ttlSeconds,ackEnabled=config.ack.enabled,maxRetries=config.ack.maxRetries,presenceEnabled=config.presence.enabled,presenceDebounceMs=config.presence.debounceMs,maxClientsPerUser=config.maxClientsPerUser,maxTopicsPerClient=config.maxTopicsPerClient??DEFAULT_MAX_TOPICS_PER_CLIENT,MAX_TOPIC_LENGTH=256;function generateClientId(){return`client_${Date.now()}_${Math.random().toString(36).substring(2,9)}`}function capTopics(topics){let accepted=[],dropped=[],seen=new Set;for(let t16 of topics){if(typeof t16!=="string")continue;let trimmed=t16.length>256?t16.slice(0,256):t16;if(seen.has(trimmed))continue;if(seen.add(trimmed),accepted.length>=maxTopicsPerClient){dropped.push(trimmed);continue}accepted.push(trimmed)}return{accepted,dropped}}function reportDropped(context,clientId,userId,dropped){if(dropped.length===0)return;logger2.warn("[PubSub] Topic subscription capped \u2014 events for these will NOT be delivered",{context,clientId,userId,cap:maxTopicsPerClient,droppedCount:dropped.length,dropped})}function registerClient(clientId,ws,userId,topics){let existingClients=getClientsByUser(userId);if(existingClients.length>=maxClientsPerUser){let oldestClientId=existingClients[0];if(oldestClientId)logger2.info("[PubSub] Max clients reached, closing oldest",{userId,oldestClientId}),unregisterClient(oldestClientId)}let{accepted,dropped}=capTopics(topics);return clients.set(clientId,{ws,userId,subscribedTopics:new Set(accepted),connectedAt:Date.now(),pendingAcks:new Map}),logger2.info("[PubSub] Client registered",{clientId,userId,topics:accepted,requestedCount:topics.length,subscribedCount:accepted.length,totalClients:clients.size}),reportDropped("registerClient",clientId,userId,dropped),{accepted,dropped}}function unregisterClient(clientId){let client=clients.get(clientId);if(!client)return;let userId=client.userId;if(clients.delete(clientId),logger2.info("[PubSub] Client unregistered",{clientId,userId,totalClients:clients.size}),presenceEnabled&&userId){if(getClientsByUser(userId).length===0)broadcastPresenceToOthers(userId,"offline")}}function updateClientTopics(clientId,topics){let client=clients.get(clientId);if(!client)return{accepted:[],dropped:[]};let{accepted,dropped}=capTopics(topics);return client.subscribedTopics=new Set(accepted),reportDropped("updateClientTopics",clientId,client.userId,dropped),{accepted,dropped}}function getClientsByUser(userId){let result=[];for(let[clientId,client]of clients)if(client.userId===userId)result.push(clientId);return result}function getConnectedUserIds(){let userIds=new Set;for(let[,client]of clients)if(client.userId)userIds.add(client.userId);return userIds}function getClientCount(){return clients.size}function sendToClient(clientId,message){let client=clients.get(clientId);if(!client)return!1;try{return client.ws.send(JSON.stringify(message)),!0}catch{return clients.delete(clientId),!1}}function broadcastEvent(topic,event){let messageId=generateMessageId(),timestamp=Date.now(),payload=JSON.stringify({type:"event",messageId:ackEnabled?messageId:void 0,topic,data:event,timestamp}),sentCount=0;for(let[clientId,client]of clients)if(client.subscribedTopics.has("*")||client.subscribedTopics.has(topic))try{if(client.ws.send(payload),sentCount++,ackEnabled){if(client.pendingAcks.set(messageId,{sentAt:timestamp,retryCount:0}),recordSent(),client.userId)storePendingMessage(redis,{messageId,topic,clientId,userId:client.userId,data:event,sentAt:timestamp,retryCount:0,maxRetries},ackTtl).catch(()=>{})}}catch{clients.delete(clientId)}logger2.info("[PubSub] Broadcasted event",{topic,messageId,sentCount})}function broadcastToUser(userId,topic,event){let messageId=generateMessageId(),timestamp=Date.now(),payload=JSON.stringify({type:"event",messageId:ackEnabled?messageId:void 0,topic,data:event,timestamp}),sentCount=0,sentToAnyClient=!1;for(let[clientId,client]of clients){if(client.userId!==userId)continue;if(!client.subscribedTopics.has("*")&&!client.subscribedTopics.has(topic))continue;try{if(client.ws.send(payload),sentCount++,sentToAnyClient=!0,ackEnabled)client.pendingAcks.set(messageId,{sentAt:timestamp,retryCount:0}),recordSent()}catch{clients.delete(clientId)}}if(ackEnabled)storePendingMessage(redis,{messageId,topic,clientId:sentToAnyClient?"delivered":"pending",userId,data:event,sentAt:timestamp,retryCount:0,maxRetries},ackTtl).catch(()=>{});logger2.info("[PubSub] Broadcasted to user",{userId,topic,messageId,sentCount,storedForOffline:!sentToAnyClient})}function broadcastToUserNoAck(userId,topic,event){let timestamp=Date.now(),payload=JSON.stringify({type:"event",topic,data:event,timestamp});for(let[clientId,client]of clients){if(client.userId!==userId)continue;if(!client.subscribedTopics.has("*")&&!client.subscribedTopics.has(topic))continue;try{client.ws.send(payload)}catch{clients.delete(clientId)}}}function broadcastPresenceToOthers(userId,status){let debounceKey=`${userId}:${status}`,now=Date.now(),lastBroadcast=presenceBroadcastDebounce.get(debounceKey);if(lastBroadcast&&now-lastBroadcast<presenceDebounceMs)return;if(presenceBroadcastDebounce.set(debounceKey,now),presenceBroadcastDebounce.size>1000){let cutoff=now-presenceDebounceMs*2;for(let[key2,timestamp]of presenceBroadcastDebounce)if(timestamp<cutoff)presenceBroadcastDebounce.delete(key2)}let connectedUserIds=getConnectedUserIds();for(let targetUserId of connectedUserIds)if(targetUserId!==userId)broadcastToUserNoAck(targetUserId,"user-presence",{type:"user-presence",data:{userId,status}})}async function handleClientAck(clientId,messageId){if(!ackEnabled)return!0;let client=clients.get(clientId);if(!client)return!1;if(client.pendingAcks.get(messageId))client.pendingAcks.delete(messageId);if(client.userId)return acknowledgeMessage(redis,client.userId,messageId,ackTtl);return!0}async function deliverPendingMessages(userId,clientId){if(!ackEnabled)return 0;let client=clients.get(clientId);if(!client||client.userId!==userId)return 0;let pendingMessages=await getPendingMessagesForUser(redis,userId);if(pendingMessages.length===0)return 0;logger2.info("[PubSub] Delivering pending messages",{userId,count:pendingMessages.length});let deliveredCount=0;for(let message of pendingMessages){if(!client.subscribedTopics.has("*")&&!client.subscribedTopics.has(message.topic))continue;let payload=JSON.stringify({type:"event",messageId:message.messageId,topic:message.topic,data:message.data,timestamp:message.sentAt,isRedelivery:!0});try{client.ws.send(payload),client.pendingAcks.set(message.messageId,{sentAt:Date.now(),retryCount:message.retryCount+1}),await incrementRetryCount(redis,userId,message.messageId,ackTtl,maxRetries),deliveredCount++}catch{break}}return deliveredCount}return{generateClientId,registerClient,unregisterClient,updateClientTopics,getClientsByUser,getConnectedUserIds,getClientCount,sendToClient,broadcastEvent,broadcastToUser,broadcastToUserNoAck,broadcastPresenceToOthers,handleClientAck,deliverPendingMessages,getPendingMessageCount:(userId)=>getPendingMessageCount(redis,userId)}}var OPAQUE_RUN=/[A-Za-z0-9_-]{24,}(?:\.[A-Za-z0-9_-]{8,}){0,2}/g;function previewOf2(frame){return frame.slice(0,120).replace(OPAQUE_RUN,(match)=>`[redacted ${match.length} chars]`)}function classifyClientFrame(message){if(typeof message==="string"){if(message.trim()==="")return{kind:"ignore"};let parsed;try{parsed=JSON.parse(message)}catch{return{kind:"malformed",preview:previewOf2(message)}}return asMessage(parsed,message.length)}if(message&&typeof message==="object")return asMessage(message,approximateSize(message));return{kind:"ignore"}}function approximateSize(value){if(ArrayBuffer.isView(value))return value.byteLength;if(value instanceof ArrayBuffer)return value.byteLength;try{return JSON.stringify(value)?.length??0}catch{return 0}}function asMessage(value,size){if(!value||typeof value!=="object"||Array.isArray(value))return{kind:"ignore"};let record=value,type=record.type;if(typeof type!=="string"||type==="")return{kind:"ignore"};return{kind:"message",parsed:record,type,size}}function tokensMatch(a,b){let ba=Buffer.from(a),bb=Buffer.from(b);if(ba.length!==bb.length)return!1;return timingSafeEqual3(ba,bb)}var HEARTBEAT_WS_TYPES=new Set(["ping","pong","ack"]);function buildHandshakeHeaders(rawHeaders,queryToken){let headers=new Headers;if(rawHeaders&&typeof rawHeaders==="object"){for(let[key2,value]of Object.entries(rawHeaders))if(typeof value==="string")headers.set(key2,value)}if(queryToken)headers.set("authorization",`Bearer ${queryToken}`),headers.delete("cookie");return headers}function createPubSubRoutes(config){let{logger:logger2,getLiveMonitoringService}=config,clientManager=createClientManager(config);if(config.ack.enabled)initAckCleanup();let cleanupTimer=null,daprApiToken=config.daprApiToken||process.env.APP_API_TOKEN||process.env.DAPR_API_TOKEN,warnedNoDaprToken=!1,plugin=new Elysia18;return plugin.onStart(()=>{if(config.cleanupIntervalMs>0)cleanupTimer=setInterval(()=>{logger2.info("[PubSub] Periodic cleanup running",{totalClients:clientManager.getClientCount()})},config.cleanupIntervalMs);logger2.info("[PubSub] Routes initialized",{basePath:config.basePath,wsPath:config.wsPath,ackEnabled:config.ack.enabled,presenceEnabled:config.presence.enabled})}),plugin.onStop(()=>{if(cleanupTimer)clearInterval(cleanupTimer),cleanupTimer=null}),plugin.post(`${config.basePath}/:topic`,async({params,body,request,set})=>{let topic=params.topic;if(daprApiToken){let presented=request.headers.get("dapr-api-token")??"";if(!tokensMatch(presented,daprApiToken))return logger2.warn("[PubSub] Rejected Dapr event: missing/invalid dapr-api-token",{topic}),set.status=403,{status:"DROP"}}else if(!warnedNoDaprToken)warnedNoDaprToken=!0,logger2.warn("[PubSub] Dapr subscription endpoint is UNAUTHENTICATED \u2014 set config.pubsub.daprApiToken or the APP_API_TOKEN/DAPR_API_TOKEN env so only the Dapr sidecar can inject events");let eventData;try{if(!body){let text2=await request.text();eventData=JSON.parse(text2)}else eventData=body}catch{return set.status=200,{status:"DROP"}}logger2.info("[PubSub] Dapr event received",{topic,eventId:eventData.id,source:eventData.source});let userId=eventData.data?.user_id;if(userId)clientManager.broadcastToUser(userId,topic,eventData);else clientManager.broadcastEvent(topic,eventData);return getLiveMonitoringService?.()?.recordDaprEvent("pubsub_subscribe",{topic,success:!0,metadata:{eventId:eventData.id,source:eventData.source,userId:userId||null}}),set.status=200,{status:"SUCCESS"}}),plugin.ws(config.wsPath,{idleTimeout:config.wsIdleTimeout,open(ws){let data=ws.data||{},query=data.query||{},userId;if(config.authenticate){let handshakeHeaders=buildHandshakeHeaders(data.headers,query.token),authResult=config.authenticate(handshakeHeaders);if(!authResult||!("userId"in authResult)){let reason=authResult?.reason??"unspecified";ws.send(JSON.stringify({type:"error",error:"Unauthorized: a valid session is required for this connection",reason,timestamp:Date.now()})),ws.close(4001,`Unauthorized: ${reason}`),getLiveMonitoringService?.()?.recordWsEvent("error",{success:!1,error:`Unauthorized WebSocket handshake: ${reason}`,metadata:{reason}});return}userId=authResult.userId}else userId=query.userId;if(!userId){ws.send(JSON.stringify({type:"error",error:"userId is required for WebSocket connection",timestamp:Date.now()})),ws.close(4001,"userId is required"),getLiveMonitoringService?.()?.recordWsEvent("error",{success:!1,error:"userId is required"});return}let clientId=clientManager.generateClientId(),topics=query.topics?.split(",").map((t16)=>t16.trim())||["*"];ws.data.clientId=clientId,ws.data.openedAt=Date.now();let registration=clientManager.registerClient(clientId,ws,userId,topics);if(ws.send(JSON.stringify({type:"connected",clientId,subscribedTopics:registration.accepted,...registration.dropped.length>0?{droppedTopics:registration.dropped,topicCap:config.maxTopicsPerClient}:{},timestamp:Date.now()})),getLiveMonitoringService?.()?.recordWsEvent("connection",{clientId,userId,topic:topics.join(","),success:!0}),config.presence.enabled)clientManager.broadcastPresenceToOthers(userId,"online");let connectedUserId=userId;if(config.ack.enabled&&connectedUserId)clientManager.getPendingMessageCount(connectedUserId).then((count2)=>{if(count2>0)logger2.info("[PubSub] Delivering pending messages",{userId:connectedUserId,count:count2}),clientManager.deliverPendingMessages(connectedUserId,clientId).catch(()=>{})}).catch(()=>{})},message(ws,message){let clientId=ws.data.clientId,frame=classifyClientFrame(message);if(frame.kind==="ignore")return;if(frame.kind==="malformed"){logger2.debug("[PubSub] Failed to parse client message",{clientId,frame:frame.preview});return}let{parsed}=frame;try{if(!HEARTBEAT_WS_TYPES.has(frame.type))getLiveMonitoringService?.()?.recordWsEvent("message",{clientId,messageType:frame.type,success:!0,dataSize:frame.size});switch(frame.type){case"subscribe":if(Array.isArray(parsed.topics)){let update=clientManager.updateClientTopics(clientId,parsed.topics);clientManager.sendToClient(clientId,{type:"subscribed",topics:update.accepted,...update.dropped.length>0?{droppedTopics:update.dropped,topicCap:config.maxTopicsPerClient}:{},timestamp:Date.now()})}break;case"unsubscribe":logger2.info("[PubSub] Unsubscribe request",{clientId,topics:parsed.topics});break;case"ping":clientManager.sendToClient(clientId,{type:"pong",timestamp:Date.now()});break;case"ack":if(parsed.messageId)clientManager.handleClientAck(clientId,parsed.messageId).catch(()=>{});break}}catch(error){logger2.error("[PubSub] Client message handler failed",error,{clientId,messageType:frame.type})}},close(ws,code,reason){let state=ws.data,clientId=state.clientId,openedAt=state.openedAt,livedMs=typeof openedAt==="number"?Date.now()-openedAt:null;if(!clientId)return;clientManager.unregisterClient(clientId),getLiveMonitoringService?.()?.recordWsEvent("close",{clientId,success:!0,metadata:{code,reason:reason||"",livedMs,shortLived:livedMs!==null&&livedMs<60000}}),logger2.info("[PubSub] Client disconnected",{clientId,code,reason,livedMs})}}),{plugin,clientManager}}function classifyRefusal(error){if(!error)return"unspecified";switch(error){case"Token expired":return"expired";case"Invalid signature":return"invalid-signature";case"Invalid token format: expected 3 parts":case"Invalid token format: missing parts":case"Invalid header: failed to decode":case"Invalid header: typ must be JWT":case"Invalid payload: failed to decode":return"malformed";case"Invalid issuer":case"Invalid audience":return"wrong-audience";case"Token issued in the future":return"not-yet-valid";default:if(error.startsWith("Unsupported algorithm:"))return"malformed";return"unspecified"}}import Elysia19,{t as t16}from"elysia";init_Secrets();var timed=async(fn)=>{let started=performance.now();return{result:await fn(),durationMs:Math.round(performance.now()-started)}},postForm=async(url,body)=>{let response=await fetch(url,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",Accept:"application/json"},body:new URLSearchParams(body).toString(),signal:AbortSignal.timeout(12000)}),text2=await response.text(),json={};try{json=JSON.parse(text2)}catch{json=Object.fromEntries(new URLSearchParams(text2))}return{status:response.status,json,text:text2}},describeError=(error)=>{if(error instanceof Error){if(error.name==="TimeoutError")return"Provider did not respond within 12s";return error.message}return String(error)},MICROSOFT_TOKEN_URL=(tenant)=>`https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`,DEFAULT_TOKEN_URLS={google:"https://oauth2.googleapis.com/token",github:"https://github.com/login/oauth/access_token"},describeRedirectUri=(value)=>{let trimmed=value?.trim();if(!trimmed)return"Y\xF6nlendirme adresi (Redirect URI) bo\u015F \u2014 giri\u015F tamamlanamaz";if(/^[A-Z][A-Z0-9_]{2,}$/.test(trimmed))return`Y\xF6nlendirme adresi bir ortam de\u011Fi\u015Fkeni ad\u0131 olarak duruyor ("${trimmed}") ve o de\u011Fi\u015Fken tan\u0131ml\u0131 de\u011Fil`;let parsed;try{parsed=new URL(trimmed)}catch{return`Y\xF6nlendirme adresi ge\xE7erli bir URL de\u011Fil: "${trimmed}"`}if(parsed.protocol!=="https:"&&parsed.hostname!=="localhost")return`Y\xF6nlendirme adresi https olmal\u0131 (\u015Fu an "${parsed.protocol}//")`;return null};async function testOAuthProvider(provider,config){if(!config.clientId||!config.clientSecret)return{ok:!1,status:"failed",message:"Client ID ve Client Secret dolu olmal\u0131",durationMs:0};let redirectProblem=describeRedirectUri(config.redirectUri);if(redirectProblem)return{ok:!1,status:"failed",message:redirectProblem,durationMs:0};try{if(provider==="microsoft"){let tenant=config.tenantId||"common";if(tenant==="common"||tenant==="organizations"||tenant==="consumers")return await probeWithBogusCode(MICROSOFT_TOKEN_URL(tenant),config);let{result,durationMs}=await timed(()=>postForm(MICROSOFT_TOKEN_URL(tenant),{grant_type:"client_credentials",client_id:config.clientId,client_secret:config.clientSecret,scope:"https://graph.microsoft.com/.default"}));if(result.status===200&&result.json.access_token)return{ok:!0,status:"ok",message:`Entra kimlik bilgileri do\u011Fruland\u0131 (tenant ${tenant})`,durationMs};let code=String(result.json.error??result.status);return{ok:!1,status:"failed",message:String(result.json.error_description??result.text).slice(0,400),code,durationMs}}let tokenUrl=config.tokenUrl||DEFAULT_TOKEN_URLS[provider];if(!tokenUrl)return{ok:!1,status:"unknown",message:`"${provider}" i\xE7in token adresi tan\u0131ml\u0131 de\u011Fil \u2014 tokenUrl girin`,durationMs:0};return await probeWithBogusCode(tokenUrl,config)}catch(error){return{ok:!1,status:"failed",message:describeError(error),durationMs:0}}}async function probeWithBogusCode(tokenUrl,config){let{result,durationMs}=await timed(()=>postForm(tokenUrl,{grant_type:"authorization_code",code:"nucleus-credential-probe",client_id:config.clientId,client_secret:config.clientSecret,redirect_uri:config.redirectUri||"https://localhost/callback"})),code=String(result.json.error??""),description=String(result.json.error_description??result.text).slice(0,400);if(code==="invalid_grant"||code==="bad_verification_code")return{ok:!0,status:"ok",message:"Kimlik bilgileri sa\u011Flay\u0131c\u0131 taraf\u0131ndan kabul edildi",code,durationMs};if(code==="invalid_client"||code==="unauthorized_client"||result.status===401)return{ok:!1,status:"failed",message:description||"Sa\u011Flay\u0131c\u0131 Client ID/Secret \xE7iftini reddetti",code:code||"invalid_client",durationMs};if(description.includes("AADSTS7000215")||description.includes("AADSTS700016"))return{ok:!1,status:"failed",message:description,code,durationMs};return{ok:!1,status:"unknown",message:description||"Sa\u011Flay\u0131c\u0131 beklenmeyen bir yan\u0131t d\xF6nd\xFC",code:code||String(result.status),durationMs}}async function testStorageProvider(provider){if(!provider)return{ok:!1,status:"failed",message:"Depolama sa\u011Flay\u0131c\u0131 yap\u0131land\u0131r\u0131lmam\u0131\u015F",durationMs:0};try{let{result,durationMs}=await timed(()=>provider.isAvailable());return result?{ok:!0,status:"ok",message:`Ba\u011Flant\u0131 do\u011Fruland\u0131: ${provider.describe}`,durationMs}:{ok:!1,status:"failed",message:`Eri\u015Filemedi: ${provider.describe} \u2014 sunucu, payla\u015F\u0131m ad\u0131 ve kimlik bilgilerini kontrol edin`,durationMs}}catch(error){return{ok:!1,status:"failed",message:describeError(error),durationMs:0}}}async function testEmailProvider(service,recipient){if(!service)return{ok:!1,status:"failed",message:"E-posta sa\u011Flay\u0131c\u0131 yap\u0131land\u0131r\u0131lmam\u0131\u015F",durationMs:0};if(!service.isAvailable())return{ok:!1,status:"failed",message:"Sa\u011Flay\u0131c\u0131 kimlik bilgileriyle ba\u015Flat\u0131lamad\u0131",durationMs:0};if(!recipient)return{ok:!0,status:"ok",message:"Sa\u011Flay\u0131c\u0131 ba\u015Flat\u0131ld\u0131 \u2014 ger\xE7ek g\xF6nderim i\xE7in bir al\u0131c\u0131 adresi girin",durationMs:0};try{let{result,durationMs}=await timed(()=>service.sendEmail({to:recipient,subject:"Nucleus ba\u011Flant\u0131 testi",html:"<p>Bu e-posta, y\xF6netim panelinden yap\u0131lan ba\u011Flant\u0131 testiyle g\xF6nderildi.</p>"}));return result.success?{ok:!0,status:"ok",message:`Test e-postas\u0131 ${recipient} adresine g\xF6nderildi`,durationMs}:{ok:!1,status:"failed",message:result.error??"G\xF6nderim ba\u015Far\u0131s\u0131z",durationMs}}catch(error){return{ok:!1,status:"failed",message:describeError(error),durationMs:0}}}init_godminGuard();var SECRET_KINDS=["text","password","json","connection_string","pem","boolean","list"];var SCOPE_PATTERN=/^[A-Za-z0-9_.-]{1,200}$/,KEY_PATTERN=/^[A-Za-z0-9_-]{1,120}$/,fail5=(set,status,message)=>{return set.status=status,{isSuccess:!1,message,data:null}};function createSecretsRoutes(routeConfig){let{logger:logger2,basePath,db,schemaTables,getService,resolvedOptions,getEmailService,getStorageProvider:getStorageProvider2}=routeConfig,plugin=new Elysia19({prefix:basePath});plugin.onBeforeHandle(createGodminGuard2({logger:logger2,db,schemaTables,area:"secrets management",allowedRoles:routeConfig.adminRoles}));let requireService=(set)=>{let service=getService();if(!service)return set.status=503,null;return service},actorOf=(request)=>request.headers.get("x-user-id")??void 0;return plugin.get("/slots",async({set})=>{let service=requireService(set);if(!service)return{isSuccess:!1,message:"Secrets management is disabled \u2014 set secrets.enabled and provide an encryption key",data:null};let slots=await service.describe(resolvedOptions),groups=[...new Set(slots.map((s)=>s.group))].sort();return{isSuccess:!0,message:"Credential slots",data:{slots,groups,counts:{total:slots.length,fromDb:slots.filter((s)=>s.source==="db").length,fromEnv:slots.filter((s)=>s.source==="env").length,missing:slots.filter((s)=>s.source==="missing").length,missingRequired:slots.filter((s)=>s.source==="missing"&&s.required).length}}}},{detail:{tags:["Secrets"],summary:"List credential slots discovered from the running config",description:"Slots are derived by walking the config tree, so a new integration appears without a code change. Values are never returned \u2014 only a masked preview and the resolution source."}}),plugin.get("/",async({set})=>{let service=requireService(set);if(!service)return{isSuccess:!1,message:"Secrets management is disabled",data:null};return{isSuccess:!0,message:"Stored credentials",data:{secrets:await service.list()}}},{detail:{tags:["Secrets"],summary:"List stored credentials",description:"Metadata only \u2014 the encrypted value is never returned."}}),plugin.put("/:scope/:key",async({params,body,set,request})=>{let service=requireService(set);if(!service)return{isSuccess:!1,message:"Secrets management is disabled",data:null};let{scope,key:key2}=params;if(!SCOPE_PATTERN.test(scope))return fail5(set,400,"Invalid scope");if(!KEY_PATTERN.test(key2))return fail5(set,400,"Invalid key");let value=body.value;if(typeof value!=="string"||value.length===0)return fail5(set,400,"value is required");if(value.length>65536)return fail5(set,400,"value exceeds 64KB");if(body.kind==="json")try{JSON.parse(value)}catch{return fail5(set,400,"value is not valid JSON")}let saved=await service.set({scope,key:key2,value,kind:body.kind,description:body.description,actorId:actorOf(request)});if(!saved)return fail5(set,500,"Could not store the credential");return{isSuccess:!0,message:"Credential stored",data:{secret:saved}}},{params:t16.Object({scope:t16.String(),key:t16.String()}),body:t16.Object({value:t16.String(),kind:t16.Optional(t16.Union(SECRET_KINDS.map((kind)=>t16.Literal(kind)))),description:t16.Optional(t16.String())}),detail:{tags:["Secrets"],summary:"Set or rotate a credential",description:"Stores the value encrypted (AES-256-GCM). Rotating retains the previous value for a one-step rollback. Takes effect within seconds on every pod \u2014 no restart."}}),plugin.post("/:scope/:key/rollback",async({params,set,request})=>{let service=requireService(set);if(!service)return{isSuccess:!1,message:"Secrets management is disabled",data:null};let restored=await service.rollback(params.scope,params.key,actorOf(request));if(!restored)return fail5(set,404,"No previous value to roll back to");return{isSuccess:!0,message:"Previous credential restored",data:{secret:restored}}},{params:t16.Object({scope:t16.String(),key:t16.String()}),detail:{tags:["Secrets"],summary:"Roll back to the value replaced by the last rotation"}}),plugin.delete("/:scope/:key",async({params,set,request})=>{let service=requireService(set);if(!service)return{isSuccess:!1,message:"Secrets management is disabled",data:null};if(!await service.remove(params.scope,params.key,actorOf(request)))return fail5(set,404,"Credential not found");return{isSuccess:!0,message:"Credential removed \u2014 the config falls back to env or literal",data:null}},{params:t16.Object({scope:t16.String(),key:t16.String()}),detail:{tags:["Secrets"],summary:"Delete a stored credential",description:"Resolution falls back to the env var or literal declared in config."}}),plugin.post("/test",async({body,set})=>{let service=requireService(set);if(!service)return{isSuccess:!1,message:"Secrets management is disabled",data:null};let{target:target2}=body,result;if(target2==="email")result=await testEmailProvider(getEmailService?.()??null,body.recipient);else if(target2==="oauth"){let provider=body.provider;if(!provider)return fail5(set,400,"provider is required for an oauth test");let scope=`authentication.oauth.providers.${provider}`,providerConfig=(resolvedOptions.authentication?.oauth?.providers??{})[provider]??{},declaredString=(key2)=>{let raw=providerConfig[key2];return typeof raw==="string"?raw:void 0};result=await testOAuthProvider(provider,{clientId:service.value(scope,"clientId",declaredString("clientId")),clientSecret:service.value(scope,"clientSecret",declaredString("clientSecret")),tenantId:service.value(scope,"tenantId",declaredString("tenantId")),tokenUrl:declaredString("tokenUrl"),redirectUri:service.value(scope,"redirectUri",declaredString("redirectUri"))})}else if(target2==="storage")result=await testStorageProvider(getStorageProvider2?.()??null);else return fail5(set,400,`Unsupported test target "${target2}"`);return logger2.info("[Secrets] Connection test",{target:target2,provider:body.provider,status:result.status,durationMs:result.durationMs}),{isSuccess:result.ok,message:result.message,data:{result}}},{body:t16.Object({target:t16.Union([t16.Literal("oauth"),t16.Literal("email"),t16.Literal("storage")]),provider:t16.Optional(t16.String()),recipient:t16.Optional(t16.String())}),detail:{tags:["Secrets"],summary:"Test a credential against the real provider",description:"Backend-to-backend only. OAuth uses client_credentials where the provider supports it and a client-authentication probe otherwise; email optionally sends a real test message."}}),plugin.post("/reencrypt",async({body,set})=>{let service=requireService(set);if(!service)return{isSuccess:!1,message:"Secrets management is disabled",data:null};if(!body.newMasterKey||body.newMasterKey.length<16)return fail5(set,400,"newMasterKey must be at least 16 characters");let result=await service.reencryptAll(body.newMasterKey),uncovered="Credentials stored outside this store \u2014 an integration connection\u2019s token, client secret and service password \u2014 are NOT re-encrypted by this. Re-enter them from the integrations panel after the new key is deployed.";return{isSuccess:result.failed.length===0,message:result.failed.length===0?`Re-encrypted ${result.migrated} credentials \u2014 deploy the new key before the next restart. ${uncovered}`:`Nothing was re-encrypted: ${result.failed.length} credential(s) could not be read with the current key, so none were rewritten.`,data:{...result,uncovered}}},{body:t16.Object({newMasterKey:t16.String()}),detail:{tags:["Secrets"],summary:"Re-encrypt every credential under a new master key",description:"Rotates the key protecting the store. The new key must be deployed to the environment before the next restart, or nothing will decrypt."}}),plugin.get("/discover",()=>({isSuccess:!0,message:"Slots discovered from the running config",data:{slots:discoverSlots(resolvedOptions).map(({configValue:_configValue,...slot})=>slot)}}),{detail:{tags:["Secrets"],summary:"Show which credential slots the config declares",description:"Diagnostics \u2014 resolution state is not consulted and no value is read."}}),plugin.post("/storage/migrate",async(ctx)=>{let body=ctx.body,build=routeConfig.buildStorageProvider;if(!build)return fail5(ctx.set,503,"Depolama yap\u0131land\u0131r\u0131lmam\u0131\u015F");let destination=build(body.to),source=build(body.to==="smb"?"local":"smb");if(!source||!destination)return fail5(ctx.set,503,"Depolama yap\u0131land\u0131r\u0131lmam\u0131\u015F");if(!await destination.isAvailable())return fail5(ctx.set,422,`Hedef depo eri\u015Filebilir de\u011Fil (${destination.describe}) \u2014 \xF6nce ba\u011Flant\u0131y\u0131 test edin`);if(body.dryRun){let preview=await migrateStorage({from:source,to:Object.assign(Object.create(Object.getPrototypeOf(destination)),destination,{exists:async()=>!1,write:async()=>{},ensureDir:async()=>{}}),maxFiles:1e5},{logger:logger2});return{isSuccess:!0,message:`${preview.copied} dosya ta\u015F\u0131nacak`,data:{preview}}}let report=await migrateStorage({from:source,to:destination},{logger:logger2}),clean=report.failed===0&&!report.truncated;return logger2.warn("[Storage] Migration run from the admin panel",{to:body.to,copied:report.copied,skipped:report.skipped,failed:report.failed,truncated:report.truncated}),{isSuccess:clean,message:clean?`${report.copied} dosya ta\u015F\u0131nd\u0131, ${report.skipped} zaten mevcuttu`:`${report.copied} ta\u015F\u0131nd\u0131, ${report.failed} ba\u015Far\u0131s\u0131z${report.truncated?", liste kesildi":""}`,data:{report}}},{body:t16.Object({to:t16.Union([t16.Literal("local"),t16.Literal("smb")]),dryRun:t16.Optional(t16.Boolean())}),detail:{tags:["Secrets"],summary:"Copy every stored file to the other backend",description:"Copy-only: the source is never deleted, and a file already on the target is skipped so a re-run resumes. Run BEFORE switching storage.provider."}}),plugin}var snakeToCamel4=(s)=>s.replace(/_([a-z])/g,(_,l)=>l.toUpperCase());function warnOnEntityBasePathCollision(feature,basePath,entities,logger2){let segment=basePath.replace(/^\/+|\/+$/g,"");if(!segment||segment.includes("/"))return!1;let clash=(entities??[]).find((entity)=>snakeToCamel4(entity.table_name)===segment);if(!clash)return!1;return logger2.error(`[${feature}] basePath "${basePath}" is also the generated entity route for "${clash.table_name}". Both are registered and one will shadow the other. Move the feature (e.g. "/nucleus${basePath}") or rename the entity.`),!0}function requiredBodyCeiling(input){if(!input.storageEnabled)return null;let perFile=input.maxFileSizeBytes;if(!perFile||!Number.isFinite(perFile)||perFile<=0)return null;let files=Math.max(1,Math.floor(input.maxFiles??1));return perFile*files+8388608}function bodyCeilingComplaint(input){if(input.required===null)return null;if((input.configured??134217728)>=input.required)return null;let mb2=(bytes)=>`${Math.round(bytes/1048576)} MB`,source=input.configured==null?`the host passed no \`serve.maxRequestBodySize\`, so Bun's default of ${mb2(134217728)} applies`:`the host passed ${mb2(input.configured)}`;return`Uploads are configured up to ${mb2(input.required)}, but ${source}. `+"Anything larger is refused by the server before this application sees it \u2014 "+`the upload appears to run and then fails with nothing to show. Fix: new Elysia({ serve: { maxRequestBodySize: ${input.required} } }).`}var APP_BASELINE_BYTES=268435456,COPIES_IN_FLIGHT=2;function memoryCeilingComplaint(input){if(input.required===null||input.cgroupLimit===null||input.cgroupLimit<=0)return null;let needed=input.required*COPIES_IN_FLIGHT+APP_BASELINE_BYTES;if(input.cgroupLimit>=needed)return null;let mb2=(bytes)=>`${Math.round(bytes/1048576)} MB`,affordable=Math.max(0,Math.floor((input.cgroupLimit-APP_BASELINE_BYTES)/COPIES_IN_FLIGHT));return`Uploads are configured up to ${mb2(input.required)}, but this container is capped at ${mb2(input.cgroupLimit)} and an upload is held in memory twice on top of the ~${mb2(APP_BASELINE_BYTES)} `+`the app already holds \u2014 it needs ~${mb2(needed)}. The kernel will kill the process mid-upload. Fix: raise the container's memory limit to at least ${mb2(needed)}, or lower storage.maxFileSizeBytes to about ${mb2(affordable)}.`}var ipMatches=(ip,entry)=>entry.includes("/")?isIpInCidr(ip,entry):ip===entry;function resolveClientIp(socketIp,forwardedFor,trustedProxies=[]){let peer=socketIp?.trim()||"",chain=(forwardedFor||"").split(",").map((s)=>s.trim()).filter(Boolean);if(!peer)return chain[0]||"unknown";if(trustedProxies.length===0)return peer;let trusted=(ip)=>trustedProxies.some((p)=>ipMatches(ip,p));if(!trusted(peer))return peer;for(let i=chain.length-1;i>=0;i--){let hop=chain[i];if(hop&&!trusted(hop))return hop}return chain[0]||peer}var DEFAULTS={cookieName:"csrf_token",headerName:"x-csrf-token"},resolveCsrfConfig=(raw)=>{if(raw===!0)return{enabled:!0,...DEFAULTS};if(!raw)return{enabled:!1,...DEFAULTS};return{enabled:raw.enabled!==!1,cookieName:raw.cookieName||DEFAULTS.cookieName,headerName:(raw.headerName||DEFAULTS.headerName).toLowerCase()}},parseCookies=(cookieHeader)=>{let out={};if(!cookieHeader)return out;for(let part of cookieHeader.split(";")){let trimmed=part.trim(),eq32=trimmed.indexOf("=");if(eq32>0)out[trimmed.slice(0,eq32)]=trimmed.slice(eq32+1)}return out},SAFE_METHODS=new Set(["GET","HEAD","OPTIONS"]),isStateChanging=(method)=>!SAFE_METHODS.has(method.toUpperCase()),requiresCsrf=(params)=>{if(!isStateChanging(params.method))return!1;if(params.hasAuthorizationHeader)return!1;return params.authCookieNames.some((n)=>!!params.cookies[n])},csrfTokenMatches=(csrfCookie,csrfHeader)=>{if(!csrfCookie||!csrfHeader)return!1;return csrfCookie===csrfHeader};var SENSITIVE_HEADER_NAMES=new Set(["authorization","proxy-authorization","cookie","set-cookie","x-access-token","x-refresh-token","x-session-id","x-user-id","x-user-roles","x-user-claims","x-user-email","x-api-key","x-api-key-id","x-api-key-owner-type","x-auth-type"]);function redactSensitiveHeaders(headers){let out={};return headers.forEach((value,key2)=>{out[key2]=SENSITIVE_HEADER_NAMES.has(key2.toLowerCase())?"[REDACTED]":value}),out}var exceedsBodyLimit=(contentLengthHeader,maxBytes)=>{if(!maxBytes||maxBytes<=0)return!1;if(!contentLengthHeader)return!1;let len=Number(contentLengthHeader);if(!Number.isFinite(len)||len<0)return!1;return len>maxBytes};init_sensitiveColumns();var TENANT_CLAIM="tenant",verifyTenantBinding=(tokenTenant,resolvedSchema,mode="lenient")=>{if(mode==="off")return{ok:!0};if(!resolvedSchema)return{ok:!0};let claim=typeof tokenTenant==="string"&&tokenTenant.length>0?tokenTenant:void 0;if(!claim)return mode==="strict"?{ok:!1,reason:"token is not bound to a tenant"}:{ok:!0};return claim===resolvedSchema?{ok:!0}:{ok:!1,reason:"token tenant does not match the requested tenant"}};init_storage2();import{eq as eq32}from"drizzle-orm";import{Elysia as Elysia20,t as t17}from"elysia";var RESERVED_SUBDOMAINS=["www","api","app","admin","dashboard","mail","ftp","cdn","static","assets","docs","help","support","status","blog","dev","staging","test","demo","localhost"],DEFAULT_SELF_SIGNUP_LIMITS={maxSignupsPerIpPerWindow:3,ipWindowMs:3600000,maxTenantsPerEmail:1};var col9=(table,name2)=>table[name2];function createCheckSubdomainRoute(config){let checkRoute=new Elysia20;return checkRoute.get("/tenants/check-subdomain/:subdomain",async(ctx)=>{let{subdomain}=ctx.params,normalized=subdomain.toLowerCase().trim();if(!/^[a-z][a-z0-9-]*$/.test(normalized)||normalized.length<2||normalized.length>63)return ctx.set.status=400,{isSuccess:!1,message:"Invalid subdomain format. Must start with a letter, contain only lowercase letters, numbers, and hyphens, and be 2-63 characters.",data:{available:!1,subdomain:normalized}};if(RESERVED_SUBDOMAINS.includes(normalized))return{isSuccess:!0,message:"Subdomain is reserved",data:{available:!1,subdomain:normalized}};let tenantRegistry=config.getTenantRegistry(),db=config.getDb();if(tenantRegistry){if(tenantRegistry.getActiveTenants().find((t18)=>t18.subdomain===normalized))return{isSuccess:!0,message:"Subdomain is already taken",data:{available:!1,subdomain:normalized}}}if(db&&tenantRegistry){let tenantsTable=tenantRegistry.getMainContext().schemaTables.tenants;if(tenantsTable)try{if((await db.select().from(tenantsTable).where(eq32(col9(tenantsTable,"subdomain"),normalized)).limit(1)).length>0)return{isSuccess:!0,message:"Subdomain is already taken",data:{available:!1,subdomain:normalized}}}catch{}}return{isSuccess:!0,message:"Subdomain is available",data:{available:!0,subdomain:normalized}}},{params:t17.Object({subdomain:t17.String({minLength:2,maxLength:63})}),detail:{tags:["Tenants"],summary:"Check subdomain availability",description:"Public endpoint to check if a subdomain is available for registration. No authentication required."}}),checkRoute}init_Logger2();import{eq as eq33}from"drizzle-orm";import{Elysia as Elysia21,t as t18}from"elysia";var col10=(table,name2)=>table[name2];function createManageRoutes(config){let log4=config.logger.scoped(TENANT_SUSPEND),manageRoutes=new Elysia21;return manageRoutes.get("/tenants",async(ctx)=>{let db=config.getDb(),tenantRegistry=config.getTenantRegistry();if(!db||!tenantRegistry)return ctx.set.status=503,{success:!1,message:"Tenant management not available",data:null};let requestingUserId=ctx.request.headers.get("x-user-id");if(!requestingUserId)return ctx.set.status=401,{success:!1,message:"Authentication required",data:null};if(!await verifyGodmin(db,tenantRegistry,requestingUserId))return ctx.set.status=403,{success:!1,message:"Godmin privileges required",data:null};let allTenants=[...tenantRegistry.getActiveTenants(),...Array.from(getAllTenants(tenantRegistry)).filter((t19)=>t19.status!=="active")];return{success:!0,message:`Found ${allTenants.length} tenants`,data:{items:allTenants.map((t19)=>({id:t19.id,subdomain:t19.subdomain,schemaName:t19.schemaName,companyId:t19.companyId,companyName:t19.companyName,godAdminEmail:t19.godAdminEmail,status:t19.status,plan:t19.plan,domain:t19.domain,maxUsers:t19.maxUsers,provisionedAt:t19.provisionedAt,suspendedAt:t19.suspendedAt})),totalCount:allTenants.length}}},{detail:{tags:["Tenants"],summary:"List tenants",description:"List all tenants in the platform. Requires godmin privileges."}}),manageRoutes.get("/tenants/:id",async(ctx)=>{let db=config.getDb(),tenantRegistry=config.getTenantRegistry();if(!db||!tenantRegistry)return ctx.set.status=503,{success:!1,message:"Tenant management not available",data:null};let requestingUserId=ctx.request.headers.get("x-user-id");if(!requestingUserId)return ctx.set.status=401,{success:!1,message:"Authentication required",data:null};if(!await verifyGodmin(db,tenantRegistry,requestingUserId))return ctx.set.status=403,{success:!1,message:"Godmin privileges required",data:null};let{id}=ctx.params,tenant=tenantRegistry.getTenantById(id);if(!tenant)return ctx.set.status=404,{success:!1,message:"Tenant not found",data:null};let features=tenantRegistry.getTenantFeatures(id);return{success:!0,message:"Tenant found",data:{...tenant,features:features.map((f)=>({featureName:f.featureName,enabled:f.enabled}))}}},{detail:{tags:["Tenants"],summary:"Get tenant details",description:"Get detailed information about a specific tenant. Requires godmin privileges."}}),manageRoutes.post("/tenants/:id/suspend",async(ctx)=>{let db=config.getDb(),tenantRegistry=config.getTenantRegistry();if(!db||!tenantRegistry)return ctx.set.status=503,{success:!1,message:"Tenant management not available"};let requestingUserId=ctx.request.headers.get("x-user-id");if(!requestingUserId)return ctx.set.status=401,{success:!1,message:"Authentication required"};if(!await verifyGodmin(db,tenantRegistry,requestingUserId))return ctx.set.status=403,{success:!1,message:"Godmin privileges required"};let{id}=ctx.params,body=ctx.body,tenant=tenantRegistry.getTenantById(id);if(!tenant)return ctx.set.status=404,{success:!1,message:"Tenant not found"};if(tenant.status==="suspended")return ctx.set.status=400,{success:!1,message:"Tenant is already suspended"};let mainContext=tenantRegistry.getMainContext(),tenantsTable=mainContext.schemaTables.tenants;if(!tenantsTable)return ctx.set.status=503,{success:!1,message:"Tenants table not available"};let now=new Date;await db.update(tenantsTable).set({status:"suspended",suspendedAt:now,suspendedReason:body.reason||"Suspended by godmin",updatedAt:now}).where(eq33(col10(tenantsTable,"id"),id));let tenantEventsTable=mainContext.schemaTables.tenantEvents;if(tenantEventsTable)try{await db.insert(tenantEventsTable).values({id:crypto.randomUUID(),tenantId:id,eventType:"suspended",eventData:JSON.stringify({reason:body.reason||"Suspended by godmin"}),performedBy:requestingUserId,ipAddress:ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||"unknown",createdAt:now,updatedAt:now})}catch{log4.warn("Failed to record suspend event")}return await tenantRegistry.refreshTenant(id),log4.info("Tenant suspended",{tenantId:id,reason:body.reason}),{success:!0,message:`Tenant "${tenant.subdomain}" suspended`}},{body:t18.Object({reason:t18.Optional(t18.String())}),detail:{tags:["Tenants"],summary:"Suspend tenant",description:"Suspend a tenant, preventing all access. Requires godmin privileges."}}),manageRoutes.post("/tenants/:id/reactivate",async(ctx)=>{let db=config.getDb(),tenantRegistry=config.getTenantRegistry();if(!db||!tenantRegistry)return ctx.set.status=503,{success:!1,message:"Tenant management not available"};let requestingUserId=ctx.request.headers.get("x-user-id");if(!requestingUserId)return ctx.set.status=401,{success:!1,message:"Authentication required"};if(!await verifyGodmin(db,tenantRegistry,requestingUserId))return ctx.set.status=403,{success:!1,message:"Godmin privileges required"};let{id}=ctx.params,tenant=tenantRegistry.getTenantById(id);if(!tenant)return ctx.set.status=404,{success:!1,message:"Tenant not found"};if(tenant.status!=="suspended")return ctx.set.status=400,{success:!1,message:`Tenant is not suspended (current: ${tenant.status})`};let mainContext=tenantRegistry.getMainContext(),tenantsTable=mainContext.schemaTables.tenants;if(!tenantsTable)return ctx.set.status=503,{success:!1,message:"Tenants table not available"};let now=new Date;await db.update(tenantsTable).set({status:"active",suspendedAt:null,suspendedReason:null,updatedAt:now}).where(eq33(col10(tenantsTable,"id"),id));let tenantEventsTable=mainContext.schemaTables.tenantEvents;if(tenantEventsTable)try{await db.insert(tenantEventsTable).values({id:crypto.randomUUID(),tenantId:id,eventType:"reactivated",eventData:JSON.stringify({previousReason:tenant.suspendedReason}),performedBy:requestingUserId,ipAddress:ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||"unknown",createdAt:now,updatedAt:now})}catch{log4.warn("Failed to record reactivate event")}return await tenantRegistry.refreshTenant(id),log4.info("Tenant reactivated",{tenantId:id}),{success:!0,message:`Tenant "${tenant.subdomain}" reactivated`}},{detail:{tags:["Tenants"],summary:"Reactivate tenant",description:"Reactivate a suspended tenant. Requires godmin privileges."}}),manageRoutes}var verifyGodmin=async(db,tenantRegistry,userId)=>{let usersTable=tenantRegistry.getMainContext().schemaTables.users;if(!usersTable)return!1;return(await db.select().from(usersTable).where(eq33(col10(usersTable,"id"),userId)).limit(1))[0]?.isGod===!0},getAllTenants=(registry2)=>{let schemaNames=registry2.getAllSchemaNames(),tenants=[];for(let name2 of schemaNames){let ctx=registry2.getSchemaContext(name2);if(ctx?.tenant)tenants.push(ctx.tenant)}return tenants};init_Logger2();init_utils6();import{eq as eq34}from"drizzle-orm";import{Elysia as Elysia22,t as t19}from"elysia";var col11=(table,name2)=>table[name2],ProvisionBodySchema=t19.Object({subdomain:t19.String({minLength:2,maxLength:63,pattern:"^[a-z][a-z0-9-]*$"}),companyId:t19.String({minLength:1}),companyName:t19.Optional(t19.String()),godAdminEmail:t19.String({format:"email"}),plan:t19.Optional(t19.String()),domain:t19.Optional(t19.String())});function createProvisionRoute(config){let log4=config.logger.scoped(TENANT_PROVISION),provisionRoutes=new Elysia22;return provisionRoutes.post("/tenants/provision",async(ctx)=>{let db=config.getDb(),tenantRegistry=config.getTenantRegistry();if(!db||!tenantRegistry)return ctx.set.status=503,{success:!1,message:"Tenant provisioning not available"};let requestingUserId=ctx.request.headers.get("x-user-id");if(!requestingUserId)return ctx.set.status=401,{success:!1,message:"Authentication required"};let mainContext=tenantRegistry.getMainContext(),usersTable=mainContext.schemaTables.users;if(!usersTable)return ctx.set.status=503,{success:!1,message:"Users table not available"};let requestingUser=(await db.select().from(usersTable).where(eq34(col11(usersTable,"id"),requestingUserId)).limit(1))[0];if(!requestingUser||!requestingUser.isGod)return ctx.set.status=403,{success:!1,message:"Godmin privileges required"};let body=ctx.body,tenantSchemaName=`tenant_${body.subdomain.replace(/-/g,"_")}`;if(tenantRegistry.getActiveTenants().find((t20)=>t20.subdomain===body.subdomain))return ctx.set.status=409,{success:!1,message:`Subdomain "${body.subdomain}" is already taken`};let tenantsTable=mainContext.schemaTables.tenants;if(!tenantsTable)return ctx.set.status=503,{success:!1,message:"Tenants table not available in main schema"};let tenantId=crypto.randomUUID(),now=new Date;try{await db.insert(tenantsTable).values({id:tenantId,subdomain:body.subdomain,schemaName:tenantSchemaName,companyId:body.companyId,companyName:body.companyName||null,godAdminEmail:body.godAdminEmail,status:"provisioning",plan:body.plan||null,domain:body.domain||null,createdAt:now,updatedAt:now}),log4.info("Tenant record created",{tenantId,subdomain:body.subdomain,schemaName:tenantSchemaName})}catch(err){let msg=err instanceof Error?err.message:String(err);return log4.error("Failed to insert tenant record",{error:msg}),ctx.set.status=500,{success:!1,message:`Failed to create tenant record: ${msg}`}}try{let tenantRecord={id:tenantId,subdomain:body.subdomain,schemaName:tenantSchemaName,companyId:body.companyId,companyName:body.companyName||null,godAdminEmail:body.godAdminEmail,status:"provisioning",plan:body.plan||null,domain:body.domain||null,settings:{},trustedSources:[],maxUsers:null,provisionedAt:null,suspendedAt:null,suspendedReason:null},tenantContext=await tenantRegistry.provisionTenant(tenantRecord);log4.info("Schema provisioned",{tenantId,schemaName:tenantSchemaName,tableCount:Object.keys(tenantContext.schemaTables).length});let tenantUsersTable=tenantContext.schemaTables.users;if(tenantUsersTable){let godminId=crypto.randomUUID(),tempPassword=crypto.randomUUID().slice(0,16),hashedPassword=await hashPassword(tempPassword);await db.insert(tenantUsersTable).values({id:godminId,email:body.godAdminEmail,password:hashedPassword,isGod:!0,isActive:!0,createdAt:now,updatedAt:now}),log4.info("Godmin user seeded",{tenantId,godminEmail:body.godAdminEmail,godminId})}await db.update(tenantsTable).set({status:"active",provisionedAt:now,updatedAt:now}).where(eq34(col11(tenantsTable,"id"),tenantId));let tenantEventsTable=mainContext.schemaTables.tenantEvents;if(tenantEventsTable)try{await db.insert(tenantEventsTable).values({id:crypto.randomUUID(),tenantId,eventType:"provisioned",eventData:JSON.stringify({schemaName:tenantSchemaName,godAdminEmail:body.godAdminEmail,plan:body.plan||null}),performedBy:requestingUserId,ipAddress:ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||"unknown",createdAt:now,updatedAt:now})}catch{log4.warn("Failed to record tenant_event, continuing")}return await tenantRegistry.refreshTenant(tenantId),log4.info("Provisioning complete",{tenantId,subdomain:body.subdomain,schemaName:tenantSchemaName}),await config.logger.audit({entityName:"tenants",entityId:tenantId,operation:"PROVISION",userId:requestingUserId,summary:`Tenant "${body.subdomain}" provisioned by godmin`,ipAddress:ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||"unknown",userAgent:ctx.request.headers.get("user-agent")||"unknown",path:new URL(ctx.request.url).pathname,query:""}),{success:!0,message:"Tenant provisioned successfully",data:{tenantId,schemaName:tenantSchemaName,subdomain:body.subdomain,godAdminEmail:body.godAdminEmail,status:"active"}}}catch(err){let msg=err instanceof Error?err.message:String(err);log4.error("Provisioning failed",{error:msg,tenantId});try{await db.update(tenantsTable).set({status:"provisioning",updatedAt:now}).where(eq34(col11(tenantsTable,"id"),tenantId))}catch{}return ctx.set.status=500,{success:!1,message:`Provisioning failed: ${msg}`}}},{body:ProvisionBodySchema,detail:{tags:["Tenants"],summary:"Provision new tenant",description:"Create a new tenant with its own schema, tables, and godmin user. Requires godmin privileges."}}),provisionRoutes}init_Authorization();init_Logger2();init_utils6();import{eq as eq35}from"drizzle-orm";import{Elysia as Elysia23,t as t20}from"elysia";var col12=(table,name2)=>table[name2],ipRateMap=new Map,lastCleanup=Date.now();function cleanupRateMap(windowMs){let now=Date.now();if(now-lastCleanup<600000)return;lastCleanup=now;for(let[key2,entry]of ipRateMap.entries())if(now-entry.windowStart>windowMs)ipRateMap.delete(key2)}function isIpRateLimited(ip,maxPerWindow,windowMs){let now=Date.now(),entry=ipRateMap.get(ip);if(!entry||now-entry.windowStart>windowMs)return ipRateMap.set(ip,{count:1,windowStart:now}),!1;return entry.count++,entry.count>maxPerWindow}var SelfSignupBodySchema=t20.Object({email:t20.String({format:"email"}),password:t20.String({minLength:8}),subdomain:t20.String({minLength:2,maxLength:63,pattern:"^[a-z][a-z0-9-]*$"}),plan:t20.Optional(t20.String()),companyName:t20.Optional(t20.String())});function createSelfSignupRoute(config){let log4=config.logger.scoped(TENANT_SELF_SIGNUP),limits={...DEFAULT_SELF_SIGNUP_LIMITS,...config.selfSignupLimits},selfSignupRoutes=new Elysia23;return selfSignupRoutes.post("/tenants/self-signup",async(ctx)=>{cleanupRateMap(limits.ipWindowMs);let db=config.getDb(),tenantRegistry=config.getTenantRegistry();if(!db||!tenantRegistry)return ctx.set.status=503,{success:!1,message:"Tenant provisioning not available"};let ip=ctx.request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()||ctx.request.headers.get("x-real-ip")?.trim()||"unknown";if(isIpRateLimited(ip,limits.maxSignupsPerIpPerWindow,limits.ipWindowMs))return log4.warn("Self-signup rate limited",{ip}),ctx.set.status=429,{success:!1,message:"Too many signup attempts. Please try again later."};let body=ctx.body,email=body.email.toLowerCase().trim(),subdomain=body.subdomain.toLowerCase().trim();if(RESERVED_SUBDOMAINS.includes(subdomain))return ctx.set.status=400,{success:!1,message:"This subdomain is reserved."};let mainContext=tenantRegistry.getMainContext(),usersTable=mainContext.schemaTables.users,tenantsTable=mainContext.schemaTables.tenants;if(!usersTable||!tenantsTable)return ctx.set.status=503,{success:!1,message:"Required tables not available."};if(tenantRegistry.getActiveTenants().find((t21)=>t21.subdomain===subdomain))return ctx.set.status=409,{success:!1,message:"This subdomain is already taken."};try{if((await db.select().from(tenantsTable).where(eq35(col12(tenantsTable,"subdomain"),subdomain)).limit(1)).length>0)return ctx.set.status=409,{success:!1,message:"This subdomain is already taken."}}catch{}try{if((await db.select().from(tenantsTable).where(eq35(col12(tenantsTable,"godAdminEmail"),email))).length>=limits.maxTenantsPerEmail)return ctx.set.status=409,{success:!1,message:`Tenant limit reached. Maximum ${limits.maxTenantsPerEmail} tenant(s) per account.`}}catch{}let userId,hashedPassword=await hashPassword(body.password),now=new Date,existingUsers=await db.select().from(usersTable).where(eq35(col12(usersTable,"email"),email)).limit(1);if(existingUsers.length>0)userId=existingUsers[0].id,log4.info("Self-signup: existing user resolved",{userId,email});else{userId=crypto.randomUUID();try{await db.insert(usersTable).values({id:userId,email,password:hashedPassword,isActive:!0,createdAt:now,updatedAt:now}),log4.info("Self-signup: user created",{userId,email})}catch(err){let msg=err instanceof Error?err.message:String(err);return log4.error("Self-signup: user creation failed",{error:msg}),ctx.set.status=500,{success:!1,message:"Failed to create account."}}}let tenantId=crypto.randomUUID(),tenantSchemaName=`tenant_${subdomain.replace(/-/g,"_")}`;try{await db.insert(tenantsTable).values({id:tenantId,subdomain,schemaName:tenantSchemaName,companyId:userId,companyName:body.companyName||subdomain,godAdminEmail:email,status:"provisioning",plan:body.plan||null,createdAt:now,updatedAt:now}),log4.info("Tenant record created",{tenantId,subdomain})}catch(err){let msg=err instanceof Error?err.message:String(err);return log4.error("Tenant record creation failed",{error:msg}),ctx.set.status=500,{success:!1,message:"Failed to create tenant."}}try{let tenantRecord={id:tenantId,subdomain,schemaName:tenantSchemaName,companyId:userId,companyName:body.companyName||subdomain,godAdminEmail:email,status:"provisioning",plan:body.plan||null,domain:null,settings:{},trustedSources:[],maxUsers:null,provisionedAt:null,suspendedAt:null,suspendedReason:null},tenantContext=await tenantRegistry.provisionTenant(tenantRecord);log4.info("Schema provisioned",{tenantId,schemaName:tenantSchemaName,tableCount:Object.keys(tenantContext.schemaTables).length});let tenantUsersTable=tenantContext.schemaTables.users;if(tenantUsersTable)await db.insert(tenantUsersTable).values({id:userId,email,password:hashedPassword,isGod:!0,isActive:!0,createdAt:now,updatedAt:now}),log4.info("Godmin seeded in tenant schema",{tenantId,email,userId});let tenantRolesTable=tenantContext.schemaTables.roles,tenantUserRolesTable=tenantContext.schemaTables.userRoles;if(tenantRolesTable&&tenantUserRolesTable)try{let godminRoles=await db.select().from(tenantRolesTable).where(eq35(tenantRolesTable.name,GODMIN_ROLE_NAME)).limit(1),godminRoleId;if(godminRoles.length>0)godminRoleId=godminRoles[0].id;else{let[newRole]=await db.insert(tenantRolesTable).values({name:GODMIN_ROLE_NAME,description:"God mode administrator - bypasses all authorization checks"}).returning();godminRoleId=newRole.id,log4.info("Created godmin role in tenant schema",{tenantId,roleId:godminRoleId})}if(godminRoleId)await db.insert(tenantUserRolesTable).values({userId,roleId:godminRoleId}),log4.info("Assigned godmin role to signup user",{tenantId,userId,roleId:godminRoleId})}catch(roleErr){log4.warn("Failed to assign godmin role to signup user",{tenantId,userId,error:roleErr instanceof Error?roleErr.message:String(roleErr)})}await db.update(tenantsTable).set({status:"active",provisionedAt:now,updatedAt:now}).where(eq35(col12(tenantsTable,"id"),tenantId));let tenantEventsTable=mainContext.schemaTables.tenantEvents;if(tenantEventsTable)try{await db.insert(tenantEventsTable).values({id:crypto.randomUUID(),tenantId,eventType:"self_signup",eventData:JSON.stringify({email,subdomain,plan:body.plan||null}),performedBy:userId,ipAddress:ip,createdAt:now,updatedAt:now})}catch{}return await tenantRegistry.refreshTenant(tenantId),log4.info("Self-signup complete",{tenantId,subdomain,email}),await config.logger.audit({entityName:"tenants",entityId:tenantId,operation:"SELF_SIGNUP",userId,summary:`Self-signup: tenant "${subdomain}" provisioned by ${email}`,ipAddress:ip,userAgent:ctx.request.headers.get("user-agent")||"unknown",path:new URL(ctx.request.url).pathname,query:""}),{success:!0,message:"Tenant created successfully",data:{tenantId,subdomain,schemaName:tenantSchemaName,userId,email,status:"active"}}}catch(err){let msg=err instanceof Error?err.message:String(err);log4.error("Self-signup provisioning failed",{error:msg,tenantId});try{await db.update(tenantsTable).set({status:"provisioning",updatedAt:now}).where(eq35(col12(tenantsTable,"id"),tenantId))}catch{}return ctx.set.status=500,{success:!1,message:"Provisioning failed. Please try again."}}},{body:SelfSignupBodySchema,detail:{tags:["Tenants"],summary:"Self-service tenant signup",description:"Public endpoint for self-service signup. Creates user account, provisions tenant schema, and seeds godmin. Rate-limited by IP with configurable max tenants per email."}}),selfSignupRoutes}function createTenantRoutes(app,config){return app.use(createCheckSubdomainRoute(config)),app.use(createProvisionRoute(config)),app.use(createSelfSignupRoute(config)),app.use(createManageRoutes(config)),app}init_Notification();init_Verification();init_adminGuard();import{Elysia as Elysia24,t as t21}from"elysia";function createVerificationRoutes(routeConfig){let{db,schemaTables,config,logger:logger2,tenantRegistry}=routeConfig,defaultVerificationService=new VerificationService({db,schemaTables,config,logger:logger2}),defaultNotificationService=new NotificationService({db,schemaTables,config:routeConfig.notificationConfig||{enabled:!1},logger:logger2,getEmailService:routeConfig.getEmailService}),wireNotificationHandler=(verSvc,notifSvc)=>{verSvc.setNotificationHandler(async(params)=>{if(!routeConfig.notificationConfig?.enabled)return;await notifSvc.triggerNotifications({trigger:params.trigger,flow_id:params.flow_id,entity_name:params.entity_name,entity_id:params.entity_id,node_id:params.node_id,verifier_id:params.verifier_id,decision:params.decision,context:params.context})})};wireNotificationHandler(defaultVerificationService,defaultNotificationService);let getServicesForRequest=(request)=>{if(!tenantRegistry)return{verificationService:defaultVerificationService,notificationService:defaultNotificationService};let schemaName=request.headers.get("x-tenant-schema");if(!schemaName)return{verificationService:defaultVerificationService,notificationService:defaultNotificationService};let ctx=tenantRegistry.getSchemaContext(schemaName);if(!ctx)return{verificationService:defaultVerificationService,notificationService:defaultNotificationService};let reqVerSvc=new VerificationService({db,schemaTables:ctx.schemaTables,config,logger:logger2}),reqNotifSvc=new NotificationService({db,schemaTables:ctx.schemaTables,config:routeConfig.notificationConfig||{enabled:!1},logger:logger2,getEmailService:routeConfig.getEmailService});return wireNotificationHandler(reqVerSvc,reqNotifSvc),{verificationService:reqVerSvc,notificationService:reqNotifSvc}},basePath=config.endpoints?.basePath||"/verifications",flowBasePath=config.flowEndpoints?.basePath||"/verification-flows",routes=new Elysia24,verificationRoutes=new Elysia24({prefix:basePath});if(verificationRoutes.onBeforeHandle(({request,set})=>{if(!request.headers.get("x-user-id"))return set.status=401,{success:!1,message:"Authentication required"}}),verificationRoutes.get("/status/:entity_name/:entity_id",async({params,request,set})=>{let{verificationService}=getServicesForRequest(request),status=await verificationService.getStatus(params.entity_name,params.entity_id),startedBy=status.instance?.started_by;if(!hasGodminRole(request)&&startedBy!=null&&String(startedBy)!==String(request.headers.get("x-user-id")))return set.status=403,{success:!1,message:"Forbidden"};return{success:!0,data:status}},{params:t21.Object({entity_name:t21.String(),entity_id:t21.String()})}),verificationRoutes.post("/:entity_name/:entity_id/decide",async({params,body,request})=>{let{verificationService}=getServicesForRequest(request),userId=request.headers.get("x-user-id");if(!userId)return{success:!1,message:"User ID required",status:401};return await verificationService.decide({entity_name:params.entity_name,entity_id:params.entity_id,user_id:userId,decision:body.decision,reason:body.reason,signature_id:body.signature_id,diff:body.diff})},{params:t21.Object({entity_name:t21.String(),entity_id:t21.String()}),body:t21.Object({decision:t21.Union([t21.Literal("approved"),t21.Literal("rejected")]),reason:t21.Optional(t21.String()),signature_id:t21.Optional(t21.String()),diff:t21.Optional(t21.Record(t21.String(),t21.Unknown()))})}),verificationRoutes.get("/pending",async({request})=>{let{verificationService}=getServicesForRequest(request),userId=request.headers.get("x-user-id");if(!userId)return{success:!1,message:"User ID required",status:401};return{success:!0,data:await verificationService.getPending(userId)}}),verificationRoutes.get("/history/:entity_name/:entity_id",async({params,request,set})=>{let{verificationService}=getServicesForRequest(request),status=await verificationService.getStatus(params.entity_name,params.entity_id),startedBy=status.instance?.started_by;if(!hasGodminRole(request)&&startedBy!=null&&String(startedBy)!==String(request.headers.get("x-user-id")))return set.status=403,{success:!1,message:"Forbidden"};return{success:!0,data:{verifications:status.verifications,current_step:status.current_step,total_steps:status.total_steps,is_completed:status.is_completed,is_rejected:status.is_rejected}}},{params:t21.Object({entity_name:t21.String(),entity_id:t21.String()})}),verificationRoutes.post("/start",async({body,request})=>{let{verificationService}=getServicesForRequest(request),userId=request.headers.get("x-user-id");return await verificationService.startFlow({flow_id:body.flow_id,entity_name:body.entity_name,entity_id:body.entity_id,started_by:userId||void 0})},{body:t21.Object({flow_id:t21.String(),entity_name:t21.String(),entity_id:t21.String()})}),verificationRoutes.post("/start-for-entity",async({body,request})=>{let{verificationService}=getServicesForRequest(request),userId=request.headers.get("x-user-id");return await verificationService.startFlowForEntity({entity_name:body.entity_name,entity_id:body.entity_id,started_by:userId||void 0})},{body:t21.Object({entity_name:t21.String(),entity_id:t21.String()})}),verificationRoutes.get("/statuses/:entity_name",async({params,query,request,set})=>{if(!hasGodminRole(request))return set.status=403,{success:!1,message:"Forbidden: operator privileges required"};let{verificationService}=getServicesForRequest(request);return{success:!0,data:await verificationService.listEntityStatuses({entity_name:params.entity_name,status:query.status||void 0,page:query.page?Number(query.page):void 0,limit:query.limit?Number(query.limit):void 0})}},{params:t21.Object({entity_name:t21.String()}),query:t21.Object({status:t21.Optional(t21.String()),page:t21.Optional(t21.String()),limit:t21.Optional(t21.String())})}),routes.use(verificationRoutes),logger2.info(`[Verification] Routes registered at ${basePath}`),config.flowEndpoints?.enabled!==!1){let flowRoutes=new Elysia24({prefix:flowBasePath});flowRoutes.onBeforeHandle(createGodminGuard({db,schemaTables,logger:logger2},"verification flow management")),flowRoutes.get("/",async({query,request})=>{let{verificationService:vs}=getServicesForRequest(request);return{success:!0,data:await vs.listFlows(query.entity_name||void 0)}},{query:t21.Object({entity_name:t21.Optional(t21.String())})}),flowRoutes.get("/:flow_id",async({params,request})=>{let{verificationService:vs}=getServicesForRequest(request),result=await vs.getFlow(params.flow_id);if(!result)return{success:!1,message:"Flow not found"};return{success:!0,data:result}},{params:t21.Object({flow_id:t21.String()})}),flowRoutes.post("/",async({body,request})=>{let{verificationService:vs}=getServicesForRequest(request);return await vs.saveFlow(body)},{body:t21.Object({flow_id:t21.String(),entity_name:t21.String(),name:t21.String(),description:t21.Optional(t21.String()),trigger_on:t21.Union([t21.Literal("create"),t21.Literal("update"),t21.Literal("delete"),t21.Literal("manual")]),trigger_fields:t21.Optional(t21.Array(t21.String())),is_draft:t21.Boolean(),viewport:t21.Optional(t21.Object({x:t21.Number(),y:t21.Number(),zoom:t21.Number()})),graph:t21.Object({steps:t21.Array(t21.Any()),edges:t21.Array(t21.Any()),verifier_configs:t21.Array(t21.Any()),notification_rules:t21.Array(t21.Any()),notification_recipients:t21.Array(t21.Any()),notification_channels:t21.Array(t21.Any())})})}),flowRoutes.post("/:flow_id/publish",async({params,request})=>{let{verificationService:vs}=getServicesForRequest(request);return await vs.publishFlow(params.flow_id)},{params:t21.Object({flow_id:t21.String()})}),flowRoutes.delete("/:flow_id",async({params,request})=>{let{verificationService:vs}=getServicesForRequest(request);return await vs.deleteFlow(params.flow_id)},{params:t21.Object({flow_id:t21.String()})}),routes.use(flowRoutes),logger2.info(`[Verification] Flow routes registered at ${flowBasePath}`)}return{routes,verificationService:defaultVerificationService,notificationService:defaultNotificationService}}function createNucleusRuntime(deps){return{sendEmail:async(payload)=>{let mailer=deps.getEmailService();if(!mailer?.isAvailable())return{success:!1,error:"No email provider is configured or available"};return mailer.sendEmail(payload)},emailProvider:()=>deps.activeEmailProvider(),isEmailAvailable:()=>!!deps.getEmailService()?.isAvailable(),emailSenderAddress:()=>deps.getEmailService()?.getSenderAddress?.()??""}}import{Elysia as Elysia25}from"elysia";var a=`/* basic theme */
|
|
554
554
|
:root {
|
|
555
555
|
--scalar-text-decoration: underline;
|
|
556
556
|
--scalar-text-decoration-hover: underline;
|
|
@@ -31,6 +31,14 @@ export type NucleusRuntime = {
|
|
|
31
31
|
emailProvider(): 'azure' | 'gmail' | 'smtp' | null;
|
|
32
32
|
/** True when a provider is configured AND holds usable credentials. */
|
|
33
33
|
isEmailAvailable(): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* The address the live provider sends FROM, or '' when none is configured.
|
|
36
|
+
*
|
|
37
|
+
* A bcc broadcast needs a real address in `To:` — most providers treat a
|
|
38
|
+
* message addressed to nobody as spam — and the sender's own box is the
|
|
39
|
+
* honest one to use.
|
|
40
|
+
*/
|
|
41
|
+
emailSenderAddress(): string;
|
|
34
42
|
};
|
|
35
43
|
/** Context shape for a host route that wants the nucleus services typed. */
|
|
36
44
|
export type NucleusContext = {
|
|
@@ -24,5 +24,7 @@ export declare class AzureEmailService extends TemplatedEmailService {
|
|
|
24
24
|
reconfigure(config: AzureEmailServiceConfig): void;
|
|
25
25
|
isAvailable(): boolean;
|
|
26
26
|
private initialize;
|
|
27
|
+
/** The address this provider sends FROM — see EmailService.getSenderAddress. */
|
|
28
|
+
getSenderAddress(): string;
|
|
27
29
|
sendEmail(payload: EmailPayload): Promise<EmailSendResult>;
|
|
28
30
|
}
|
|
@@ -56,6 +56,8 @@ export declare class SmtpEmailService extends TemplatedEmailService {
|
|
|
56
56
|
* here from a form must not be able to inject `Bcc:` by including a newline.
|
|
57
57
|
*/
|
|
58
58
|
private sanitizeHeaderValue;
|
|
59
|
+
/** The address this provider sends FROM — see EmailService.getSenderAddress. */
|
|
60
|
+
getSenderAddress(): string;
|
|
59
61
|
sendEmail(payload: EmailPayload): Promise<EmailSendResult>;
|
|
60
62
|
}
|
|
61
63
|
/**
|
|
@@ -5,6 +5,25 @@ export type EmailAttachment = {
|
|
|
5
5
|
};
|
|
6
6
|
export type EmailPayload = {
|
|
7
7
|
to: string | string[];
|
|
8
|
+
/**
|
|
9
|
+
* Recipients who receive the message without appearing in it.
|
|
10
|
+
*
|
|
11
|
+
* A broadcast has to go out in batches — one message per person is hundreds
|
|
12
|
+
* of provider round-trips, one message with hundreds of addresses is refused
|
|
13
|
+
* — and a batch shares one `To:` header, so without this every member of the
|
|
14
|
+
* batch reads every other member's address off the mail they were sent.
|
|
15
|
+
*
|
|
16
|
+
* That was the shipped behaviour, and the reasoning written down for it was
|
|
17
|
+
* that the staff directory already shows those addresses to signed-in
|
|
18
|
+
* colleagues. It does not hold: a mail leaves the building. It is forwarded,
|
|
19
|
+
* quoted, read on a phone that is not the company's, and kept by whoever
|
|
20
|
+
* receives it. The customer asked for `bcc` and they were right to.
|
|
21
|
+
*
|
|
22
|
+
* `to` stays required — a message with only bcc recipients is spam-filtered
|
|
23
|
+
* by most providers. Callers put the sender (or a no-reply box) in `to` and
|
|
24
|
+
* the audience here.
|
|
25
|
+
*/
|
|
26
|
+
bcc?: string | string[];
|
|
8
27
|
subject: string;
|
|
9
28
|
html?: string;
|
|
10
29
|
text?: string;
|
|
@@ -31,6 +50,15 @@ export type EmailSendResult = {
|
|
|
31
50
|
*/
|
|
32
51
|
export type EmailService = {
|
|
33
52
|
isAvailable(): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* The address this provider sends FROM.
|
|
55
|
+
*
|
|
56
|
+
* Exposed because a bcc broadcast needs something real in `To:` — a message
|
|
57
|
+
* addressed to nobody is treated as spam by most providers — and the sender's
|
|
58
|
+
* own box is the honest answer: the mail did come from there, and a reply to
|
|
59
|
+
* it lands where replies to a no-reply belong.
|
|
60
|
+
*/
|
|
61
|
+
getSenderAddress?(): string;
|
|
34
62
|
sendEmail(payload: EmailPayload): Promise<EmailSendResult>;
|
|
35
63
|
sendWelcomeEmail(email: string, name: string, appName?: string): Promise<EmailSendResult>;
|
|
36
64
|
sendVerificationEmail(email: string, name: string, verificationLink: string, appName?: string): Promise<EmailSendResult>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nucleus-core-ts",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.991",
|
|
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",
|