@volcanicminds/tools 0.0.3 → 0.0.5

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 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 for smaller bundles)
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 Setup
56
- const { secret, uri, qrCode } = await mfa.generateSetupDetails('MyApp', 'user@example.com')
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
- // Verify Token
59
- const isValid = mfa.verifyToken('123456', secret)
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
- ## Logging
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
- Use Pino logger if in your project you have a `global.log` with a valid instance.
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
@@ -1,3 +1,6 @@
1
1
  export * as mfa from './lib/mfa/index.js';
2
+ export * as mailer from './lib/mailer/index.js';
2
3
  export * as log from './lib/util/logger.js';
4
+ export * as storage from './lib/storage/index.js';
5
+ export * as transfer from './lib/transfer/index.js';
3
6
  //# sourceMappingURL=index.d.ts.map
@@ -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;AAC3C,OAAO,KAAK,OAAO,MAAM,wBAAwB,CAAA;AACjD,OAAO,KAAK,QAAQ,MAAM,yBAAyB,CAAA"}
package/dist/index.js CHANGED
@@ -1,3 +1,6 @@
1
1
  export * as mfa from './lib/mfa/index.js';
2
+ export * as mailer from './lib/mailer/index.js';
2
3
  export * as log from './lib/util/logger.js';
4
+ export * as storage from './lib/storage/index.js';
5
+ export * as transfer from './lib/transfer/index.js';
3
6
  //# sourceMappingURL=index.js.map
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;AAC3C,OAAO,KAAK,OAAO,MAAM,wBAAwB,CAAA;AACjD,OAAO,KAAK,QAAQ,MAAM,yBAAyB,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,40 @@
1
+ import { Client, BucketItemStat } from 'minio';
2
+ import { Readable } from 'stream';
3
+ export interface StorageConfig {
4
+ endPoint: string;
5
+ port: number;
6
+ useSSL: boolean;
7
+ accessKey: string;
8
+ secretKey: string;
9
+ bucket: string;
10
+ region?: string;
11
+ pathStyle?: boolean;
12
+ }
13
+ export interface UploadOptions {
14
+ size?: number;
15
+ contentType?: string;
16
+ metadata?: Record<string, string | number | boolean>;
17
+ }
18
+ export interface UploadedObjectInfo {
19
+ etag: string;
20
+ versionId: string | null;
21
+ }
22
+ export declare class StorageManager {
23
+ private client;
24
+ private bucket;
25
+ private region;
26
+ constructor(config: StorageConfig);
27
+ getClient(): Client;
28
+ getBucketName(): string;
29
+ verifyConnection(): Promise<boolean>;
30
+ ensureBucket(bucketName?: string): Promise<void>;
31
+ uploadFile(objectName: string, stream: Readable | Buffer | string, options?: UploadOptions): Promise<UploadedObjectInfo>;
32
+ getFileUrl(objectName: string, expiry?: number): Promise<string>;
33
+ getUploadUrl(objectName: string, expiry?: number): Promise<string>;
34
+ deleteFile(objectName: string): Promise<void>;
35
+ deleteFiles(objectNames: string[]): Promise<void>;
36
+ fileExists(objectName: string): Promise<boolean>;
37
+ getFileStream(objectName: string): Promise<Readable>;
38
+ getFileStat(objectName: string): Promise<BucketItemStat>;
39
+ }
40
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../lib/storage/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAqC,cAAc,EAAE,MAAM,OAAO,CAAA;AACjF,OAAO,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAA;AAEjC,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,OAAO,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAA;CACrD;AAID,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CACzB;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,MAAM,CAAQ;gBAEV,MAAM,EAAE,aAAa;IAiB1B,SAAS,IAAI,MAAM;IAInB,aAAa,IAAI,MAAM;IAIjB,gBAAgB,IAAI,OAAO,CAAC,OAAO,CAAC;IAUpC,YAAY,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAYhD,UAAU,CACrB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,QAAQ,GAAG,MAAM,GAAG,MAAM,EAClC,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,kBAAkB,CAAC;IAgBjB,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,GAAE,MAAqB,GAAG,OAAO,CAAC,MAAM,CAAC;IAI9E,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,GAAE,MAAqB,GAAG,OAAO,CAAC,MAAM,CAAC;IAIhF,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI7C,WAAW,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAIjD,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAYhD,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAIpD,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;CAGtE"}
@@ -0,0 +1,84 @@
1
+ import { Client } from 'minio';
2
+ export class StorageManager {
3
+ client;
4
+ bucket;
5
+ region;
6
+ constructor(config) {
7
+ this.bucket = config.bucket;
8
+ this.region = config.region || 'us-east-1';
9
+ const clientOptions = {
10
+ endPoint: config.endPoint,
11
+ port: config.port,
12
+ useSSL: config.useSSL,
13
+ accessKey: config.accessKey,
14
+ secretKey: config.secretKey,
15
+ region: this.region,
16
+ pathStyle: config.pathStyle
17
+ };
18
+ this.client = new Client(clientOptions);
19
+ }
20
+ getClient() {
21
+ return this.client;
22
+ }
23
+ getBucketName() {
24
+ return this.bucket;
25
+ }
26
+ async verifyConnection() {
27
+ try {
28
+ await this.client.listBuckets();
29
+ return true;
30
+ }
31
+ catch (error) {
32
+ console.error('Storage connection verification failed:', error);
33
+ return false;
34
+ }
35
+ }
36
+ async ensureBucket(bucketName) {
37
+ const targetBucket = bucketName || this.bucket;
38
+ try {
39
+ const exists = await this.client.bucketExists(targetBucket);
40
+ if (!exists) {
41
+ await this.client.makeBucket(targetBucket, this.region);
42
+ }
43
+ }
44
+ catch (error) {
45
+ throw new Error(`Failed to ensure bucket ${targetBucket}: ${error}`);
46
+ }
47
+ }
48
+ async uploadFile(objectName, stream, options = {}) {
49
+ await this.ensureBucket();
50
+ const metaData = options.metadata || {};
51
+ return this.client.putObject(this.bucket, objectName, stream, options.size, metaData);
52
+ }
53
+ async getFileUrl(objectName, expiry = 24 * 60 * 60) {
54
+ return this.client.presignedGetObject(this.bucket, objectName, expiry);
55
+ }
56
+ async getUploadUrl(objectName, expiry = 24 * 60 * 60) {
57
+ return this.client.presignedPutObject(this.bucket, objectName, expiry);
58
+ }
59
+ async deleteFile(objectName) {
60
+ await this.client.removeObject(this.bucket, objectName);
61
+ }
62
+ async deleteFiles(objectNames) {
63
+ await this.client.removeObjects(this.bucket, objectNames);
64
+ }
65
+ async fileExists(objectName) {
66
+ try {
67
+ await this.client.statObject(this.bucket, objectName);
68
+ return true;
69
+ }
70
+ catch (error) {
71
+ if (error.code === 'NotFound') {
72
+ return false;
73
+ }
74
+ throw error;
75
+ }
76
+ }
77
+ async getFileStream(objectName) {
78
+ return this.client.getObject(this.bucket, objectName);
79
+ }
80
+ async getFileStat(objectName) {
81
+ return this.client.statObject(this.bucket, objectName);
82
+ }
83
+ }
84
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../lib/storage/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ import { Server, Upload } from '@tus/server';
2
+ import type { IncomingMessage, ServerResponse } from 'http';
3
+ export interface TransferConfig {
4
+ driver: 'local' | 's3';
5
+ maxSize?: number;
6
+ path: string;
7
+ local?: {
8
+ directory: string;
9
+ };
10
+ s3?: {
11
+ bucket: string;
12
+ endPoint: string;
13
+ port?: number;
14
+ useSSL?: boolean;
15
+ accessKey: string;
16
+ secretKey: string;
17
+ region?: string;
18
+ partSize?: number;
19
+ };
20
+ }
21
+ export type TransferEventCallback = (uploadOrId: Upload | string, req?: IncomingMessage, res?: ServerResponse) => void;
22
+ export declare class TransferManager {
23
+ private server;
24
+ private config;
25
+ constructor(config: TransferConfig);
26
+ private createStore;
27
+ getServer(): Server;
28
+ onUploadCreate(callback: TransferEventCallback): void;
29
+ onUploadFinish(callback: TransferEventCallback): void;
30
+ onUploadTerminate(callback: TransferEventCallback): void;
31
+ handle(req: IncomingMessage, res: ServerResponse): Promise<void>;
32
+ }
33
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../lib/transfer/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAU,MAAM,EAAE,MAAM,aAAa,CAAA;AAGpD,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,MAAM,CAAA;AAE3D,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,OAAO,GAAG,IAAI,CAAA;IACtB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IAGZ,KAAK,CAAC,EAAE;QACN,SAAS,EAAE,MAAM,CAAA;KAClB,CAAA;IAGD,EAAE,CAAC,EAAE;QACH,MAAM,EAAE,MAAM,CAAA;QACd,QAAQ,EAAE,MAAM,CAAA;QAChB,IAAI,CAAC,EAAE,MAAM,CAAA;QACb,MAAM,CAAC,EAAE,OAAO,CAAA;QAChB,SAAS,EAAE,MAAM,CAAA;QACjB,SAAS,EAAE,MAAM,CAAA;QACjB,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,QAAQ,CAAC,EAAE,MAAM,CAAA;KAClB,CAAA;CACF;AAGD,MAAM,MAAM,qBAAqB,GAAG,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,CAAC,EAAE,eAAe,EAAE,GAAG,CAAC,EAAE,cAAc,KAAK,IAAI,CAAA;AAEtH,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,MAAM,CAAgB;gBAElB,MAAM,EAAE,cAAc;IAYlC,OAAO,CAAC,WAAW;IAsCZ,SAAS,IAAI,MAAM;IAMnB,cAAc,CAAC,QAAQ,EAAE,qBAAqB,GAAG,IAAI;IAMrD,cAAc,CAAC,QAAQ,EAAE,qBAAqB,GAAG,IAAI;IAMrD,iBAAiB,CAAC,QAAQ,EAAE,qBAAqB,GAAG,IAAI;IAQxD,MAAM,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;CAGxE"}
@@ -0,0 +1,71 @@
1
+ import { Server, EVENTS } from '@tus/server';
2
+ import { FileStore } from '@tus/file-store';
3
+ import { S3Store } from '@tus/s3-store';
4
+ export class TransferManager {
5
+ server;
6
+ config;
7
+ constructor(config) {
8
+ this.config = config;
9
+ const store = this.createStore();
10
+ this.server = new Server({
11
+ path: config.path,
12
+ datastore: store,
13
+ maxSize: config.maxSize,
14
+ respectForwardedHeaders: true
15
+ });
16
+ }
17
+ createStore() {
18
+ if (this.config.driver === 'local') {
19
+ if (!this.config.local?.directory) {
20
+ throw new Error('TransferManager: Local driver requires "directory" path');
21
+ }
22
+ return new FileStore({
23
+ directory: this.config.local.directory
24
+ });
25
+ }
26
+ if (this.config.driver === 's3') {
27
+ if (!this.config.s3) {
28
+ throw new Error('TransferManager: S3 driver requires s3 config object');
29
+ }
30
+ const { endPoint, port, useSSL, accessKey, secretKey, bucket, region, partSize } = this.config.s3;
31
+ const protocol = useSSL ? 'https:' : 'http:';
32
+ const endpointUrl = port ? `${protocol}//${endPoint}:${port}` : `${protocol}//${endPoint}`;
33
+ return new S3Store({
34
+ partSize: partSize || 8 * 1024 * 1024,
35
+ s3ClientConfig: {
36
+ bucket: bucket,
37
+ region: region || 'us-east-1',
38
+ endpoint: endpointUrl,
39
+ credentials: {
40
+ accessKeyId: accessKey,
41
+ secretAccessKey: secretKey
42
+ },
43
+ forcePathStyle: true
44
+ }
45
+ });
46
+ }
47
+ throw new Error(`TransferManager: Unsupported driver ${this.config.driver}`);
48
+ }
49
+ getServer() {
50
+ return this.server;
51
+ }
52
+ onUploadCreate(callback) {
53
+ this.server.on(EVENTS.POST_CREATE, (req, res, upload) => {
54
+ callback(upload, req, res);
55
+ });
56
+ }
57
+ onUploadFinish(callback) {
58
+ this.server.on(EVENTS.POST_FINISH, (req, res, upload) => {
59
+ callback(upload, req, res);
60
+ });
61
+ }
62
+ onUploadTerminate(callback) {
63
+ this.server.on(EVENTS.POST_TERMINATE, (req, res, id) => {
64
+ callback(id, req, res);
65
+ });
66
+ }
67
+ handle(req, res) {
68
+ return this.server.handle(req, res);
69
+ }
70
+ }
71
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../lib/transfer/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
+ }
@@ -0,0 +1,135 @@
1
+ import { Client, ClientOptions, ItemBucketMetadata, BucketItemStat } from 'minio'
2
+ import { Readable } from 'stream'
3
+
4
+ export interface StorageConfig {
5
+ endPoint: string
6
+ port: number
7
+ useSSL: boolean
8
+ accessKey: string
9
+ secretKey: string
10
+ bucket: string
11
+ region?: string
12
+ pathStyle?: boolean
13
+ }
14
+
15
+ export interface UploadOptions {
16
+ size?: number
17
+ contentType?: string
18
+ metadata?: Record<string, string | number | boolean>
19
+ }
20
+
21
+ // Definizione manuale del tipo di ritorno di putObject
22
+ // Necessario perché @types/minio non esporta 'UploadedObjectInfo' o 'Result' pubblicamente
23
+ export interface UploadedObjectInfo {
24
+ etag: string
25
+ versionId: string | null
26
+ }
27
+
28
+ export class StorageManager {
29
+ private client: Client
30
+ private bucket: string
31
+ private region: string
32
+
33
+ constructor(config: StorageConfig) {
34
+ this.bucket = config.bucket
35
+ this.region = config.region || 'us-east-1'
36
+
37
+ const clientOptions: ClientOptions = {
38
+ endPoint: config.endPoint,
39
+ port: config.port,
40
+ useSSL: config.useSSL,
41
+ accessKey: config.accessKey,
42
+ secretKey: config.secretKey,
43
+ region: this.region,
44
+ pathStyle: config.pathStyle
45
+ }
46
+
47
+ this.client = new Client(clientOptions)
48
+ }
49
+
50
+ public getClient(): Client {
51
+ return this.client
52
+ }
53
+
54
+ public getBucketName(): string {
55
+ return this.bucket
56
+ }
57
+
58
+ public async verifyConnection(): Promise<boolean> {
59
+ try {
60
+ await this.client.listBuckets()
61
+ return true
62
+ } catch (error) {
63
+ console.error('Storage connection verification failed:', error)
64
+ return false
65
+ }
66
+ }
67
+
68
+ public async ensureBucket(bucketName?: string): Promise<void> {
69
+ const targetBucket = bucketName || this.bucket
70
+ try {
71
+ const exists = await this.client.bucketExists(targetBucket)
72
+ if (!exists) {
73
+ await this.client.makeBucket(targetBucket, this.region)
74
+ }
75
+ } catch (error) {
76
+ throw new Error(`Failed to ensure bucket ${targetBucket}: ${error}`)
77
+ }
78
+ }
79
+
80
+ public async uploadFile(
81
+ objectName: string,
82
+ stream: Readable | Buffer | string,
83
+ options: UploadOptions = {}
84
+ ): Promise<UploadedObjectInfo> {
85
+ await this.ensureBucket()
86
+
87
+ const metaData = options.metadata || {}
88
+
89
+ // Casting a any/Promise<UploadedObjectInfo> per compatibilità TS
90
+ // putObject ritorna un oggetto compatibile con l'interfaccia definita sopra
91
+ return this.client.putObject(
92
+ this.bucket,
93
+ objectName,
94
+ stream,
95
+ options.size,
96
+ metaData as ItemBucketMetadata
97
+ ) as Promise<UploadedObjectInfo>
98
+ }
99
+
100
+ public async getFileUrl(objectName: string, expiry: number = 24 * 60 * 60): Promise<string> {
101
+ return this.client.presignedGetObject(this.bucket, objectName, expiry)
102
+ }
103
+
104
+ public async getUploadUrl(objectName: string, expiry: number = 24 * 60 * 60): Promise<string> {
105
+ return this.client.presignedPutObject(this.bucket, objectName, expiry)
106
+ }
107
+
108
+ public async deleteFile(objectName: string): Promise<void> {
109
+ await this.client.removeObject(this.bucket, objectName)
110
+ }
111
+
112
+ public async deleteFiles(objectNames: string[]): Promise<void> {
113
+ await this.client.removeObjects(this.bucket, objectNames)
114
+ }
115
+
116
+ public async fileExists(objectName: string): Promise<boolean> {
117
+ try {
118
+ await this.client.statObject(this.bucket, objectName)
119
+ return true
120
+ } catch (error: any) {
121
+ if (error.code === 'NotFound') {
122
+ return false
123
+ }
124
+ throw error
125
+ }
126
+ }
127
+
128
+ public async getFileStream(objectName: string): Promise<Readable> {
129
+ return this.client.getObject(this.bucket, objectName)
130
+ }
131
+
132
+ public async getFileStat(objectName: string): Promise<BucketItemStat> {
133
+ return this.client.statObject(this.bucket, objectName)
134
+ }
135
+ }
@@ -0,0 +1,115 @@
1
+ import { Server, EVENTS, Upload } from '@tus/server'
2
+ import { FileStore } from '@tus/file-store'
3
+ import { S3Store } from '@tus/s3-store'
4
+ import type { IncomingMessage, ServerResponse } from 'http'
5
+
6
+ export interface TransferConfig {
7
+ driver: 'local' | 's3'
8
+ maxSize?: number // Bytes
9
+ path: string // URL Path prefix (e.g. /files)
10
+
11
+ // Local Config
12
+ local?: {
13
+ directory: string
14
+ }
15
+
16
+ // S3/Minio Config
17
+ s3?: {
18
+ bucket: string
19
+ endPoint: string
20
+ port?: number
21
+ useSSL?: boolean
22
+ accessKey: string
23
+ secretKey: string
24
+ region?: string
25
+ partSize?: number // Minimum 5MB for S3
26
+ }
27
+ }
28
+
29
+ // Updated definition to handle 'string' ID from POST_TERMINATE
30
+ export type TransferEventCallback = (uploadOrId: Upload | string, req?: IncomingMessage, res?: ServerResponse) => void
31
+
32
+ export class TransferManager {
33
+ private server: Server
34
+ private config: TransferConfig
35
+
36
+ constructor(config: TransferConfig) {
37
+ this.config = config
38
+ const store = this.createStore()
39
+
40
+ this.server = new Server({
41
+ path: config.path,
42
+ datastore: store,
43
+ maxSize: config.maxSize,
44
+ respectForwardedHeaders: true
45
+ })
46
+ }
47
+
48
+ private createStore() {
49
+ if (this.config.driver === 'local') {
50
+ if (!this.config.local?.directory) {
51
+ throw new Error('TransferManager: Local driver requires "directory" path')
52
+ }
53
+ return new FileStore({
54
+ directory: this.config.local.directory
55
+ })
56
+ }
57
+
58
+ if (this.config.driver === 's3') {
59
+ if (!this.config.s3) {
60
+ throw new Error('TransferManager: S3 driver requires s3 config object')
61
+ }
62
+
63
+ const { endPoint, port, useSSL, accessKey, secretKey, bucket, region, partSize } = this.config.s3
64
+
65
+ const protocol = useSSL ? 'https:' : 'http:'
66
+ const endpointUrl = port ? `${protocol}//${endPoint}:${port}` : `${protocol}//${endPoint}`
67
+
68
+ return new S3Store({
69
+ partSize: partSize || 8 * 1024 * 1024,
70
+ s3ClientConfig: {
71
+ bucket: bucket, // Required inside s3ClientConfig by @tus/s3-store types
72
+ region: region || 'us-east-1',
73
+ endpoint: endpointUrl,
74
+ credentials: {
75
+ accessKeyId: accessKey,
76
+ secretAccessKey: secretKey
77
+ },
78
+ forcePathStyle: true
79
+ }
80
+ })
81
+ }
82
+
83
+ throw new Error(`TransferManager: Unsupported driver ${this.config.driver}`)
84
+ }
85
+
86
+ public getServer(): Server {
87
+ return this.server
88
+ }
89
+
90
+ // Hook system wrappers with double casting (as unknown) to bypass TS structural check
91
+
92
+ public onUploadCreate(callback: TransferEventCallback): void {
93
+ this.server.on(EVENTS.POST_CREATE, (req, res, upload) => {
94
+ callback(upload, req as unknown as IncomingMessage, res as unknown as ServerResponse)
95
+ })
96
+ }
97
+
98
+ public onUploadFinish(callback: TransferEventCallback): void {
99
+ this.server.on(EVENTS.POST_FINISH, (req, res, upload) => {
100
+ callback(upload, req as unknown as IncomingMessage, res as unknown as ServerResponse)
101
+ })
102
+ }
103
+
104
+ public onUploadTerminate(callback: TransferEventCallback): void {
105
+ // Note: POST_TERMINATE passes 'id' (string) instead of 'upload' object in recent tus versions
106
+ this.server.on(EVENTS.POST_TERMINATE, (req, res, id) => {
107
+ callback(id, req as unknown as IncomingMessage, res as unknown as ServerResponse)
108
+ })
109
+ }
110
+
111
+ // Handle Request helper
112
+ public handle(req: IncomingMessage, res: ServerResponse): Promise<void> {
113
+ return this.server.handle(req, res)
114
+ }
115
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volcanicminds/tools",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "Tools for the volcanic (minds) backend",
5
5
  "keywords": [
6
6
  "volcanic",
@@ -10,7 +10,15 @@
10
10
  "esm",
11
11
  "mfa",
12
12
  "otp",
13
- "totp"
13
+ "totp",
14
+ "mailer",
15
+ "nodemailer",
16
+ "storage",
17
+ "minio",
18
+ "s3",
19
+ "tus",
20
+ "upload",
21
+ "transfer"
14
22
  ],
15
23
  "homepage": "https://volcanicminds.com",
16
24
  "bugs": {
@@ -41,10 +49,25 @@
41
49
  "import": "./dist/lib/mfa/index.js",
42
50
  "require": "./dist/lib/mfa/index.js"
43
51
  },
52
+ "./mailer": {
53
+ "types": "./dist/lib/mailer/index.d.ts",
54
+ "import": "./dist/lib/mailer/index.js",
55
+ "require": "./dist/lib/mailer/index.js"
56
+ },
44
57
  "./logger": {
45
58
  "types": "./dist/lib/util/logger.d.ts",
46
59
  "import": "./dist/lib/util/logger.js",
47
60
  "require": "./dist/lib/util/logger.js"
61
+ },
62
+ "./storage": {
63
+ "types": "./dist/lib/storage/index.d.ts",
64
+ "import": "./dist/lib/storage/index.js",
65
+ "require": "./dist/lib/storage/index.js"
66
+ },
67
+ "./transfer": {
68
+ "types": "./dist/lib/transfer/index.d.ts",
69
+ "import": "./dist/lib/transfer/index.js",
70
+ "require": "./dist/lib/transfer/index.js"
48
71
  }
49
72
  },
50
73
  "main": "dist/index.js",
@@ -69,18 +92,26 @@
69
92
  "combine": "node combine.js"
70
93
  },
71
94
  "dependencies": {
72
- "otpauth": "^9.3.5",
95
+ "@aws-sdk/client-s3": "^3.954.0",
96
+ "@tus/file-store": "^2.0.0",
97
+ "@tus/s3-store": "^2.0.1",
98
+ "@tus/server": "^2.3.0",
99
+ "minio": "^8.0.6",
100
+ "nodemailer": "^7.0.11",
101
+ "otpauth": "^9.4.1",
73
102
  "qrcode": "^1.5.4"
74
103
  },
75
104
  "devDependencies": {
76
- "@eslint/js": "^9.39.1",
77
- "@types/node": "^24.10.1",
78
- "@types/qrcode": "^1.5.5",
79
- "eslint": "^9.39.1",
105
+ "@eslint/js": "^9.39.2",
106
+ "@types/minio": "^7.1.1",
107
+ "@types/node": "^25.0.3",
108
+ "@types/nodemailer": "^7.0.4",
109
+ "@types/qrcode": "^1.5.6",
110
+ "eslint": "^9.39.2",
80
111
  "globals": "^16.5.0",
81
- "tsx": "^4.19.2",
112
+ "tsx": "^4.21.0",
82
113
  "typescript": "^5.9.3",
83
- "typescript-eslint": "^8.48.0"
114
+ "typescript-eslint": "^8.50.0"
84
115
  },
85
116
  "engines": {
86
117
  "node": ">=24"
@@ -95,8 +126,17 @@
95
126
  "mfa": [
96
127
  "dist/lib/mfa/index.d.ts"
97
128
  ],
129
+ "mailer": [
130
+ "dist/lib/mailer/index.d.ts"
131
+ ],
98
132
  "logger": [
99
133
  "dist/lib/util/logger.d.ts"
134
+ ],
135
+ "storage": [
136
+ "dist/lib/storage/index.d.ts"
137
+ ],
138
+ "transfer": [
139
+ "dist/lib/transfer/index.d.ts"
100
140
  ]
101
141
  }
102
142
  }