UNPKG

mikroauth

Version:

Dead-simple magic link authentication that is useful, lightweight, and uncluttered.

83 lines (77 loc) 28.6 kB
import y,{createHash,createHmac,scryptSync,randomBytes,createCipheriv,createDecipheriv}from'crypto';import {URL}from'url';import {MikroConf,parsers}from'mikroconf';import {EventEmitter}from'events';import {MikroMail}from'mikromail';var f=()=>{let a=H(process.env.DEBUG)||false;return {auth:{jwtSecret:process.env.AUTH_JWT_SECRET||"your-jwt-secret",magicLinkExpirySeconds:900,jwtExpirySeconds:3600,refreshTokenExpirySeconds:604800,maxActiveSessions:3,appUrl:process.env.APP_URL||"http://localhost:3000",templates:null,debug:a},email:{emailSubject:"Your Secure Login Link",user:process.env.EMAIL_USER||"",host:process.env.EMAIL_HOST||"",password:process.env.EMAIL_PASSWORD||"",port:465,secure:true,maxRetries:2,debug:a},storage:{databaseDirectory:"mikroauth",encryptionKey:process.env.STORAGE_KEY||"",debug:a},server:{port:Number(process.env.PORT)||3e3,host:process.env.HOST||"0.0.0.0",useHttps:false,useHttp2:false,sslCert:"",sslKey:"",sslCa:"",rateLimit:{enabled:true,requestsPerMinute:100},allowedDomains:["*"],debug:a}}};function H(a){return a==="true"||a===true}var x=class{algorithm="HS256";secret="HS256";constructor(e){if(process.env.NODE_ENV==="production"&&(!e||e.length<32||e===f().auth.jwtSecret))throw new Error("Production environment requires a strong JWT secret (min 32 chars)");this.secret=e;}sign(e,t={}){let r={alg:this.algorithm,typ:"JWT"},i=Math.floor(Date.now()/1e3),s={...e,iat:i};t.exp!==void 0&&(s.exp=i+t.exp),t.notBefore!==void 0&&(s.nbf=i+t.notBefore),t.issuer&&(s.iss=t.issuer),t.audience&&(s.aud=t.audience),t.subject&&(s.sub=t.subject),t.jwtid&&(s.jti=t.jwtid);let o=this.base64UrlEncode(JSON.stringify(r)),c=this.base64UrlEncode(JSON.stringify(s)),n=`${o}.${c}`,l=this.createSignature(n);return `${n}.${l}`}verify(e,t={}){let r=this.decode(e);if(r.header.alg!==this.algorithm)throw new Error(`Invalid algorithm. Expected ${this.algorithm}, got ${r.header.alg}`);let[i,s]=e.split("."),o=`${i}.${s}`;if(this.createSignature(o)!==r.signature)throw new Error("Invalid signature");let n=r.payload,l=Math.floor(Date.now()/1e3),u=t.clockTolerance||0;if(n.exp!==void 0&&n.exp+u<l)throw new Error("Token expired");if(n.nbf!==void 0&&n.nbf-u>l)throw new Error("Token not yet valid");if(t.issuer&&n.iss!==t.issuer)throw new Error("Invalid issuer");if(t.audience&&n.aud!==t.audience)throw new Error("Invalid audience");if(t.subject&&n.sub!==t.subject)throw new Error("Invalid subject");return n}decode(e){let t=e.split(".");if(t.length!==3)throw new Error("Invalid token format");try{let[r,i,s]=t,o=JSON.parse(this.base64UrlDecode(r)),c=JSON.parse(this.base64UrlDecode(i));return {header:o,payload:c,signature:s}}catch{throw new Error("Failed to decode token")}}createSignature(e){let t=y.createHmac("sha256",this.secret).update(e).digest();return this.base64UrlEncode(t)}base64UrlEncode(e){let t;return typeof e=="string"?t=Buffer.from(e):t=e,t.toString("base64").replace(/=/g,"").replace(/\+/g,"-").replace(/\//g,"_")}base64UrlDecode(e){let t=e.replace(/-/g,"+").replace(/_/g,"/");switch(t.length%4){case 0:break;case 2:t+="==";break;case 3:t+="=";break;default:throw new Error("Invalid base64 string")}return Buffer.from(t,"base64").toString()}};var k=class{templates;constructor(e){e?this.templates=e:this.templates=z;}getText(e,t,r){return this.templates.textVersion(e,t,r).trim()}getHtml(e,t,r){return this.templates.htmlVersion(e,t,r).trim()}},z={textVersion:(a,e,t)=>` Click this link to login: ${a} Security Information: - Expires in ${e} minutes - Can only be used once - Should only be used by you If you didn't request this link, please ignore this email. `,htmlVersion:(a,e,t)=>` <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Your Login Link</title> <style> body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; } .container { border: 1px solid #e1e1e1; border-radius: 5px; padding: 20px; } .button { display: inline-block; background-color: #4CAF50; color: white; text-decoration: none; padding: 10px 20px; border-radius: 5px; margin: 20px 0; } .security-info { background-color: #f8f8f8; padding: 15px; border-radius: 5px; margin-top: 20px; } .footer { margin-top: 20px; font-size: 12px; color: #888; } </style> </head> <body> <div class="container"> <h2>Your Secure Login Link</h2> <p>Click the button below to log in to your account:</p> <a href="${a}" class="button">Login to Your Account</a> <p> Hello, this is a test email! Hall\xE5, MikroMail has international support for, among others, espa\xF1ol, fran\xE7ais, portugu\xEAs, \u4E2D\u6587, \u65E5\u672C\u8A9E, and \u0420\u0443\u0441\u0441\u043A\u0438\u0439! </p> <div class="security-info"> <h3>Security Information:</h3> <ul> <li>This link expires in ${e} minutes</li> <li>Can only be used once</li> <li>Should only be used by you</li> </ul> </div> <p>If you didn't request this link, please ignore this email.</p> <div class="footer"> <p>This is an automated message, please do not reply to this email.</p> </div> </div> </body> </html> `};var b=class{constructor(e){this.options=e;}sentEmails=[];async sendMail(e){this.sentEmails.push(e),this.options?.logToConsole&&(console.log("Email sent:"),console.log(`From: ${e.from}`),console.log(`To: ${e.to}`),console.log(`Subject: ${e.subject}`),console.log(`Text: ${e.text}`)),this.options?.onSend&&this.options.onSend(e);}getSentEmails(){return [...this.sentEmails]}clearSentEmails(){this.sentEmails=[];}};var v=class{data=new Map;collections=new Map;expiryEmitter=new EventEmitter;expiryCheckInterval;constructor(e=1e3){this.expiryCheckInterval=setInterval(()=>this.checkExpiredItems(),e);}destroy(){clearInterval(this.expiryCheckInterval),this.data.clear(),this.collections.clear(),this.expiryEmitter.removeAllListeners();}checkExpiredItems(){let e=Date.now();for(let[t,r]of this.data.entries())r.expiry&&r.expiry<e&&(this.data.delete(t),this.expiryEmitter.emit("expired",t));for(let[t,r]of this.collections.entries())r.expiry&&r.expiry<e&&(this.collections.delete(t),this.expiryEmitter.emit("expired",t));}async set(e,t,r){let i=r?Date.now()+r*1e3:null;this.data.set(e,{value:t,expiry:i});}async get(e){let t=this.data.get(e);return t?t.expiry&&t.expiry<Date.now()?(this.data.delete(e),null):t.value:null}async delete(e){this.data.delete(e),this.collections.delete(e);}async addToCollection(e,t,r){this.collections.has(e)||this.collections.set(e,{items:[],expiry:r?Date.now()+r*1e3:null});let i=this.collections.get(e);i&&(r&&(i.expiry=Date.now()+r*1e3),i.items.push(t));}async removeFromCollection(e,t){let r=this.collections.get(e);r&&(r.items=r.items.filter(i=>i!==t));}async getCollection(e){let t=this.collections.get(e);return t?[...t.items]:[]}async getCollectionSize(e){let t=this.collections.get(e);return t?t.items.length:0}async removeOldestFromCollection(e){let t=this.collections.get(e);return !t||t.items.length===0?null:t.items.shift()||null}async findKeys(e){let t=e.replace(/\*/g,".*").replace(/\?/g,"."),r=new RegExp(`^${t}$`),i=Array.from(this.data.keys()).filter(o=>r.test(o)),s=Array.from(this.collections.keys()).filter(o=>r.test(o));return [...new Set([...i,...s])]}};function j(a){if(!a||a.trim()===""||(a.match(/@/g)||[]).length!==1)return false;let[t,r]=a.split("@");return !(!t||!r||a.includes("..")||!X(t)||!G(r))}function X(a){return a.startsWith('"')&&a.endsWith('"')?!a.slice(1,-1).includes('"'):a.length>64||a.startsWith(".")||a.endsWith(".")?false:/^[a-zA-Z0-9!#$%&'*+/=?^_`{|}~.-]+$/.test(a)}function G(a){if(a.startsWith("[")&&a.endsWith("]")){let t=a.slice(1,-1);return t.startsWith("IPv6:")?Z(t.slice(5)):Y(t)}let e=a.split(".");if(e.length===0)return false;for(let t of e)if(!t||t.length>63||!/^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/.test(t))return false;if(e.length>1){let t=e[e.length-1];if(!/^[a-zA-Z]{2,}$/.test(t))return false}return true}function Y(a){return /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/.test(a)}function Z(a){if(!/^[a-fA-F0-9:]+$/.test(a))return false;let t=a.split(":");return !(t.length<2||t.length>8)}var p=f(),B=a=>{let e={configFilePath:"mikroauth.config.json",args:process.argv,options:[{flag:"--jwtSecret",path:"auth.jwtSecret",defaultValue:p.auth.jwtSecret},{flag:"--magicLinkExpirySeconds",path:"auth.magicLinkExpirySeconds",defaultValue:p.auth.magicLinkExpirySeconds},{flag:"--jwtExpirySeconds",path:"auth.jwtExpirySeconds",defaultValue:p.auth.jwtExpirySeconds},{flag:"--refreshTokenExpirySeconds",path:"auth.refreshTokenExpirySeconds",defaultValue:p.auth.refreshTokenExpirySeconds},{flag:"--maxActiveSessions",path:"auth.maxActiveSessions",defaultValue:p.auth.maxActiveSessions},{flag:"--appUrl",path:"auth.appUrl",defaultValue:p.auth.appUrl},{flag:"--debug",path:"auth.debug",isFlag:true,defaultValue:p.auth.debug},{flag:"--emailSubject",path:"email.emailSubject",defaultValue:"Your Secure Login Link"},{flag:"--emailHost",path:"email.host",defaultValue:p.email.host},{flag:"--emailUser",path:"email.user",defaultValue:p.email.user},{flag:"--emailPassword",path:"email.password",defaultValue:p.email.password},{flag:"--emailPort",path:"email.port",defaultValue:p.email.port},{flag:"--emailSecure",path:"email.secure",isFlag:true,defaultValue:p.email.secure},{flag:"--emailMaxRetries",path:"email.maxRetries",defaultValue:p.email.maxRetries},{flag:"--debug",path:"email.debug",isFlag:true,defaultValue:p.email.debug},{flag:"--dir",path:"storage.databaseDirectory",defaultValue:p.storage.databaseDirectory},{flag:"--encryptionKey",path:"storage.encryptionKey",defaultValue:p.storage.encryptionKey},{flag:"--debug",path:"storage.debug",defaultValue:p.storage.debug},{flag:"--port",path:"server.port",defaultValue:p.server.port},{flag:"--host",path:"server.host",defaultValue:p.server.host},{flag:"--https",path:"server.useHttps",isFlag:true,defaultValue:p.server.useHttps},{flag:"--https",path:"server.useHttp2",isFlag:true,defaultValue:p.server.useHttp2},{flag:"--cert",path:"server.sslCert",defaultValue:p.server.sslCert},{flag:"--key",path:"server.sslKey",defaultValue:p.server.sslKey},{flag:"--ca",path:"server.sslCa",defaultValue:p.server.sslCa},{flag:"--ratelimit",path:"server.rateLimit.enabled",defaultValue:p.server.rateLimit.enabled,isFlag:true},{flag:"--rps",path:"server.rateLimit.requestsPerMinute",defaultValue:p.server.rateLimit.requestsPerMinute},{flag:"--allowed",path:"server.allowedDomains",defaultValue:p.server.allowedDomains,parser:parsers.array},{flag:"--debug",path:"server.debug",isFlag:true,defaultValue:p.server.debug}]};return a&&(e.config=a),e};var A={linkSent:"If a matching account was found, a magic link has been sent.",revokedSuccess:"All other sessions revoked successfully.",logoutSuccess:"Logged out successfully."},$=class{config;email;storage;jwtService;templates;constructor(e,t,r){let i=new MikroConf(B({auth:e.auth,email:e.email})).get();i.auth.debug&&console.log("Using configuration:",i),this.config=i,this.email=t||new b,this.storage=r||new v,this.jwtService=new x(i.auth.jwtSecret),this.templates=new k(i?.auth.templates),this.checkIfUsingDefaultCredentialsInProduction();}checkIfUsingDefaultCredentialsInProduction(){process.env.NODE_ENV==="production"&&this.config.auth.jwtSecret===f().auth.jwtSecret&&(console.error("WARNING: Using default secrets in production environment!"),process.exit(1));}generateToken(e){let t=Date.now().toString(),r=y.randomBytes(32).toString("hex");return y.createHash("sha256").update(`${e}:${t}:${r}`).digest("hex")}generateJsonWebToken(e){return this.jwtService.sign({sub:e.id,email:e.email,username:e.username,role:e.role,exp:Math.floor(Date.now()/1e3)+3600*24})}generateRefreshToken(){return y.randomBytes(40).toString("hex")}async trackSession(e,t,r){let i=`sessions:${e}`;if(await this.storage.getCollectionSize(i)>=this.config.auth.maxActiveSessions){let o=await this.storage.removeOldestFromCollection(i);o&&await this.storage.delete(`refresh:${o}`);}await this.storage.addToCollection(i,t,this.config.auth.refreshTokenExpirySeconds),await this.storage.set(`refresh:${t}`,JSON.stringify(r),this.config.auth.refreshTokenExpirySeconds);}generateMagicLinkUrl(e){let{token:t,email:r,appUrl:i}=e,s=i||this.config.auth.appUrl;try{return new URL(s),`${s}?token=${encodeURIComponent(t)}&email=${encodeURIComponent(r)}`}catch{throw new Error("Invalid base URL configuration")}}async createMagicLink(e){let{email:t,ip:r,metadata:i,appUrl:s,subject:o}=e;if(!j(t))throw new Error("Valid email required");try{let c=this.generateToken(t),n=`magic_link:${c}`,l={email:t,ipAddress:r||"unknown",createdAt:Date.now()};await this.storage.set(n,JSON.stringify(l),this.config.auth.magicLinkExpirySeconds);let u=await this.storage.findKeys("magic_link:*");for(let m of u){if(m===n)continue;let w=await this.storage.get(m);if(w)try{JSON.parse(w).email===t&&await this.storage.delete(m);}catch{}}let d=this.generateMagicLinkUrl({token:c,email:t,appUrl:s}),h=Math.ceil(this.config.auth.magicLinkExpirySeconds/60);return await this.email.sendMail({from:this.config.email.user,to:t,subject:o||this.config.email.emailSubject,text:this.templates.getText(d,h,i),html:this.templates.getHtml(d,h,i)}),{message:A.linkSent}}catch(c){throw console.error(`Failed to process magic link request: ${c}`),new Error("Failed to process magic link request")}}async createToken(e){let{email:t,username:r,role:i,ip:s}=e;if(!j(t))throw new Error("Valid email required");try{let o=y.randomBytes(16).toString("hex"),c=this.generateRefreshToken(),n=Date.now(),l={sub:t,username:r,role:i,jti:o,lastLogin:n,metadata:{ip:s||"unknown"},exp:Math.floor(Date.now()/1e3)+3600*24},u=this.jwtService.sign(l,{exp:this.config.auth.jwtExpirySeconds}),d={email:t,username:r,role:i,ipAddress:s||"unknown",tokenId:o,createdAt:n,lastLogin:n};return await this.trackSession(t,c,d),{accessToken:u,refreshToken:c,exp:this.config.auth.jwtExpirySeconds,tokenType:"Bearer"}}catch(o){throw console.error("Token creation error:",o),new Error("Token creation failed")}}async verifyToken(e){let{token:t,email:r}=e;try{let i=`magic_link:${t}`,s=await this.storage.get(i);if(!s)throw new Error("Invalid or expired token");let o=JSON.parse(s);if(o.email!==r)throw new Error("Email mismatch");let c=o.username,n=o.role;await this.storage.delete(i);let l=y.randomBytes(16).toString("hex"),u=this.generateRefreshToken(),d={sub:r,username:c,role:n,jti:l,lastLogin:o.createdAt,metadata:{ip:o.ipAddress},exp:Math.floor(Date.now()/1e3)+3600*24},h=this.jwtService.sign(d,{exp:this.config.auth.jwtExpirySeconds});return await this.trackSession(r,u,{...o,tokenId:l,createdAt:Date.now()}),{accessToken:h,refreshToken:u,exp:this.config.auth.jwtExpirySeconds,tokenType:"Bearer"}}catch(i){throw console.error("Token verification error:",i),new Error("Verification failed")}}async refreshAccessToken(e){try{let t=await this.storage.get(`refresh:${e}`);if(!t)throw new Error("Invalid or expired refresh token");let r=JSON.parse(t),i=r.email;if(!i)throw new Error("Invalid refresh token data");let s=r.username,o=r.role,c=y.randomBytes(16).toString("hex"),n={sub:i,username:s,role:o,jti:c,lastLogin:r.lastLogin||r.createdAt,metadata:{ip:r.ipAddress}},l=this.jwtService.sign(n,{exp:this.config.auth.jwtExpirySeconds});return r.lastUsed=Date.now(),await this.storage.set(`refresh:${e}`,JSON.stringify(r),this.config.auth.refreshTokenExpirySeconds),{accessToken:l,refreshToken:e,exp:this.config.auth.jwtExpirySeconds,tokenType:"Bearer"}}catch(t){throw console.error("Token refresh error:",t),new Error("Token refresh failed")}}verify(e){try{return this.jwtService.verify(e)}catch{throw new Error("Invalid token")}}async logout(e){try{if(!e||typeof e!="string")throw new Error("Refresh token is required");let t=await this.storage.get(`refresh:${e}`);if(!t)return {message:A.logoutSuccess};let i=JSON.parse(t).email;if(!i)throw new Error("Invalid refresh token data");await this.storage.delete(`refresh:${e}`);let s=`sessions:${i}`;return await this.storage.removeFromCollection(s,e),{message:A.logoutSuccess}}catch(t){throw console.error("Logout error:",t),new Error("Logout failed")}}async getSessions(e){try{if(!e.user?.email)throw new Error("User not authenticated");let t=e.user.email,r=e.body?.refreshToken,i=`sessions:${t}`,o=(await this.storage.getCollection(i)).map(async n=>{try{let l=await this.storage.get(`refresh:${n}`);if(!l)return await this.storage.removeFromCollection(i,n),null;let u=JSON.parse(l);return {id:`${n.substring(0,8)}...`,createdAt:u.createdAt||0,lastLogin:u.lastLogin||u.createdAt||0,lastUsed:u.lastUsed||u.createdAt||0,metadata:{ip:u.ipAddress},isCurrentSession:n===r}}catch{return await this.storage.removeFromCollection(i,n),null}}),c=(await Promise.all(o)).filter(Boolean);return c.sort((n,l)=>l.createdAt-n.createdAt),{sessions:c}}catch(t){throw console.error("Get sessions error:",t),new Error("Failed to fetch sessions")}}async revokeSessions(e){try{if(!e.user?.email)throw new Error("User not authenticated");let t=e.user.email,r=e.body?.refreshToken,i=`sessions:${t}`,s=await this.storage.getCollection(i);for(let o of s)r&&o===r||await this.storage.delete(`refresh:${o}`);return await this.storage.delete(i),r&&await this.storage.get(`refresh:${r}`)&&await this.storage.addToCollection(i,r,this.config.auth.refreshTokenExpirySeconds),{message:A.revokedSuccess}}catch(t){throw console.error("Revoke sessions error:",t),new Error("Failed to revoke sessions")}}authenticate(e,t){try{let r=e.headers?.authorization;if(!r||!r.startsWith("Bearer "))throw new Error("Authentication required");let i=r.split(" ")[1];try{let s=this.verify(i);e.user={email:s.sub},t();}catch{throw new Error("Invalid or expired token")}}catch(r){t(r);}}};var P=class{key;algorithm="aes-256-gcm";keyLength=32;constructor(e){this.key=scryptSync(e,"mikroauth-salt",this.keyLength);}encrypt(e){let t=randomBytes(12),r=createCipheriv(this.algorithm,this.key,t),i=Buffer.concat([r.update(e,"utf8"),r.final()]),s=r.getAuthTag();return `${t.toString("hex")}:${s.toString("hex")}:${i.toString("hex")}`}decrypt(e){let t=e.split(":");if(t.length!==3)throw new Error("Invalid encrypted data format");let[r,i,s]=t,o=Buffer.from(r,"hex"),c=Buffer.from(i,"hex"),n=Buffer.from(s,"hex"),l=createDecipheriv(this.algorithm,this.key,o);return l.setAuthTag(c),Buffer.concat([l.update(n),l.final()]).toString("utf8")}};var C=class{db;encryption;PREFIX_KV="kv:";PREFIX_COLLECTION="coll:";TABLE_NAME="mikroauth";constructor(e,t){this.db=e,t&&(this.encryption=new P(t));}async start(){await this.db.start();}async close(){await this.db.close();}async set(e,t,r){let i=`${this.PREFIX_KV}${e}`,s=this.encryption?this.encryption.encrypt(t):t,o=r?Date.now()+r*1e3:void 0;await this.db.write(this.TABLE_NAME,i,s,o);}async get(e){let t=`${this.PREFIX_KV}${e}`,r=await this.db.get(this.TABLE_NAME,t);return r?this.encryption?this.encryption.decrypt(r):r:null}async delete(e){let t=`${this.PREFIX_KV}${e}`;await this.db.delete(this.TABLE_NAME,t);}async addToCollection(e,t,r){let i=`${this.PREFIX_COLLECTION}${e}`,s=await this.db.get(this.TABLE_NAME,i),o=[];if(s){let u=this.encryption?this.encryption.decrypt(s):s;o=JSON.parse(u);}o.includes(t)||o.push(t);let c=JSON.stringify(o),n=this.encryption?this.encryption.encrypt(c):c,l=r?Date.now()+r*1e3:void 0;await this.db.write(this.TABLE_NAME,i,n,l);}async removeFromCollection(e,t){let r=`${this.PREFIX_COLLECTION}${e}`,i=await this.db.get(this.TABLE_NAME,r);if(!i)return;let s=this.encryption?this.encryption.decrypt(i):i,o=JSON.parse(s);o=o.filter(l=>l!==t);let c=JSON.stringify(o),n=this.encryption?this.encryption.encrypt(c):c;await this.db.write(this.TABLE_NAME,r,n);}async getCollection(e){let t=`${this.PREFIX_COLLECTION}${e}`,r=await this.db.get(this.TABLE_NAME,t);if(!r)return [];let i=this.encryption?this.encryption.decrypt(r):r;return JSON.parse(i)}async getCollectionSize(e){return (await this.getCollection(e)).length}async removeOldestFromCollection(e){let t=`${this.PREFIX_COLLECTION}${e}`,r=await this.db.get(this.TABLE_NAME,t);if(!r)return null;let i=this.encryption?this.encryption.decrypt(r):r,s=JSON.parse(i);if(s.length===0)return null;let o=s.shift(),c=JSON.stringify(s),n=this.encryption?this.encryption.encrypt(c):c;return await this.db.write(this.TABLE_NAME,t,n),o}async findKeys(e){let t=e.replace(/\./g,"\\.").replace(/\*/g,".*").replace(/\?/g,"."),r=new RegExp(`^${t}$`),i=await this.db.get(this.TABLE_NAME);return Array.isArray(i)?i.filter(s=>{let o=s[0];return typeof o=="string"&&o.startsWith(this.PREFIX_KV)}).map(s=>s[0].substring(this.PREFIX_KV.length)).filter(s=>r.test(s)):[]}};var I=class{email;sender;constructor(e){this.sender=e.user,this.email=new MikroMail({config:e});}async sendMail(e){await this.email.send({from:this.sender,to:e.to,cc:e.cc,bcc:e.bcc,subject:e.subject,text:e.text,html:e.html});}};var R=class{apiKey;debug;constructor(e){if(!e.apiKey)throw new Error("ResendProvider requires an apiKey");this.apiKey=e.apiKey,this.debug=e.debug??false;}async sendMail(e){let t="https://api.resend.com/emails",r={from:e.from,to:Array.isArray(e.to)?e.to:[e.to],subject:e.subject,html:e.html,text:e.text,...e.cc&&{cc:Array.isArray(e.cc)?e.cc:[e.cc]},...e.bcc&&{bcc:Array.isArray(e.bcc)?e.bcc:[e.bcc]}};this.debug&&console.log("[ResendProvider] Sending email:",{endpoint:t,to:r.to,subject:r.subject});let i=await fetch(t,{method:"POST",headers:{Authorization:`Bearer ${this.apiKey}`,"Content-Type":"application/json"},body:JSON.stringify(r)});if(!i.ok){let o=await i.json().catch(()=>({})),c=new Error(`Resend API error: ${i.status} ${i.statusText}`);throw c.status=i.status,c.response=o,this.debug&&console.error("[ResendProvider] Error:",c),c}let s=await i.json();this.debug&&console.log("[ResendProvider] Email sent successfully:",s);}};var M=class{apiKey;debug;constructor(e){if(!e.apiKey)throw new Error("BrevoProvider requires an apiKey");this.apiKey=e.apiKey,this.debug=e.debug??false;}async sendMail(e){let t="https://api.brevo.com/v3/smtp/email",r=e.from.match(/^(?:"?([^"]*)"?\s)?<?([^>]+)>?$/),i=r?.[1]?.trim()||"",s=r?.[2]?.trim()||e.from,c=(Array.isArray(e.to)?e.to:[e.to]).map(d=>{let h=d.match(/^(?:"?([^"]*)"?\s)?<?([^>]+)>?$/);return {email:h?.[2]?.trim()||d,...h?.[1]&&{name:h[1].trim()}}}),n={sender:{email:s,...i&&{name:i}},to:c,subject:e.subject,htmlContent:e.html,textContent:e.text};if(e.cc){let d=Array.isArray(e.cc)?e.cc:[e.cc];n.cc=d.map(h=>{let m=h.match(/^(?:"?([^"]*)"?\s)?<?([^>]+)>?$/);return {email:m?.[2]?.trim()||h,...m?.[1]&&{name:m[1].trim()}}});}if(e.bcc){let d=Array.isArray(e.bcc)?e.bcc:[e.bcc];n.bcc=d.map(h=>{let m=h.match(/^(?:"?([^"]*)"?\s)?<?([^>]+)>?$/);return {email:m?.[2]?.trim()||h,...m?.[1]&&{name:m[1].trim()}}});}this.debug&&console.log("[BrevoProvider] Sending email:",{endpoint:t,to:n.to,subject:n.subject});let l=await fetch(t,{method:"POST",headers:{"api-key":this.apiKey,"content-type":"application/json",accept:"application/json"},body:JSON.stringify(n)});if(!l.ok){let d=await l.json().catch(()=>({})),h=new Error(`Brevo API error: ${l.status} ${l.statusText}`);throw h.status=l.status,h.response=d,this.debug&&console.error("[BrevoProvider] Error:",h),h}let u=await l.json();this.debug&&console.log("[BrevoProvider] Email sent successfully:",u);}};var L=class{serverToken;messageStream;debug;constructor(e){if(!e.serverToken)throw new Error("PostmarkProvider requires a serverToken");this.serverToken=e.serverToken,this.messageStream=e.messageStream??"outbound",this.debug=e.debug??false;}async sendMail(e){let t="https://api.postmarkapp.com/email",r={From:e.from,To:Array.isArray(e.to)?e.to.join(","):e.to,Subject:e.subject,HtmlBody:e.html,TextBody:e.text,MessageStream:this.messageStream};e.cc&&(r.Cc=Array.isArray(e.cc)?e.cc.join(","):e.cc),e.bcc&&(r.Bcc=Array.isArray(e.bcc)?e.bcc.join(","):e.bcc),this.debug&&console.log("[PostmarkProvider] Sending email:",{endpoint:t,To:r.To,Subject:r.Subject});let i=await fetch(t,{method:"POST",headers:{"X-Postmark-Server-Token":this.serverToken,"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify(r)});if(!i.ok){let o=await i.json().catch(()=>({})),c=new Error(`Postmark API error: ${i.status} ${i.statusText}`);throw c.status=i.status,c.response=o,this.debug&&console.error("[PostmarkProvider] Error:",c),c}let s=await i.json();this.debug&&console.log("[PostmarkProvider] Email sent successfully:",s);}};var K=class{apiKey;debug;constructor(e){if(!e.apiKey)throw new Error("SendGridProvider requires an apiKey");this.apiKey=e.apiKey,this.debug=e.debug??false;}async sendMail(e){let t="https://api.sendgrid.com/v3/mail/send",r=e.from.match(/^(?:"?([^"]*)"?\s)?<?([^>]+)>?$/),i=r?.[1]?.trim(),s=r?.[2]?.trim()||e.from,n={personalizations:[{to:(Array.isArray(e.to)?e.to:[e.to]).map(u=>{let d=u.match(/^(?:"?([^"]*)"?\s)?<?([^>]+)>?$/);return {email:d?.[2]?.trim()||u,...d?.[1]&&{name:d[1].trim()}}})}],from:{email:s,...i&&{name:i}},subject:e.subject,content:[]};if(e.text&&n.content.push({type:"text/plain",value:e.text}),e.html&&n.content.push({type:"text/html",value:e.html}),e.cc){let d=(Array.isArray(e.cc)?e.cc:[e.cc]).map(h=>{let m=h.match(/^(?:"?([^"]*)"?\s)?<?([^>]+)>?$/);return {email:m?.[2]?.trim()||h,...m?.[1]&&{name:m[1].trim()}}});n.personalizations[0].cc=d;}if(e.bcc){let d=(Array.isArray(e.bcc)?e.bcc:[e.bcc]).map(h=>{let m=h.match(/^(?:"?([^"]*)"?\s)?<?([^>]+)>?$/);return {email:m?.[2]?.trim()||h,...m?.[1]&&{name:m[1].trim()}}});n.personalizations[0].bcc=d;}this.debug&&console.log("[SendGridProvider] Sending email:",{endpoint:t,to:n.personalizations[0].to,subject:n.subject});let l=await fetch(t,{method:"POST",headers:{Authorization:`Bearer ${this.apiKey}`,"Content-Type":"application/json"},body:JSON.stringify(n)});if(!l.ok){let u=await l.json().catch(()=>({})),d=new Error(`SendGrid API error: ${l.status} ${l.statusText}`);throw d.status=l.status,d.response=u,this.debug&&console.error("[SendGridProvider] Error:",d),d}this.debug&&console.log("[SendGridProvider] Email sent successfully");}};var D=class{accessKeyId;secretAccessKey;region;debug;constructor(e){if(!e.accessKeyId||!e.secretAccessKey||!e.region)throw new Error("AWSESProvider requires accessKeyId, secretAccessKey, and region");this.accessKeyId=e.accessKeyId,this.secretAccessKey=e.secretAccessKey,this.region=e.region,this.debug=e.debug??false;}async sendMail(e){let t=`https://email.${this.region}.amazonaws.com/v2/email/outbound-emails`,r=Array.isArray(e.to)?e.to:[e.to],i=e.cc?Array.isArray(e.cc)?e.cc:[e.cc]:void 0,s=e.bcc?Array.isArray(e.bcc)?e.bcc:[e.bcc]:void 0,o={FromEmailAddress:e.from,Destination:{ToAddresses:r,...i&&{CcAddresses:i},...s&&{BccAddresses:s}},Content:{Simple:{Subject:{Data:e.subject,Charset:"UTF-8"},Body:{...e.text&&{Text:{Data:e.text,Charset:"UTF-8"}},...e.html&&{Html:{Data:e.html,Charset:"UTF-8"}}}}}},c=JSON.stringify(o),l=new Date().toISOString().replace(/[:-]|\.\d{3}/g,""),u=l.substring(0,8),d={"content-type":"application/json",host:`email.${this.region}.amazonaws.com`,"x-amz-date":l},h=Object.keys(d).sort().join(";"),m=Object.keys(d).sort().map(E=>`${E}:${d[E]}`).join(` `),w=this.sha256(c),T=["POST","/v2/email/outbound-emails","",m,"",h,w].join(` `),O="AWS4-HMAC-SHA256",V=`${u}/${this.region}/ses/aws4_request`,N=[O,l,V,this.sha256(T)].join(` `),J=this.getSignatureKey(this.secretAccessKey,u,this.region,"ses"),_=this.hmacSha256(J,N).toString("hex"),U=`${O} Credential=${this.accessKeyId}/${V}, SignedHeaders=${h}, Signature=${_}`;this.debug&&console.log("[AWSESProvider] Sending email:",{endpoint:t,to:r,subject:e.subject});let g=await fetch(t,{method:"POST",headers:{...d,Authorization:U},body:c});if(!g.ok){let E=await g.text().catch(()=>""),S=new Error(`AWS SES API error: ${g.status} ${g.statusText}`);throw S.status=g.status,S.response=E,this.debug&&console.error("[AWSESProvider] Error:",S),S}let F=await g.json();this.debug&&console.log("[AWSESProvider] Email sent successfully:",F);}sha256(e){return createHash("sha256").update(e).digest("hex")}hmacSha256(e,t){return createHmac("sha256",e).update(t).digest()}getSignatureKey(e,t,r,i){let s=this.hmacSha256(`AWS4${e}`,t),o=this.hmacSha256(s,r),c=this.hmacSha256(o,i);return this.hmacSha256(c,"aws4_request")}};export{D as AWSESProvider,M as BrevoProvider,b as InMemoryEmailProvider,v as InMemoryStorageProvider,$ as MikroAuth,I as MikroMailProvider,C as PikoDBProvider,L as PostmarkProvider,R as ResendProvider,K as SendGridProvider};