@momentumcms/auth 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.
package/index.cjs ADDED
@@ -0,0 +1,1227 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // libs/auth/src/index.ts
31
+ var src_exports = {};
32
+ __export(src_exports, {
33
+ AuthAccountCollection: () => AuthAccountCollection,
34
+ AuthApiKeysCollection: () => AuthApiKeysCollection,
35
+ AuthSessionCollection: () => AuthSessionCollection,
36
+ AuthUserCollection: () => AuthUserCollection,
37
+ AuthVerificationCollection: () => AuthVerificationCollection,
38
+ BASE_AUTH_COLLECTIONS: () => BASE_AUTH_COLLECTIONS,
39
+ authAdmin: () => authAdmin,
40
+ authOrganization: () => authOrganization,
41
+ authTwoFactor: () => authTwoFactor,
42
+ createEmailService: () => createEmailService,
43
+ createMomentumAuth: () => createMomentumAuth,
44
+ getEnabledOAuthProviders: () => getEnabledOAuthProviders,
45
+ getPasswordResetEmail: () => getPasswordResetEmail,
46
+ getVerificationEmail: () => getVerificationEmail,
47
+ momentumAuth: () => momentumAuth
48
+ });
49
+ module.exports = __toCommonJS(src_exports);
50
+
51
+ // libs/auth/src/lib/auth.ts
52
+ var import_better_auth = require("better-auth");
53
+ var import_plugins = require("better-auth/plugins");
54
+
55
+ // libs/auth/src/lib/email.ts
56
+ var nodemailer = __toESM(require("nodemailer"));
57
+ function getEnvConfig() {
58
+ const config = {};
59
+ if (process.env["SMTP_HOST"]) {
60
+ config.host = process.env["SMTP_HOST"];
61
+ }
62
+ if (process.env["SMTP_PORT"]) {
63
+ config.port = parseInt(process.env["SMTP_PORT"], 10);
64
+ }
65
+ if (process.env["SMTP_FROM"]) {
66
+ config.from = process.env["SMTP_FROM"];
67
+ }
68
+ if (process.env["SMTP_SECURE"]) {
69
+ config.secure = process.env["SMTP_SECURE"] === "true";
70
+ }
71
+ if (process.env["SMTP_USER"] && process.env["SMTP_PASS"]) {
72
+ config.auth = {
73
+ user: process.env["SMTP_USER"],
74
+ pass: process.env["SMTP_PASS"]
75
+ };
76
+ }
77
+ return config;
78
+ }
79
+ function createEmailService(config) {
80
+ const envConfig = getEnvConfig();
81
+ const finalConfig = {
82
+ host: config?.host ?? envConfig.host ?? "localhost",
83
+ port: config?.port ?? envConfig.port ?? 1025,
84
+ from: config?.from ?? envConfig.from ?? "noreply@momentum.local",
85
+ secure: config?.secure ?? envConfig.secure ?? false,
86
+ auth: config?.auth ?? envConfig.auth
87
+ };
88
+ const transportOptions = {
89
+ host: finalConfig.host,
90
+ port: finalConfig.port,
91
+ secure: finalConfig.secure
92
+ };
93
+ if (finalConfig.auth) {
94
+ transportOptions.auth = finalConfig.auth;
95
+ }
96
+ const transporter = nodemailer.createTransport(transportOptions);
97
+ return {
98
+ async sendEmail(options) {
99
+ await transporter.sendMail({
100
+ from: finalConfig.from,
101
+ to: options.to,
102
+ subject: options.subject,
103
+ text: options.text,
104
+ html: options.html
105
+ });
106
+ }
107
+ };
108
+ }
109
+
110
+ // libs/auth/src/lib/email-templates.ts
111
+ function escapeHtml(unsafe) {
112
+ return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
113
+ }
114
+ function wrapEmail(content, safeAppName) {
115
+ return `
116
+ <!DOCTYPE html>
117
+ <html lang="en">
118
+ <head>
119
+ <meta charset="UTF-8">
120
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
121
+ <title>${safeAppName}</title>
122
+ </head>
123
+ <body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f4f4f5; line-height: 1.6;">
124
+ <table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background-color: #f4f4f5;">
125
+ <tr>
126
+ <td style="padding: 40px 20px;">
127
+ <table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 480px; margin: 0 auto; background-color: #ffffff; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
128
+ <tr>
129
+ <td style="padding: 40px;">
130
+ ${content}
131
+ </td>
132
+ </tr>
133
+ </table>
134
+ <table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 480px; margin: 20px auto 0;">
135
+ <tr>
136
+ <td style="text-align: center; color: #71717a; font-size: 12px;">
137
+ <p style="margin: 0;">&copy; ${(/* @__PURE__ */ new Date()).getFullYear()} ${safeAppName}. All rights reserved.</p>
138
+ </td>
139
+ </tr>
140
+ </table>
141
+ </td>
142
+ </tr>
143
+ </table>
144
+ </body>
145
+ </html>
146
+ `.trim();
147
+ }
148
+ function getPasswordResetEmail(options) {
149
+ const { name, url, appName = "Momentum CMS", expiresIn = "1 hour" } = options;
150
+ const greeting = name ? `Hi ${name},` : "Hi,";
151
+ const subject = `Reset your password - ${appName}`;
152
+ const text2 = `
153
+ ${greeting}
154
+
155
+ We received a request to reset your password. Click the link below to choose a new password:
156
+
157
+ ${url}
158
+
159
+ This link will expire in ${expiresIn}.
160
+
161
+ If you didn't request a password reset, you can safely ignore this email. Your password will remain unchanged.
162
+
163
+ Thanks,
164
+ The ${appName} Team
165
+ `.trim();
166
+ const safeGreeting = name ? `Hi ${escapeHtml(name)},` : "Hi,";
167
+ const safeUrl = escapeHtml(url);
168
+ const safeAppName = escapeHtml(appName);
169
+ const safeExpiresIn = escapeHtml(expiresIn);
170
+ const html = wrapEmail(
171
+ `
172
+ <h1 style="margin: 0 0 24px; font-size: 24px; font-weight: 600; color: #18181b;">Reset your password</h1>
173
+ <p style="margin: 0 0 16px; color: #3f3f46;">${safeGreeting}</p>
174
+ <p style="margin: 0 0 24px; color: #3f3f46;">We received a request to reset your password. Click the button below to choose a new password:</p>
175
+ <table role="presentation" width="100%" cellspacing="0" cellpadding="0">
176
+ <tr>
177
+ <td style="padding: 0 0 24px;">
178
+ <a href="${safeUrl}" style="display: inline-block; padding: 12px 24px; background-color: #18181b; color: #ffffff; text-decoration: none; border-radius: 6px; font-weight: 500;">Reset Password</a>
179
+ </td>
180
+ </tr>
181
+ </table>
182
+ <p style="margin: 0 0 8px; color: #71717a; font-size: 14px;">This link will expire in ${safeExpiresIn}.</p>
183
+ <p style="margin: 0 0 24px; color: #71717a; font-size: 14px;">If you didn't request a password reset, you can safely ignore this email.</p>
184
+ <hr style="border: none; border-top: 1px solid #e4e4e7; margin: 24px 0;">
185
+ <p style="margin: 0; color: #71717a; font-size: 12px;">If the button doesn't work, copy and paste this URL into your browser:</p>
186
+ <p style="margin: 8px 0 0; color: #71717a; font-size: 12px; word-break: break-all;">${safeUrl}</p>
187
+ `,
188
+ safeAppName
189
+ );
190
+ return { subject, text: text2, html };
191
+ }
192
+ function getVerificationEmail(options) {
193
+ const { name, url, appName = "Momentum CMS", expiresIn = "24 hours" } = options;
194
+ const greeting = name ? `Hi ${name},` : "Hi,";
195
+ const subject = `Verify your email - ${appName}`;
196
+ const text2 = `
197
+ ${greeting}
198
+
199
+ Welcome to ${appName}! Please verify your email address by clicking the link below:
200
+
201
+ ${url}
202
+
203
+ This link will expire in ${expiresIn}.
204
+
205
+ If you didn't create an account, you can safely ignore this email.
206
+
207
+ Thanks,
208
+ The ${appName} Team
209
+ `.trim();
210
+ const safeGreeting = name ? `Hi ${escapeHtml(name)},` : "Hi,";
211
+ const safeUrl = escapeHtml(url);
212
+ const safeAppName = escapeHtml(appName);
213
+ const safeExpiresIn = escapeHtml(expiresIn);
214
+ const html = wrapEmail(
215
+ `
216
+ <h1 style="margin: 0 0 24px; font-size: 24px; font-weight: 600; color: #18181b;">Verify your email</h1>
217
+ <p style="margin: 0 0 16px; color: #3f3f46;">${safeGreeting}</p>
218
+ <p style="margin: 0 0 24px; color: #3f3f46;">Welcome to ${safeAppName}! Please verify your email address by clicking the button below:</p>
219
+ <table role="presentation" width="100%" cellspacing="0" cellpadding="0">
220
+ <tr>
221
+ <td style="padding: 0 0 24px;">
222
+ <a href="${safeUrl}" style="display: inline-block; padding: 12px 24px; background-color: #18181b; color: #ffffff; text-decoration: none; border-radius: 6px; font-weight: 500;">Verify Email</a>
223
+ </td>
224
+ </tr>
225
+ </table>
226
+ <p style="margin: 0 0 8px; color: #71717a; font-size: 14px;">This link will expire in ${safeExpiresIn}.</p>
227
+ <p style="margin: 0 0 24px; color: #71717a; font-size: 14px;">If you didn't create an account, you can safely ignore this email.</p>
228
+ <hr style="border: none; border-top: 1px solid #e4e4e7; margin: 24px 0;">
229
+ <p style="margin: 0; color: #71717a; font-size: 12px;">If the button doesn't work, copy and paste this URL into your browser:</p>
230
+ <p style="margin: 8px 0 0; color: #71717a; font-size: 12px; word-break: break-all;">${safeUrl}</p>
231
+ `,
232
+ safeAppName
233
+ );
234
+ return { subject, text: text2, html };
235
+ }
236
+
237
+ // libs/logger/src/lib/log-level.ts
238
+ var LOG_LEVEL_VALUES = {
239
+ debug: 0,
240
+ info: 1,
241
+ warn: 2,
242
+ error: 3,
243
+ fatal: 4,
244
+ silent: 5
245
+ };
246
+ function shouldLog(messageLevel, configuredLevel) {
247
+ return LOG_LEVEL_VALUES[messageLevel] >= LOG_LEVEL_VALUES[configuredLevel];
248
+ }
249
+
250
+ // libs/logger/src/lib/ansi-colors.ts
251
+ var ANSI = {
252
+ reset: "\x1B[0m",
253
+ bold: "\x1B[1m",
254
+ dim: "\x1B[2m",
255
+ // Foreground colors
256
+ red: "\x1B[31m",
257
+ green: "\x1B[32m",
258
+ yellow: "\x1B[33m",
259
+ blue: "\x1B[34m",
260
+ magenta: "\x1B[35m",
261
+ cyan: "\x1B[36m",
262
+ white: "\x1B[37m",
263
+ gray: "\x1B[90m",
264
+ // Background colors
265
+ bgRed: "\x1B[41m",
266
+ bgYellow: "\x1B[43m"
267
+ };
268
+ function colorize(text2, ...codes) {
269
+ if (codes.length === 0)
270
+ return text2;
271
+ return `${codes.join("")}${text2}${ANSI.reset}`;
272
+ }
273
+ function supportsColor() {
274
+ if (process.env["FORCE_COLOR"] === "1")
275
+ return true;
276
+ if (process.env["NO_COLOR"] !== void 0)
277
+ return false;
278
+ if (process.env["TERM"] === "dumb")
279
+ return false;
280
+ return process.stdout.isTTY === true;
281
+ }
282
+
283
+ // libs/logger/src/lib/formatters.ts
284
+ var LEVEL_COLORS = {
285
+ debug: [ANSI.dim, ANSI.gray],
286
+ info: [ANSI.cyan],
287
+ warn: [ANSI.yellow],
288
+ error: [ANSI.red],
289
+ fatal: [ANSI.bold, ANSI.white, ANSI.bgRed]
290
+ };
291
+ function padLevel(level) {
292
+ return level.toUpperCase().padEnd(5);
293
+ }
294
+ function formatTimestamp(date2) {
295
+ const y = date2.getFullYear();
296
+ const mo = String(date2.getMonth() + 1).padStart(2, "0");
297
+ const d = String(date2.getDate()).padStart(2, "0");
298
+ const h = String(date2.getHours()).padStart(2, "0");
299
+ const mi = String(date2.getMinutes()).padStart(2, "0");
300
+ const s = String(date2.getSeconds()).padStart(2, "0");
301
+ const ms = String(date2.getMilliseconds()).padStart(3, "0");
302
+ return `${y}-${mo}-${d} ${h}:${mi}:${s}.${ms}`;
303
+ }
304
+ function formatData(data) {
305
+ const entries = Object.entries(data);
306
+ if (entries.length === 0)
307
+ return "";
308
+ return " " + entries.map(([k, v]) => `${k}=${typeof v === "string" ? v : JSON.stringify(v)}`).join(" ");
309
+ }
310
+ function prettyFormatter(entry) {
311
+ const useColor = supportsColor();
312
+ const level = entry.level;
313
+ const ts = formatTimestamp(entry.timestamp);
314
+ const levelStr = padLevel(entry.level);
315
+ const ctx = `[${entry.context}]`;
316
+ const msg = entry.message;
317
+ const enrichmentStr = entry.enrichments ? formatData(entry.enrichments) : "";
318
+ const dataStr = entry.data ? formatData(entry.data) : "";
319
+ const extra = `${enrichmentStr}${dataStr}`;
320
+ if (useColor) {
321
+ const colors = LEVEL_COLORS[level];
322
+ const coloredLevel = colorize(levelStr, ...colors);
323
+ const coloredCtx = colorize(ctx, ANSI.magenta);
324
+ const coloredTs = colorize(ts, ANSI.gray);
325
+ return `${coloredTs} ${coloredLevel} ${coloredCtx} ${msg}${extra}
326
+ `;
327
+ }
328
+ return `${ts} ${levelStr} ${ctx} ${msg}${extra}
329
+ `;
330
+ }
331
+ function jsonFormatter(entry) {
332
+ const output = {
333
+ timestamp: entry.timestamp.toISOString(),
334
+ level: entry.level,
335
+ context: entry.context,
336
+ message: entry.message
337
+ };
338
+ if (entry.enrichments && Object.keys(entry.enrichments).length > 0) {
339
+ Object.assign(output, entry.enrichments);
340
+ }
341
+ if (entry.data && Object.keys(entry.data).length > 0) {
342
+ output["data"] = entry.data;
343
+ }
344
+ return JSON.stringify(output) + "\n";
345
+ }
346
+
347
+ // libs/logger/src/lib/logger-config.types.ts
348
+ function resolveLoggingConfig(config) {
349
+ return {
350
+ level: config?.level ?? "info",
351
+ format: config?.format ?? "pretty",
352
+ timestamps: config?.timestamps ?? true,
353
+ output: config?.output ?? ((msg) => {
354
+ process.stdout.write(msg);
355
+ }),
356
+ errorOutput: config?.errorOutput ?? ((msg) => {
357
+ process.stderr.write(msg);
358
+ })
359
+ };
360
+ }
361
+
362
+ // libs/logger/src/lib/logger.ts
363
+ var ERROR_LEVELS = /* @__PURE__ */ new Set(["warn", "error", "fatal"]);
364
+ var MomentumLogger = class _MomentumLogger {
365
+ static {
366
+ this.enrichers = [];
367
+ }
368
+ constructor(context, config) {
369
+ this.context = context;
370
+ this.config = isResolvedConfig(config) ? config : resolveLoggingConfig(config);
371
+ this.formatter = this.config.format === "json" ? jsonFormatter : prettyFormatter;
372
+ }
373
+ debug(message, data) {
374
+ this.log("debug", message, data);
375
+ }
376
+ info(message, data) {
377
+ this.log("info", message, data);
378
+ }
379
+ warn(message, data) {
380
+ this.log("warn", message, data);
381
+ }
382
+ error(message, data) {
383
+ this.log("error", message, data);
384
+ }
385
+ fatal(message, data) {
386
+ this.log("fatal", message, data);
387
+ }
388
+ /**
389
+ * Creates a child logger with a sub-context.
390
+ * e.g., `Momentum:DB` → `Momentum:DB:Migrate`
391
+ */
392
+ child(subContext) {
393
+ return new _MomentumLogger(`${this.context}:${subContext}`, this.config);
394
+ }
395
+ /**
396
+ * Registers a global enricher that adds extra fields to all log entries.
397
+ */
398
+ static registerEnricher(enricher) {
399
+ _MomentumLogger.enrichers.push(enricher);
400
+ }
401
+ /**
402
+ * Removes a previously registered enricher.
403
+ */
404
+ static removeEnricher(enricher) {
405
+ const index = _MomentumLogger.enrichers.indexOf(enricher);
406
+ if (index >= 0) {
407
+ _MomentumLogger.enrichers.splice(index, 1);
408
+ }
409
+ }
410
+ /**
411
+ * Clears all registered enrichers. Primarily for testing.
412
+ */
413
+ static clearEnrichers() {
414
+ _MomentumLogger.enrichers.length = 0;
415
+ }
416
+ log(level, message, data) {
417
+ if (!shouldLog(level, this.config.level))
418
+ return;
419
+ const enrichments = this.collectEnrichments();
420
+ const entry = {
421
+ timestamp: /* @__PURE__ */ new Date(),
422
+ level,
423
+ context: this.context,
424
+ message,
425
+ data,
426
+ enrichments: Object.keys(enrichments).length > 0 ? enrichments : void 0
427
+ };
428
+ const formatted = this.formatter(entry);
429
+ if (ERROR_LEVELS.has(level)) {
430
+ this.config.errorOutput(formatted);
431
+ } else {
432
+ this.config.output(formatted);
433
+ }
434
+ }
435
+ collectEnrichments() {
436
+ const result = {};
437
+ for (const enricher of _MomentumLogger.enrichers) {
438
+ Object.assign(result, enricher.enrich());
439
+ }
440
+ return result;
441
+ }
442
+ };
443
+ function isResolvedConfig(config) {
444
+ if (!config)
445
+ return false;
446
+ return typeof config.level === "string" && typeof config.format === "string" && typeof config.timestamps === "boolean" && // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- type guard narrows union
447
+ typeof config.output === "function" && // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- type guard narrows union
448
+ typeof config.errorOutput === "function";
449
+ }
450
+
451
+ // libs/logger/src/lib/logger-singleton.ts
452
+ var loggerInstance = null;
453
+ var ROOT_CONTEXT = "Momentum";
454
+ function getMomentumLogger() {
455
+ if (!loggerInstance) {
456
+ loggerInstance = new MomentumLogger(ROOT_CONTEXT);
457
+ }
458
+ return loggerInstance;
459
+ }
460
+ function createLogger(context) {
461
+ return getMomentumLogger().child(context);
462
+ }
463
+
464
+ // libs/auth/src/lib/auth.ts
465
+ function isLegacyConfig(config) {
466
+ return "database" in config && !("db" in config);
467
+ }
468
+ function buildSocialProviders(config, baseURL) {
469
+ const providers = {};
470
+ const resolvedBaseURL = baseURL ?? "http://localhost:4000";
471
+ const googleClientId = config?.google?.clientId ?? process.env["GOOGLE_CLIENT_ID"];
472
+ const googleClientSecret = config?.google?.clientSecret ?? process.env["GOOGLE_CLIENT_SECRET"];
473
+ if (googleClientId && googleClientSecret) {
474
+ providers["google"] = {
475
+ clientId: googleClientId,
476
+ clientSecret: googleClientSecret,
477
+ redirectURI: config?.google?.redirectURI ?? `${resolvedBaseURL}/api/auth/callback/google`
478
+ };
479
+ }
480
+ const githubClientId = config?.github?.clientId ?? process.env["GITHUB_CLIENT_ID"];
481
+ const githubClientSecret = config?.github?.clientSecret ?? process.env["GITHUB_CLIENT_SECRET"];
482
+ if (githubClientId && githubClientSecret) {
483
+ providers["github"] = {
484
+ clientId: githubClientId,
485
+ clientSecret: githubClientSecret,
486
+ redirectURI: config?.github?.redirectURI ?? `${resolvedBaseURL}/api/auth/callback/github`
487
+ };
488
+ }
489
+ return Object.keys(providers).length > 0 ? providers : void 0;
490
+ }
491
+ function getEnabledOAuthProviders(config) {
492
+ const providers = [];
493
+ const googleClientId = config?.google?.clientId ?? process.env["GOOGLE_CLIENT_ID"];
494
+ const googleClientSecret = config?.google?.clientSecret ?? process.env["GOOGLE_CLIENT_SECRET"];
495
+ if (googleClientId && googleClientSecret) {
496
+ providers.push("google");
497
+ }
498
+ const githubClientId = config?.github?.clientId ?? process.env["GITHUB_CLIENT_ID"];
499
+ const githubClientSecret = config?.github?.clientSecret ?? process.env["GITHUB_CLIENT_SECRET"];
500
+ if (githubClientId && githubClientSecret) {
501
+ providers.push("github");
502
+ }
503
+ return providers;
504
+ }
505
+ function convertFieldsToAdditionalFields(fields) {
506
+ const result = {};
507
+ for (const field of fields) {
508
+ let baType;
509
+ switch (field.type) {
510
+ case "checkbox":
511
+ baType = "boolean";
512
+ break;
513
+ case "number":
514
+ baType = "number";
515
+ break;
516
+ case "date":
517
+ baType = "string";
518
+ break;
519
+ default:
520
+ baType = "string";
521
+ break;
522
+ }
523
+ result[field.name] = {
524
+ type: baType,
525
+ required: field.required ?? false,
526
+ input: false
527
+ // Sub-plugin fields are not user-settable by default
528
+ };
529
+ }
530
+ return result;
531
+ }
532
+ function createMomentumAuth(config) {
533
+ const dbConfig = isLegacyConfig(config) ? { type: "sqlite", database: config.database } : config.db;
534
+ const {
535
+ baseURL,
536
+ secret,
537
+ trustedOrigins,
538
+ email: emailConfig,
539
+ socialProviders,
540
+ twoFactorAuth
541
+ } = config;
542
+ const extraPlugins = !isLegacyConfig(config) ? config.plugins ?? [] : [];
543
+ const extraUserFields = !isLegacyConfig(config) ? config.userFields ?? [] : [];
544
+ const databaseOption = dbConfig.type === "sqlite" ? dbConfig.database : dbConfig.pool;
545
+ const emailEnabled = emailConfig?.enabled ?? !!process.env["SMTP_HOST"];
546
+ const appName = emailConfig?.appName ?? "Momentum CMS";
547
+ let emailService = null;
548
+ if (emailEnabled) {
549
+ emailService = createEmailService(emailConfig);
550
+ }
551
+ const emailAndPasswordConfig = {
552
+ enabled: true,
553
+ minPasswordLength: 8
554
+ };
555
+ if (emailService) {
556
+ emailAndPasswordConfig.sendResetPassword = async ({ user, url }) => {
557
+ const { subject, text: text2, html } = getPasswordResetEmail({
558
+ name: user.name,
559
+ url,
560
+ appName,
561
+ expiresIn: "1 hour"
562
+ });
563
+ emailService.sendEmail({
564
+ to: user.email,
565
+ subject,
566
+ text: text2,
567
+ html
568
+ }).catch((err) => {
569
+ createLogger("Auth").error(
570
+ `Failed to send password reset email: ${err instanceof Error ? err.message : String(err)}`
571
+ );
572
+ });
573
+ };
574
+ }
575
+ const requireVerification = emailConfig?.requireEmailVerification ?? false;
576
+ const emailVerificationConfig = emailService ? {
577
+ sendOnSignUp: true,
578
+ autoSignInAfterVerification: true,
579
+ expiresIn: 86400,
580
+ // 24 hours
581
+ sendVerificationEmail: async ({
582
+ user,
583
+ url
584
+ }) => {
585
+ const { subject, text: text2, html } = getVerificationEmail({
586
+ name: user.name,
587
+ url,
588
+ appName,
589
+ expiresIn: "24 hours"
590
+ });
591
+ emailService.sendEmail({
592
+ to: user.email,
593
+ subject,
594
+ text: text2,
595
+ html
596
+ }).catch((err) => {
597
+ createLogger("Auth").error(
598
+ `Failed to send verification email: ${err instanceof Error ? err.message : String(err)}`
599
+ );
600
+ });
601
+ }
602
+ } : void 0;
603
+ if (requireVerification && emailAndPasswordConfig) {
604
+ emailAndPasswordConfig.requireEmailVerification = true;
605
+ }
606
+ const socialProvidersConfig = buildSocialProviders(socialProviders, baseURL);
607
+ const plugins = [];
608
+ if (twoFactorAuth) {
609
+ plugins.push((0, import_plugins.twoFactor)());
610
+ }
611
+ for (const p of extraPlugins) {
612
+ if (p !== void 0) {
613
+ plugins.push(p);
614
+ }
615
+ }
616
+ return (0, import_better_auth.betterAuth)({
617
+ database: databaseOption,
618
+ baseURL: baseURL ?? "http://localhost:4000",
619
+ secret: secret ?? process.env["AUTH_SECRET"] ?? "momentum-cms-dev-secret-change-in-production",
620
+ trustedOrigins: trustedOrigins ?? [baseURL ?? "http://localhost:4000"],
621
+ // Enable email/password authentication with optional password reset
622
+ emailAndPassword: emailAndPasswordConfig,
623
+ // Email verification (only if email is enabled)
624
+ ...emailVerificationConfig && { emailVerification: emailVerificationConfig },
625
+ // Social login providers (only if configured)
626
+ ...socialProvidersConfig && { socialProviders: socialProvidersConfig },
627
+ // Plugins (2FA, etc.)
628
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/consistent-type-assertions -- Better Auth plugin types are opaque from sub-plugins
629
+ ...plugins.length > 0 && { plugins },
630
+ // Add custom role field to users + any extra user fields from sub-plugins.
631
+ // Base role is spread AFTER sub-plugin fields so it cannot be overwritten
632
+ // (a sub-plugin field named 'role' would lose defaultValue and input protection).
633
+ user: {
634
+ additionalFields: {
635
+ // Convert Momentum Field definitions to Better Auth additionalFields format
636
+ ...convertFieldsToAdditionalFields(extraUserFields.filter((f) => f.name !== "role")),
637
+ role: {
638
+ type: "string",
639
+ required: false,
640
+ defaultValue: "user",
641
+ input: false
642
+ // Don't allow users to set their own role
643
+ }
644
+ }
645
+ },
646
+ // Session configuration
647
+ // Note: cookieCache is intentionally disabled. It caches session data
648
+ // (including role) in a signed cookie, which causes stale role issues
649
+ // when roles are updated after session creation (e.g., setup flow).
650
+ session: {
651
+ expiresIn: 60 * 60 * 24 * 7,
652
+ // 7 days
653
+ updateAge: 60 * 60 * 24
654
+ // Update session every 24 hours
655
+ }
656
+ });
657
+ }
658
+
659
+ // libs/core/src/lib/collections/define-collection.ts
660
+ function defineCollection(config) {
661
+ const collection = {
662
+ timestamps: true,
663
+ // Enable timestamps by default
664
+ ...config
665
+ };
666
+ if (!collection.slug) {
667
+ throw new Error("Collection must have a slug");
668
+ }
669
+ if (!collection.fields || collection.fields.length === 0) {
670
+ throw new Error(`Collection "${collection.slug}" must have at least one field`);
671
+ }
672
+ if (!/^[a-z][a-z0-9-]*$/.test(collection.slug)) {
673
+ throw new Error(
674
+ `Collection slug "${collection.slug}" must be kebab-case (lowercase letters, numbers, and hyphens, starting with a letter)`
675
+ );
676
+ }
677
+ return collection;
678
+ }
679
+
680
+ // libs/core/src/lib/fields/field-builders.ts
681
+ function text(name, options = {}) {
682
+ return {
683
+ name,
684
+ type: "text",
685
+ ...options
686
+ };
687
+ }
688
+ function number(name, options = {}) {
689
+ return {
690
+ name,
691
+ type: "number",
692
+ ...options
693
+ };
694
+ }
695
+ function date(name, options = {}) {
696
+ return {
697
+ name,
698
+ type: "date",
699
+ ...options
700
+ };
701
+ }
702
+ function checkbox(name, options = {}) {
703
+ return {
704
+ name,
705
+ type: "checkbox",
706
+ ...options,
707
+ defaultValue: options.defaultValue ?? false
708
+ };
709
+ }
710
+ function select(name, options) {
711
+ return {
712
+ name,
713
+ type: "select",
714
+ ...options
715
+ };
716
+ }
717
+ function email(name, options = {}) {
718
+ return {
719
+ name,
720
+ type: "email",
721
+ ...options
722
+ };
723
+ }
724
+ function relationship(name, options) {
725
+ return {
726
+ name,
727
+ type: "relationship",
728
+ ...options
729
+ };
730
+ }
731
+ function json(name, options = {}) {
732
+ return {
733
+ name,
734
+ type: "json",
735
+ ...options
736
+ };
737
+ }
738
+
739
+ // libs/core/src/lib/collections/media.collection.ts
740
+ var MediaCollection = defineCollection({
741
+ slug: "media",
742
+ labels: {
743
+ singular: "Media",
744
+ plural: "Media"
745
+ },
746
+ admin: {
747
+ useAsTitle: "filename",
748
+ defaultColumns: ["filename", "mimeType", "filesize", "createdAt"]
749
+ },
750
+ fields: [
751
+ text("filename", {
752
+ required: true,
753
+ label: "Filename",
754
+ description: "Original filename of the uploaded file"
755
+ }),
756
+ text("mimeType", {
757
+ required: true,
758
+ label: "MIME Type",
759
+ description: "File MIME type (e.g., image/jpeg, application/pdf)"
760
+ }),
761
+ number("filesize", {
762
+ label: "File Size",
763
+ description: "File size in bytes"
764
+ }),
765
+ text("path", {
766
+ required: true,
767
+ label: "Storage Path",
768
+ description: "Path/key where the file is stored",
769
+ admin: {
770
+ hidden: true
771
+ }
772
+ }),
773
+ text("url", {
774
+ label: "URL",
775
+ description: "Public URL to access the file"
776
+ }),
777
+ text("alt", {
778
+ label: "Alt Text",
779
+ description: "Alternative text for accessibility"
780
+ }),
781
+ number("width", {
782
+ label: "Width",
783
+ description: "Image width in pixels (for images only)"
784
+ }),
785
+ number("height", {
786
+ label: "Height",
787
+ description: "Image height in pixels (for images only)"
788
+ }),
789
+ json("focalPoint", {
790
+ label: "Focal Point",
791
+ description: "Focal point coordinates for image cropping",
792
+ admin: {
793
+ hidden: true
794
+ }
795
+ })
796
+ ],
797
+ access: {
798
+ // Media is readable by anyone by default
799
+ read: () => true,
800
+ // Only authenticated users can create/update/delete
801
+ create: ({ req }) => !!req?.user,
802
+ update: ({ req }) => !!req?.user,
803
+ delete: ({ req }) => !!req?.user
804
+ }
805
+ });
806
+
807
+ // libs/auth/src/lib/auth-collections.ts
808
+ var AUTH_ROLES = [
809
+ { label: "Admin", value: "admin" },
810
+ { label: "Editor", value: "editor" },
811
+ { label: "User", value: "user" },
812
+ { label: "Viewer", value: "viewer" }
813
+ ];
814
+ var AuthUserCollection = defineCollection({
815
+ slug: "auth-user",
816
+ dbName: "user",
817
+ timestamps: true,
818
+ labels: { singular: "User", plural: "Users" },
819
+ fields: [
820
+ text("name", { required: true }),
821
+ email("email", { required: true }),
822
+ checkbox("emailVerified"),
823
+ text("image"),
824
+ select("role", {
825
+ options: AUTH_ROLES,
826
+ defaultValue: "user"
827
+ })
828
+ ],
829
+ indexes: [{ columns: ["email"], unique: true }],
830
+ admin: {
831
+ group: "Authentication",
832
+ useAsTitle: "email",
833
+ defaultColumns: ["name", "email", "role", "createdAt"],
834
+ description: "Users authenticated via Better Auth"
835
+ },
836
+ access: {
837
+ admin: ({ req }) => req.user?.role === "admin",
838
+ read: ({ req }) => req.user?.role === "admin",
839
+ create: ({ req }) => req.user?.role === "admin",
840
+ update: ({ req }) => req.user?.role === "admin",
841
+ delete: ({ req }) => req.user?.role === "admin"
842
+ }
843
+ });
844
+ var AuthSessionCollection = defineCollection({
845
+ slug: "auth-session",
846
+ dbName: "session",
847
+ managed: true,
848
+ timestamps: true,
849
+ fields: [
850
+ text("userId", { required: true }),
851
+ text("token", { required: true }),
852
+ date("expiresAt", { required: true }),
853
+ text("ipAddress"),
854
+ text("userAgent")
855
+ ],
856
+ indexes: [{ columns: ["userId"] }, { columns: ["token"], unique: true }],
857
+ admin: {
858
+ group: "Authentication",
859
+ hidden: true,
860
+ description: "Active user sessions"
861
+ },
862
+ access: {
863
+ admin: ({ req }) => req.user?.role === "admin",
864
+ read: ({ req }) => req.user?.role === "admin",
865
+ create: () => false,
866
+ update: () => false,
867
+ delete: ({ req }) => req.user?.role === "admin"
868
+ }
869
+ });
870
+ var AuthAccountCollection = defineCollection({
871
+ slug: "auth-account",
872
+ dbName: "account",
873
+ managed: true,
874
+ timestamps: true,
875
+ fields: [
876
+ text("userId", { required: true }),
877
+ text("accountId", { required: true }),
878
+ text("providerId", { required: true }),
879
+ text("accessToken"),
880
+ text("refreshToken"),
881
+ date("accessTokenExpiresAt"),
882
+ date("refreshTokenExpiresAt"),
883
+ text("scope"),
884
+ text("idToken"),
885
+ text("password")
886
+ ],
887
+ indexes: [{ columns: ["userId"] }],
888
+ admin: {
889
+ group: "Authentication",
890
+ hidden: true,
891
+ description: "OAuth and credential accounts"
892
+ },
893
+ access: {
894
+ admin: ({ req }) => req.user?.role === "admin",
895
+ read: () => false,
896
+ // Never expose OAuth tokens/password hashes via API — Better Auth owns this data
897
+ create: () => false,
898
+ update: () => false,
899
+ delete: () => false
900
+ }
901
+ });
902
+ var AuthVerificationCollection = defineCollection({
903
+ slug: "auth-verification",
904
+ dbName: "verification",
905
+ managed: true,
906
+ timestamps: true,
907
+ fields: [
908
+ text("identifier", { required: true }),
909
+ text("value", { required: true }),
910
+ date("expiresAt", { required: true })
911
+ ],
912
+ admin: {
913
+ group: "Authentication",
914
+ hidden: true,
915
+ description: "Email verification and password reset tokens"
916
+ },
917
+ access: {
918
+ admin: ({ req }) => req.user?.role === "admin",
919
+ read: () => false,
920
+ create: () => false,
921
+ update: () => false,
922
+ delete: () => false
923
+ }
924
+ });
925
+ var AuthApiKeysCollection = defineCollection({
926
+ slug: "auth-api-keys",
927
+ dbName: "_api_keys",
928
+ timestamps: true,
929
+ fields: [
930
+ text("name", { required: true }),
931
+ text("keyHash", { required: true, admin: { hidden: true }, access: { read: () => false } }),
932
+ text("keyPrefix", { required: true }),
933
+ relationship("createdBy", {
934
+ required: true,
935
+ collection: () => AuthUserCollection,
936
+ label: "Created By"
937
+ }),
938
+ select("role", {
939
+ options: AUTH_ROLES,
940
+ defaultValue: "user"
941
+ }),
942
+ date("expiresAt"),
943
+ date("lastUsedAt")
944
+ ],
945
+ indexes: [{ columns: ["keyHash"], unique: true }, { columns: ["createdBy"] }],
946
+ admin: {
947
+ group: "Authentication",
948
+ useAsTitle: "name",
949
+ defaultColumns: ["name", "keyPrefix", "role", "createdBy", "createdAt", "lastUsedAt"],
950
+ description: "API keys for programmatic access",
951
+ headerActions: [
952
+ { id: "generate-key", label: "Generate API Key", endpoint: "/api/auth/api-keys" }
953
+ ]
954
+ },
955
+ access: {
956
+ admin: ({ req }) => !!req.user,
957
+ read: ({ req }) => !!req.user,
958
+ create: () => false,
959
+ // API keys must be created through dedicated /api/auth/api-keys endpoint
960
+ update: () => false,
961
+ delete: () => false
962
+ // Deletion only via dedicated /api/auth/api-keys/:id (has ownership checks)
963
+ },
964
+ defaultWhere: (req) => {
965
+ if (!req.user)
966
+ return { createdBy: "__none__" };
967
+ if (req.user.role === "admin")
968
+ return void 0;
969
+ return { createdBy: req.user.id };
970
+ }
971
+ });
972
+ var BASE_AUTH_COLLECTIONS = [
973
+ AuthUserCollection,
974
+ AuthSessionCollection,
975
+ AuthAccountCollection,
976
+ AuthVerificationCollection,
977
+ AuthApiKeysCollection
978
+ ];
979
+
980
+ // libs/auth/src/lib/auth-plugin.ts
981
+ function momentumAuth(config) {
982
+ let authInstance = null;
983
+ const subPlugins = config.plugins ?? [];
984
+ const allCollections = [];
985
+ const allUserFields = [...config.userFields ?? []];
986
+ const allSessionFields = [];
987
+ const allBetterAuthPlugins = [];
988
+ for (const sp of subPlugins) {
989
+ if (sp.collections)
990
+ allCollections.push(...sp.collections);
991
+ if (sp.userFields)
992
+ allUserFields.push(...sp.userFields);
993
+ if (sp.sessionFields)
994
+ allSessionFields.push(...sp.sessionFields);
995
+ if (sp.betterAuthPlugin !== void 0)
996
+ allBetterAuthPlugins.push(sp.betterAuthPlugin);
997
+ }
998
+ const authUserWithFields = {
999
+ ...AuthUserCollection,
1000
+ fields: [...AuthUserCollection.fields, ...allUserFields]
1001
+ };
1002
+ const authSessionWithFields = {
1003
+ ...AuthSessionCollection,
1004
+ fields: [...AuthSessionCollection.fields, ...allSessionFields]
1005
+ };
1006
+ const finalAuthCollections = [
1007
+ authUserWithFields,
1008
+ authSessionWithFields,
1009
+ // All base collections except user and session (which we replaced above)
1010
+ ...BASE_AUTH_COLLECTIONS.filter((c) => c.slug !== "auth-user" && c.slug !== "auth-session"),
1011
+ // Sub-plugin collections
1012
+ ...allCollections
1013
+ ];
1014
+ const showInAdmin = config.admin?.showCollections ?? true;
1015
+ if (!showInAdmin) {
1016
+ for (const c of finalAuthCollections) {
1017
+ c.admin = { ...c.admin, hidden: true };
1018
+ }
1019
+ }
1020
+ return {
1021
+ name: "momentum-auth",
1022
+ // Static collections for admin UI route data (read at config time)
1023
+ collections: finalAuthCollections,
1024
+ getAuth() {
1025
+ if (!authInstance) {
1026
+ throw new Error("Auth not initialized. Call onInit first (via initializeMomentum).");
1027
+ }
1028
+ return authInstance;
1029
+ },
1030
+ tryGetAuth() {
1031
+ return authInstance;
1032
+ },
1033
+ getPluginConfig() {
1034
+ return {
1035
+ db: config.db,
1036
+ socialProviders: config.socialProviders
1037
+ };
1038
+ },
1039
+ async onInit(context) {
1040
+ const { logger } = context;
1041
+ context.collections.push(...finalAuthCollections);
1042
+ logger.info(`Injected ${finalAuthCollections.length} auth collections`);
1043
+ authInstance = createMomentumAuth({
1044
+ db: config.db,
1045
+ baseURL: config.baseURL,
1046
+ secret: config.secret,
1047
+ trustedOrigins: config.trustedOrigins,
1048
+ email: config.email,
1049
+ socialProviders: config.socialProviders,
1050
+ plugins: allBetterAuthPlugins,
1051
+ userFields: allUserFields
1052
+ });
1053
+ logger.info("Better Auth instance created");
1054
+ }
1055
+ };
1056
+ }
1057
+
1058
+ // libs/auth/src/lib/plugins/two-factor.ts
1059
+ var import_plugins2 = require("better-auth/plugins");
1060
+ var AuthTwoFactorCollection = defineCollection({
1061
+ slug: "auth-two-factor",
1062
+ dbName: "twoFactor",
1063
+ managed: true,
1064
+ timestamps: false,
1065
+ fields: [
1066
+ text("secret", { required: true }),
1067
+ text("backupCodes", { required: true }),
1068
+ text("userId", { required: true })
1069
+ ],
1070
+ indexes: [{ columns: ["secret"] }, { columns: ["userId"] }],
1071
+ admin: {
1072
+ group: "Authentication",
1073
+ hidden: true,
1074
+ description: "Two-factor authentication secrets"
1075
+ },
1076
+ access: {
1077
+ read: () => false,
1078
+ create: () => false,
1079
+ update: () => false,
1080
+ delete: () => false
1081
+ }
1082
+ });
1083
+ function authTwoFactor() {
1084
+ return {
1085
+ name: "two-factor",
1086
+ betterAuthPlugin: (0, import_plugins2.twoFactor)(),
1087
+ collections: [AuthTwoFactorCollection],
1088
+ userFields: [checkbox("twoFactorEnabled")]
1089
+ };
1090
+ }
1091
+
1092
+ // libs/auth/src/lib/plugins/admin.ts
1093
+ function authAdmin() {
1094
+ return {
1095
+ name: "admin",
1096
+ // Stub: Better Auth admin plugin will be added here
1097
+ betterAuthPlugin: void 0,
1098
+ userFields: [checkbox("banned"), text("banReason"), date("banExpires")],
1099
+ sessionFields: [text("impersonatedBy")]
1100
+ };
1101
+ }
1102
+
1103
+ // libs/auth/src/lib/plugins/organization.ts
1104
+ var AuthOrganizationCollection = defineCollection({
1105
+ slug: "auth-organization",
1106
+ dbName: "organization",
1107
+ managed: true,
1108
+ timestamps: true,
1109
+ fields: [
1110
+ text("name", { required: true }),
1111
+ text("slug", { required: true }),
1112
+ text("logo"),
1113
+ text("metadata")
1114
+ ],
1115
+ indexes: [{ columns: ["slug"], unique: true }],
1116
+ admin: {
1117
+ group: "Authentication",
1118
+ useAsTitle: "name",
1119
+ description: "Organizations for multi-tenant access"
1120
+ },
1121
+ access: {
1122
+ read: ({ req }) => req.user?.role === "admin",
1123
+ create: ({ req }) => req.user?.role === "admin",
1124
+ update: ({ req }) => req.user?.role === "admin",
1125
+ delete: ({ req }) => req.user?.role === "admin"
1126
+ }
1127
+ });
1128
+ var AuthMemberCollection = defineCollection({
1129
+ slug: "auth-member",
1130
+ dbName: "member",
1131
+ managed: true,
1132
+ timestamps: true,
1133
+ fields: [
1134
+ text("userId", { required: true }),
1135
+ text("organizationId", { required: true }),
1136
+ select("role", {
1137
+ options: [
1138
+ { label: "Owner", value: "owner" },
1139
+ { label: "Admin", value: "admin" },
1140
+ { label: "Member", value: "member" }
1141
+ ],
1142
+ defaultValue: "member"
1143
+ })
1144
+ ],
1145
+ indexes: [
1146
+ { columns: ["userId"] },
1147
+ { columns: ["organizationId"] },
1148
+ { columns: ["userId", "organizationId"], unique: true }
1149
+ ],
1150
+ admin: {
1151
+ group: "Authentication",
1152
+ hidden: true,
1153
+ description: "Organization membership"
1154
+ },
1155
+ access: {
1156
+ read: ({ req }) => req.user?.role === "admin",
1157
+ create: () => false,
1158
+ update: () => false,
1159
+ delete: () => false
1160
+ }
1161
+ });
1162
+ var AuthInvitationCollection = defineCollection({
1163
+ slug: "auth-invitation",
1164
+ dbName: "invitation",
1165
+ managed: true,
1166
+ timestamps: true,
1167
+ fields: [
1168
+ text("email", { required: true }),
1169
+ text("organizationId", { required: true }),
1170
+ text("inviterId", { required: true }),
1171
+ select("role", {
1172
+ options: [
1173
+ { label: "Admin", value: "admin" },
1174
+ { label: "Member", value: "member" }
1175
+ ],
1176
+ defaultValue: "member"
1177
+ }),
1178
+ select("status", {
1179
+ options: [
1180
+ { label: "Pending", value: "pending" },
1181
+ { label: "Accepted", value: "accepted" },
1182
+ { label: "Rejected", value: "rejected" },
1183
+ { label: "Cancelled", value: "cancelled" }
1184
+ ],
1185
+ defaultValue: "pending"
1186
+ }),
1187
+ date("expiresAt", { required: true })
1188
+ ],
1189
+ indexes: [{ columns: ["organizationId"] }, { columns: ["email"] }],
1190
+ admin: {
1191
+ group: "Authentication",
1192
+ hidden: true,
1193
+ description: "Pending organization invitations"
1194
+ },
1195
+ access: {
1196
+ read: ({ req }) => req.user?.role === "admin",
1197
+ create: () => false,
1198
+ update: () => false,
1199
+ delete: () => false
1200
+ }
1201
+ });
1202
+ function authOrganization() {
1203
+ return {
1204
+ name: "organization",
1205
+ // Stub: Better Auth organization plugin will be added here
1206
+ betterAuthPlugin: void 0,
1207
+ collections: [AuthOrganizationCollection, AuthMemberCollection, AuthInvitationCollection]
1208
+ };
1209
+ }
1210
+ // Annotate the CommonJS export names for ESM import in node:
1211
+ 0 && (module.exports = {
1212
+ AuthAccountCollection,
1213
+ AuthApiKeysCollection,
1214
+ AuthSessionCollection,
1215
+ AuthUserCollection,
1216
+ AuthVerificationCollection,
1217
+ BASE_AUTH_COLLECTIONS,
1218
+ authAdmin,
1219
+ authOrganization,
1220
+ authTwoFactor,
1221
+ createEmailService,
1222
+ createMomentumAuth,
1223
+ getEnabledOAuthProviders,
1224
+ getPasswordResetEmail,
1225
+ getVerificationEmail,
1226
+ momentumAuth
1227
+ });