irismail 0.1.0 → 0.5.0

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/index.js CHANGED
@@ -32,193 +32,54 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
32
32
  // src/index.ts
33
33
  var index_exports = {};
34
34
  __export(index_exports, {
35
- EmailService: () => EmailService,
36
35
  InputOTP: () => InputOTP,
37
36
  InputOTPGroup: () => InputOTPGroup,
38
37
  InputOTPSeparator: () => InputOTPSeparator,
39
38
  InputOTPSlot: () => InputOTPSlot,
40
- IrisMailService: () => IrisMailService,
41
- OTPService: () => OTPService,
42
- OTP_DEFAULTS: () => OTP_DEFAULTS,
43
- cn: () => cn,
44
- decrypt: () => decrypt,
45
- encrypt: () => encrypt,
46
- useOTP: () => useOTP
39
+ IrisMail: () => IrisMail,
40
+ OTP: () => OTP,
41
+ cn: () => cn
47
42
  });
48
43
  module.exports = __toCommonJS(index_exports);
49
44
 
50
45
  // src/server/email.ts
51
46
  var import_nodemailer = __toESM(require("nodemailer"));
52
- var EmailService = class {
47
+ var IrisMail = class {
53
48
  constructor(config) {
54
49
  __publicField(this, "transporter");
55
- __publicField(this, "config");
56
- this.config = config;
57
- this.transporter = import_nodemailer.default.createTransport(config.transport);
58
- }
59
- /**
60
- * Verify SMTP connection configuration
61
- */
62
- async verifyConnection() {
63
- try {
64
- await this.transporter.verify();
65
- return true;
66
- } catch (error) {
67
- console.error("SMTP connection verification failed:", error);
68
- return false;
69
- }
50
+ this.transporter = import_nodemailer.default.createTransport({
51
+ host: "smtp.gmail.com",
52
+ port: 587,
53
+ secure: false,
54
+ // true for 465, false for other ports
55
+ auth: config.auth
56
+ });
70
57
  }
71
58
  /**
72
59
  * Send an email
73
- * @param options - Email options (to, subject, html, text, from)
74
- * @returns - Result of the send operation
60
+ * @param options - Email options (from, to, subject, html)
61
+ * @returns Promise with success status and messageId
75
62
  */
76
- async sendEmail(options) {
77
- const from = options.from || this.config.defaults?.from;
78
- if (!from) {
79
- throw new Error("From address is required either in options or config defaults");
80
- }
81
- const mailOptions = {
82
- from: {
83
- name: from.name,
84
- address: from.address
85
- },
86
- to: options.to,
87
- subject: options.subject,
88
- html: options.html,
89
- text: options.text || options.html.replace(/<[^>]*>/g, "")
90
- // Simple HTML to text fallback
63
+ async sendMail(options) {
64
+ const { from, to, subject, html } = options;
65
+ const text = html.replace(/<[^>]*>/g, "");
66
+ const info = await this.transporter.sendMail({
67
+ from,
68
+ to,
69
+ subject,
70
+ html,
71
+ text
72
+ });
73
+ return {
74
+ success: true,
75
+ messageId: info.messageId
91
76
  };
92
- try {
93
- const info = await this.transporter.sendMail(mailOptions);
94
- return {
95
- success: true,
96
- messageId: info.messageId
97
- };
98
- } catch (error) {
99
- console.error("Error sending email:", error);
100
- throw error;
101
- }
102
- }
103
- };
104
-
105
- // src/server/crypto.ts
106
- var import_crypto = __toESM(require("crypto"));
107
- var ALGORITHM = "aes-256-cbc";
108
- function encrypt(text, secretKey) {
109
- try {
110
- const key = Buffer.from(secretKey.padEnd(32).slice(0, 32));
111
- const iv = import_crypto.default.randomBytes(16);
112
- const cipher = import_crypto.default.createCipheriv(ALGORITHM, key, iv);
113
- let encrypted = cipher.update(text, "utf8", "base64");
114
- encrypted += cipher.final("base64");
115
- return iv.toString("hex") + ":" + encrypted;
116
- } catch (error) {
117
- console.error("Encryption error:", error);
118
- throw new Error("Failed to encrypt data");
119
- }
120
- }
121
- function decrypt(encryptedText, secretKey) {
122
- try {
123
- const key = Buffer.from(secretKey.padEnd(32).slice(0, 32));
124
- const parts = encryptedText.split(":");
125
- if (parts.length !== 2) {
126
- throw new Error("Invalid encrypted text format");
127
- }
128
- const iv = Buffer.from(parts[0], "hex");
129
- const encrypted = parts[1];
130
- const decipher = import_crypto.default.createDecipheriv(ALGORITHM, key, iv);
131
- let decrypted = decipher.update(encrypted, "base64", "utf8");
132
- decrypted += decipher.final("utf8");
133
- return decrypted;
134
- } catch (error) {
135
- console.error("Decryption error:", error);
136
- throw new Error("Failed to decrypt data");
137
- }
138
- }
139
-
140
- // src/server/otp.ts
141
- var OTPService = class {
142
- constructor(secretKey) {
143
- __publicField(this, "secretKey");
144
- if (!secretKey) {
145
- throw new Error("OTPService: secretKey is required");
146
- }
147
- this.secretKey = secretKey;
148
- }
149
- /**
150
- * Generate a numeric OTP of specified length
151
- * @param length - Length of the OTP (default: 6)
152
- * @returns - The generated OTP string
153
- */
154
- generateOTP(length = 6) {
155
- const min = Math.pow(10, length - 1);
156
- const max = Math.pow(10, length) - 1;
157
- return Math.floor(min + Math.random() * (max - min + 1)).toString();
158
- }
159
- /**
160
- * Encrypt OTP data for secure storage/transmission
161
- * @param otp - The OTP string
162
- * @param timestamp - The timestamp when OTP was generated (default: now)
163
- * @returns - Encrypted OTP data string
164
- */
165
- encryptOTP(otp, timestamp = Date.now()) {
166
- const data = JSON.stringify({ otp, timestamp });
167
- return encrypt(data, this.secretKey);
168
- }
169
- /**
170
- * Decrypt OTP data
171
- * @param encryptedData - The encrypted OTP string
172
- * @returns - Decrypted OTP data object or null if failed
173
- */
174
- decryptOTP(encryptedData) {
175
- try {
176
- const decryptedData = decrypt(encryptedData, this.secretKey);
177
- return JSON.parse(decryptedData);
178
- } catch (error) {
179
- console.error("Error decrypting OTP data:", error);
180
- return null;
181
- }
182
- }
183
- /**
184
- * Verify an OTP against encrypted data
185
- * @param inputOtp - The OTP provided by the user
186
- * @param encryptedData - The encrypted OTP data
187
- * @param expiryMinutes - Expiry time in minutes (default: 5)
188
- * @returns - Object containing valid status and message
189
- */
190
- verifyOTP(inputOtp, encryptedData, expiryMinutes = 5) {
191
- const data = this.decryptOTP(encryptedData);
192
- if (!data) {
193
- return { valid: false, message: "Invalid OTP data" };
194
- }
195
- const { otp, timestamp } = data;
196
- const currentTime = Date.now();
197
- const otpAge = currentTime - timestamp;
198
- const expiryTime = expiryMinutes * 60 * 1e3;
199
- if (otpAge > expiryTime) {
200
- return { valid: false, message: "OTP has expired" };
201
- }
202
- if (otp !== inputOtp) {
203
- return { valid: false, message: "Incorrect OTP" };
204
- }
205
- return { valid: true, message: "OTP verified successfully" };
206
- }
207
- };
208
-
209
- // src/server/index.ts
210
- var IrisMailService = class {
211
- constructor(config) {
212
- __publicField(this, "email");
213
- __publicField(this, "otp");
214
- this.email = new EmailService(config.email);
215
- this.otp = new OTPService(config.secretKey);
216
77
  }
217
78
  };
218
79
 
219
80
  // src/react/components/input-otp.tsx
220
- var React = __toESM(require("react"));
221
81
  var import_input_otp = require("input-otp");
82
+ var React = __toESM(require("react"));
222
83
 
223
84
  // src/utils/constants.ts
224
85
  var import_clsx = require("clsx");
@@ -226,221 +87,121 @@ var import_tailwind_merge = require("tailwind-merge");
226
87
  function cn(...inputs) {
227
88
  return (0, import_tailwind_merge.twMerge)((0, import_clsx.clsx)(inputs));
228
89
  }
229
- var OTP_DEFAULTS = {
230
- LENGTH: 6,
231
- EXPIRY_MINUTES: 5,
232
- RESEND_COOLDOWN_SECONDS: 120,
233
- // 2 minutes
234
- MAX_ATTEMPTS: 5,
235
- RATE_LIMIT_RESET_HOURS: 1
236
- };
237
90
 
238
91
  // src/react/components/input-otp.tsx
239
92
  var import_jsx_runtime = require("react/jsx-runtime");
240
- var Dot = () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
241
- "svg",
93
+ var InputOTPStyleContext = React.createContext({});
94
+ var InputOTP = React.forwardRef(({ className, containerClassName, error, ...props }, ref) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(InputOTPStyleContext.Provider, { value: { error }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
95
+ import_input_otp.OTPInput,
242
96
  {
243
- width: "15",
244
- height: "15",
245
- viewBox: "0 0 15 15",
246
- fill: "none",
247
- xmlns: "http://www.w3.org/2000/svg",
248
- className: "h-4 w-4",
249
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
250
- "path",
251
- {
252
- d: "M7.5 7.5C7.5 8.32843 6.82843 9 6 9C5.17157 9 4.5 8.32843 4.5 7.5C4.5 6.67157 5.17157 6 6 6C6.82843 6 7.5 6.67157 7.5 7.5Z",
253
- fill: "currentColor",
254
- fillRule: "evenodd",
255
- clipRule: "evenodd"
256
- }
257
- )
97
+ ref,
98
+ containerClassName: cn(
99
+ "flex items-center gap-2 has-[:disabled]:opacity-50",
100
+ containerClassName
101
+ ),
102
+ className: cn("disabled:cursor-not-allowed", className),
103
+ ...props
258
104
  }
259
- );
260
- var InputOTP = React.forwardRef(
261
- ({ className, containerClassName, ...props }, ref) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
262
- import_input_otp.OTPInput,
263
- {
264
- ref,
265
- containerClassName: cn("flex items-center gap-2 has-[:disabled]:opacity-50", containerClassName),
266
- className: cn("disabled:cursor-not-allowed", className),
267
- ...props
268
- }
269
- )
270
- );
105
+ ) }));
271
106
  InputOTP.displayName = "InputOTP";
272
- var InputOTPGroup = React.forwardRef(
273
- ({ className, ...props }, ref) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { ref, className: cn("flex items-center", className), ...props })
274
- );
107
+ var InputOTPGroup = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
108
+ "div",
109
+ {
110
+ ref,
111
+ className: cn("flex items-center gap-2", className),
112
+ ...props
113
+ }
114
+ ));
275
115
  InputOTPGroup.displayName = "InputOTPGroup";
276
116
  var InputOTPSlot = React.forwardRef(({ index, className, ...props }, ref) => {
277
117
  const inputOTPContext = React.useContext(import_input_otp.OTPInputContext);
278
- const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index];
118
+ const { error } = React.useContext(InputOTPStyleContext);
119
+ const slot = inputOTPContext.slots[index];
120
+ const { char, hasFakeCaret, isActive } = slot;
279
121
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
280
122
  "div",
281
123
  {
282
124
  ref,
283
125
  className: cn(
284
- "relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
285
- isActive && "z-10 ring-2 ring-ring ring-offset-background",
126
+ // Base styles
127
+ "relative flex h-12 w-10 items-center justify-center",
128
+ "border-2 rounded-lg",
129
+ "font-mono text-lg font-medium",
130
+ "transition-all duration-150",
131
+ // Default state
132
+ "border-zinc-700 bg-zinc-900 text-white",
133
+ // Filled state
134
+ char && "border-zinc-500",
135
+ // Active/focus state
136
+ isActive && "ring-2 ring-offset-2 ring-offset-zinc-950 ring-indigo-500 border-indigo-500",
137
+ // Error state
138
+ error && "border-red-500/70 bg-red-500/10",
139
+ error && isActive && "ring-red-500",
286
140
  className
287
141
  ),
142
+ "data-active": isActive,
143
+ "data-filled": Boolean(char),
288
144
  ...props,
289
145
  children: [
290
146
  char,
291
- hasFakeCaret && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "pointer-events-none absolute inset-0 flex items-center justify-center", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "h-4 w-px animate-caret-blink bg-foreground duration-1000" }) })
147
+ hasFakeCaret && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "pointer-events-none absolute inset-0 flex items-center justify-center", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "h-5 w-0.5 animate-caret-blink rounded-full bg-indigo-400" }) })
292
148
  ]
293
149
  }
294
150
  );
295
151
  });
296
152
  InputOTPSlot.displayName = "InputOTPSlot";
297
- var InputOTPSeparator = React.forwardRef(
298
- ({ ...props }, ref) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { ref, role: "separator", ...props, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Dot, {}) })
299
- );
153
+ var InputOTPSeparator = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
154
+ "div",
155
+ {
156
+ ref,
157
+ role: "separator",
158
+ className: cn("flex items-center justify-center text-zinc-500", className),
159
+ ...props,
160
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "w-3 h-0.5 bg-zinc-600 rounded-full" })
161
+ }
162
+ ));
300
163
  InputOTPSeparator.displayName = "InputOTPSeparator";
301
-
302
- // src/react/hooks/use-otp.ts
303
- var import_react = require("react");
304
- function useOTP({ email, onRateLimit }) {
305
- const [otpTimeLeft, setOtpTimeLeft] = (0, import_react.useState)(OTP_DEFAULTS.EXPIRY_MINUTES * 60);
306
- const [resendTimeLeft, setResendTimeLeft] = (0, import_react.useState)(0);
307
- const [attemptsLeft, setAttemptsLeft] = (0, import_react.useState)(OTP_DEFAULTS.MAX_ATTEMPTS);
308
- const [isRateLimited, setIsRateLimited] = (0, import_react.useState)(false);
309
- const [rateLimitResetTime, setRateLimitResetTime] = (0, import_react.useState)(null);
310
- const getStorageKey = (prefix) => `${prefix}_${email}`;
311
- const getStoredData = (key) => {
312
- if (typeof window === "undefined") return null;
313
- const data = sessionStorage.getItem(key);
314
- return data ? JSON.parse(data) : null;
315
- };
316
- const setStoredData = (key, data) => {
317
- if (typeof window !== "undefined") {
318
- sessionStorage.setItem(key, JSON.stringify(data));
319
- }
320
- };
321
- const removeStoredData = (key) => {
322
- if (typeof window !== "undefined") {
323
- sessionStorage.removeItem(key);
324
- }
325
- };
326
- const checkRateLimit = () => {
327
- const key = getStorageKey("rate_limit");
328
- const data = getStoredData(key);
329
- if (!data) {
330
- setAttemptsLeft(OTP_DEFAULTS.MAX_ATTEMPTS);
331
- setIsRateLimited(false);
332
- return { limited: false, attemptsLeft: OTP_DEFAULTS.MAX_ATTEMPTS };
333
- }
334
- const currentTime = Date.now();
335
- const resetTime = OTP_DEFAULTS.RATE_LIMIT_RESET_HOURS * 60 * 60 * 1e3;
336
- if (currentTime - data.firstAttemptTime > resetTime) {
337
- removeStoredData(key);
338
- setAttemptsLeft(OTP_DEFAULTS.MAX_ATTEMPTS);
339
- setIsRateLimited(false);
340
- return { limited: false, attemptsLeft: OTP_DEFAULTS.MAX_ATTEMPTS };
341
- }
342
- const attempts = OTP_DEFAULTS.MAX_ATTEMPTS - data.attempts;
343
- const limited = data.attempts >= OTP_DEFAULTS.MAX_ATTEMPTS;
344
- const limitResetTime = data.firstAttemptTime + resetTime;
345
- setAttemptsLeft(Math.max(0, attempts));
346
- setIsRateLimited(limited);
347
- setRateLimitResetTime(limited ? limitResetTime : null);
348
- if (limited && onRateLimit) {
349
- onRateLimit(limitResetTime);
350
- }
351
- return { limited, attemptsLeft: Math.max(0, attempts), resetTime: limited ? limitResetTime : void 0 };
352
- };
353
- const updateRateLimit = () => {
354
- const key = getStorageKey("rate_limit");
355
- const data = getStoredData(key);
356
- const currentTime = Date.now();
357
- let newData;
358
- if (!data) {
359
- newData = {
360
- attempts: 1,
361
- firstAttemptTime: currentTime,
362
- lastAttemptTime: currentTime
363
- };
364
- } else {
365
- const resetTime = OTP_DEFAULTS.RATE_LIMIT_RESET_HOURS * 60 * 60 * 1e3;
366
- if (currentTime - data.firstAttemptTime > resetTime) {
367
- newData = {
368
- attempts: 1,
369
- firstAttemptTime: currentTime,
370
- lastAttemptTime: currentTime
371
- };
372
- } else {
373
- newData = {
374
- ...data,
375
- attempts: data.attempts + 1,
376
- lastAttemptTime: currentTime
377
- };
164
+ var OTP = React.forwardRef(
165
+ ({
166
+ length = 6,
167
+ value,
168
+ onChange,
169
+ onComplete,
170
+ disabled,
171
+ error,
172
+ autoFocus,
173
+ name,
174
+ className,
175
+ pattern = "^[0-9]*$"
176
+ }, ref) => {
177
+ const slotIndices = Array.from({ length }, (_, i) => i);
178
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
179
+ InputOTP,
180
+ {
181
+ ref,
182
+ maxLength: length,
183
+ value,
184
+ onChange,
185
+ onComplete,
186
+ disabled,
187
+ error,
188
+ autoFocus,
189
+ name,
190
+ pattern,
191
+ containerClassName: className,
192
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(InputOTPGroup, { children: slotIndices.map((index) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(InputOTPSlot, { index }, index)) })
378
193
  }
379
- }
380
- setStoredData(key, newData);
381
- checkRateLimit();
382
- return newData;
383
- };
384
- const checkResendCooldown = () => {
385
- const key = getStorageKey("last_sent");
386
- const lastSentTime = getStoredData(key);
387
- if (!lastSentTime) {
388
- setResendTimeLeft(0);
389
- return { canResend: true, timeLeft: 0 };
390
- }
391
- const currentTime = Date.now();
392
- const timeSinceLastSent = currentTime - lastSentTime;
393
- const cooldown = OTP_DEFAULTS.RESEND_COOLDOWN_SECONDS * 1e3;
394
- const canResend = timeSinceLastSent >= cooldown;
395
- const timeLeft = canResend ? 0 : Math.ceil((cooldown - timeSinceLastSent) / 1e3);
396
- setResendTimeLeft(timeLeft);
397
- return { canResend, timeLeft };
398
- };
399
- const recordSentOTP = () => {
400
- const key = getStorageKey("last_sent");
401
- setStoredData(key, Date.now());
402
- updateRateLimit();
403
- setOtpTimeLeft(OTP_DEFAULTS.EXPIRY_MINUTES * 60);
404
- setResendTimeLeft(OTP_DEFAULTS.RESEND_COOLDOWN_SECONDS);
405
- };
406
- (0, import_react.useEffect)(() => {
407
- checkRateLimit();
408
- checkResendCooldown();
409
- const timer = setInterval(() => {
410
- setOtpTimeLeft((prev) => Math.max(0, prev - 1));
411
- const { timeLeft } = checkResendCooldown();
412
- setResendTimeLeft(timeLeft);
413
- }, 1e3);
414
- return () => clearInterval(timer);
415
- }, [email]);
416
- const formatTime = (seconds) => {
417
- const mins = Math.floor(seconds / 60);
418
- const secs = seconds % 60;
419
- return `${mins}:${secs.toString().padStart(2, "0")}`;
420
- };
421
- return {
422
- otpTimeLeft,
423
- resendTimeLeft,
424
- attemptsLeft,
425
- isRateLimited,
426
- rateLimitResetTime,
427
- formatTime,
428
- recordSentOTP,
429
- checkRateLimit
430
- };
431
- }
194
+ );
195
+ }
196
+ );
197
+ OTP.displayName = "OTP";
432
198
  // Annotate the CommonJS export names for ESM import in node:
433
199
  0 && (module.exports = {
434
- EmailService,
435
200
  InputOTP,
436
201
  InputOTPGroup,
437
202
  InputOTPSeparator,
438
203
  InputOTPSlot,
439
- IrisMailService,
440
- OTPService,
441
- OTP_DEFAULTS,
442
- cn,
443
- decrypt,
444
- encrypt,
445
- useOTP
204
+ IrisMail,
205
+ OTP,
206
+ cn
446
207
  });
package/dist/index.mjs CHANGED
@@ -3,29 +3,19 @@ import {
3
3
  InputOTPGroup,
4
4
  InputOTPSeparator,
5
5
  InputOTPSlot,
6
- OTP_DEFAULTS,
7
- cn,
8
- useOTP
9
- } from "./chunk-FTNSOYOW.mjs";
6
+ OTP,
7
+ cn
8
+ } from "./chunk-IQG3OBQL.mjs";
10
9
  import {
11
- EmailService,
12
- IrisMailService,
13
- OTPService,
14
- decrypt,
15
- encrypt
16
- } from "./chunk-XGASTZZ6.mjs";
10
+ IrisMail
11
+ } from "./chunk-64MVNASE.mjs";
17
12
  import "./chunk-QZ7TP4HQ.mjs";
18
13
  export {
19
- EmailService,
20
14
  InputOTP,
21
15
  InputOTPGroup,
22
16
  InputOTPSeparator,
23
17
  InputOTPSlot,
24
- IrisMailService,
25
- OTPService,
26
- OTP_DEFAULTS,
27
- cn,
28
- decrypt,
29
- encrypt,
30
- useOTP
18
+ IrisMail,
19
+ OTP,
20
+ cn
31
21
  };