servcraft 0.1.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.
Files changed (106) hide show
  1. package/.dockerignore +45 -0
  2. package/.env.example +46 -0
  3. package/.husky/commit-msg +1 -0
  4. package/.husky/pre-commit +1 -0
  5. package/.prettierignore +4 -0
  6. package/.prettierrc +11 -0
  7. package/Dockerfile +76 -0
  8. package/Dockerfile.dev +31 -0
  9. package/README.md +232 -0
  10. package/commitlint.config.js +24 -0
  11. package/dist/cli/index.cjs +3968 -0
  12. package/dist/cli/index.cjs.map +1 -0
  13. package/dist/cli/index.d.cts +1 -0
  14. package/dist/cli/index.d.ts +1 -0
  15. package/dist/cli/index.js +3945 -0
  16. package/dist/cli/index.js.map +1 -0
  17. package/dist/index.cjs +2458 -0
  18. package/dist/index.cjs.map +1 -0
  19. package/dist/index.d.cts +828 -0
  20. package/dist/index.d.ts +828 -0
  21. package/dist/index.js +2332 -0
  22. package/dist/index.js.map +1 -0
  23. package/docker-compose.prod.yml +118 -0
  24. package/docker-compose.yml +147 -0
  25. package/eslint.config.js +27 -0
  26. package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
  27. package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
  28. package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
  29. package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
  30. package/npm-cache/_cacache/index-v5/58/94/c2cba79e0f16b4c10e95a87e32255741149e8222cc314a476aab67c39cc0 +5 -0
  31. package/npm-cache/_update-notifier-last-checked +0 -0
  32. package/package.json +112 -0
  33. package/prisma/schema.prisma +157 -0
  34. package/src/cli/commands/add-module.ts +422 -0
  35. package/src/cli/commands/db.ts +137 -0
  36. package/src/cli/commands/docs.ts +16 -0
  37. package/src/cli/commands/generate.ts +459 -0
  38. package/src/cli/commands/init.ts +640 -0
  39. package/src/cli/index.ts +32 -0
  40. package/src/cli/templates/controller.ts +67 -0
  41. package/src/cli/templates/dynamic-prisma.ts +89 -0
  42. package/src/cli/templates/dynamic-schemas.ts +232 -0
  43. package/src/cli/templates/dynamic-types.ts +60 -0
  44. package/src/cli/templates/module-index.ts +33 -0
  45. package/src/cli/templates/prisma-model.ts +17 -0
  46. package/src/cli/templates/repository.ts +104 -0
  47. package/src/cli/templates/routes.ts +70 -0
  48. package/src/cli/templates/schemas.ts +26 -0
  49. package/src/cli/templates/service.ts +58 -0
  50. package/src/cli/templates/types.ts +27 -0
  51. package/src/cli/utils/docs-generator.ts +47 -0
  52. package/src/cli/utils/field-parser.ts +315 -0
  53. package/src/cli/utils/helpers.ts +89 -0
  54. package/src/config/env.ts +80 -0
  55. package/src/config/index.ts +97 -0
  56. package/src/core/index.ts +5 -0
  57. package/src/core/logger.ts +43 -0
  58. package/src/core/server.ts +132 -0
  59. package/src/database/index.ts +7 -0
  60. package/src/database/prisma.ts +54 -0
  61. package/src/database/seed.ts +59 -0
  62. package/src/index.ts +63 -0
  63. package/src/middleware/error-handler.ts +73 -0
  64. package/src/middleware/index.ts +3 -0
  65. package/src/middleware/security.ts +116 -0
  66. package/src/modules/audit/audit.service.ts +192 -0
  67. package/src/modules/audit/index.ts +2 -0
  68. package/src/modules/audit/types.ts +37 -0
  69. package/src/modules/auth/auth.controller.ts +182 -0
  70. package/src/modules/auth/auth.middleware.ts +87 -0
  71. package/src/modules/auth/auth.routes.ts +123 -0
  72. package/src/modules/auth/auth.service.ts +142 -0
  73. package/src/modules/auth/index.ts +49 -0
  74. package/src/modules/auth/schemas.ts +52 -0
  75. package/src/modules/auth/types.ts +69 -0
  76. package/src/modules/email/email.service.ts +212 -0
  77. package/src/modules/email/index.ts +10 -0
  78. package/src/modules/email/templates.ts +213 -0
  79. package/src/modules/email/types.ts +57 -0
  80. package/src/modules/swagger/index.ts +3 -0
  81. package/src/modules/swagger/schema-builder.ts +263 -0
  82. package/src/modules/swagger/swagger.service.ts +169 -0
  83. package/src/modules/swagger/types.ts +68 -0
  84. package/src/modules/user/index.ts +30 -0
  85. package/src/modules/user/schemas.ts +49 -0
  86. package/src/modules/user/types.ts +78 -0
  87. package/src/modules/user/user.controller.ts +139 -0
  88. package/src/modules/user/user.repository.ts +156 -0
  89. package/src/modules/user/user.routes.ts +199 -0
  90. package/src/modules/user/user.service.ts +145 -0
  91. package/src/modules/validation/index.ts +18 -0
  92. package/src/modules/validation/validator.ts +104 -0
  93. package/src/types/common.ts +61 -0
  94. package/src/types/index.ts +10 -0
  95. package/src/utils/errors.ts +66 -0
  96. package/src/utils/index.ts +33 -0
  97. package/src/utils/pagination.ts +38 -0
  98. package/src/utils/response.ts +63 -0
  99. package/tests/integration/auth.test.ts +59 -0
  100. package/tests/setup.ts +17 -0
  101. package/tests/unit/modules/validation.test.ts +88 -0
  102. package/tests/unit/utils/errors.test.ts +113 -0
  103. package/tests/unit/utils/pagination.test.ts +82 -0
  104. package/tsconfig.json +33 -0
  105. package/tsup.config.ts +14 -0
  106. package/vitest.config.ts +34 -0
@@ -0,0 +1,142 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import bcrypt from 'bcryptjs';
3
+ import { config } from '../../config/index.js';
4
+ import { logger } from '../../core/logger.js';
5
+ import {
6
+ UnauthorizedError,
7
+ BadRequestError,
8
+ ConflictError,
9
+ NotFoundError,
10
+ } from '../../utils/errors.js';
11
+ import type { TokenPair, JwtPayload, AuthUser } from './types.js';
12
+ import type { LoginInput, RegisterInput, ChangePasswordInput } from './schemas.js';
13
+
14
+ // Token blacklist (in production, use Redis)
15
+ const tokenBlacklist = new Set<string>();
16
+
17
+ export class AuthService {
18
+ private app: FastifyInstance;
19
+ private readonly SALT_ROUNDS = 12;
20
+
21
+ constructor(app: FastifyInstance) {
22
+ this.app = app;
23
+ }
24
+
25
+ async hashPassword(password: string): Promise<string> {
26
+ return bcrypt.hash(password, this.SALT_ROUNDS);
27
+ }
28
+
29
+ async verifyPassword(password: string, hash: string): Promise<boolean> {
30
+ return bcrypt.compare(password, hash);
31
+ }
32
+
33
+ generateTokenPair(user: AuthUser): TokenPair {
34
+ const accessPayload: Omit<JwtPayload, 'iat' | 'exp'> = {
35
+ sub: user.id,
36
+ email: user.email,
37
+ role: user.role,
38
+ type: 'access',
39
+ };
40
+
41
+ const refreshPayload: Omit<JwtPayload, 'iat' | 'exp'> = {
42
+ sub: user.id,
43
+ email: user.email,
44
+ role: user.role,
45
+ type: 'refresh',
46
+ };
47
+
48
+ const accessToken = this.app.jwt.sign(accessPayload, {
49
+ expiresIn: config.jwt.accessExpiresIn,
50
+ });
51
+
52
+ const refreshToken = this.app.jwt.sign(refreshPayload, {
53
+ expiresIn: config.jwt.refreshExpiresIn,
54
+ });
55
+
56
+ // Parse expiration time to seconds
57
+ const expiresIn = this.parseExpiration(config.jwt.accessExpiresIn);
58
+
59
+ return { accessToken, refreshToken, expiresIn };
60
+ }
61
+
62
+ private parseExpiration(expiration: string): number {
63
+ const match = expiration.match(/^(\d+)([smhd])$/);
64
+ if (!match) return 900; // default 15 minutes
65
+
66
+ const value = parseInt(match[1] || '0', 10);
67
+ const unit = match[2];
68
+
69
+ switch (unit) {
70
+ case 's':
71
+ return value;
72
+ case 'm':
73
+ return value * 60;
74
+ case 'h':
75
+ return value * 3600;
76
+ case 'd':
77
+ return value * 86400;
78
+ default:
79
+ return 900;
80
+ }
81
+ }
82
+
83
+ async verifyAccessToken(token: string): Promise<JwtPayload> {
84
+ try {
85
+ if (this.isTokenBlacklisted(token)) {
86
+ throw new UnauthorizedError('Token has been revoked');
87
+ }
88
+
89
+ const payload = this.app.jwt.verify<JwtPayload>(token);
90
+
91
+ if (payload.type !== 'access') {
92
+ throw new UnauthorizedError('Invalid token type');
93
+ }
94
+
95
+ return payload;
96
+ } catch (error) {
97
+ if (error instanceof UnauthorizedError) throw error;
98
+ logger.debug({ err: error }, 'Token verification failed');
99
+ throw new UnauthorizedError('Invalid or expired token');
100
+ }
101
+ }
102
+
103
+ async verifyRefreshToken(token: string): Promise<JwtPayload> {
104
+ try {
105
+ if (this.isTokenBlacklisted(token)) {
106
+ throw new UnauthorizedError('Token has been revoked');
107
+ }
108
+
109
+ const payload = this.app.jwt.verify<JwtPayload>(token);
110
+
111
+ if (payload.type !== 'refresh') {
112
+ throw new UnauthorizedError('Invalid token type');
113
+ }
114
+
115
+ return payload;
116
+ } catch (error) {
117
+ if (error instanceof UnauthorizedError) throw error;
118
+ logger.debug({ err: error }, 'Refresh token verification failed');
119
+ throw new UnauthorizedError('Invalid or expired refresh token');
120
+ }
121
+ }
122
+
123
+ blacklistToken(token: string): void {
124
+ tokenBlacklist.add(token);
125
+ logger.debug('Token blacklisted');
126
+ }
127
+
128
+ isTokenBlacklisted(token: string): boolean {
129
+ return tokenBlacklist.has(token);
130
+ }
131
+
132
+ // Clear expired tokens from blacklist periodically
133
+ cleanupBlacklist(): void {
134
+ // In production, this should be handled by Redis TTL
135
+ tokenBlacklist.clear();
136
+ logger.debug('Token blacklist cleared');
137
+ }
138
+ }
139
+
140
+ export function createAuthService(app: FastifyInstance): AuthService {
141
+ return new AuthService(app);
142
+ }
@@ -0,0 +1,49 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import jwt from '@fastify/jwt';
3
+ import cookie from '@fastify/cookie';
4
+ import { config } from '../../config/index.js';
5
+ import { logger } from '../../core/logger.js';
6
+ import { AuthService, createAuthService } from './auth.service.js';
7
+ import { AuthController, createAuthController } from './auth.controller.js';
8
+ import { registerAuthRoutes } from './auth.routes.js';
9
+ import { createUserService } from '../user/user.service.js';
10
+
11
+ export async function registerAuthModule(app: FastifyInstance): Promise<AuthService> {
12
+ // Register JWT plugin
13
+ await app.register(jwt, {
14
+ secret: config.jwt.secret,
15
+ sign: {
16
+ algorithm: 'HS256',
17
+ },
18
+ });
19
+
20
+ // Register cookie plugin for refresh tokens
21
+ await app.register(cookie, {
22
+ secret: config.jwt.secret,
23
+ hook: 'onRequest',
24
+ });
25
+
26
+ // Create services
27
+ const authService = createAuthService(app);
28
+ const userService = createUserService();
29
+
30
+ // Create controller
31
+ const authController = createAuthController(authService, userService);
32
+
33
+ // Register routes
34
+ registerAuthRoutes(app, authController, authService);
35
+
36
+ logger.info('Auth module registered');
37
+ return authService;
38
+ }
39
+
40
+ export { AuthService, createAuthService } from './auth.service.js';
41
+ export { AuthController, createAuthController } from './auth.controller.js';
42
+ export {
43
+ createAuthMiddleware,
44
+ createRoleMiddleware,
45
+ createPermissionMiddleware,
46
+ createOptionalAuthMiddleware,
47
+ } from './auth.middleware.js';
48
+ export * from './types.js';
49
+ export * from './schemas.js';
@@ -0,0 +1,52 @@
1
+ import { z } from 'zod';
2
+
3
+ export const loginSchema = z.object({
4
+ email: z.string().email('Invalid email address'),
5
+ password: z.string().min(1, 'Password is required'),
6
+ });
7
+
8
+ export const registerSchema = z.object({
9
+ email: z.string().email('Invalid email address'),
10
+ password: z
11
+ .string()
12
+ .min(8, 'Password must be at least 8 characters')
13
+ .regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
14
+ .regex(/[a-z]/, 'Password must contain at least one lowercase letter')
15
+ .regex(/[0-9]/, 'Password must contain at least one number'),
16
+ name: z.string().min(2, 'Name must be at least 2 characters').optional(),
17
+ });
18
+
19
+ export const refreshTokenSchema = z.object({
20
+ refreshToken: z.string().min(1, 'Refresh token is required'),
21
+ });
22
+
23
+ export const passwordResetRequestSchema = z.object({
24
+ email: z.string().email('Invalid email address'),
25
+ });
26
+
27
+ export const passwordResetConfirmSchema = z.object({
28
+ token: z.string().min(1, 'Token is required'),
29
+ password: z
30
+ .string()
31
+ .min(8, 'Password must be at least 8 characters')
32
+ .regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
33
+ .regex(/[a-z]/, 'Password must contain at least one lowercase letter')
34
+ .regex(/[0-9]/, 'Password must contain at least one number'),
35
+ });
36
+
37
+ export const changePasswordSchema = z.object({
38
+ currentPassword: z.string().min(1, 'Current password is required'),
39
+ newPassword: z
40
+ .string()
41
+ .min(8, 'Password must be at least 8 characters')
42
+ .regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
43
+ .regex(/[a-z]/, 'Password must contain at least one lowercase letter')
44
+ .regex(/[0-9]/, 'Password must contain at least one number'),
45
+ });
46
+
47
+ export type LoginInput = z.infer<typeof loginSchema>;
48
+ export type RegisterInput = z.infer<typeof registerSchema>;
49
+ export type RefreshTokenInput = z.infer<typeof refreshTokenSchema>;
50
+ export type PasswordResetRequestInput = z.infer<typeof passwordResetRequestSchema>;
51
+ export type PasswordResetConfirmInput = z.infer<typeof passwordResetConfirmSchema>;
52
+ export type ChangePasswordInput = z.infer<typeof changePasswordSchema>;
@@ -0,0 +1,69 @@
1
+ import type { FastifyRequest } from 'fastify';
2
+
3
+ export interface JwtPayload {
4
+ sub: string; // user id
5
+ email: string;
6
+ role: string;
7
+ type: 'access' | 'refresh';
8
+ iat?: number;
9
+ exp?: number;
10
+ }
11
+
12
+ export interface TokenPair {
13
+ accessToken: string;
14
+ refreshToken: string;
15
+ expiresIn: number;
16
+ }
17
+
18
+ export interface AuthUser {
19
+ id: string;
20
+ email: string;
21
+ role: string;
22
+ }
23
+
24
+ export interface AuthenticatedRequest extends FastifyRequest {
25
+ user: AuthUser;
26
+ }
27
+
28
+ export interface LoginCredentials {
29
+ email: string;
30
+ password: string;
31
+ }
32
+
33
+ export interface RegisterData {
34
+ email: string;
35
+ password: string;
36
+ name?: string;
37
+ }
38
+
39
+ export interface RefreshTokenData {
40
+ refreshToken: string;
41
+ }
42
+
43
+ export interface PasswordResetRequest {
44
+ email: string;
45
+ }
46
+
47
+ export interface PasswordResetConfirm {
48
+ token: string;
49
+ password: string;
50
+ }
51
+
52
+ export interface ChangePasswordData {
53
+ currentPassword: string;
54
+ newPassword: string;
55
+ }
56
+
57
+ // Extend Fastify types
58
+ declare module 'fastify' {
59
+ interface FastifyRequest {
60
+ user: AuthUser;
61
+ }
62
+ }
63
+
64
+ declare module '@fastify/jwt' {
65
+ interface FastifyJWT {
66
+ payload: JwtPayload;
67
+ user: AuthUser;
68
+ }
69
+ }
@@ -0,0 +1,212 @@
1
+ import nodemailer from 'nodemailer';
2
+ import type { Transporter } from 'nodemailer';
3
+ import { config } from '../../config/index.js';
4
+ import { logger } from '../../core/logger.js';
5
+ import { renderTemplate, renderCustomTemplate } from './templates.js';
6
+ import type { EmailOptions, EmailResult, EmailConfig, TemplateData } from './types.js';
7
+
8
+ export class EmailService {
9
+ private transporter: Transporter | null = null;
10
+ private config: EmailConfig | null = null;
11
+
12
+ constructor(emailConfig?: Partial<EmailConfig>) {
13
+ if (emailConfig?.host || config.email.host) {
14
+ this.config = {
15
+ host: emailConfig?.host || config.email.host || '',
16
+ port: emailConfig?.port || config.email.port || 587,
17
+ secure: (emailConfig?.port || config.email.port || 587) === 465,
18
+ auth: {
19
+ user: emailConfig?.auth?.user || config.email.user || '',
20
+ pass: emailConfig?.auth?.pass || config.email.pass || '',
21
+ },
22
+ from: emailConfig?.from || config.email.from || 'noreply@localhost',
23
+ };
24
+
25
+ this.transporter = nodemailer.createTransport({
26
+ host: this.config.host,
27
+ port: this.config.port,
28
+ secure: this.config.secure,
29
+ auth: {
30
+ user: this.config.auth.user,
31
+ pass: this.config.auth.pass,
32
+ },
33
+ });
34
+
35
+ logger.info('Email service initialized');
36
+ } else {
37
+ logger.warn('Email service not configured - emails will be logged only');
38
+ }
39
+ }
40
+
41
+ async send(options: EmailOptions): Promise<EmailResult> {
42
+ try {
43
+ let html = options.html;
44
+ let text = options.text;
45
+
46
+ // Render template if provided
47
+ if (options.template && options.data) {
48
+ html = renderTemplate(options.template, options.data);
49
+ }
50
+
51
+ // Generate plain text from HTML if not provided
52
+ if (html && !text) {
53
+ text = this.htmlToText(html);
54
+ }
55
+
56
+ const mailOptions = {
57
+ from: this.config?.from || 'noreply@localhost',
58
+ to: Array.isArray(options.to) ? options.to.join(', ') : options.to,
59
+ subject: options.subject,
60
+ html,
61
+ text,
62
+ replyTo: options.replyTo,
63
+ cc: options.cc,
64
+ bcc: options.bcc,
65
+ attachments: options.attachments,
66
+ };
67
+
68
+ // If no transporter, just log the email
69
+ if (!this.transporter) {
70
+ logger.info({ email: mailOptions }, 'Email would be sent (no transporter configured)');
71
+ return { success: true, messageId: 'dev-mode' };
72
+ }
73
+
74
+ const result = await this.transporter.sendMail(mailOptions);
75
+
76
+ logger.info(
77
+ { messageId: result.messageId, to: options.to },
78
+ 'Email sent successfully'
79
+ );
80
+
81
+ return {
82
+ success: true,
83
+ messageId: result.messageId,
84
+ };
85
+ } catch (error) {
86
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
87
+ logger.error({ err: error, to: options.to }, 'Failed to send email');
88
+
89
+ return {
90
+ success: false,
91
+ error: errorMessage,
92
+ };
93
+ }
94
+ }
95
+
96
+ async sendTemplate(
97
+ to: string,
98
+ template: string,
99
+ data: TemplateData
100
+ ): Promise<EmailResult> {
101
+ const subjects: Record<string, string> = {
102
+ welcome: `Welcome to ${data.appName || 'Servcraft'}!`,
103
+ 'verify-email': 'Verify Your Email',
104
+ 'password-reset': 'Reset Your Password',
105
+ 'password-changed': 'Password Changed Successfully',
106
+ 'login-alert': 'New Login Detected',
107
+ 'account-suspended': 'Account Suspended',
108
+ };
109
+
110
+ return this.send({
111
+ to,
112
+ subject: subjects[template] || 'Notification',
113
+ template,
114
+ data,
115
+ });
116
+ }
117
+
118
+ async sendWelcome(email: string, name: string, verifyUrl?: string): Promise<EmailResult> {
119
+ return this.sendTemplate(email, 'welcome', {
120
+ userName: name,
121
+ userEmail: email,
122
+ actionUrl: verifyUrl,
123
+ });
124
+ }
125
+
126
+ async sendVerifyEmail(email: string, name: string, verifyUrl: string): Promise<EmailResult> {
127
+ return this.sendTemplate(email, 'verify-email', {
128
+ userName: name,
129
+ userEmail: email,
130
+ actionUrl: verifyUrl,
131
+ expiresIn: '24 hours',
132
+ });
133
+ }
134
+
135
+ async sendPasswordReset(email: string, name: string, resetUrl: string): Promise<EmailResult> {
136
+ return this.sendTemplate(email, 'password-reset', {
137
+ userName: name,
138
+ userEmail: email,
139
+ actionUrl: resetUrl,
140
+ expiresIn: '1 hour',
141
+ });
142
+ }
143
+
144
+ async sendPasswordChanged(
145
+ email: string,
146
+ name: string,
147
+ ipAddress?: string,
148
+ userAgent?: string
149
+ ): Promise<EmailResult> {
150
+ return this.sendTemplate(email, 'password-changed', {
151
+ userName: name,
152
+ userEmail: email,
153
+ ipAddress: ipAddress || 'Unknown',
154
+ userAgent: userAgent || 'Unknown',
155
+ timestamp: new Date().toISOString(),
156
+ });
157
+ }
158
+
159
+ async sendLoginAlert(
160
+ email: string,
161
+ name: string,
162
+ ipAddress: string,
163
+ userAgent: string,
164
+ location?: string
165
+ ): Promise<EmailResult> {
166
+ return this.sendTemplate(email, 'login-alert', {
167
+ userName: name,
168
+ userEmail: email,
169
+ ipAddress,
170
+ userAgent,
171
+ location: location || 'Unknown',
172
+ timestamp: new Date().toISOString(),
173
+ });
174
+ }
175
+
176
+ async verify(): Promise<boolean> {
177
+ if (!this.transporter) {
178
+ return false;
179
+ }
180
+
181
+ try {
182
+ await this.transporter.verify();
183
+ logger.info('Email service connection verified');
184
+ return true;
185
+ } catch (error) {
186
+ logger.error({ err: error }, 'Email service connection failed');
187
+ return false;
188
+ }
189
+ }
190
+
191
+ private htmlToText(html: string): string {
192
+ return html
193
+ .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
194
+ .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
195
+ .replace(/<[^>]+>/g, ' ')
196
+ .replace(/\s+/g, ' ')
197
+ .trim();
198
+ }
199
+ }
200
+
201
+ let emailService: EmailService | null = null;
202
+
203
+ export function getEmailService(): EmailService {
204
+ if (!emailService) {
205
+ emailService = new EmailService();
206
+ }
207
+ return emailService;
208
+ }
209
+
210
+ export function createEmailService(config?: Partial<EmailConfig>): EmailService {
211
+ return new EmailService(config);
212
+ }
@@ -0,0 +1,10 @@
1
+ export { EmailService, getEmailService, createEmailService } from './email.service.js';
2
+ export { renderTemplate, renderCustomTemplate } from './templates.js';
3
+ export type {
4
+ EmailConfig,
5
+ EmailOptions,
6
+ EmailResult,
7
+ EmailAttachment,
8
+ EmailTemplate,
9
+ TemplateData,
10
+ } from './types.js';