@volcanicminds/tools 0.0.3 → 0.0.4
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/README.md +96 -22
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/mailer/index.d.ts +43 -0
- package/dist/lib/mailer/index.d.ts.map +1 -0
- package/dist/lib/mailer/index.js +73 -0
- package/dist/lib/mailer/index.js.map +1 -0
- package/lib/mailer/index.ts +132 -0
- package/package.json +14 -2
package/README.md
CHANGED
|
@@ -19,46 +19,120 @@ npm install @volcanicminds/tools
|
|
|
19
19
|
npm run upgrade-deps
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
## Environment
|
|
23
|
-
|
|
24
|
-
```bash
|
|
25
|
-
# or automatically use LOG_LEVEL
|
|
26
|
-
SOME_KEY=true
|
|
27
|
-
```
|
|
28
|
-
|
|
29
22
|
## Usage
|
|
30
23
|
|
|
31
|
-
This package supports both root imports and sub-path imports to optimize bundle size.
|
|
32
|
-
|
|
33
|
-
### Import everything
|
|
34
|
-
|
|
35
|
-
```typescript
|
|
36
|
-
import { mfa, log } from '@volcanicminds/tools'
|
|
37
|
-
```
|
|
24
|
+
This package supports both root imports and sub-path imports to optimize bundle size and tree-shaking.
|
|
38
25
|
|
|
39
|
-
### Import specific features (Recommended
|
|
26
|
+
### Import specific features (Recommended)
|
|
40
27
|
|
|
41
28
|
```typescript
|
|
42
29
|
import * as mfa from '@volcanicminds/tools/mfa'
|
|
30
|
+
import { Mailer } from '@volcanicminds/tools/mailer'
|
|
43
31
|
import * as logger from '@volcanicminds/tools/logger'
|
|
44
32
|
```
|
|
45
33
|
|
|
34
|
+
---
|
|
35
|
+
|
|
46
36
|
## Features
|
|
47
37
|
|
|
48
38
|
### MFA (Multi-Factor Authentication)
|
|
49
39
|
|
|
50
|
-
Utilities for generating secrets, QR codes, and verifying TOTP tokens
|
|
40
|
+
Utilities for generating secrets, QR codes, and verifying TOTP tokens based on `otpauth`.
|
|
51
41
|
|
|
52
42
|
```typescript
|
|
53
43
|
import * as mfa from '@volcanicminds/tools/mfa'
|
|
54
44
|
|
|
55
|
-
// Generate
|
|
56
|
-
const
|
|
45
|
+
// 1. Generate a generic base32 secret (Optional, useful if you need to store it before setup)
|
|
46
|
+
const secret = mfa.generateSecret()
|
|
47
|
+
|
|
48
|
+
// 2. Generate Setup Details for the User (returns secret, otpauth URI, and QR Code Data URL)
|
|
49
|
+
// If secret is omitted, a new one is generated automatically.
|
|
50
|
+
const setup = await mfa.generateSetupDetails('MyApp', 'user@example.com', secret)
|
|
51
|
+
|
|
52
|
+
console.log(setup.secret) // Save this to DB
|
|
53
|
+
console.log(setup.qrCode) // Send this to Frontend to display QR
|
|
54
|
+
|
|
55
|
+
// 3. Verify a Token provided by the user
|
|
56
|
+
const userToken = '123456' // From input
|
|
57
|
+
const isValid = mfa.verifyToken(userToken, setup.secret)
|
|
57
58
|
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
if (isValid) {
|
|
60
|
+
// Proceed with login/action
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 4. Generate a valid token (Useful for testing or recovery codes)
|
|
64
|
+
const currentToken = mfa.generateToken(setup.secret)
|
|
60
65
|
```
|
|
61
66
|
|
|
62
|
-
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
### Mailer
|
|
70
|
+
|
|
71
|
+
A wrapper around `nodemailer` designed for simplicity and configuration injection. It automatically handles HTML-to-Text conversion if the text body is missing.
|
|
63
72
|
|
|
64
|
-
|
|
73
|
+
#### Configuration & Initialization
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
import { Mailer } from '@volcanicminds/tools/mailer'
|
|
77
|
+
|
|
78
|
+
// Initialize with a config object (not bound to process.env)
|
|
79
|
+
const mailer = new Mailer({
|
|
80
|
+
host: 'smtp.example.com',
|
|
81
|
+
port: 587,
|
|
82
|
+
secure: false, // true for 465, false for other ports
|
|
83
|
+
auth: {
|
|
84
|
+
user: 'my-user',
|
|
85
|
+
pass: 'my-password'
|
|
86
|
+
},
|
|
87
|
+
defaultFrom: '"My Service" <noreply@example.com>', // Optional: used if not specified in send()
|
|
88
|
+
defaultReplyTo: 'support@example.com' // Optional
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
// Optional: Verify connection on startup
|
|
92
|
+
const isConnected = await mailer.verifyConnection()
|
|
93
|
+
if (isConnected) console.log('SMTP Ready')
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
#### Sending Emails
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
try {
|
|
100
|
+
const info = await mailer.send({
|
|
101
|
+
// Optional if defaultFrom is set in config, otherwise Mandatory
|
|
102
|
+
from: '"Support Team" <support@example.com>',
|
|
103
|
+
|
|
104
|
+
to: 'user@destination.com', // Can be a string or array of strings
|
|
105
|
+
cc: ['manager@destination.com'],
|
|
106
|
+
subject: 'Welcome to Volcanic Tools',
|
|
107
|
+
|
|
108
|
+
// Text version is automatically generated from HTML if omitted,
|
|
109
|
+
// converting <br> to newlines and stripping tags.
|
|
110
|
+
text: 'Hello, World! Welcome aboard.',
|
|
111
|
+
html: '<p>Hello, <strong>World</strong>!<br/>Welcome aboard.</p>',
|
|
112
|
+
|
|
113
|
+
attachments: [
|
|
114
|
+
{
|
|
115
|
+
filename: 'license.txt',
|
|
116
|
+
content: 'MIT License...'
|
|
117
|
+
}
|
|
118
|
+
]
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
console.log('Message sent: %s', info.messageId)
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error('Error sending email:', error)
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
### Logging
|
|
130
|
+
|
|
131
|
+
Use Pino logger wrapper if in your project you have a `global.log` with a valid instance.
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
import * as log from '@volcanicminds/tools/logger'
|
|
135
|
+
|
|
136
|
+
log.info('Application started')
|
|
137
|
+
log.error({ err: new Error('Oops') }, 'Something went wrong')
|
|
138
|
+
```
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,GAAG,MAAM,oBAAoB,CAAA;AACzC,OAAO,KAAK,GAAG,MAAM,sBAAsB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,GAAG,MAAM,oBAAoB,CAAA;AACzC,OAAO,KAAK,MAAM,MAAM,uBAAuB,CAAA;AAC/C,OAAO,KAAK,GAAG,MAAM,sBAAsB,CAAA"}
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,GAAG,MAAM,oBAAoB,CAAA;AACzC,OAAO,KAAK,GAAG,MAAM,sBAAsB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,GAAG,MAAM,oBAAoB,CAAA;AACzC,OAAO,KAAK,MAAM,MAAM,uBAAuB,CAAA;AAC/C,OAAO,KAAK,GAAG,MAAM,sBAAsB,CAAA"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export interface MailerConfig {
|
|
2
|
+
host: string;
|
|
3
|
+
port: number;
|
|
4
|
+
secure?: boolean;
|
|
5
|
+
auth?: {
|
|
6
|
+
user: string;
|
|
7
|
+
pass: string;
|
|
8
|
+
};
|
|
9
|
+
defaultFrom?: string;
|
|
10
|
+
defaultReplyTo?: string;
|
|
11
|
+
tls?: {
|
|
12
|
+
rejectUnauthorized?: boolean;
|
|
13
|
+
ciphers?: string;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export interface MailAttachment {
|
|
17
|
+
filename: string;
|
|
18
|
+
content?: string | Buffer;
|
|
19
|
+
path?: string;
|
|
20
|
+
contentType?: string;
|
|
21
|
+
cid?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface MailOptions {
|
|
24
|
+
to: string | string[];
|
|
25
|
+
subject: string;
|
|
26
|
+
html: string;
|
|
27
|
+
text?: string;
|
|
28
|
+
from?: string;
|
|
29
|
+
cc?: string | string[];
|
|
30
|
+
bcc?: string | string[];
|
|
31
|
+
replyTo?: string;
|
|
32
|
+
attachments?: MailAttachment[];
|
|
33
|
+
}
|
|
34
|
+
export declare class Mailer {
|
|
35
|
+
private transporter;
|
|
36
|
+
private defaultFrom?;
|
|
37
|
+
private defaultReplyTo?;
|
|
38
|
+
constructor(config: MailerConfig);
|
|
39
|
+
private stripHtml;
|
|
40
|
+
send(options: MailOptions): Promise<any>;
|
|
41
|
+
verifyConnection(): Promise<boolean>;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../lib/mailer/index.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,IAAI,CAAC,EAAE;QACL,IAAI,EAAE,MAAM,CAAA;QACZ,IAAI,EAAE,MAAM,CAAA;KACb,CAAA;IACD,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,GAAG,CAAC,EAAE;QACJ,kBAAkB,CAAC,EAAE,OAAO,CAAA;QAC5B,OAAO,CAAC,EAAE,MAAM,CAAA;KACjB,CAAA;CACF;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACzB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,GAAG,CAAC,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IACrB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IACtB,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IACvB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,WAAW,CAAC,EAAE,cAAc,EAAE,CAAA;CAC/B;AAMD,qBAAa,MAAM;IACjB,OAAO,CAAC,WAAW,CAAa;IAChC,OAAO,CAAC,WAAW,CAAC,CAAQ;IAC5B,OAAO,CAAC,cAAc,CAAC,CAAQ;gBAEnB,MAAM,EAAE,YAAY;IAqBhC,OAAO,CAAC,SAAS;IAcJ,IAAI,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC;IAwCxC,gBAAgB,IAAI,OAAO,CAAC,OAAO,CAAC;CASlD"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import nodemailer from 'nodemailer';
|
|
2
|
+
export class Mailer {
|
|
3
|
+
transporter;
|
|
4
|
+
defaultFrom;
|
|
5
|
+
defaultReplyTo;
|
|
6
|
+
constructor(config) {
|
|
7
|
+
this.defaultFrom = config.defaultFrom;
|
|
8
|
+
this.defaultReplyTo = config.defaultReplyTo;
|
|
9
|
+
this.transporter = nodemailer.createTransport({
|
|
10
|
+
host: config.host,
|
|
11
|
+
port: config.port,
|
|
12
|
+
secure: config.secure ?? config.port === 465,
|
|
13
|
+
auth: config.auth
|
|
14
|
+
? {
|
|
15
|
+
user: config.auth.user,
|
|
16
|
+
pass: config.auth.pass
|
|
17
|
+
}
|
|
18
|
+
: undefined,
|
|
19
|
+
tls: config.tls
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
stripHtml(html) {
|
|
23
|
+
if (!html)
|
|
24
|
+
return '';
|
|
25
|
+
return html
|
|
26
|
+
.replace(/<br\s*\/?>/gi, '\n')
|
|
27
|
+
.replace(/<\/p>/gi, '\n\n')
|
|
28
|
+
.replace(/<[^>]*>?/gm, '')
|
|
29
|
+
.trim();
|
|
30
|
+
}
|
|
31
|
+
async send(options) {
|
|
32
|
+
const from = options.from || this.defaultFrom;
|
|
33
|
+
const replyTo = options.replyTo || this.defaultReplyTo;
|
|
34
|
+
if (!from) {
|
|
35
|
+
throw new Error('Email "from" address is required (either in options or config.defaultFrom)');
|
|
36
|
+
}
|
|
37
|
+
const mailPayload = {
|
|
38
|
+
from,
|
|
39
|
+
to: Array.isArray(options.to) ? options.to.join(', ') : options.to,
|
|
40
|
+
subject: options.subject,
|
|
41
|
+
html: options.html,
|
|
42
|
+
text: options.text || this.stripHtml(options.html),
|
|
43
|
+
attachments: options.attachments
|
|
44
|
+
};
|
|
45
|
+
if (options.cc) {
|
|
46
|
+
mailPayload.cc = Array.isArray(options.cc) ? options.cc.join(', ') : options.cc;
|
|
47
|
+
}
|
|
48
|
+
if (options.bcc) {
|
|
49
|
+
mailPayload.bcc = Array.isArray(options.bcc) ? options.bcc.join(', ') : options.bcc;
|
|
50
|
+
}
|
|
51
|
+
if (replyTo) {
|
|
52
|
+
mailPayload.replyTo = replyTo;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const info = await this.transporter.sendMail(mailPayload);
|
|
56
|
+
return info;
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
throw new Error(`Failed to send email to ${mailPayload.to}: ${error.message}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async verifyConnection() {
|
|
63
|
+
try {
|
|
64
|
+
await this.transporter.verify();
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
console.error('Mailer connection verification failed:', error);
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../lib/mailer/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import nodemailer, { Transporter, SendMailOptions } from 'nodemailer'
|
|
2
|
+
|
|
3
|
+
export interface MailerConfig {
|
|
4
|
+
host: string
|
|
5
|
+
port: number
|
|
6
|
+
secure?: boolean // true for 465, false for other ports
|
|
7
|
+
auth?: {
|
|
8
|
+
user: string
|
|
9
|
+
pass: string
|
|
10
|
+
}
|
|
11
|
+
defaultFrom?: string
|
|
12
|
+
defaultReplyTo?: string
|
|
13
|
+
tls?: {
|
|
14
|
+
rejectUnauthorized?: boolean
|
|
15
|
+
ciphers?: string
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface MailAttachment {
|
|
20
|
+
filename: string
|
|
21
|
+
content?: string | Buffer
|
|
22
|
+
path?: string
|
|
23
|
+
contentType?: string
|
|
24
|
+
cid?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface MailOptions {
|
|
28
|
+
to: string | string[]
|
|
29
|
+
subject: string
|
|
30
|
+
html: string
|
|
31
|
+
text?: string // If not provided, it will be generated from html
|
|
32
|
+
from?: string
|
|
33
|
+
cc?: string | string[]
|
|
34
|
+
bcc?: string | string[]
|
|
35
|
+
replyTo?: string
|
|
36
|
+
attachments?: MailAttachment[]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Mailer class wrapper around nodemailer.
|
|
41
|
+
* Designed to be tree-shakeable and configuration-driven without relying on process.env directly.
|
|
42
|
+
*/
|
|
43
|
+
export class Mailer {
|
|
44
|
+
private transporter: Transporter
|
|
45
|
+
private defaultFrom?: string
|
|
46
|
+
private defaultReplyTo?: string
|
|
47
|
+
|
|
48
|
+
constructor(config: MailerConfig) {
|
|
49
|
+
this.defaultFrom = config.defaultFrom
|
|
50
|
+
this.defaultReplyTo = config.defaultReplyTo
|
|
51
|
+
|
|
52
|
+
this.transporter = nodemailer.createTransport({
|
|
53
|
+
host: config.host,
|
|
54
|
+
port: config.port,
|
|
55
|
+
secure: config.secure ?? config.port === 465, // Default secure if port is 465
|
|
56
|
+
auth: config.auth
|
|
57
|
+
? {
|
|
58
|
+
user: config.auth.user,
|
|
59
|
+
pass: config.auth.pass
|
|
60
|
+
}
|
|
61
|
+
: undefined,
|
|
62
|
+
tls: config.tls
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Helper to strip HTML tags for plain text fallback
|
|
68
|
+
*/
|
|
69
|
+
private stripHtml(html: string): string {
|
|
70
|
+
if (!html) return ''
|
|
71
|
+
return html
|
|
72
|
+
.replace(/<br\s*\/?>/gi, '\n') // Replace <br> with newlines
|
|
73
|
+
.replace(/<\/p>/gi, '\n\n') // Replace </p> with double newlines
|
|
74
|
+
.replace(/<[^>]*>?/gm, '') // Remove all other tags
|
|
75
|
+
.trim()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Sends an email.
|
|
80
|
+
* @param options Mail options including to, subject, html, etc.
|
|
81
|
+
* @returns The info object from nodemailer.
|
|
82
|
+
*/
|
|
83
|
+
public async send(options: MailOptions): Promise<any> {
|
|
84
|
+
const from = options.from || this.defaultFrom
|
|
85
|
+
const replyTo = options.replyTo || this.defaultReplyTo
|
|
86
|
+
|
|
87
|
+
if (!from) {
|
|
88
|
+
throw new Error('Email "from" address is required (either in options or config.defaultFrom)')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const mailPayload: SendMailOptions = {
|
|
92
|
+
from,
|
|
93
|
+
to: Array.isArray(options.to) ? options.to.join(', ') : options.to,
|
|
94
|
+
subject: options.subject,
|
|
95
|
+
html: options.html,
|
|
96
|
+
text: options.text || this.stripHtml(options.html),
|
|
97
|
+
attachments: options.attachments
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (options.cc) {
|
|
101
|
+
mailPayload.cc = Array.isArray(options.cc) ? options.cc.join(', ') : options.cc
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (options.bcc) {
|
|
105
|
+
mailPayload.bcc = Array.isArray(options.bcc) ? options.bcc.join(', ') : options.bcc
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (replyTo) {
|
|
109
|
+
mailPayload.replyTo = replyTo
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const info = await this.transporter.sendMail(mailPayload)
|
|
114
|
+
return info
|
|
115
|
+
} catch (error: any) {
|
|
116
|
+
throw new Error(`Failed to send email to ${mailPayload.to}: ${error.message}`)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Verifies the connection configuration.
|
|
122
|
+
*/
|
|
123
|
+
public async verifyConnection(): Promise<boolean> {
|
|
124
|
+
try {
|
|
125
|
+
await this.transporter.verify()
|
|
126
|
+
return true
|
|
127
|
+
} catch (error) {
|
|
128
|
+
console.error('Mailer connection verification failed:', error)
|
|
129
|
+
return false
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volcanicminds/tools",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "Tools for the volcanic (minds) backend",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"volcanic",
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
"esm",
|
|
11
11
|
"mfa",
|
|
12
12
|
"otp",
|
|
13
|
-
"totp"
|
|
13
|
+
"totp",
|
|
14
|
+
"mailer",
|
|
15
|
+
"nodemailer"
|
|
14
16
|
],
|
|
15
17
|
"homepage": "https://volcanicminds.com",
|
|
16
18
|
"bugs": {
|
|
@@ -41,6 +43,11 @@
|
|
|
41
43
|
"import": "./dist/lib/mfa/index.js",
|
|
42
44
|
"require": "./dist/lib/mfa/index.js"
|
|
43
45
|
},
|
|
46
|
+
"./mailer": {
|
|
47
|
+
"types": "./dist/lib/mailer/index.d.ts",
|
|
48
|
+
"import": "./dist/lib/mailer/index.js",
|
|
49
|
+
"require": "./dist/lib/mailer/index.js"
|
|
50
|
+
},
|
|
44
51
|
"./logger": {
|
|
45
52
|
"types": "./dist/lib/util/logger.d.ts",
|
|
46
53
|
"import": "./dist/lib/util/logger.js",
|
|
@@ -69,12 +76,14 @@
|
|
|
69
76
|
"combine": "node combine.js"
|
|
70
77
|
},
|
|
71
78
|
"dependencies": {
|
|
79
|
+
"nodemailer": "^6.10.1",
|
|
72
80
|
"otpauth": "^9.3.5",
|
|
73
81
|
"qrcode": "^1.5.4"
|
|
74
82
|
},
|
|
75
83
|
"devDependencies": {
|
|
76
84
|
"@eslint/js": "^9.39.1",
|
|
77
85
|
"@types/node": "^24.10.1",
|
|
86
|
+
"@types/nodemailer": "^6.4.14",
|
|
78
87
|
"@types/qrcode": "^1.5.5",
|
|
79
88
|
"eslint": "^9.39.1",
|
|
80
89
|
"globals": "^16.5.0",
|
|
@@ -95,6 +104,9 @@
|
|
|
95
104
|
"mfa": [
|
|
96
105
|
"dist/lib/mfa/index.d.ts"
|
|
97
106
|
],
|
|
107
|
+
"mailer": [
|
|
108
|
+
"dist/lib/mailer/index.d.ts"
|
|
109
|
+
],
|
|
98
110
|
"logger": [
|
|
99
111
|
"dist/lib/util/logger.d.ts"
|
|
100
112
|
]
|