webspresso 0.0.99 → 0.0.101

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 (108) hide show
  1. package/.agents/ADMIN.md +47 -4
  2. package/.agents/AGENTS.md +4 -2
  3. package/.agents/AUTH.md +309 -0
  4. package/.agents/CLI.md +128 -0
  5. package/.agents/EMAIL.md +84 -2
  6. package/.agents/ERRORS.md +59 -0
  7. package/.agents/EVENTS.md +179 -0
  8. package/.agents/ORM.md +149 -0
  9. package/.agents/PLUGINS.md +44 -7
  10. package/.agents/QUEUE.md +18 -10
  11. package/.agents/ROUTING.md +97 -1
  12. package/.agents/STREAMING.md +35 -0
  13. package/AGENTS.md +2 -0
  14. package/README.md +145 -93
  15. package/bin/commands/add-docker.js +467 -0
  16. package/bin/commands/db-makemigrations.js +83 -0
  17. package/bin/commands/deploy-check.js +339 -0
  18. package/bin/commands/list.js +96 -0
  19. package/bin/commands/make-command.js +93 -0
  20. package/bin/commands/make-event.js +80 -0
  21. package/bin/commands/make-factory.js +72 -0
  22. package/bin/commands/make-mail.js +93 -0
  23. package/bin/commands/make-policy.js +106 -0
  24. package/bin/commands/run.js +195 -0
  25. package/bin/webspresso.js +21 -0
  26. package/core/auth/bouncer.js +196 -0
  27. package/core/auth/index.js +73 -1
  28. package/core/auth/magic-link.js +170 -0
  29. package/core/auth/manager.js +343 -3
  30. package/core/auth/middleware.js +45 -6
  31. package/core/auth/session-manager.js +255 -0
  32. package/core/auth/sessions.js +351 -0
  33. package/core/auth/tokens.js +1 -0
  34. package/core/auth/totp.js +4 -1
  35. package/core/auth/user-agent.js +161 -0
  36. package/core/commands/define-command.js +48 -0
  37. package/core/commands/index.js +18 -0
  38. package/core/commands/registry.js +156 -0
  39. package/core/commands/runner.js +76 -0
  40. package/core/compression/compressible.js +1 -1
  41. package/core/compression/middleware.js +35 -251
  42. package/core/content/service.js +8 -2
  43. package/core/events/discovery.js +142 -0
  44. package/core/events/dispatcher.js +552 -0
  45. package/core/events/index.js +23 -0
  46. package/core/events/signal.js +92 -0
  47. package/core/forms/flash.js +234 -0
  48. package/core/forms/form-builder.js +214 -0
  49. package/core/forms/index.js +16 -0
  50. package/core/mail/index.js +23 -0
  51. package/core/mail/mailable.js +250 -0
  52. package/core/mail/mailer.js +184 -0
  53. package/core/mail/transports.js +238 -0
  54. package/core/orm/factory.js +250 -0
  55. package/core/orm/index.js +8 -1
  56. package/core/orm/migrations/diff.js +185 -0
  57. package/core/orm/migrations/index.js +22 -4
  58. package/core/orm/model.js +55 -1
  59. package/core/orm/repository.js +283 -13
  60. package/core/orm/scopes.js +3 -2
  61. package/core/orm/types.js +6 -1
  62. package/core/queue/cron.js +252 -39
  63. package/core/queue/manager.js +20 -2
  64. package/core/queue/scheduler.js +115 -59
  65. package/core/ssr/stream.js +62 -2
  66. package/index.d.ts +540 -14
  67. package/index.js +44 -0
  68. package/package.json +11 -5
  69. package/plugins/admin-panel/api.js +509 -3
  70. package/plugins/admin-panel/index.js +17 -0
  71. package/plugins/admin-panel/modules/admin-users.js +1 -1
  72. package/plugins/audit-log/admin-component.js +54 -54
  73. package/plugins/audit-log/index.js +2 -2
  74. package/plugins/content/client/inline-edit.js +2 -46
  75. package/plugins/content/models/content-entry.js +4 -3
  76. package/plugins/content/models/content-type.js +4 -3
  77. package/plugins/cors.js +15 -4
  78. package/plugins/email/auth-bridge.js +66 -0
  79. package/plugins/email/bundled-templates.js +15 -0
  80. package/plugins/email/index.js +48 -0
  81. package/plugins/email/preview-handler.js +555 -0
  82. package/plugins/email/service.js +23 -12
  83. package/plugins/email/templates/auth-magic-link.mjml +13 -0
  84. package/plugins/email/transport.js +30 -11
  85. package/plugins/file-manager/admin-component.js +49 -49
  86. package/plugins/file-manager/index.js +4 -4
  87. package/plugins/index.js +2 -0
  88. package/plugins/redirect/index.js +1 -1
  89. package/plugins/webhooks/admin-component.js +608 -0
  90. package/plugins/webhooks/api-handlers.js +237 -0
  91. package/plugins/webhooks/crypto.js +144 -0
  92. package/plugins/webhooks/dispatcher.js +708 -0
  93. package/plugins/webhooks/index.js +263 -0
  94. package/plugins/webhooks/matcher.js +113 -0
  95. package/plugins/webhooks/migration-template.js +75 -0
  96. package/plugins/webhooks/models.js +98 -0
  97. package/plugins/webhooks/sender.js +176 -0
  98. package/src/api/api-loader.js +1 -1
  99. package/src/app-context.js +48 -6
  100. package/src/bundle/index.js +2 -1
  101. package/src/dev-reload/index.js +9 -1
  102. package/src/discovery/scan-routes.js +10 -1
  103. package/src/file-router.js +173 -4
  104. package/src/helpers.js +150 -1
  105. package/src/njk-frontmatter.js +3 -0
  106. package/src/pages/page-loader.js +1 -1
  107. package/src/routing/cascading.js +120 -4
  108. package/src/server.js +180 -26
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Webspresso - Flash Messages & Old Input Manager (AdonisJS & Django-inspired)
3
+ * Provides session-backed or cookie-backed one-time flash storage across redirects
4
+ * @module core/forms/flash
5
+ */
6
+
7
+ const FLASH_SESSION_KEY = '__ws_flash';
8
+ const FLASH_COOKIE_NAME = '__ws_flash';
9
+
10
+ /**
11
+ * Format and normalize errors into a standard `{ fieldName: string[] }` shape
12
+ * Supports ZodError, plain object, array, or string error.
13
+ * @param {any} errors
14
+ * @returns {Record<string, string[]>}
15
+ */
16
+ function normalizeErrors(errors) {
17
+ if (!errors) return {};
18
+
19
+ // 1. ZodError with format() or flatten() or issues
20
+ if (errors.issues && Array.isArray(errors.issues)) {
21
+ const result = {};
22
+ for (const issue of errors.issues) {
23
+ const field = issue.path && issue.path.length > 0 ? issue.path.join('.') : '_form';
24
+ if (!result[field]) result[field] = [];
25
+ result[field].push(issue.message || 'Invalid value');
26
+ }
27
+ return result;
28
+ }
29
+
30
+ if (typeof errors.flatten === 'function') {
31
+ const flat = errors.flatten();
32
+ const result = { ...(flat.fieldErrors || {}) };
33
+ if (flat.formErrors && flat.formErrors.length > 0) {
34
+ result._form = flat.formErrors;
35
+ }
36
+ return result;
37
+ }
38
+
39
+ // 2. Simple string error
40
+ if (typeof errors === 'string') {
41
+ return { _form: [errors] };
42
+ }
43
+
44
+ // 3. Array of strings or objects
45
+ if (Array.isArray(errors)) {
46
+ return {
47
+ _form: errors.map((e) => (typeof e === 'string' ? e : e.message || String(e))),
48
+ };
49
+ }
50
+
51
+ // 4. Plain object `{ field: string | string[] }`
52
+ if (typeof errors === 'object') {
53
+ const result = {};
54
+ for (const [key, val] of Object.entries(errors)) {
55
+ if (Array.isArray(val)) {
56
+ result[key] = val.map((v) => (typeof v === 'string' ? v : v.message || String(v)));
57
+ } else if (typeof val === 'string') {
58
+ result[key] = [val];
59
+ } else if (val && typeof val === 'object' && val.message) {
60
+ result[key] = [String(val.message)];
61
+ } else if (val !== null && val !== undefined) {
62
+ result[key] = [String(val)];
63
+ }
64
+ }
65
+ return result;
66
+ }
67
+
68
+ return { _form: [String(errors)] };
69
+ }
70
+
71
+ /**
72
+ * Encode flash data for cookie transport (base64url JSON)
73
+ * @param {Object} data
74
+ * @returns {string}
75
+ */
76
+ function encodeFlashCookie(data) {
77
+ try {
78
+ return Buffer.from(JSON.stringify(data), 'utf8').toString('base64url');
79
+ } catch {
80
+ return '';
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Decode flash data from cookie
86
+ * @param {string} raw
87
+ * @returns {Object|null}
88
+ */
89
+ function decodeFlashCookie(raw) {
90
+ if (!raw || typeof raw !== 'string') return null;
91
+ try {
92
+ const json = Buffer.from(raw, 'base64url').toString('utf8');
93
+ return JSON.parse(json);
94
+ } catch {
95
+ return null;
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Flash and Old Input Express Middleware
101
+ * @param {Object} [options={}]
102
+ * @returns {import('express').RequestHandler}
103
+ */
104
+ function createFlashMiddleware(options = {}) {
105
+ const cookieName = options.cookieName || FLASH_COOKIE_NAME;
106
+
107
+ return function flashMiddleware(req, res, next) {
108
+ // 1. Load incoming flash messages from session or cookie
109
+ let incomingFlash = {};
110
+
111
+ if (req.session && req.session[FLASH_SESSION_KEY]) {
112
+ incomingFlash = req.session[FLASH_SESSION_KEY] || {};
113
+ // Delete from session for next request (consume)
114
+ delete req.session[FLASH_SESSION_KEY];
115
+ } else if (req.cookies && req.cookies[cookieName]) {
116
+ incomingFlash = decodeFlashCookie(req.cookies[cookieName]) || {};
117
+ // Clear cookie immediately
118
+ res.clearCookie(cookieName, { path: '/' });
119
+ }
120
+
121
+ // Outgoing flash state to be persisted for the NEXT request
122
+ const outgoingFlash = {};
123
+
124
+ /**
125
+ * Set or get a flash message
126
+ * @param {string} [key]
127
+ * @param {any} [value]
128
+ * @returns {any}
129
+ */
130
+ req.flash = function (key, value) {
131
+ if (arguments.length === 0) {
132
+ return incomingFlash;
133
+ }
134
+ if (arguments.length === 1) {
135
+ return incomingFlash[key];
136
+ }
137
+ outgoingFlash[key] = value;
138
+ saveOutgoingFlash();
139
+ return value;
140
+ };
141
+
142
+ /**
143
+ * Get old input value from previous request
144
+ * @param {string} [name]
145
+ * @param {any} [defaultValue=null]
146
+ * @returns {any}
147
+ */
148
+ req.old = function (name, defaultValue = null) {
149
+ const oldInputs = incomingFlash.old || {};
150
+ if (!name) return oldInputs;
151
+ return oldInputs[name] !== undefined ? oldInputs[name] : defaultValue;
152
+ };
153
+
154
+ /**
155
+ * Flash current request body / input for the next request
156
+ * @param {Object} [customInput]
157
+ * @returns {res}
158
+ */
159
+ res.withInput = function (customInput) {
160
+ const input = customInput || req.body || {};
161
+ // Exclude sensitive fields by default
162
+ const sanitized = { ...input };
163
+ delete sanitized.password;
164
+ delete sanitized.password_confirmation;
165
+ delete sanitized.confirmPassword;
166
+ delete sanitized._csrf;
167
+
168
+ outgoingFlash.old = sanitized;
169
+ saveOutgoingFlash();
170
+ return res;
171
+ };
172
+
173
+ /**
174
+ * Flash validation errors for the next request
175
+ * @param {any} errors - ZodError, object, array, or string
176
+ * @returns {res}
177
+ */
178
+ res.withErrors = function (errors) {
179
+ outgoingFlash.errors = normalizeErrors(errors);
180
+ saveOutgoingFlash();
181
+ return res;
182
+ };
183
+
184
+ /**
185
+ * Flash a key-value message for the next request
186
+ * @param {string} key - e.g. 'success', 'warning', 'info'
187
+ * @param {any} message
188
+ * @returns {res}
189
+ */
190
+ res.withFlash = function (key, message) {
191
+ outgoingFlash[key] = message;
192
+ saveOutgoingFlash();
193
+ return res;
194
+ };
195
+
196
+ function saveOutgoingFlash() {
197
+ if (req.session) {
198
+ req.session[FLASH_SESSION_KEY] = outgoingFlash;
199
+ } else {
200
+ // Remove any previously set cookie with the same name from response headers
201
+ const existing = res.getHeader('Set-Cookie');
202
+ if (existing) {
203
+ const arr = Array.isArray(existing) ? existing : [existing];
204
+ const filtered = arr.filter((c) => !c.startsWith(`${cookieName}=`));
205
+ if (filtered.length > 0) {
206
+ res.setHeader('Set-Cookie', filtered);
207
+ } else {
208
+ res.removeHeader('Set-Cookie');
209
+ }
210
+ }
211
+ const encoded = encodeFlashCookie(outgoingFlash);
212
+ res.cookie(cookieName, encoded, {
213
+ path: '/',
214
+ httpOnly: true,
215
+ sameSite: 'lax',
216
+ maxAge: 300000, // 5 minutes
217
+ });
218
+ }
219
+ }
220
+
221
+ // Attach to request context for Nunjucks helpers
222
+ req.__flash = incomingFlash;
223
+ req.__outgoingFlash = outgoingFlash;
224
+
225
+ next();
226
+ };
227
+ }
228
+
229
+ module.exports = {
230
+ createFlashMiddleware,
231
+ normalizeErrors,
232
+ FLASH_SESSION_KEY,
233
+ FLASH_COOKIE_NAME,
234
+ };
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Webspresso - Form Builder & HTML Generator (Django ModelForm-inspired)
3
+ * Generates accessible HTML form inputs, field wrappers, and auto-forms from ORM models
4
+ * @module core/forms/form-builder
5
+ */
6
+
7
+ function escapeHtml(str) {
8
+ if (str === null || str === undefined) return '';
9
+ return String(str)
10
+ .replace(/&/g, '&amp;')
11
+ .replace(/</g, '&lt;')
12
+ .replace(/>/g, '&gt;')
13
+ .replace(/"/g, '&quot;')
14
+ .replace(/'/g, '&#039;');
15
+ }
16
+
17
+ /**
18
+ * Render a single form field with label, input, and validation error message
19
+ * @param {string} name - Field name
20
+ * @param {Object} [options={}] - Field configuration options
21
+ * @param {Object} [ctx={}] - Request / helpers context
22
+ * @returns {string} HTML string
23
+ */
24
+ function renderField(name, options = {}, ctx = {}) {
25
+ const {
26
+ label = humanize(name),
27
+ type = 'text',
28
+ placeholder = '',
29
+ required = false,
30
+ disabled = false,
31
+ readonly = false,
32
+ className = 'form-control',
33
+ wrapperClass = 'form-group mb-3',
34
+ errorClass = 'is-invalid',
35
+ showError = true,
36
+ rows = 4,
37
+ selectOptions = [],
38
+ helpText = '',
39
+ attributes = {},
40
+ } = options;
41
+
42
+ // Resolve old value or default
43
+ const req = ctx.req;
44
+ const oldVal = req && typeof req.old === 'function' ? req.old(name) : undefined;
45
+ const value = oldVal !== undefined ? oldVal : (options.value !== undefined ? options.value : (options.default ?? ''));
46
+
47
+ // Check error state
48
+ const errors = req?.__flash?.errors || {};
49
+ const fieldErrors = errors[name] || [];
50
+ const hasError = fieldErrors.length > 0;
51
+ const inputClass = `${className}${hasError ? ` ${errorClass}` : ''}`.trim();
52
+
53
+ const extraAttrs = Object.entries(attributes)
54
+ .map(([k, v]) => `${escapeHtml(k)}="${escapeHtml(v)}"`)
55
+ .join(' ');
56
+
57
+ let inputHtml = '';
58
+
59
+ if (type === 'textarea') {
60
+ inputHtml = `<textarea id="field_${escapeHtml(name)}" name="${escapeHtml(name)}" class="${escapeHtml(inputClass)}" placeholder="${escapeHtml(placeholder)}"${required ? ' required' : ''}${disabled ? ' disabled' : ''}${readonly ? ' readonly' : ''} rows="${rows}"${extraAttrs ? ' ' + extraAttrs : ''}>${escapeHtml(value)}</textarea>`;
61
+ } else if (type === 'select') {
62
+ const optsHtml = selectOptions
63
+ .map((opt) => {
64
+ const optVal = typeof opt === 'object' && opt !== null ? opt.value : opt;
65
+ const optLabel = typeof opt === 'object' && opt !== null ? opt.label : opt;
66
+ const isSelected = String(optVal) === String(value);
67
+ return `<option value="${escapeHtml(optVal)}"${isSelected ? ' selected' : ''}>${escapeHtml(optLabel)}</option>`;
68
+ })
69
+ .join('\n ');
70
+ inputHtml = `<select id="field_${escapeHtml(name)}" name="${escapeHtml(name)}" class="${escapeHtml(inputClass)}"${required ? ' required' : ''}${disabled ? ' disabled' : ''}${extraAttrs ? ' ' + extraAttrs : ''}>\n ${optsHtml}\n </select>`;
71
+ } else if (type === 'checkbox') {
72
+ const isChecked = Boolean(value) && value !== '0' && value !== 'false';
73
+ inputHtml = `<div class="form-check">
74
+ <input type="checkbox" id="field_${escapeHtml(name)}" name="${escapeHtml(name)}" value="true" class="form-check-input${hasError ? ` ${errorClass}` : ''}"${isChecked ? ' checked' : ''}${disabled ? ' disabled' : ''}${extraAttrs ? ' ' + extraAttrs : ''}>
75
+ <label class="form-check-label" for="field_${escapeHtml(name)}">${escapeHtml(label)}</label>
76
+ </div>`;
77
+ } else {
78
+ // Standard input (text, email, password, number, date, file, etc.)
79
+ inputHtml = `<input type="${escapeHtml(type)}" id="field_${escapeHtml(name)}" name="${escapeHtml(name)}" value="${escapeHtml(value)}" class="${escapeHtml(inputClass)}" placeholder="${escapeHtml(placeholder)}"${required ? ' required' : ''}${disabled ? ' disabled' : ''}${readonly ? ' readonly' : ''}${extraAttrs ? ' ' + extraAttrs : ''}>`;
80
+ }
81
+
82
+ const labelHtml =
83
+ type !== 'checkbox' && label
84
+ ? `<label for="field_${escapeHtml(name)}" class="form-label">${escapeHtml(label)}${required ? ' <span class="text-danger">*</span>' : ''}</label>`
85
+ : '';
86
+
87
+ const helpHtml = helpText ? `<div class="form-text text-muted">${escapeHtml(helpText)}</div>` : '';
88
+
89
+ const errorHtml =
90
+ showError && hasError
91
+ ? `<div class="invalid-feedback d-block text-danger mt-1">${escapeHtml(fieldErrors[0])}</div>`
92
+ : '';
93
+
94
+ return `<div class="${escapeHtml(wrapperClass)}">
95
+ ${labelHtml ? labelHtml + '\n ' : ''}${inputHtml}
96
+ ${helpHtml ? helpHtml + '\n ' : ''}${errorHtml}
97
+ </div>`.trim();
98
+ }
99
+
100
+ /**
101
+ * Generate a complete HTML form from an ORM model definition (Django ModelForm-inspired)
102
+ * @param {Object} modelOrRepo - ORM model or repository
103
+ * @param {Object} [options={}] - Form options (action, method, exclude, include, submitLabel)
104
+ * @param {Object} [ctx={}] - Request / helpers context
105
+ * @returns {string} HTML string
106
+ */
107
+ function renderFormFor(modelOrRepo, options = {}, ctx = {}) {
108
+ const model = modelOrRepo.model || modelOrRepo;
109
+ if (!model || !model.columns) {
110
+ throw new Error('renderFormFor requires an ORM model or repository with column definitions');
111
+ }
112
+
113
+ const {
114
+ action = '',
115
+ method = 'POST',
116
+ exclude = ['id', 'created_at', 'updated_at', 'deleted_at'],
117
+ include = null,
118
+ submitLabel = 'Save',
119
+ csrf = true,
120
+ formClass = 'webspresso-form',
121
+ } = options;
122
+
123
+ const excludeSet = new Set(exclude);
124
+ const includeSet = include ? new Set(include) : null;
125
+
126
+ const fieldsHtml = [];
127
+
128
+ for (const [colName, meta] of model.columns.entries()) {
129
+ if (excludeSet.has(colName)) continue;
130
+ if (includeSet && !includeSet.has(colName)) continue;
131
+
132
+ const fieldConfig = inferFieldConfig(colName, meta);
133
+ fieldsHtml.push(renderField(colName, fieldConfig, ctx));
134
+ }
135
+
136
+ let csrfHtml = '';
137
+ if (csrf && ctx.req) {
138
+ const token = ctx.req.csrfToken ? ctx.req.csrfToken() : ctx.req.session?._csrf || '';
139
+ if (token) {
140
+ csrfHtml = ` <input type="hidden" name="_csrf" value="${escapeHtml(token)}">\n`;
141
+ }
142
+ }
143
+
144
+ const submitHtml = submitLabel
145
+ ? ` <div class="form-actions mt-4">\n <button type="submit" class="btn btn-primary">${escapeHtml(submitLabel)}</button>\n </div>`
146
+ : '';
147
+
148
+ return `<form action="${escapeHtml(action)}" method="${escapeHtml(method)}" class="${escapeHtml(formClass)}">
149
+ ${csrfHtml} ${fieldsHtml.join('\n ')}
150
+ ${submitHtml}
151
+ </form>`;
152
+ }
153
+
154
+ function inferFieldConfig(name, meta) {
155
+ const config = {
156
+ label: humanize(name),
157
+ required: !meta.nullable && meta.default === undefined && !meta.auto,
158
+ };
159
+
160
+ switch (meta.type) {
161
+ case 'text':
162
+ config.type = 'textarea';
163
+ break;
164
+ case 'boolean':
165
+ config.type = 'checkbox';
166
+ break;
167
+ case 'integer':
168
+ case 'bigint':
169
+ case 'float':
170
+ case 'decimal':
171
+ config.type = 'number';
172
+ break;
173
+ case 'date':
174
+ config.type = 'date';
175
+ break;
176
+ case 'datetime':
177
+ case 'timestamp':
178
+ config.type = 'datetime-local';
179
+ break;
180
+ case 'file':
181
+ case 'files':
182
+ config.type = 'file';
183
+ break;
184
+ case 'enum':
185
+ config.type = 'select';
186
+ config.selectOptions = (meta.enumValues || []).map((v) => ({ label: humanize(v), value: v }));
187
+ break;
188
+ default:
189
+ if (name.includes('email')) {
190
+ config.type = 'email';
191
+ } else if (name.includes('password')) {
192
+ config.type = 'password';
193
+ } else {
194
+ config.type = 'text';
195
+ }
196
+ }
197
+
198
+ return config;
199
+ }
200
+
201
+ function humanize(str) {
202
+ if (!str) return '';
203
+ return str
204
+ .replace(/[_-]+/g, ' ')
205
+ .replace(/([a-z\d])([A-Z])/g, '$1 $2')
206
+ .replace(/^./, (m) => m.toUpperCase());
207
+ }
208
+
209
+ module.exports = {
210
+ renderField,
211
+ renderFormFor,
212
+ humanize,
213
+ escapeHtml,
214
+ };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Webspresso Forms & Flash Subsystem (AdonisJS & Django-inspired)
3
+ * @module core/forms
4
+ */
5
+
6
+ const { createFlashMiddleware, normalizeErrors } = require('./flash');
7
+ const { renderField, renderFormFor, humanize, escapeHtml } = require('./form-builder');
8
+
9
+ module.exports = {
10
+ createFlashMiddleware,
11
+ normalizeErrors,
12
+ renderField,
13
+ renderFormFor,
14
+ humanize,
15
+ escapeHtml,
16
+ };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Core Mail Module
3
+ * @module core/mail
4
+ */
5
+
6
+ const { Mailable } = require('./mailable');
7
+ const { Mailer, createMailer } = require('./mailer');
8
+ const {
9
+ MemoryTransport,
10
+ ConsoleTransport,
11
+ FileTransport,
12
+ resolveTransport,
13
+ } = require('./transports');
14
+
15
+ module.exports = {
16
+ Mailable,
17
+ Mailer,
18
+ createMailer,
19
+ MemoryTransport,
20
+ ConsoleTransport,
21
+ FileTransport,
22
+ resolveTransport,
23
+ };