@sonicjs-cms/core 2.3.12 → 2.3.14

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 (54) hide show
  1. package/dist/{chunk-REY542YK.js → chunk-AVPUX57O.js} +3 -3
  2. package/dist/{chunk-REY542YK.js.map → chunk-AVPUX57O.js.map} +1 -1
  3. package/dist/{chunk-RIOIKM3Y.cjs → chunk-AZLU3ROK.cjs} +4 -2
  4. package/dist/chunk-AZLU3ROK.cjs.map +1 -0
  5. package/dist/{chunk-NTXPL746.js → chunk-CAJOP354.js} +34 -2
  6. package/dist/chunk-CAJOP354.js.map +1 -0
  7. package/dist/{chunk-HTJLBF6F.cjs → chunk-D4PJFFOV.cjs} +652 -475
  8. package/dist/chunk-D4PJFFOV.cjs.map +1 -0
  9. package/dist/{chunk-P6NMVNJJ.cjs → chunk-ETS5XSAG.cjs} +34 -2
  10. package/dist/chunk-ETS5XSAG.cjs.map +1 -0
  11. package/dist/{chunk-EIE35JCC.js → chunk-H34L445M.js} +3 -3
  12. package/dist/{chunk-EIE35JCC.js.map → chunk-H34L445M.js.map} +1 -1
  13. package/dist/{chunk-74RYBO6J.js → chunk-SKPETEM5.js} +10 -5
  14. package/dist/chunk-SKPETEM5.js.map +1 -0
  15. package/dist/{chunk-IB6UBZVD.cjs → chunk-SZE3XVET.cjs} +10 -5
  16. package/dist/chunk-SZE3XVET.cjs.map +1 -0
  17. package/dist/{chunk-HDSRB23N.js → chunk-T4XRPNX2.js} +507 -330
  18. package/dist/chunk-T4XRPNX2.js.map +1 -0
  19. package/dist/{chunk-KQCYQKSV.js → chunk-V5LBQN3I.js} +4 -2
  20. package/dist/chunk-V5LBQN3I.js.map +1 -0
  21. package/dist/{chunk-OJ5WUCSH.cjs → chunk-XWPGIFS7.cjs} +4 -4
  22. package/dist/{chunk-OJ5WUCSH.cjs.map → chunk-XWPGIFS7.cjs.map} +1 -1
  23. package/dist/{chunk-K6BFUYJH.cjs → chunk-YIXSSJWD.cjs} +5 -5
  24. package/dist/{chunk-K6BFUYJH.cjs.map → chunk-YIXSSJWD.cjs.map} +1 -1
  25. package/dist/index.cjs +1080 -87
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.js +1003 -10
  28. package/dist/index.js.map +1 -1
  29. package/dist/middleware.cjs +23 -23
  30. package/dist/middleware.js +2 -2
  31. package/dist/migrations-3A53GREK.cjs +13 -0
  32. package/dist/{migrations-DQ74P6V4.cjs.map → migrations-3A53GREK.cjs.map} +1 -1
  33. package/dist/migrations-WF6VIVU2.js +4 -0
  34. package/dist/{migrations-YAFC5JVO.js.map → migrations-WF6VIVU2.js.map} +1 -1
  35. package/dist/routes.cjs +25 -25
  36. package/dist/routes.js +5 -5
  37. package/dist/services.cjs +2 -2
  38. package/dist/services.js +1 -1
  39. package/dist/templates.cjs +17 -17
  40. package/dist/templates.js +2 -2
  41. package/dist/utils.cjs +11 -11
  42. package/dist/utils.js +1 -1
  43. package/migrations/025_add_easymde_plugin.sql +25 -0
  44. package/package.json +8 -3
  45. package/dist/chunk-74RYBO6J.js.map +0 -1
  46. package/dist/chunk-HDSRB23N.js.map +0 -1
  47. package/dist/chunk-HTJLBF6F.cjs.map +0 -1
  48. package/dist/chunk-IB6UBZVD.cjs.map +0 -1
  49. package/dist/chunk-KQCYQKSV.js.map +0 -1
  50. package/dist/chunk-NTXPL746.js.map +0 -1
  51. package/dist/chunk-P6NMVNJJ.cjs.map +0 -1
  52. package/dist/chunk-RIOIKM3Y.cjs.map +0 -1
  53. package/dist/migrations-DQ74P6V4.cjs +0 -13
  54. package/dist/migrations-YAFC5JVO.js +0 -4
package/dist/index.js CHANGED
@@ -1,23 +1,24 @@
1
- import { PluginBuilder, api_default, api_media_default, api_system_default, admin_api_default, router, adminCollectionsRoutes, adminSettingsRoutes, admin_content_default, adminMediaRoutes, adminPluginRoutes, adminLogsRoutes, userRoutes, auth_default, test_cleanup_default } from './chunk-HDSRB23N.js';
2
- export { ROUTES_INFO, admin_api_default as adminApiRoutes, adminCheckboxRoutes, admin_code_examples_default as adminCodeExamplesRoutes, adminCollectionsRoutes, admin_content_default as adminContentRoutes, router as adminDashboardRoutes, adminDesignRoutes, adminLogsRoutes, adminMediaRoutes, adminPluginRoutes, adminSettingsRoutes, admin_testimonials_default as adminTestimonialsRoutes, userRoutes as adminUsersRoutes, api_content_crud_default as apiContentCrudRoutes, api_media_default as apiMediaRoutes, api_default as apiRoutes, api_system_default as apiSystemRoutes, auth_default as authRoutes } from './chunk-HDSRB23N.js';
1
+ import { PluginBuilder, api_default, api_media_default, api_system_default, admin_api_default, router, adminCollectionsRoutes, adminSettingsRoutes, admin_content_default, adminMediaRoutes, adminPluginRoutes, adminLogsRoutes, userRoutes, auth_default, test_cleanup_default } from './chunk-T4XRPNX2.js';
2
+ export { ROUTES_INFO, admin_api_default as adminApiRoutes, adminCheckboxRoutes, admin_code_examples_default as adminCodeExamplesRoutes, adminCollectionsRoutes, admin_content_default as adminContentRoutes, router as adminDashboardRoutes, adminDesignRoutes, adminLogsRoutes, adminMediaRoutes, adminPluginRoutes, adminSettingsRoutes, admin_testimonials_default as adminTestimonialsRoutes, userRoutes as adminUsersRoutes, api_content_crud_default as apiContentCrudRoutes, api_media_default as apiMediaRoutes, api_default as apiRoutes, api_system_default as apiSystemRoutes, auth_default as authRoutes } from './chunk-T4XRPNX2.js';
3
3
  import { schema_exports } from './chunk-3YNNVSMC.js';
4
4
  export { Logger, apiTokens, collections, content, contentVersions, getLogger, initLogger, insertCollectionSchema, insertContentSchema, insertLogConfigSchema, insertMediaSchema, insertPluginActivityLogSchema, insertPluginAssetSchema, insertPluginHookSchema, insertPluginRouteSchema, insertPluginSchema, insertSystemLogSchema, insertUserSchema, insertWorkflowHistorySchema, logConfig, media, pluginActivityLog, pluginAssets, pluginHooks, pluginRoutes, plugins, selectCollectionSchema, selectContentSchema, selectLogConfigSchema, selectMediaSchema, selectPluginActivityLogSchema, selectPluginAssetSchema, selectPluginHookSchema, selectPluginRouteSchema, selectPluginSchema, selectSystemLogSchema, selectUserSchema, selectWorkflowHistorySchema, systemLogs, users, workflowHistory } from './chunk-3YNNVSMC.js';
5
- import { metricsMiddleware, bootstrapMiddleware, requireAuth } from './chunk-EIE35JCC.js';
6
- export { AuthManager, PermissionManager, bootstrapMiddleware, cacheHeaders, compressionMiddleware, detailedLoggingMiddleware, getActivePlugins, isPluginActive, logActivity, loggingMiddleware, optionalAuth, performanceLoggingMiddleware, requireActivePlugin, requireActivePlugins, requireAnyPermission, requireAuth, requirePermission, requireRole, securityHeaders, securityLoggingMiddleware } from './chunk-EIE35JCC.js';
5
+ import { AuthManager, metricsMiddleware, bootstrapMiddleware, requireAuth } from './chunk-H34L445M.js';
6
+ export { AuthManager, PermissionManager, bootstrapMiddleware, cacheHeaders, compressionMiddleware, detailedLoggingMiddleware, getActivePlugins, isPluginActive, logActivity, loggingMiddleware, optionalAuth, performanceLoggingMiddleware, requireActivePlugin, requireActivePlugins, requireAnyPermission, requireAuth, requirePermission, requireRole, securityHeaders, securityLoggingMiddleware } from './chunk-H34L445M.js';
7
7
  export { PluginBootstrapService, PluginService as PluginServiceClass, cleanupRemovedCollections, fullCollectionSync, getAvailableCollectionNames, getManagedCollections, isCollectionManaged, loadCollectionConfig, loadCollectionConfigs, registerCollections, syncCollection, syncCollections, validateCollectionConfig } from './chunk-SGAG6FD3.js';
8
- export { MigrationService } from './chunk-NTXPL746.js';
9
- export { renderFilterBar } from './chunk-REY542YK.js';
10
- import { init_admin_layout_catalyst_template, renderAdminLayout, renderAdminLayoutCatalyst } from './chunk-KQCYQKSV.js';
11
- export { getConfirmationDialogScript, renderAlert, renderConfirmationDialog, renderForm, renderFormField, renderPagination, renderTable } from './chunk-KQCYQKSV.js';
8
+ export { MigrationService } from './chunk-CAJOP354.js';
9
+ export { renderFilterBar } from './chunk-AVPUX57O.js';
10
+ import { init_admin_layout_catalyst_template, renderAdminLayout, adminLayoutV2, renderAdminLayoutCatalyst } from './chunk-V5LBQN3I.js';
11
+ export { getConfirmationDialogScript, renderAlert, renderConfirmationDialog, renderForm, renderFormField, renderPagination, renderTable } from './chunk-V5LBQN3I.js';
12
12
  export { HookSystemImpl, HookUtils, PluginManager as PluginManagerClass, PluginRegistryImpl, PluginValidator as PluginValidatorClass, ScopedHookSystem as ScopedHookSystemClass } from './chunk-CPXAVWCU.js';
13
- import { package_default, getCoreVersion } from './chunk-74RYBO6J.js';
14
- export { QueryFilterBuilder, SONICJS_VERSION, TemplateRenderer, buildQuery, escapeHtml, getCoreVersion, renderTemplate, sanitizeInput, sanitizeObject, templateRenderer } from './chunk-74RYBO6J.js';
13
+ import { package_default, getCoreVersion } from './chunk-SKPETEM5.js';
14
+ export { QueryFilterBuilder, SONICJS_VERSION, TemplateRenderer, buildQuery, escapeHtml, getCoreVersion, renderTemplate, sanitizeInput, sanitizeObject, templateRenderer } from './chunk-SKPETEM5.js';
15
15
  import './chunk-X7ZAEI5S.js';
16
16
  export { metricsTracker } from './chunk-FICTAGD4.js';
17
17
  export { HOOKS } from './chunk-LOUJRBXV.js';
18
18
  import './chunk-V4OQ3NZ2.js';
19
19
  import { Hono } from 'hono';
20
20
  import { html } from 'hono/html';
21
+ import { z } from 'zod';
21
22
  import { drizzle } from 'drizzle-orm/d1';
22
23
 
23
24
  // src/plugins/core-plugins/database-tools-plugin/services/database-service.ts
@@ -1113,6 +1114,987 @@ function createEmailPlugin() {
1113
1114
  }
1114
1115
  var emailPlugin = createEmailPlugin();
1115
1116
 
1117
+ // src/plugins/core-plugins/otp-login-plugin/otp-service.ts
1118
+ var OTPService = class {
1119
+ constructor(db) {
1120
+ this.db = db;
1121
+ }
1122
+ /**
1123
+ * Generate a secure random OTP code
1124
+ */
1125
+ generateCode(length = 6) {
1126
+ const digits = "0123456789";
1127
+ let code = "";
1128
+ for (let i = 0; i < length; i++) {
1129
+ const randomValues = new Uint8Array(1);
1130
+ crypto.getRandomValues(randomValues);
1131
+ const randomValue = randomValues[0] ?? 0;
1132
+ code += digits[randomValue % digits.length];
1133
+ }
1134
+ return code;
1135
+ }
1136
+ /**
1137
+ * Create and store a new OTP code
1138
+ */
1139
+ async createOTPCode(email, settings, ipAddress, userAgent) {
1140
+ const code = this.generateCode(settings.codeLength);
1141
+ const id = crypto.randomUUID();
1142
+ const now = Date.now();
1143
+ const expiresAt = now + settings.codeExpiryMinutes * 60 * 1e3;
1144
+ const otpCode = {
1145
+ id,
1146
+ user_email: email.toLowerCase(),
1147
+ code,
1148
+ expires_at: expiresAt,
1149
+ used: 0,
1150
+ used_at: null,
1151
+ ip_address: ipAddress || null,
1152
+ user_agent: userAgent || null,
1153
+ attempts: 0,
1154
+ created_at: now
1155
+ };
1156
+ await this.db.prepare(`
1157
+ INSERT INTO otp_codes (
1158
+ id, user_email, code, expires_at, used, used_at,
1159
+ ip_address, user_agent, attempts, created_at
1160
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1161
+ `).bind(
1162
+ otpCode.id,
1163
+ otpCode.user_email,
1164
+ otpCode.code,
1165
+ otpCode.expires_at,
1166
+ otpCode.used,
1167
+ otpCode.used_at,
1168
+ otpCode.ip_address,
1169
+ otpCode.user_agent,
1170
+ otpCode.attempts,
1171
+ otpCode.created_at
1172
+ ).run();
1173
+ return otpCode;
1174
+ }
1175
+ /**
1176
+ * Verify an OTP code
1177
+ */
1178
+ async verifyCode(email, code, settings) {
1179
+ const normalizedEmail = email.toLowerCase();
1180
+ const now = Date.now();
1181
+ const otpCode = await this.db.prepare(`
1182
+ SELECT * FROM otp_codes
1183
+ WHERE user_email = ? AND code = ? AND used = 0
1184
+ ORDER BY created_at DESC
1185
+ LIMIT 1
1186
+ `).bind(normalizedEmail, code).first();
1187
+ if (!otpCode) {
1188
+ return { valid: false, error: "Invalid or expired code" };
1189
+ }
1190
+ if (now > otpCode.expires_at) {
1191
+ return { valid: false, error: "Code has expired" };
1192
+ }
1193
+ if (otpCode.attempts >= settings.maxAttempts) {
1194
+ return { valid: false, error: "Maximum attempts exceeded" };
1195
+ }
1196
+ await this.db.prepare(`
1197
+ UPDATE otp_codes
1198
+ SET used = 1, used_at = ?, attempts = attempts + 1
1199
+ WHERE id = ?
1200
+ `).bind(now, otpCode.id).run();
1201
+ return { valid: true };
1202
+ }
1203
+ /**
1204
+ * Increment failed attempt count
1205
+ */
1206
+ async incrementAttempts(email, code) {
1207
+ const normalizedEmail = email.toLowerCase();
1208
+ const result = await this.db.prepare(`
1209
+ UPDATE otp_codes
1210
+ SET attempts = attempts + 1
1211
+ WHERE user_email = ? AND code = ? AND used = 0
1212
+ RETURNING attempts
1213
+ `).bind(normalizedEmail, code).first();
1214
+ return result?.attempts || 0;
1215
+ }
1216
+ /**
1217
+ * Check rate limiting
1218
+ */
1219
+ async checkRateLimit(email, settings) {
1220
+ const normalizedEmail = email.toLowerCase();
1221
+ const oneHourAgo = Date.now() - 60 * 60 * 1e3;
1222
+ const result = await this.db.prepare(`
1223
+ SELECT COUNT(*) as count
1224
+ FROM otp_codes
1225
+ WHERE user_email = ? AND created_at > ?
1226
+ `).bind(normalizedEmail, oneHourAgo).first();
1227
+ const count = result?.count || 0;
1228
+ return count < settings.rateLimitPerHour;
1229
+ }
1230
+ /**
1231
+ * Get recent OTP requests for activity log
1232
+ */
1233
+ async getRecentRequests(limit = 50) {
1234
+ const result = await this.db.prepare(`
1235
+ SELECT * FROM otp_codes
1236
+ ORDER BY created_at DESC
1237
+ LIMIT ?
1238
+ `).bind(limit).all();
1239
+ const rows = result.results || [];
1240
+ return rows.map((row) => this.mapRowToOTP(row));
1241
+ }
1242
+ /**
1243
+ * Clean up expired codes (for maintenance)
1244
+ */
1245
+ async cleanupExpiredCodes() {
1246
+ const now = Date.now();
1247
+ const result = await this.db.prepare(`
1248
+ DELETE FROM otp_codes
1249
+ WHERE expires_at < ? OR (used = 1 AND used_at < ?)
1250
+ `).bind(now, now - 30 * 24 * 60 * 60 * 1e3).run();
1251
+ return result.meta.changes || 0;
1252
+ }
1253
+ mapRowToOTP(row) {
1254
+ return {
1255
+ id: String(row.id),
1256
+ user_email: String(row.user_email),
1257
+ code: String(row.code),
1258
+ expires_at: Number(row.expires_at ?? Date.now()),
1259
+ used: Number(row.used ?? 0),
1260
+ used_at: row.used_at === null || row.used_at === void 0 ? null : Number(row.used_at),
1261
+ ip_address: typeof row.ip_address === "string" ? row.ip_address : null,
1262
+ user_agent: typeof row.user_agent === "string" ? row.user_agent : null,
1263
+ attempts: Number(row.attempts ?? 0),
1264
+ created_at: Number(row.created_at ?? Date.now())
1265
+ };
1266
+ }
1267
+ /**
1268
+ * Get OTP statistics
1269
+ */
1270
+ async getStats(days = 7) {
1271
+ const since = Date.now() - days * 24 * 60 * 60 * 1e3;
1272
+ const stats = await this.db.prepare(`
1273
+ SELECT
1274
+ COUNT(*) as total,
1275
+ SUM(CASE WHEN used = 1 THEN 1 ELSE 0 END) as successful,
1276
+ SUM(CASE WHEN attempts >= 3 AND used = 0 THEN 1 ELSE 0 END) as failed,
1277
+ SUM(CASE WHEN expires_at < ? AND used = 0 THEN 1 ELSE 0 END) as expired
1278
+ FROM otp_codes
1279
+ WHERE created_at > ?
1280
+ `).bind(Date.now(), since).first();
1281
+ return {
1282
+ total: stats?.total || 0,
1283
+ successful: stats?.successful || 0,
1284
+ failed: stats?.failed || 0,
1285
+ expired: stats?.expired || 0
1286
+ };
1287
+ }
1288
+ };
1289
+
1290
+ // src/plugins/core-plugins/otp-login-plugin/email-templates.ts
1291
+ function renderOTPEmailHTML(data) {
1292
+ return `<!DOCTYPE html>
1293
+ <html>
1294
+ <head>
1295
+ <meta charset="utf-8">
1296
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1297
+ <title>Your Login Code</title>
1298
+ </head>
1299
+ <body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; background-color: #f5f5f5;">
1300
+
1301
+ <div style="background: white; border-radius: 12px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
1302
+
1303
+ ${data.logoUrl ? `
1304
+ <div style="text-align: center; padding: 30px 20px 20px;">
1305
+ <img src="${data.logoUrl}" alt="Logo" style="max-width: 150px; height: auto;">
1306
+ </div>
1307
+ ` : ""}
1308
+
1309
+ <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 40px 30px; text-align: center;">
1310
+ <h1 style="margin: 0 0 10px 0; font-size: 32px; font-weight: 600;">Your Login Code</h1>
1311
+ <p style="margin: 0; opacity: 0.95; font-size: 16px;">Enter this code to sign in to ${data.appName}</p>
1312
+ </div>
1313
+
1314
+ <div style="padding: 40px 30px;">
1315
+ <div style="background: #f8f9fa; border: 2px dashed #667eea; border-radius: 12px; padding: 30px; text-align: center; margin: 0 0 30px 0;">
1316
+ <div style="font-size: 56px; font-weight: bold; letter-spacing: 12px; color: #667eea; font-family: 'Courier New', Courier, monospace; line-height: 1;">
1317
+ ${data.code}
1318
+ </div>
1319
+ </div>
1320
+
1321
+ <div style="background: #fff3cd; border-left: 4px solid #ffc107; padding: 16px 20px; margin: 0 0 30px 0; border-radius: 6px;">
1322
+ <p style="margin: 0; font-size: 14px; color: #856404;">
1323
+ <strong>\u26A0\uFE0F This code expires in ${data.expiryMinutes} minutes</strong>
1324
+ </p>
1325
+ </div>
1326
+
1327
+ <div style="margin: 0 0 30px 0;">
1328
+ <h3 style="color: #333; margin: 0 0 15px 0; font-size: 18px;">Quick Tips:</h3>
1329
+ <ul style="color: #666; font-size: 14px; line-height: 1.8; margin: 0; padding-left: 20px;">
1330
+ <li>Enter the code exactly as shown (${data.codeLength} digits)</li>
1331
+ <li>The code can only be used once</li>
1332
+ <li>You have ${data.maxAttempts} attempts to enter the correct code</li>
1333
+ <li>Request a new code if this one expires</li>
1334
+ </ul>
1335
+ </div>
1336
+
1337
+ <div style="background: #e8f4ff; border-radius: 8px; padding: 20px; margin: 0 0 30px 0;">
1338
+ <p style="margin: 0 0 10px 0; font-size: 14px; color: #0066cc; font-weight: 600;">
1339
+ \u{1F512} Security Notice
1340
+ </p>
1341
+ <p style="margin: 0; font-size: 13px; color: #004080; line-height: 1.6;">
1342
+ Never share this code with anyone. ${data.appName} will never ask you for this code via phone, email, or social media.
1343
+ </p>
1344
+ </div>
1345
+ </div>
1346
+
1347
+ <div style="border-top: 1px solid #eee; padding: 30px; background: #f8f9fa;">
1348
+ <p style="margin: 0 0 15px 0; font-size: 14px; color: #666; text-align: center;">
1349
+ <strong>Didn't request this code?</strong><br>
1350
+ Someone may have entered your email by mistake. You can safely ignore this email.
1351
+ </p>
1352
+
1353
+ <div style="text-align: center; color: #999; font-size: 12px; line-height: 1.6;">
1354
+ <p style="margin: 5px 0;">This email was sent to ${data.email}</p>
1355
+ ${data.ipAddress ? `<p style="margin: 5px 0;">IP Address: ${data.ipAddress}</p>` : ""}
1356
+ <p style="margin: 5px 0;">Time: ${data.timestamp}</p>
1357
+ </div>
1358
+ </div>
1359
+
1360
+ </div>
1361
+
1362
+ <div style="text-align: center; padding: 20px; color: #999; font-size: 12px;">
1363
+ <p style="margin: 0;">&copy; ${(/* @__PURE__ */ new Date()).getFullYear()} ${data.appName}. All rights reserved.</p>
1364
+ </div>
1365
+
1366
+ </body>
1367
+ </html>`;
1368
+ }
1369
+ function renderOTPEmailText(data) {
1370
+ return `Your Login Code for ${data.appName}
1371
+
1372
+ Your one-time verification code is:
1373
+
1374
+ ${data.code}
1375
+
1376
+ This code expires in ${data.expiryMinutes} minutes.
1377
+
1378
+ Quick Tips:
1379
+ \u2022 Enter the code exactly as shown (${data.codeLength} digits)
1380
+ \u2022 The code can only be used once
1381
+ \u2022 You have ${data.maxAttempts} attempts to enter the correct code
1382
+ \u2022 Request a new code if this one expires
1383
+
1384
+ Security Notice:
1385
+ Never share this code with anyone. ${data.appName} will never ask you for this code via phone, email, or social media.
1386
+
1387
+ Didn't request this code?
1388
+ Someone may have entered your email by mistake. You can safely ignore this email.
1389
+
1390
+ ---
1391
+ This email was sent to ${data.email}
1392
+ ${data.ipAddress ? `IP Address: ${data.ipAddress}` : ""}
1393
+ Time: ${data.timestamp}
1394
+
1395
+ \xA9 ${(/* @__PURE__ */ new Date()).getFullYear()} ${data.appName}. All rights reserved.`;
1396
+ }
1397
+ function renderOTPEmail(data) {
1398
+ return {
1399
+ html: renderOTPEmailHTML(data),
1400
+ text: renderOTPEmailText(data)
1401
+ };
1402
+ }
1403
+
1404
+ // src/plugins/core-plugins/otp-login-plugin/index.ts
1405
+ var otpRequestSchema = z.object({
1406
+ email: z.string().email("Valid email is required")
1407
+ });
1408
+ var otpVerifySchema = z.object({
1409
+ email: z.string().email("Valid email is required"),
1410
+ code: z.string().min(4).max(8)
1411
+ });
1412
+ var DEFAULT_SETTINGS = {
1413
+ codeLength: 6,
1414
+ codeExpiryMinutes: 10,
1415
+ maxAttempts: 3,
1416
+ rateLimitPerHour: 5,
1417
+ allowNewUserRegistration: false,
1418
+ appName: "SonicJS"
1419
+ };
1420
+ function createOTPLoginPlugin() {
1421
+ const builder = PluginBuilder.create({
1422
+ name: "otp-login",
1423
+ version: "1.0.0-beta.1",
1424
+ description: "Passwordless authentication via email one-time codes"
1425
+ });
1426
+ builder.metadata({
1427
+ author: {
1428
+ name: "SonicJS Team",
1429
+ email: "team@sonicjs.com"
1430
+ },
1431
+ license: "MIT",
1432
+ compatibility: "^2.0.0"
1433
+ });
1434
+ const otpAPI = new Hono();
1435
+ otpAPI.post("/request", async (c) => {
1436
+ try {
1437
+ const body = await c.req.json();
1438
+ const validation = otpRequestSchema.safeParse(body);
1439
+ if (!validation.success) {
1440
+ return c.json({
1441
+ error: "Validation failed",
1442
+ details: validation.error.issues
1443
+ }, 400);
1444
+ }
1445
+ const { email } = validation.data;
1446
+ const normalizedEmail = email.toLowerCase();
1447
+ const db = c.env.DB;
1448
+ const otpService = new OTPService(db);
1449
+ const settings = { ...DEFAULT_SETTINGS };
1450
+ const canRequest = await otpService.checkRateLimit(normalizedEmail, settings);
1451
+ if (!canRequest) {
1452
+ return c.json({
1453
+ error: "Too many requests. Please try again in an hour."
1454
+ }, 429);
1455
+ }
1456
+ const user = await db.prepare(`
1457
+ SELECT id, email, role, is_active
1458
+ FROM users
1459
+ WHERE email = ?
1460
+ `).bind(normalizedEmail).first();
1461
+ if (!user && !settings.allowNewUserRegistration) {
1462
+ return c.json({
1463
+ message: "If an account exists for this email, you will receive a verification code shortly.",
1464
+ expiresIn: settings.codeExpiryMinutes * 60
1465
+ });
1466
+ }
1467
+ if (user && !user.is_active) {
1468
+ return c.json({
1469
+ error: "This account has been deactivated."
1470
+ }, 403);
1471
+ }
1472
+ const ipAddress = c.req.header("cf-connecting-ip") || c.req.header("x-forwarded-for") || "unknown";
1473
+ const userAgent = c.req.header("user-agent") || "unknown";
1474
+ const otpCode = await otpService.createOTPCode(
1475
+ normalizedEmail,
1476
+ settings,
1477
+ ipAddress,
1478
+ userAgent
1479
+ );
1480
+ try {
1481
+ const isDevMode = c.env.ENVIRONMENT === "development";
1482
+ if (isDevMode) {
1483
+ console.log(`[DEV] OTP Code for ${normalizedEmail}: ${otpCode.code}`);
1484
+ }
1485
+ const emailContent = renderOTPEmail({
1486
+ code: otpCode.code,
1487
+ expiryMinutes: settings.codeExpiryMinutes,
1488
+ codeLength: settings.codeLength,
1489
+ maxAttempts: settings.maxAttempts,
1490
+ email: normalizedEmail,
1491
+ ipAddress,
1492
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1493
+ appName: settings.appName
1494
+ });
1495
+ const response = {
1496
+ message: "If an account exists for this email, you will receive a verification code shortly.",
1497
+ expiresIn: settings.codeExpiryMinutes * 60
1498
+ };
1499
+ if (isDevMode) {
1500
+ response.dev_code = otpCode.code;
1501
+ }
1502
+ return c.json(response);
1503
+ } catch (emailError) {
1504
+ console.error("Error sending OTP email:", emailError);
1505
+ return c.json({
1506
+ error: "Failed to send verification code. Please try again."
1507
+ }, 500);
1508
+ }
1509
+ } catch (error) {
1510
+ console.error("OTP request error:", error);
1511
+ return c.json({
1512
+ error: "An error occurred. Please try again."
1513
+ }, 500);
1514
+ }
1515
+ });
1516
+ otpAPI.post("/verify", async (c) => {
1517
+ try {
1518
+ const body = await c.req.json();
1519
+ const validation = otpVerifySchema.safeParse(body);
1520
+ if (!validation.success) {
1521
+ return c.json({
1522
+ error: "Validation failed",
1523
+ details: validation.error.issues
1524
+ }, 400);
1525
+ }
1526
+ const { email, code } = validation.data;
1527
+ const normalizedEmail = email.toLowerCase();
1528
+ const db = c.env.DB;
1529
+ const otpService = new OTPService(db);
1530
+ const settings = { ...DEFAULT_SETTINGS };
1531
+ const verification = await otpService.verifyCode(normalizedEmail, code, settings);
1532
+ if (!verification.valid) {
1533
+ await otpService.incrementAttempts(normalizedEmail, code);
1534
+ return c.json({
1535
+ error: verification.error || "Invalid code",
1536
+ attemptsRemaining: verification.attemptsRemaining
1537
+ }, 401);
1538
+ }
1539
+ const user = await db.prepare(`
1540
+ SELECT id, email, role, is_active
1541
+ FROM users
1542
+ WHERE email = ?
1543
+ `).bind(normalizedEmail).first();
1544
+ if (!user) {
1545
+ return c.json({
1546
+ error: "User not found"
1547
+ }, 404);
1548
+ }
1549
+ if (!user.is_active) {
1550
+ return c.json({
1551
+ error: "Account is deactivated"
1552
+ }, 403);
1553
+ }
1554
+ return c.json({
1555
+ success: true,
1556
+ user: {
1557
+ id: user.id,
1558
+ email: user.email,
1559
+ role: user.role
1560
+ },
1561
+ message: "Authentication successful"
1562
+ });
1563
+ } catch (error) {
1564
+ console.error("OTP verify error:", error);
1565
+ return c.json({
1566
+ error: "An error occurred. Please try again."
1567
+ }, 500);
1568
+ }
1569
+ });
1570
+ otpAPI.post("/resend", async (c) => {
1571
+ try {
1572
+ const body = await c.req.json();
1573
+ const validation = otpRequestSchema.safeParse(body);
1574
+ if (!validation.success) {
1575
+ return c.json({
1576
+ error: "Validation failed",
1577
+ details: validation.error.issues
1578
+ }, 400);
1579
+ }
1580
+ return otpAPI.fetch(
1581
+ new Request(c.req.url.replace("/resend", "/request"), {
1582
+ method: "POST",
1583
+ headers: c.req.raw.headers,
1584
+ body: JSON.stringify({ email: validation.data.email })
1585
+ }),
1586
+ c.env
1587
+ );
1588
+ } catch (error) {
1589
+ console.error("OTP resend error:", error);
1590
+ return c.json({
1591
+ error: "An error occurred. Please try again."
1592
+ }, 500);
1593
+ }
1594
+ });
1595
+ builder.addRoute("/auth/otp", otpAPI, {
1596
+ description: "OTP authentication endpoints",
1597
+ requiresAuth: false,
1598
+ priority: 100
1599
+ });
1600
+ const adminRoutes = new Hono();
1601
+ adminRoutes.get("/settings", async (c) => {
1602
+ const user = c.get("user");
1603
+ const contentHTML = await html`
1604
+ <div class="p-8">
1605
+ <div class="mb-8">
1606
+ <h1 class="text-3xl font-bold mb-2">OTP Login Settings</h1>
1607
+ <p class="text-zinc-600 dark:text-zinc-400">Configure passwordless authentication via email codes</p>
1608
+ </div>
1609
+
1610
+ <div class="max-w-3xl">
1611
+ <div class="backdrop-blur-md bg-black/20 border border-white/10 shadow-xl rounded-xl p-6 mb-6">
1612
+ <h2 class="text-xl font-semibold mb-4">Code Settings</h2>
1613
+
1614
+ <form id="otpSettingsForm" class="space-y-6">
1615
+ <div>
1616
+ <label for="codeLength" class="block text-sm font-medium mb-2">
1617
+ Code Length
1618
+ </label>
1619
+ <input
1620
+ type="number"
1621
+ id="codeLength"
1622
+ name="codeLength"
1623
+ min="4"
1624
+ max="8"
1625
+ value="6"
1626
+ class="w-full px-4 py-2 rounded-lg bg-white/5 border border-white/10 focus:border-blue-500 focus:outline-none"
1627
+ />
1628
+ <p class="text-xs text-zinc-500 mt-1">Number of digits in OTP code (4-8)</p>
1629
+ </div>
1630
+
1631
+ <div>
1632
+ <label for="codeExpiryMinutes" class="block text-sm font-medium mb-2">
1633
+ Code Expiry (minutes)
1634
+ </label>
1635
+ <input
1636
+ type="number"
1637
+ id="codeExpiryMinutes"
1638
+ name="codeExpiryMinutes"
1639
+ min="5"
1640
+ max="60"
1641
+ value="10"
1642
+ class="w-full px-4 py-2 rounded-lg bg-white/5 border border-white/10 focus:border-blue-500 focus:outline-none"
1643
+ />
1644
+ <p class="text-xs text-zinc-500 mt-1">How long codes remain valid (5-60 minutes)</p>
1645
+ </div>
1646
+
1647
+ <div>
1648
+ <label for="maxAttempts" class="block text-sm font-medium mb-2">
1649
+ Maximum Attempts
1650
+ </label>
1651
+ <input
1652
+ type="number"
1653
+ id="maxAttempts"
1654
+ name="maxAttempts"
1655
+ min="3"
1656
+ max="10"
1657
+ value="3"
1658
+ class="w-full px-4 py-2 rounded-lg bg-white/5 border border-white/10 focus:border-blue-500 focus:outline-none"
1659
+ />
1660
+ <p class="text-xs text-zinc-500 mt-1">Max verification attempts before invalidation</p>
1661
+ </div>
1662
+
1663
+ <div>
1664
+ <label for="rateLimitPerHour" class="block text-sm font-medium mb-2">
1665
+ Rate Limit (per hour)
1666
+ </label>
1667
+ <input
1668
+ type="number"
1669
+ id="rateLimitPerHour"
1670
+ name="rateLimitPerHour"
1671
+ min="3"
1672
+ max="20"
1673
+ value="5"
1674
+ class="w-full px-4 py-2 rounded-lg bg-white/5 border border-white/10 focus:border-blue-500 focus:outline-none"
1675
+ />
1676
+ <p class="text-xs text-zinc-500 mt-1">Max code requests per email per hour</p>
1677
+ </div>
1678
+
1679
+ <div class="flex items-center">
1680
+ <input
1681
+ type="checkbox"
1682
+ id="allowNewUserRegistration"
1683
+ name="allowNewUserRegistration"
1684
+ class="w-4 h-4 rounded border-white/10"
1685
+ />
1686
+ <label for="allowNewUserRegistration" class="ml-2 text-sm">
1687
+ Allow new user registration via OTP
1688
+ </label>
1689
+ </div>
1690
+
1691
+ <div class="flex gap-3 pt-4">
1692
+ <button
1693
+ type="submit"
1694
+ class="px-6 py-2 bg-gradient-to-r from-blue-500 to-purple-600 text-white rounded-lg font-medium hover:from-blue-600 hover:to-purple-700 transition-all"
1695
+ >
1696
+ Save Settings
1697
+ </button>
1698
+ <button
1699
+ type="button"
1700
+ id="testOTPBtn"
1701
+ class="px-6 py-2 bg-white/10 hover:bg-white/20 text-white rounded-lg font-medium transition-all"
1702
+ >
1703
+ Send Test Code
1704
+ </button>
1705
+ </div>
1706
+ </form>
1707
+ </div>
1708
+
1709
+ <div id="statusMessage" class="hidden backdrop-blur-md bg-black/20 border border-white/10 rounded-xl p-4 mb-6"></div>
1710
+
1711
+ <div class="backdrop-blur-md bg-blue-500/10 border border-blue-500/20 rounded-xl p-6">
1712
+ <h3 class="font-semibold text-blue-400 mb-3">
1713
+ 🔢 Features
1714
+ </h3>
1715
+ <ul class="text-sm text-blue-200 space-y-2">
1716
+ <li>✓ Passwordless authentication</li>
1717
+ <li>✓ Secure random code generation</li>
1718
+ <li>✓ Rate limiting protection</li>
1719
+ <li>✓ Brute force prevention</li>
1720
+ <li>✓ Mobile-friendly UX</li>
1721
+ </ul>
1722
+ </div>
1723
+ </div>
1724
+ </div>
1725
+
1726
+ <script>
1727
+ document.getElementById('otpSettingsForm').addEventListener('submit', async (e) => {
1728
+ e.preventDefault()
1729
+ const statusEl = document.getElementById('statusMessage')
1730
+ statusEl.className = 'backdrop-blur-md bg-green-500/20 border border-green-500/30 rounded-xl p-4 mb-6'
1731
+ statusEl.innerHTML = '✅ Settings saved successfully!'
1732
+ statusEl.classList.remove('hidden')
1733
+ setTimeout(() => statusEl.classList.add('hidden'), 3000)
1734
+ })
1735
+
1736
+ document.getElementById('testOTPBtn').addEventListener('click', async () => {
1737
+ const email = prompt('Enter email address for test:')
1738
+ if (!email) return
1739
+
1740
+ const statusEl = document.getElementById('statusMessage')
1741
+ statusEl.className = 'backdrop-blur-md bg-blue-500/20 border border-blue-500/30 rounded-xl p-4 mb-6'
1742
+ statusEl.innerHTML = '📧 Sending test code...'
1743
+ statusEl.classList.remove('hidden')
1744
+
1745
+ try {
1746
+ const response = await fetch('/auth/otp/request', {
1747
+ method: 'POST',
1748
+ headers: { 'Content-Type': 'application/json' },
1749
+ body: JSON.stringify({ email })
1750
+ })
1751
+
1752
+ const data = await response.json()
1753
+
1754
+ if (response.ok) {
1755
+ statusEl.className = 'backdrop-blur-md bg-green-500/20 border border-green-500/30 rounded-xl p-4 mb-6'
1756
+ statusEl.innerHTML = '✅ Test code sent!' + (data.dev_code ? \` Code: <strong>\${data.dev_code}</strong>\` : '')
1757
+ } else {
1758
+ throw new Error(data.error || 'Failed')
1759
+ }
1760
+ } catch (error) {
1761
+ statusEl.className = 'backdrop-blur-md bg-red-500/20 border border-red-500/30 rounded-xl p-4 mb-6'
1762
+ statusEl.innerHTML = '❌ Failed to send test code'
1763
+ }
1764
+ })
1765
+ </script>
1766
+ `;
1767
+ const templateUser = user ? {
1768
+ name: user.name ?? user.email ?? "Admin",
1769
+ email: user.email ?? "admin@sonicjs.com",
1770
+ role: user.role ?? "admin"
1771
+ } : void 0;
1772
+ return c.html(
1773
+ adminLayoutV2({
1774
+ title: "OTP Login Settings",
1775
+ content: contentHTML,
1776
+ user: templateUser,
1777
+ currentPath: "/admin/plugins/otp-login/settings"
1778
+ })
1779
+ );
1780
+ });
1781
+ builder.addRoute("/admin/plugins/otp-login", adminRoutes, {
1782
+ description: "OTP login admin interface",
1783
+ requiresAuth: true,
1784
+ priority: 85
1785
+ });
1786
+ builder.addMenuItem("OTP Login", "/admin/plugins/otp-login/settings", {
1787
+ icon: "key",
1788
+ order: 85,
1789
+ permissions: ["otp:manage"]
1790
+ });
1791
+ builder.lifecycle({
1792
+ activate: async () => {
1793
+ console.info("\u2705 OTP Login plugin activated");
1794
+ },
1795
+ deactivate: async () => {
1796
+ console.info("\u274C OTP Login plugin deactivated");
1797
+ }
1798
+ });
1799
+ return builder.build();
1800
+ }
1801
+ var otpLoginPlugin = createOTPLoginPlugin();
1802
+ var magicLinkRequestSchema = z.object({
1803
+ email: z.string().email("Valid email is required")
1804
+ });
1805
+ function createMagicLinkAuthPlugin() {
1806
+ const magicLinkRoutes = new Hono();
1807
+ magicLinkRoutes.post("/request", async (c) => {
1808
+ try {
1809
+ const body = await c.req.json();
1810
+ const validation = magicLinkRequestSchema.safeParse(body);
1811
+ if (!validation.success) {
1812
+ return c.json({
1813
+ error: "Validation failed",
1814
+ details: validation.error.issues
1815
+ }, 400);
1816
+ }
1817
+ const { email } = validation.data;
1818
+ const normalizedEmail = email.toLowerCase();
1819
+ const db = c.env.DB;
1820
+ const oneHourAgo = Date.now() - 60 * 60 * 1e3;
1821
+ const recentLinks = await db.prepare(`
1822
+ SELECT COUNT(*) as count
1823
+ FROM magic_links
1824
+ WHERE user_email = ? AND created_at > ?
1825
+ `).bind(normalizedEmail, oneHourAgo).first();
1826
+ const rateLimitPerHour = 5;
1827
+ if (recentLinks && recentLinks.count >= rateLimitPerHour) {
1828
+ return c.json({
1829
+ error: "Too many requests. Please try again later."
1830
+ }, 429);
1831
+ }
1832
+ const user = await db.prepare(`
1833
+ SELECT id, email, role, is_active
1834
+ FROM users
1835
+ WHERE email = ?
1836
+ `).bind(normalizedEmail).first();
1837
+ const allowNewUsers = false;
1838
+ if (!user && !allowNewUsers) {
1839
+ return c.json({
1840
+ message: "If an account exists for this email, you will receive a magic link shortly."
1841
+ });
1842
+ }
1843
+ if (user && !user.is_active) {
1844
+ return c.json({
1845
+ error: "This account has been deactivated."
1846
+ }, 403);
1847
+ }
1848
+ const token = crypto.randomUUID() + "-" + crypto.randomUUID();
1849
+ const tokenId = crypto.randomUUID();
1850
+ const linkExpiryMinutes = 15;
1851
+ const expiresAt = Date.now() + linkExpiryMinutes * 60 * 1e3;
1852
+ await db.prepare(`
1853
+ INSERT INTO magic_links (
1854
+ id, user_email, token, expires_at, used, created_at, ip_address, user_agent
1855
+ ) VALUES (?, ?, ?, ?, 0, ?, ?, ?)
1856
+ `).bind(
1857
+ tokenId,
1858
+ normalizedEmail,
1859
+ token,
1860
+ expiresAt,
1861
+ Date.now(),
1862
+ c.req.header("cf-connecting-ip") || c.req.header("x-forwarded-for") || "unknown",
1863
+ c.req.header("user-agent") || "unknown"
1864
+ ).run();
1865
+ const baseUrl = new URL(c.req.url).origin;
1866
+ const magicLink = `${baseUrl}/auth/magic-link/verify?token=${token}`;
1867
+ try {
1868
+ const emailPlugin2 = c.env.plugins?.get("email");
1869
+ if (emailPlugin2 && emailPlugin2.sendEmail) {
1870
+ await emailPlugin2.sendEmail({
1871
+ to: normalizedEmail,
1872
+ subject: "Your Magic Link to Sign In",
1873
+ html: renderMagicLinkEmail(magicLink, linkExpiryMinutes)
1874
+ });
1875
+ } else {
1876
+ console.error("Email plugin not available");
1877
+ console.log(`Magic link for ${normalizedEmail}: ${magicLink}`);
1878
+ }
1879
+ } catch (error) {
1880
+ console.error("Failed to send magic link email:", error);
1881
+ return c.json({
1882
+ error: "Failed to send email. Please try again later."
1883
+ }, 500);
1884
+ }
1885
+ return c.json({
1886
+ message: "If an account exists for this email, you will receive a magic link shortly.",
1887
+ // For development only - remove in production
1888
+ ...c.env.ENVIRONMENT === "development" && { dev_link: magicLink }
1889
+ });
1890
+ } catch (error) {
1891
+ console.error("Magic link request error:", error);
1892
+ return c.json({ error: "Failed to process request" }, 500);
1893
+ }
1894
+ });
1895
+ magicLinkRoutes.get("/verify", async (c) => {
1896
+ try {
1897
+ const token = c.req.query("token");
1898
+ if (!token) {
1899
+ return c.redirect("/auth/login?error=Invalid magic link");
1900
+ }
1901
+ const db = c.env.DB;
1902
+ const magicLink = await db.prepare(`
1903
+ SELECT * FROM magic_links
1904
+ WHERE token = ? AND used = 0
1905
+ `).bind(token).first();
1906
+ if (!magicLink) {
1907
+ return c.redirect("/auth/login?error=Invalid or expired magic link");
1908
+ }
1909
+ if (magicLink.expires_at < Date.now()) {
1910
+ return c.redirect("/auth/login?error=This magic link has expired");
1911
+ }
1912
+ let user = await db.prepare(`
1913
+ SELECT * FROM users WHERE email = ? AND is_active = 1
1914
+ `).bind(magicLink.user_email).first();
1915
+ const allowNewUsers = false;
1916
+ if (!user && allowNewUsers) {
1917
+ const userId = crypto.randomUUID();
1918
+ const username = magicLink.user_email.split("@")[0];
1919
+ const now = Date.now();
1920
+ await db.prepare(`
1921
+ INSERT INTO users (
1922
+ id, email, username, first_name, last_name,
1923
+ password_hash, role, is_active, created_at, updated_at
1924
+ ) VALUES (?, ?, ?, ?, ?, NULL, 'viewer', 1, ?, ?)
1925
+ `).bind(
1926
+ userId,
1927
+ magicLink.user_email,
1928
+ username,
1929
+ username,
1930
+ "",
1931
+ now,
1932
+ now
1933
+ ).run();
1934
+ user = {
1935
+ id: userId,
1936
+ email: magicLink.user_email,
1937
+ username,
1938
+ role: "viewer"
1939
+ };
1940
+ } else if (!user) {
1941
+ return c.redirect("/auth/login?error=No account found for this email");
1942
+ }
1943
+ await db.prepare(`
1944
+ UPDATE magic_links
1945
+ SET used = 1, used_at = ?
1946
+ WHERE id = ?
1947
+ `).bind(Date.now(), magicLink.id).run();
1948
+ const jwtToken = await AuthManager.generateToken(
1949
+ user.id,
1950
+ user.email,
1951
+ user.role
1952
+ );
1953
+ AuthManager.setAuthCookie(c, jwtToken);
1954
+ await db.prepare(`
1955
+ UPDATE users SET last_login_at = ? WHERE id = ?
1956
+ `).bind(Date.now(), user.id).run();
1957
+ return c.redirect("/admin/dashboard?message=Successfully signed in");
1958
+ } catch (error) {
1959
+ console.error("Magic link verification error:", error);
1960
+ return c.redirect("/auth/login?error=Authentication failed");
1961
+ }
1962
+ });
1963
+ return {
1964
+ name: "magic-link-auth",
1965
+ version: "1.0.0",
1966
+ description: "Passwordless authentication via email magic links",
1967
+ author: {
1968
+ name: "SonicJS Team",
1969
+ email: "team@sonicjs.com"
1970
+ },
1971
+ dependencies: ["email"],
1972
+ routes: [{
1973
+ path: "/auth/magic-link",
1974
+ handler: magicLinkRoutes,
1975
+ description: "Magic link authentication endpoints",
1976
+ requiresAuth: false
1977
+ }],
1978
+ async install(context) {
1979
+ console.log("Installing magic-link-auth plugin...");
1980
+ },
1981
+ async activate(context) {
1982
+ console.log("Magic link authentication activated");
1983
+ console.log("Users can now sign in via /auth/magic-link/request");
1984
+ },
1985
+ async deactivate(context) {
1986
+ console.log("Magic link authentication deactivated");
1987
+ },
1988
+ async uninstall(context) {
1989
+ console.log("Uninstalling magic-link-auth plugin...");
1990
+ }
1991
+ };
1992
+ }
1993
+ function renderMagicLinkEmail(magicLink, expiryMinutes) {
1994
+ return `
1995
+ <!DOCTYPE html>
1996
+ <html>
1997
+ <head>
1998
+ <meta charset="utf-8">
1999
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
2000
+ <title>Your Magic Link</title>
2001
+ <style>
2002
+ body {
2003
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
2004
+ line-height: 1.6;
2005
+ color: #333;
2006
+ max-width: 600px;
2007
+ margin: 0 auto;
2008
+ padding: 20px;
2009
+ }
2010
+ .container {
2011
+ background: #ffffff;
2012
+ border-radius: 8px;
2013
+ padding: 40px;
2014
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
2015
+ }
2016
+ .header {
2017
+ text-align: center;
2018
+ margin-bottom: 30px;
2019
+ }
2020
+ .header h1 {
2021
+ color: #0ea5e9;
2022
+ margin: 0;
2023
+ font-size: 24px;
2024
+ }
2025
+ .content {
2026
+ margin-bottom: 30px;
2027
+ }
2028
+ .button {
2029
+ display: inline-block;
2030
+ padding: 14px 32px;
2031
+ background: linear-gradient(135deg, #0ea5e9 0%, #06b6d4 100%);
2032
+ color: #ffffff !important;
2033
+ text-decoration: none;
2034
+ border-radius: 6px;
2035
+ font-weight: 600;
2036
+ text-align: center;
2037
+ margin: 20px 0;
2038
+ }
2039
+ .button:hover {
2040
+ opacity: 0.9;
2041
+ }
2042
+ .expiry {
2043
+ color: #ef4444;
2044
+ font-size: 14px;
2045
+ margin-top: 20px;
2046
+ }
2047
+ .footer {
2048
+ margin-top: 40px;
2049
+ padding-top: 20px;
2050
+ border-top: 1px solid #e5e7eb;
2051
+ font-size: 12px;
2052
+ color: #6b7280;
2053
+ text-align: center;
2054
+ }
2055
+ .security-note {
2056
+ background: #fef3c7;
2057
+ border-left: 4px solid #f59e0b;
2058
+ padding: 12px 16px;
2059
+ margin-top: 20px;
2060
+ border-radius: 4px;
2061
+ font-size: 14px;
2062
+ }
2063
+ </style>
2064
+ </head>
2065
+ <body>
2066
+ <div class="container">
2067
+ <div class="header">
2068
+ <h1>\u{1F517} Your Magic Link</h1>
2069
+ </div>
2070
+
2071
+ <div class="content">
2072
+ <p>Hello!</p>
2073
+ <p>You requested a magic link to sign in to your account. Click the button below to continue:</p>
2074
+
2075
+ <div style="text-align: center;">
2076
+ <a href="${magicLink}" class="button">Sign In</a>
2077
+ </div>
2078
+
2079
+ <p class="expiry">\u23F0 This link expires in ${expiryMinutes} minutes</p>
2080
+
2081
+ <div class="security-note">
2082
+ <strong>Security Notice:</strong> If you didn't request this link, you can safely ignore this email.
2083
+ Someone may have entered your email address by mistake.
2084
+ </div>
2085
+ </div>
2086
+
2087
+ <div class="footer">
2088
+ <p>This is an automated email from SonicJS.</p>
2089
+ <p>For security, this link can only be used once.</p>
2090
+ </div>
2091
+ </div>
2092
+ </body>
2093
+ </html>
2094
+ `;
2095
+ }
2096
+ createMagicLinkAuthPlugin();
2097
+
1116
2098
  // src/app.ts
1117
2099
  function createSonicJSApp(config = {}) {
1118
2100
  const app = new Hono();
@@ -1160,6 +2142,17 @@ function createSonicJSApp(config = {}) {
1160
2142
  app.route(route.path, route.handler);
1161
2143
  }
1162
2144
  }
2145
+ if (otpLoginPlugin.routes && otpLoginPlugin.routes.length > 0) {
2146
+ for (const route of otpLoginPlugin.routes) {
2147
+ app.route(route.path, route.handler);
2148
+ }
2149
+ }
2150
+ const magicLinkPlugin = createMagicLinkAuthPlugin();
2151
+ if (magicLinkPlugin.routes && magicLinkPlugin.routes.length > 0) {
2152
+ for (const route of magicLinkPlugin.routes) {
2153
+ app.route(route.path, route.handler);
2154
+ }
2155
+ }
1163
2156
  app.get("/files/*", async (c) => {
1164
2157
  try {
1165
2158
  const url = new URL(c.req.url);