@sonicjs-cms/core 2.5.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/dist/{app-Db0AfT5F.d.cts → app-DV27cjPy.d.cts} +1 -1
  2. package/dist/{app-Db0AfT5F.d.ts → app-DV27cjPy.d.ts} +1 -1
  3. package/dist/{chunk-YIXSSJWD.cjs → chunk-AYPF6C4D.cjs} +5 -5
  4. package/dist/{chunk-YIXSSJWD.cjs.map → chunk-AYPF6C4D.cjs.map} +1 -1
  5. package/dist/chunk-CLIH2T74.js +403 -0
  6. package/dist/chunk-CLIH2T74.js.map +1 -0
  7. package/dist/{chunk-BHNDALCA.js → chunk-DNHJS6RN.js} +6 -4
  8. package/dist/chunk-DNHJS6RN.js.map +1 -0
  9. package/dist/{chunk-YYV3XQOQ.cjs → chunk-E2BXLXPW.cjs} +7 -7
  10. package/dist/{chunk-YYV3XQOQ.cjs.map → chunk-E2BXLXPW.cjs.map} +1 -1
  11. package/dist/{chunk-AZLU3ROK.cjs → chunk-EHSZ6TAN.cjs} +11 -4
  12. package/dist/chunk-EHSZ6TAN.cjs.map +1 -0
  13. package/dist/{chunk-3YUHXWSG.js → chunk-F332TENF.js} +3 -3
  14. package/dist/{chunk-3YUHXWSG.js.map → chunk-F332TENF.js.map} +1 -1
  15. package/dist/{chunk-V5LBQN3I.js → chunk-GRN3GHUG.js} +11 -4
  16. package/dist/chunk-GRN3GHUG.js.map +1 -0
  17. package/dist/{chunk-UAQL2VWX.cjs → chunk-J7F3NPAP.cjs} +2436 -707
  18. package/dist/chunk-J7F3NPAP.cjs.map +1 -0
  19. package/dist/{chunk-VEL7QRYI.js → chunk-L2IDZI7F.js} +9 -2
  20. package/dist/chunk-L2IDZI7F.js.map +1 -0
  21. package/dist/{chunk-ILZ3DP4I.cjs → chunk-MPT5PA6U.cjs} +24 -2
  22. package/dist/chunk-MPT5PA6U.cjs.map +1 -0
  23. package/dist/{chunk-ZWV3EBZ7.cjs → chunk-MYB5RY7H.cjs} +6 -4
  24. package/dist/chunk-MYB5RY7H.cjs.map +1 -0
  25. package/dist/{chunk-OJZ45OJD.js → chunk-UISZ2MBW.js} +2272 -544
  26. package/dist/chunk-UISZ2MBW.js.map +1 -0
  27. package/dist/{chunk-AVPUX57O.js → chunk-V3KVSEG6.js} +3 -3
  28. package/dist/{chunk-AVPUX57O.js.map → chunk-V3KVSEG6.js.map} +1 -1
  29. package/dist/{chunk-TJTWRO4G.js → chunk-Y3EWJQ4D.js} +4 -4
  30. package/dist/{chunk-TJTWRO4G.js.map → chunk-Y3EWJQ4D.js.map} +1 -1
  31. package/dist/{chunk-LWG2MWDA.cjs → chunk-Y72M3MVX.cjs} +4 -4
  32. package/dist/{chunk-LWG2MWDA.cjs.map → chunk-Y72M3MVX.cjs.map} +1 -1
  33. package/dist/{chunk-SGAG6FD3.js → chunk-YFJJU26H.js} +24 -2
  34. package/dist/chunk-YFJJU26H.js.map +1 -0
  35. package/dist/chunk-YHW27CBV.cjs +406 -0
  36. package/dist/chunk-YHW27CBV.cjs.map +1 -0
  37. package/dist/{chunk-I4V3VZWF.cjs → chunk-YRFAQ6MI.cjs} +9 -2
  38. package/dist/chunk-YRFAQ6MI.cjs.map +1 -0
  39. package/dist/{collection-config-B6gMPunn.d.cts → collection-config-BF95LgQb.d.cts} +1 -1
  40. package/dist/{collection-config-B6gMPunn.d.ts → collection-config-BF95LgQb.d.ts} +1 -1
  41. package/dist/index.cjs +4098 -424
  42. package/dist/index.cjs.map +1 -1
  43. package/dist/index.d.cts +503 -8
  44. package/dist/index.d.ts +503 -8
  45. package/dist/index.js +4008 -341
  46. package/dist/index.js.map +1 -1
  47. package/dist/middleware.cjs +24 -24
  48. package/dist/middleware.d.cts +1 -1
  49. package/dist/middleware.d.ts +1 -1
  50. package/dist/middleware.js +3 -3
  51. package/dist/migrations-LEMFV2ND.cjs +13 -0
  52. package/dist/{migrations-NIEUFG44.cjs.map → migrations-LEMFV2ND.cjs.map} +1 -1
  53. package/dist/migrations-RKQES6XY.js +4 -0
  54. package/dist/{migrations-TGZKJKV4.js.map → migrations-RKQES6XY.js.map} +1 -1
  55. package/dist/{plugin-bootstrap-dYhD9fQR.d.ts → plugin-bootstrap-CB-xaBfK.d.ts} +2 -2
  56. package/dist/{plugin-bootstrap-SHsdjE6X.d.cts → plugin-bootstrap-U-cw9jn3.d.cts} +2 -2
  57. package/dist/plugins.cjs +11 -11
  58. package/dist/plugins.js +2 -2
  59. package/dist/routes.cjs +27 -27
  60. package/dist/routes.d.cts +1 -1
  61. package/dist/routes.d.ts +1 -1
  62. package/dist/routes.js +7 -7
  63. package/dist/services.cjs +16 -16
  64. package/dist/services.d.cts +2 -2
  65. package/dist/services.d.ts +2 -2
  66. package/dist/services.js +2 -2
  67. package/dist/templates.cjs +17 -17
  68. package/dist/templates.js +2 -2
  69. package/dist/types.d.cts +1 -1
  70. package/dist/types.d.ts +1 -1
  71. package/dist/utils.cjs +14 -14
  72. package/dist/utils.d.cts +1 -1
  73. package/dist/utils.d.ts +1 -1
  74. package/dist/utils.js +1 -1
  75. package/migrations/029_ai_search_plugin.sql +45 -0
  76. package/package.json +4 -2
  77. package/dist/chunk-AI2JJIJX.cjs +0 -211
  78. package/dist/chunk-AI2JJIJX.cjs.map +0 -1
  79. package/dist/chunk-AZLU3ROK.cjs.map +0 -1
  80. package/dist/chunk-BHNDALCA.js.map +0 -1
  81. package/dist/chunk-I4V3VZWF.cjs.map +0 -1
  82. package/dist/chunk-ILZ3DP4I.cjs.map +0 -1
  83. package/dist/chunk-OJZ45OJD.js.map +0 -1
  84. package/dist/chunk-QDBNW7KQ.js +0 -209
  85. package/dist/chunk-QDBNW7KQ.js.map +0 -1
  86. package/dist/chunk-SGAG6FD3.js.map +0 -1
  87. package/dist/chunk-UAQL2VWX.cjs.map +0 -1
  88. package/dist/chunk-V5LBQN3I.js.map +0 -1
  89. package/dist/chunk-VEL7QRYI.js.map +0 -1
  90. package/dist/chunk-ZWV3EBZ7.cjs.map +0 -1
  91. package/dist/migrations-NIEUFG44.cjs +0 -13
  92. package/dist/migrations-TGZKJKV4.js +0 -4
package/dist/index.js CHANGED
@@ -1,18 +1,19 @@
1
- import { 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, checkAdminUserExists } from './chunk-OJZ45OJD.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-OJZ45OJD.js';
1
+ import { renderConfirmationDialog, getConfirmationDialogScript, 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-UISZ2MBW.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-UISZ2MBW.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 { AuthManager, metricsMiddleware, bootstrapMiddleware, requireAuth } from './chunk-TJTWRO4G.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-TJTWRO4G.js';
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-VEL7QRYI.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
- export { HookSystemImpl, HookUtils, PluginManager as PluginManagerClass, PluginRegistryImpl, PluginValidator as PluginValidatorClass, ScopedHookSystem as ScopedHookSystemClass } from './chunk-3YUHXWSG.js';
13
- import { PluginBuilder } from './chunk-QDBNW7KQ.js';
14
- import { package_default, getCoreVersion } from './chunk-BHNDALCA.js';
15
- export { QueryFilterBuilder, SONICJS_VERSION, TemplateRenderer, buildQuery, escapeHtml, getCoreVersion, renderTemplate, sanitizeInput, sanitizeObject, templateRenderer } from './chunk-BHNDALCA.js';
5
+ import { requireAuth, AuthManager, metricsMiddleware, bootstrapMiddleware } from './chunk-Y3EWJQ4D.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-Y3EWJQ4D.js';
7
+ export { PluginBootstrapService, PluginService as PluginServiceClass, cleanupRemovedCollections, fullCollectionSync, getAvailableCollectionNames, getManagedCollections, isCollectionManaged, loadCollectionConfig, loadCollectionConfigs, registerCollections, syncCollection, syncCollections, validateCollectionConfig } from './chunk-YFJJU26H.js';
8
+ export { MigrationService } from './chunk-L2IDZI7F.js';
9
+ export { renderFilterBar } from './chunk-V3KVSEG6.js';
10
+ import { init_admin_layout_catalyst_template, renderAdminLayout, adminLayoutV2, renderAdminLayoutCatalyst } from './chunk-GRN3GHUG.js';
11
+ export { getConfirmationDialogScript, renderAlert, renderConfirmationDialog, renderForm, renderFormField, renderPagination, renderTable } from './chunk-GRN3GHUG.js';
12
+ export { HookSystemImpl, HookUtils, PluginManager as PluginManagerClass, PluginRegistryImpl, PluginValidator as PluginValidatorClass, ScopedHookSystem as ScopedHookSystemClass } from './chunk-F332TENF.js';
13
+ import { PluginBuilder } from './chunk-CLIH2T74.js';
14
+ export { PluginBuilder, PluginHelpers } from './chunk-CLIH2T74.js';
15
+ import { package_default, getCoreVersion } from './chunk-DNHJS6RN.js';
16
+ export { QueryFilterBuilder, SONICJS_VERSION, TemplateRenderer, buildQuery, escapeHtml, getCoreVersion, renderTemplate, sanitizeInput, sanitizeObject, templateRenderer } from './chunk-DNHJS6RN.js';
16
17
  import './chunk-X7ZAEI5S.js';
17
18
  export { metricsTracker } from './chunk-FICTAGD4.js';
18
19
  export { HOOKS } from './chunk-LOUJRBXV.js';
@@ -23,33 +24,6 @@ import { setCookie } from 'hono/cookie';
23
24
  import { z } from 'zod';
24
25
  import { drizzle } from 'drizzle-orm/d1';
25
26
 
26
- // src/middleware/admin-setup.ts
27
- function adminSetupMiddleware() {
28
- return async (c, next) => {
29
- const path = new URL(c.req.url).pathname;
30
- if (path.startsWith("/auth/")) {
31
- return next();
32
- }
33
- if (path.match(/\.(js|css|ico|png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot)$/)) {
34
- return next();
35
- }
36
- if (path === "/health") {
37
- return next();
38
- }
39
- if (path.startsWith("/api/")) {
40
- return next();
41
- }
42
- const db = c.env.DB;
43
- const adminExists = await checkAdminUserExists(db);
44
- if (!adminExists) {
45
- if (path.startsWith("/admin")) {
46
- return c.redirect("/auth/register?setup=true");
47
- }
48
- }
49
- return next();
50
- };
51
- }
52
-
53
27
  // src/plugins/core-plugins/database-tools-plugin/services/database-service.ts
54
28
  var DatabaseToolsService = class {
55
29
  constructor(db) {
@@ -1354,10 +1328,10 @@ function createEmailPlugin() {
1354
1328
  emailRoutes.get("/settings", async (c) => {
1355
1329
  const user = c.get("user");
1356
1330
  const db = c.env.DB;
1357
- const plugin = await db.prepare(`
1331
+ const plugin2 = await db.prepare(`
1358
1332
  SELECT settings FROM plugins WHERE id = 'email'
1359
1333
  `).first();
1360
- const settings = plugin?.settings ? JSON.parse(plugin.settings) : {};
1334
+ const settings = plugin2?.settings ? JSON.parse(plugin2.settings) : {};
1361
1335
  const contentHTML = await html`
1362
1336
  <div class="p-8">
1363
1337
  <!-- Header -->
@@ -1618,16 +1592,16 @@ function createEmailPlugin() {
1618
1592
  try {
1619
1593
  const db = c.env.DB;
1620
1594
  const body = await c.req.json();
1621
- const plugin = await db.prepare(`
1595
+ const plugin2 = await db.prepare(`
1622
1596
  SELECT settings FROM plugins WHERE id = 'email'
1623
1597
  `).first();
1624
- if (!plugin?.settings) {
1598
+ if (!plugin2?.settings) {
1625
1599
  return c.json({
1626
1600
  success: false,
1627
1601
  error: "Email settings not configured. Please save your settings first."
1628
1602
  }, 400);
1629
1603
  }
1630
- const settings = JSON.parse(plugin.settings);
1604
+ const settings = JSON.parse(plugin2.settings);
1631
1605
  if (!settings.apiKey || !settings.fromEmail || !settings.fromName) {
1632
1606
  return c.json({
1633
1607
  success: false,
@@ -2233,8 +2207,8 @@ function createOTPLoginPlugin() {
2233
2207
  requiresAuth: false,
2234
2208
  priority: 100
2235
2209
  });
2236
- const adminRoutes = new Hono();
2237
- adminRoutes.get("/settings", async (c) => {
2210
+ const adminRoutes2 = new Hono();
2211
+ adminRoutes2.get("/settings", async (c) => {
2238
2212
  const user = c.get("user");
2239
2213
  const contentHTML = await html`
2240
2214
  <div class="p-8">
@@ -2414,7 +2388,7 @@ function createOTPLoginPlugin() {
2414
2388
  })
2415
2389
  );
2416
2390
  });
2417
- builder.addRoute("/admin/plugins/otp-login", adminRoutes, {
2391
+ builder.addRoute("/admin/plugins/otp-login", adminRoutes2, {
2418
2392
  description: "OTP login admin interface",
2419
2393
  requiresAuth: true,
2420
2394
  priority: 85
@@ -2435,363 +2409,4056 @@ function createOTPLoginPlugin() {
2435
2409
  return builder.build();
2436
2410
  }
2437
2411
  var otpLoginPlugin = createOTPLoginPlugin();
2438
- var magicLinkRequestSchema = z.object({
2439
- email: z.string().email("Valid email is required")
2440
- });
2441
- function createMagicLinkAuthPlugin() {
2442
- const magicLinkRoutes = new Hono();
2443
- magicLinkRoutes.post("/request", async (c) => {
2412
+
2413
+ // src/plugins/core-plugins/ai-search-plugin/services/embedding.service.ts
2414
+ var EmbeddingService = class {
2415
+ constructor(ai) {
2416
+ this.ai = ai;
2417
+ }
2418
+ /**
2419
+ * Generate embedding for a single text
2420
+ */
2421
+ async generateEmbedding(text) {
2444
2422
  try {
2445
- const body = await c.req.json();
2446
- const validation = magicLinkRequestSchema.safeParse(body);
2447
- if (!validation.success) {
2448
- return c.json({
2449
- error: "Validation failed",
2450
- details: validation.error.issues
2451
- }, 400);
2423
+ const response = await this.ai.run("@cf/baai/bge-base-en-v1.5", {
2424
+ text: this.preprocessText(text)
2425
+ });
2426
+ if (response.data && response.data.length > 0) {
2427
+ return response.data[0];
2452
2428
  }
2453
- const { email } = validation.data;
2454
- const normalizedEmail = email.toLowerCase();
2455
- const db = c.env.DB;
2456
- const oneHourAgo = Date.now() - 60 * 60 * 1e3;
2457
- const recentLinks = await db.prepare(`
2458
- SELECT COUNT(*) as count
2459
- FROM magic_links
2460
- WHERE user_email = ? AND created_at > ?
2461
- `).bind(normalizedEmail, oneHourAgo).first();
2462
- const rateLimitPerHour = 5;
2463
- if (recentLinks && recentLinks.count >= rateLimitPerHour) {
2464
- return c.json({
2465
- error: "Too many requests. Please try again later."
2466
- }, 429);
2429
+ throw new Error("No embedding data returned");
2430
+ } catch (error) {
2431
+ console.error("[EmbeddingService] Error generating embedding:", error);
2432
+ throw error;
2433
+ }
2434
+ }
2435
+ /**
2436
+ * Generate embeddings for multiple texts (batch processing)
2437
+ */
2438
+ async generateBatch(texts) {
2439
+ try {
2440
+ const batchSize = 10;
2441
+ const batches = [];
2442
+ for (let i = 0; i < texts.length; i += batchSize) {
2443
+ batches.push(texts.slice(i, i + batchSize));
2467
2444
  }
2468
- const user = await db.prepare(`
2469
- SELECT id, email, role, is_active
2470
- FROM users
2471
- WHERE email = ?
2472
- `).bind(normalizedEmail).first();
2473
- const allowNewUsers = false;
2474
- if (!user && !allowNewUsers) {
2475
- return c.json({
2476
- message: "If an account exists for this email, you will receive a magic link shortly."
2445
+ const allEmbeddings = [];
2446
+ for (const batch of batches) {
2447
+ const batchEmbeddings = await Promise.all(
2448
+ batch.map((text) => this.generateEmbedding(text))
2449
+ );
2450
+ allEmbeddings.push(...batchEmbeddings);
2451
+ }
2452
+ return allEmbeddings;
2453
+ } catch (error) {
2454
+ console.error("[EmbeddingService] Error generating batch embeddings:", error);
2455
+ throw error;
2456
+ }
2457
+ }
2458
+ /**
2459
+ * Preprocess text before generating embedding
2460
+ * - Trim whitespace
2461
+ * - Limit length to avoid token limits
2462
+ * - Remove special characters that might cause issues
2463
+ */
2464
+ preprocessText(text) {
2465
+ if (!text) return "";
2466
+ let processed = text.trim().replace(/\s+/g, " ");
2467
+ if (processed.length > 8e3) {
2468
+ processed = processed.substring(0, 8e3);
2469
+ }
2470
+ return processed;
2471
+ }
2472
+ /**
2473
+ * Calculate cosine similarity between two embeddings
2474
+ */
2475
+ cosineSimilarity(a, b) {
2476
+ if (a.length !== b.length) {
2477
+ throw new Error("Embeddings must have same dimensions");
2478
+ }
2479
+ let dotProduct = 0;
2480
+ let normA = 0;
2481
+ let normB = 0;
2482
+ for (let i = 0; i < a.length; i++) {
2483
+ const aVal = a[i] ?? 0;
2484
+ const bVal = b[i] ?? 0;
2485
+ dotProduct += aVal * bVal;
2486
+ normA += aVal * aVal;
2487
+ normB += bVal * bVal;
2488
+ }
2489
+ return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
2490
+ }
2491
+ };
2492
+
2493
+ // src/plugins/core-plugins/ai-search-plugin/services/chunking.service.ts
2494
+ var ChunkingService = class {
2495
+ // Default chunk size (in approximate tokens)
2496
+ CHUNK_SIZE = 500;
2497
+ CHUNK_OVERLAP = 50;
2498
+ /**
2499
+ * Chunk a single content item
2500
+ */
2501
+ chunkContent(contentId, collectionId, title, data, metadata = {}) {
2502
+ const text = this.extractText(data);
2503
+ if (!text || text.trim().length === 0) {
2504
+ console.warn(`[ChunkingService] No text found for content ${contentId}`);
2505
+ return [];
2506
+ }
2507
+ const textChunks = this.splitIntoChunks(text);
2508
+ return textChunks.map((chunkText, index) => ({
2509
+ id: `${contentId}_chunk_${index}`,
2510
+ content_id: contentId,
2511
+ collection_id: collectionId,
2512
+ title,
2513
+ text: chunkText,
2514
+ chunk_index: index,
2515
+ metadata: {
2516
+ ...metadata,
2517
+ total_chunks: textChunks.length
2518
+ }
2519
+ }));
2520
+ }
2521
+ /**
2522
+ * Chunk multiple content items
2523
+ */
2524
+ chunkContentBatch(items) {
2525
+ const allChunks = [];
2526
+ for (const item of items) {
2527
+ const chunks = this.chunkContent(
2528
+ item.id,
2529
+ item.collection_id,
2530
+ item.title,
2531
+ item.data,
2532
+ item.metadata
2533
+ );
2534
+ allChunks.push(...chunks);
2535
+ }
2536
+ return allChunks;
2537
+ }
2538
+ /**
2539
+ * Extract all text from content data
2540
+ */
2541
+ extractText(data) {
2542
+ const parts = [];
2543
+ if (data.title) parts.push(String(data.title));
2544
+ if (data.name) parts.push(String(data.name));
2545
+ if (data.description) parts.push(String(data.description));
2546
+ if (data.content) parts.push(String(data.content));
2547
+ if (data.body) parts.push(String(data.body));
2548
+ if (data.text) parts.push(String(data.text));
2549
+ if (data.summary) parts.push(String(data.summary));
2550
+ const extractRecursive = (obj) => {
2551
+ if (typeof obj === "string") {
2552
+ if (obj.length > 10 && !obj.startsWith("http")) {
2553
+ parts.push(obj);
2554
+ }
2555
+ } else if (Array.isArray(obj)) {
2556
+ obj.forEach(extractRecursive);
2557
+ } else if (obj && typeof obj === "object") {
2558
+ const skipKeys = ["id", "slug", "url", "image", "thumbnail", "metadata"];
2559
+ Object.entries(obj).forEach(([key, value]) => {
2560
+ if (!skipKeys.includes(key.toLowerCase())) {
2561
+ extractRecursive(value);
2562
+ }
2477
2563
  });
2478
2564
  }
2479
- if (user && !user.is_active) {
2480
- return c.json({
2481
- error: "This account has been deactivated."
2482
- }, 403);
2565
+ };
2566
+ extractRecursive(data);
2567
+ return parts.join("\n\n").trim();
2568
+ }
2569
+ /**
2570
+ * Split text into overlapping chunks
2571
+ */
2572
+ splitIntoChunks(text) {
2573
+ const words = text.split(/\s+/);
2574
+ if (words.length <= this.CHUNK_SIZE) {
2575
+ return [text];
2576
+ }
2577
+ const chunks = [];
2578
+ let startIndex = 0;
2579
+ while (startIndex < words.length) {
2580
+ const endIndex = Math.min(startIndex + this.CHUNK_SIZE, words.length);
2581
+ const chunk = words.slice(startIndex, endIndex).join(" ");
2582
+ chunks.push(chunk);
2583
+ startIndex += this.CHUNK_SIZE - this.CHUNK_OVERLAP;
2584
+ if (startIndex >= words.length - this.CHUNK_OVERLAP) {
2585
+ break;
2483
2586
  }
2484
- const token = crypto.randomUUID() + "-" + crypto.randomUUID();
2485
- const tokenId = crypto.randomUUID();
2486
- const linkExpiryMinutes = 15;
2487
- const expiresAt = Date.now() + linkExpiryMinutes * 60 * 1e3;
2488
- await db.prepare(`
2489
- INSERT INTO magic_links (
2490
- id, user_email, token, expires_at, used, created_at, ip_address, user_agent
2491
- ) VALUES (?, ?, ?, ?, 0, ?, ?, ?)
2492
- `).bind(
2493
- tokenId,
2494
- normalizedEmail,
2495
- token,
2496
- expiresAt,
2497
- Date.now(),
2498
- c.req.header("cf-connecting-ip") || c.req.header("x-forwarded-for") || "unknown",
2499
- c.req.header("user-agent") || "unknown"
2500
- ).run();
2501
- const baseUrl = new URL(c.req.url).origin;
2502
- const magicLink = `${baseUrl}/auth/magic-link/verify?token=${token}`;
2503
- try {
2504
- const emailPlugin2 = c.env.plugins?.get("email");
2505
- if (emailPlugin2 && emailPlugin2.sendEmail) {
2506
- await emailPlugin2.sendEmail({
2507
- to: normalizedEmail,
2508
- subject: "Your Magic Link to Sign In",
2509
- html: renderMagicLinkEmail(magicLink, linkExpiryMinutes)
2510
- });
2511
- } else {
2512
- console.error("Email plugin not available");
2513
- console.log(`Magic link for ${normalizedEmail}: ${magicLink}`);
2587
+ }
2588
+ return chunks;
2589
+ }
2590
+ /**
2591
+ * Get optimal chunk size based on content type
2592
+ */
2593
+ getOptimalChunkSize(contentType) {
2594
+ switch (contentType) {
2595
+ case "blog_posts":
2596
+ case "articles":
2597
+ return 600;
2598
+ // Larger chunks for long-form content
2599
+ case "products":
2600
+ case "pages":
2601
+ return 400;
2602
+ // Medium chunks for structured content
2603
+ case "messages":
2604
+ case "comments":
2605
+ return 200;
2606
+ // Small chunks for short content
2607
+ default:
2608
+ return this.CHUNK_SIZE;
2609
+ }
2610
+ }
2611
+ };
2612
+
2613
+ // src/plugins/core-plugins/ai-search-plugin/services/custom-rag.service.ts
2614
+ var CustomRAGService = class {
2615
+ constructor(db, ai, vectorize) {
2616
+ this.db = db;
2617
+ this.ai = ai;
2618
+ this.vectorize = vectorize;
2619
+ this.embeddingService = new EmbeddingService(ai);
2620
+ this.chunkingService = new ChunkingService();
2621
+ }
2622
+ embeddingService;
2623
+ chunkingService;
2624
+ /**
2625
+ * Index all content from a collection
2626
+ */
2627
+ async indexCollection(collectionId) {
2628
+ console.log(`[CustomRAG] Starting indexing for collection: ${collectionId}`);
2629
+ try {
2630
+ const { results: contentItems } = await this.db.prepare(`
2631
+ SELECT c.id, c.title, c.data, c.collection_id, c.status,
2632
+ c.created_at, c.updated_at, c.author_id,
2633
+ col.name as collection_name, col.display_name as collection_display_name
2634
+ FROM content c
2635
+ JOIN collections col ON c.collection_id = col.id
2636
+ WHERE c.collection_id = ? AND c.status = 'published'
2637
+ `).bind(collectionId).all();
2638
+ const totalItems = contentItems?.length || 0;
2639
+ if (totalItems === 0) {
2640
+ console.log(`[CustomRAG] No content found in collection ${collectionId}`);
2641
+ return { total_items: 0, total_chunks: 0, indexed_chunks: 0, errors: 0 };
2642
+ }
2643
+ const items = (contentItems || []).map((item) => ({
2644
+ id: item.id,
2645
+ collection_id: item.collection_id,
2646
+ title: item.title || "Untitled",
2647
+ data: typeof item.data === "string" ? JSON.parse(item.data) : item.data,
2648
+ metadata: {
2649
+ status: item.status,
2650
+ created_at: item.created_at,
2651
+ updated_at: item.updated_at,
2652
+ author_id: item.author_id,
2653
+ collection_name: item.collection_name,
2654
+ collection_display_name: item.collection_display_name
2655
+ }
2656
+ }));
2657
+ const chunks = this.chunkingService.chunkContentBatch(items);
2658
+ const totalChunks = chunks.length;
2659
+ console.log(`[CustomRAG] Generated ${totalChunks} chunks from ${totalItems} items`);
2660
+ const embeddings = await this.embeddingService.generateBatch(
2661
+ chunks.map((c) => `${c.title}
2662
+
2663
+ ${c.text}`)
2664
+ );
2665
+ console.log(`[CustomRAG] Generated ${embeddings.length} embeddings`);
2666
+ let indexedChunks = 0;
2667
+ let errors = 0;
2668
+ const batchSize = 100;
2669
+ for (let i = 0; i < chunks.length; i += batchSize) {
2670
+ const chunkBatch = chunks.slice(i, i + batchSize);
2671
+ const embeddingBatch = embeddings.slice(i, i + batchSize);
2672
+ try {
2673
+ await this.vectorize.upsert(
2674
+ chunkBatch.map((chunk, idx) => ({
2675
+ id: chunk.id,
2676
+ values: embeddingBatch[idx],
2677
+ metadata: {
2678
+ content_id: chunk.content_id,
2679
+ collection_id: chunk.collection_id,
2680
+ title: chunk.title,
2681
+ text: chunk.text.substring(0, 500),
2682
+ // Store snippet for display
2683
+ chunk_index: chunk.chunk_index,
2684
+ ...chunk.metadata
2685
+ }
2686
+ }))
2687
+ );
2688
+ indexedChunks += chunkBatch.length;
2689
+ console.log(`[CustomRAG] Indexed batch ${i / batchSize + 1}: ${chunkBatch.length} chunks`);
2690
+ } catch (error) {
2691
+ console.error(`[CustomRAG] Error indexing batch ${i / batchSize + 1}:`, error);
2692
+ errors += chunkBatch.length;
2514
2693
  }
2515
- } catch (error) {
2516
- console.error("Failed to send magic link email:", error);
2517
- return c.json({
2518
- error: "Failed to send email. Please try again later."
2519
- }, 500);
2520
2694
  }
2521
- return c.json({
2522
- message: "If an account exists for this email, you will receive a magic link shortly.",
2523
- // For development only - remove in production
2524
- ...c.env.ENVIRONMENT === "development" && { dev_link: magicLink }
2525
- });
2695
+ console.log(`[CustomRAG] Indexing complete: ${indexedChunks}/${totalChunks} chunks indexed`);
2696
+ return {
2697
+ total_items: totalItems,
2698
+ total_chunks: totalChunks,
2699
+ indexed_chunks: indexedChunks,
2700
+ errors
2701
+ };
2526
2702
  } catch (error) {
2527
- console.error("Magic link request error:", error);
2528
- return c.json({ error: "Failed to process request" }, 500);
2703
+ console.error(`[CustomRAG] Error indexing collection ${collectionId}:`, error);
2704
+ throw error;
2529
2705
  }
2530
- });
2531
- magicLinkRoutes.get("/verify", async (c) => {
2706
+ }
2707
+ /**
2708
+ * Search using RAG (semantic search with Vectorize)
2709
+ */
2710
+ async search(query, settings) {
2711
+ const startTime = Date.now();
2532
2712
  try {
2533
- const token = c.req.query("token");
2534
- if (!token) {
2535
- return c.redirect("/auth/login?error=Invalid magic link");
2713
+ console.log(`[CustomRAG] Searching for: "${query.query}"`);
2714
+ const queryEmbedding = await this.embeddingService.generateEmbedding(query.query);
2715
+ const filter = {};
2716
+ if (query.filters?.collections && query.filters.collections.length > 0) {
2717
+ filter.collection_id = { $in: query.filters.collections };
2718
+ } else if (settings.selected_collections.length > 0) {
2719
+ filter.collection_id = { $in: settings.selected_collections };
2536
2720
  }
2537
- const db = c.env.DB;
2538
- const magicLink = await db.prepare(`
2539
- SELECT * FROM magic_links
2540
- WHERE token = ? AND used = 0
2541
- `).bind(token).first();
2542
- if (!magicLink) {
2543
- return c.redirect("/auth/login?error=Invalid or expired magic link");
2721
+ if (query.filters?.status && query.filters.status.length > 0) {
2722
+ filter.status = { $in: query.filters.status };
2544
2723
  }
2545
- if (magicLink.expires_at < Date.now()) {
2546
- return c.redirect("/auth/login?error=This magic link has expired");
2724
+ const vectorResults = await this.vectorize.query(queryEmbedding, {
2725
+ topK: 50,
2726
+ // Max allowed with returnMetadata: true
2727
+ returnMetadata: true
2728
+ });
2729
+ let filteredMatches = vectorResults.matches || [];
2730
+ if (filter.collection_id?.$in && Array.isArray(filter.collection_id.$in)) {
2731
+ const allowedCollections = filter.collection_id.$in;
2732
+ filteredMatches = filteredMatches.filter(
2733
+ (match) => allowedCollections.includes(match.metadata?.collection_id)
2734
+ );
2547
2735
  }
2548
- let user = await db.prepare(`
2549
- SELECT * FROM users WHERE email = ? AND is_active = 1
2550
- `).bind(magicLink.user_email).first();
2551
- const allowNewUsers = false;
2552
- if (!user && allowNewUsers) {
2553
- const userId = crypto.randomUUID();
2554
- const username = magicLink.user_email.split("@")[0];
2555
- const now = Date.now();
2556
- await db.prepare(`
2557
- INSERT INTO users (
2558
- id, email, username, first_name, last_name,
2559
- password_hash, role, is_active, created_at, updated_at
2560
- ) VALUES (?, ?, ?, ?, ?, NULL, 'viewer', 1, ?, ?)
2561
- `).bind(
2562
- userId,
2563
- magicLink.user_email,
2564
- username,
2565
- username,
2566
- "",
2567
- now,
2568
- now
2569
- ).run();
2570
- user = {
2571
- id: userId,
2572
- email: magicLink.user_email,
2573
- username,
2574
- role: "viewer"
2736
+ if (filter.status?.$in && Array.isArray(filter.status.$in)) {
2737
+ const allowedStatuses = filter.status.$in;
2738
+ filteredMatches = filteredMatches.filter(
2739
+ (match) => allowedStatuses.includes(match.metadata?.status)
2740
+ );
2741
+ }
2742
+ const topK = query.limit || settings.results_limit || 20;
2743
+ filteredMatches = filteredMatches.slice(0, topK);
2744
+ vectorResults.matches = filteredMatches;
2745
+ if (!vectorResults.matches || vectorResults.matches.length === 0) {
2746
+ return {
2747
+ results: [],
2748
+ total: 0,
2749
+ query_time_ms: Date.now() - startTime,
2750
+ mode: "ai"
2575
2751
  };
2576
- } else if (!user) {
2577
- return c.redirect("/auth/login?error=No account found for this email");
2578
2752
  }
2579
- await db.prepare(`
2580
- UPDATE magic_links
2581
- SET used = 1, used_at = ?
2582
- WHERE id = ?
2583
- `).bind(Date.now(), magicLink.id).run();
2584
- const jwtToken = await AuthManager.generateToken(
2585
- user.id,
2586
- user.email,
2587
- user.role
2753
+ const contentIds = [...new Set(
2754
+ vectorResults.matches.map((m) => m.metadata.content_id)
2755
+ )];
2756
+ const placeholders = contentIds.map(() => "?").join(",");
2757
+ const { results: contentItems } = await this.db.prepare(`
2758
+ SELECT c.id, c.title, c.slug, c.collection_id, c.status,
2759
+ c.created_at, c.updated_at, c.author_id,
2760
+ col.display_name as collection_name
2761
+ FROM content c
2762
+ JOIN collections col ON c.collection_id = col.id
2763
+ WHERE c.id IN (${placeholders})
2764
+ `).bind(...contentIds).all();
2765
+ const searchResults = (contentItems || []).map((item) => {
2766
+ const matchingChunks = vectorResults.matches.filter(
2767
+ (m) => m.metadata.content_id === item.id
2768
+ );
2769
+ const bestMatch = matchingChunks.reduce(
2770
+ (best, current) => current.score > (best?.score || 0) ? current : best,
2771
+ null
2772
+ );
2773
+ return {
2774
+ id: item.id,
2775
+ title: item.title || "Untitled",
2776
+ slug: item.slug || "",
2777
+ collection_id: item.collection_id,
2778
+ collection_name: item.collection_name,
2779
+ snippet: bestMatch?.metadata?.text || "",
2780
+ relevance_score: bestMatch?.score || 0,
2781
+ status: item.status,
2782
+ created_at: item.created_at,
2783
+ updated_at: item.updated_at
2784
+ };
2785
+ });
2786
+ searchResults.sort((a, b) => (b.relevance_score || 0) - (a.relevance_score || 0));
2787
+ const queryTime = Date.now() - startTime;
2788
+ console.log(`[CustomRAG] Search completed in ${queryTime}ms, ${searchResults.length} results`);
2789
+ return {
2790
+ results: searchResults,
2791
+ total: searchResults.length,
2792
+ query_time_ms: queryTime,
2793
+ mode: "ai"
2794
+ };
2795
+ } catch (error) {
2796
+ console.error("[CustomRAG] Search error:", error);
2797
+ throw error;
2798
+ }
2799
+ }
2800
+ /**
2801
+ * Update index for a single content item
2802
+ */
2803
+ async updateContentIndex(contentId) {
2804
+ try {
2805
+ const content2 = await this.db.prepare(`
2806
+ SELECT c.id, c.title, c.data, c.collection_id, c.status,
2807
+ c.created_at, c.updated_at, c.author_id,
2808
+ col.name as collection_name, col.display_name as collection_display_name
2809
+ FROM content c
2810
+ JOIN collections col ON c.collection_id = col.id
2811
+ WHERE c.id = ?
2812
+ `).bind(contentId).first();
2813
+ if (!content2) {
2814
+ console.warn(`[CustomRAG] Content ${contentId} not found`);
2815
+ return;
2816
+ }
2817
+ if (content2.status !== "published") {
2818
+ await this.removeContentFromIndex(contentId);
2819
+ return;
2820
+ }
2821
+ const chunks = this.chunkingService.chunkContent(
2822
+ content2.id,
2823
+ content2.collection_id,
2824
+ content2.title || "Untitled",
2825
+ typeof content2.data === "string" ? JSON.parse(content2.data) : content2.data,
2826
+ {
2827
+ status: content2.status,
2828
+ created_at: content2.created_at,
2829
+ updated_at: content2.updated_at,
2830
+ author_id: content2.author_id,
2831
+ collection_name: content2.collection_name,
2832
+ collection_display_name: content2.collection_display_name
2833
+ }
2588
2834
  );
2589
- AuthManager.setAuthCookie(c, jwtToken);
2590
- await db.prepare(`
2591
- UPDATE users SET last_login_at = ? WHERE id = ?
2592
- `).bind(Date.now(), user.id).run();
2593
- return c.redirect("/admin/dashboard?message=Successfully signed in");
2835
+ const embeddings = await this.embeddingService.generateBatch(
2836
+ chunks.map((c) => `${c.title}
2837
+
2838
+ ${c.text}`)
2839
+ );
2840
+ await this.vectorize.upsert(
2841
+ chunks.map((chunk, idx) => ({
2842
+ id: chunk.id,
2843
+ values: embeddings[idx],
2844
+ metadata: {
2845
+ content_id: chunk.content_id,
2846
+ collection_id: chunk.collection_id,
2847
+ title: chunk.title,
2848
+ text: chunk.text.substring(0, 500),
2849
+ chunk_index: chunk.chunk_index,
2850
+ ...chunk.metadata
2851
+ }
2852
+ }))
2853
+ );
2854
+ console.log(`[CustomRAG] Updated index for content ${contentId}: ${chunks.length} chunks`);
2594
2855
  } catch (error) {
2595
- console.error("Magic link verification error:", error);
2596
- return c.redirect("/auth/login?error=Authentication failed");
2856
+ console.error(`[CustomRAG] Error updating index for ${contentId}:`, error);
2857
+ throw error;
2597
2858
  }
2598
- });
2599
- return {
2600
- name: "magic-link-auth",
2601
- version: "1.0.0",
2602
- description: "Passwordless authentication via email magic links",
2603
- author: {
2604
- name: "SonicJS Team",
2605
- email: "team@sonicjs.com"
2606
- },
2607
- dependencies: ["email"],
2608
- routes: [{
2609
- path: "/auth/magic-link",
2610
- handler: magicLinkRoutes,
2611
- description: "Magic link authentication endpoints",
2612
- requiresAuth: false
2613
- }],
2614
- async install(context) {
2615
- console.log("Installing magic-link-auth plugin...");
2616
- },
2617
- async activate(context) {
2618
- console.log("Magic link authentication activated");
2619
- console.log("Users can now sign in via /auth/magic-link/request");
2620
- },
2621
- async deactivate(context) {
2622
- console.log("Magic link authentication deactivated");
2623
- },
2624
- async uninstall(context) {
2625
- console.log("Uninstalling magic-link-auth plugin...");
2859
+ }
2860
+ /**
2861
+ * Remove content from index
2862
+ */
2863
+ async removeContentFromIndex(contentId) {
2864
+ try {
2865
+ console.log(`[CustomRAG] Removing content ${contentId} from index`);
2866
+ } catch (error) {
2867
+ console.error(`[CustomRAG] Error removing content ${contentId}:`, error);
2868
+ throw error;
2626
2869
  }
2627
- };
2628
- }
2629
- function renderMagicLinkEmail(magicLink, expiryMinutes) {
2630
- return `
2631
- <!DOCTYPE html>
2632
- <html>
2633
- <head>
2634
- <meta charset="utf-8">
2635
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
2636
- <title>Your Magic Link</title>
2637
- <style>
2638
- body {
2639
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
2640
- line-height: 1.6;
2641
- color: #333;
2642
- max-width: 600px;
2643
- margin: 0 auto;
2644
- padding: 20px;
2645
- }
2646
- .container {
2647
- background: #ffffff;
2648
- border-radius: 8px;
2649
- padding: 40px;
2650
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
2651
- }
2652
- .header {
2653
- text-align: center;
2654
- margin-bottom: 30px;
2655
- }
2656
- .header h1 {
2657
- color: #0ea5e9;
2658
- margin: 0;
2659
- font-size: 24px;
2870
+ }
2871
+ /**
2872
+ * Get search suggestions based on query
2873
+ */
2874
+ async getSuggestions(partialQuery, limit = 5) {
2875
+ try {
2876
+ const queryEmbedding = await this.embeddingService.generateEmbedding(partialQuery);
2877
+ const results = await this.vectorize.query(queryEmbedding, {
2878
+ topK: limit * 2,
2879
+ // Get more to filter
2880
+ returnMetadata: true
2881
+ });
2882
+ const suggestions = [...new Set(
2883
+ results.matches?.map((m) => m.metadata.title).filter(Boolean) || []
2884
+ )].slice(0, limit);
2885
+ return suggestions;
2886
+ } catch (error) {
2887
+ console.error("[CustomRAG] Error getting suggestions:", error);
2888
+ return [];
2889
+ }
2890
+ }
2891
+ /**
2892
+ * Check if Vectorize is available and configured
2893
+ */
2894
+ isAvailable() {
2895
+ return !!this.vectorize && !!this.ai;
2896
+ }
2897
+ };
2898
+
2899
+ // src/plugins/core-plugins/ai-search-plugin/services/ai-search.ts
2900
+ var AISearchService = class {
2901
+ constructor(db, ai, vectorize) {
2902
+ this.db = db;
2903
+ this.ai = ai;
2904
+ this.vectorize = vectorize;
2905
+ if (this.ai && this.vectorize) {
2906
+ this.customRAG = new CustomRAGService(db, ai, vectorize);
2907
+ console.log("[AISearchService] Custom RAG initialized");
2908
+ } else {
2909
+ console.log("[AISearchService] Custom RAG not available, using keyword search only");
2910
+ }
2911
+ }
2912
+ customRAG;
2913
+ /**
2914
+ * Get plugin settings
2915
+ */
2916
+ async getSettings() {
2917
+ try {
2918
+ const plugin2 = await this.db.prepare(`SELECT settings FROM plugins WHERE id = ? LIMIT 1`).bind("ai-search").first();
2919
+ if (!plugin2 || !plugin2.settings) {
2920
+ return this.getDefaultSettings();
2921
+ }
2922
+ return JSON.parse(plugin2.settings);
2923
+ } catch (error) {
2924
+ console.error("Error fetching AI Search settings:", error);
2925
+ return this.getDefaultSettings();
2926
+ }
2927
+ }
2928
+ /**
2929
+ * Get default settings
2930
+ */
2931
+ getDefaultSettings() {
2932
+ return {
2933
+ enabled: true,
2934
+ ai_mode_enabled: true,
2935
+ selected_collections: [],
2936
+ dismissed_collections: [],
2937
+ autocomplete_enabled: true,
2938
+ cache_duration: 1,
2939
+ results_limit: 20,
2940
+ index_media: false
2941
+ };
2942
+ }
2943
+ /**
2944
+ * Update plugin settings
2945
+ */
2946
+ async updateSettings(settings) {
2947
+ const existing = await this.getSettings();
2948
+ const updated = {
2949
+ ...existing,
2950
+ ...settings
2951
+ };
2952
+ try {
2953
+ await this.db.prepare(`
2954
+ UPDATE plugins
2955
+ SET settings = ?,
2956
+ updated_at = unixepoch()
2957
+ WHERE id = 'ai-search'
2958
+ `).bind(JSON.stringify(updated)).run();
2959
+ return updated;
2960
+ } catch (error) {
2961
+ console.error("Error updating AI Search settings:", error);
2962
+ throw error;
2963
+ }
2964
+ }
2965
+ /**
2966
+ * Detect new collections that aren't indexed or dismissed
2967
+ */
2968
+ async detectNewCollections() {
2969
+ try {
2970
+ const collectionsStmt = this.db.prepare(
2971
+ "SELECT id, name, display_name, description FROM collections WHERE is_active = 1"
2972
+ );
2973
+ const { results: allCollections } = await collectionsStmt.all();
2974
+ const collections2 = (allCollections || []).filter(
2975
+ (col) => {
2976
+ if (!col.name) return false;
2977
+ const name = col.name.toLowerCase();
2978
+ return !name.startsWith("test_") && !name.endsWith("_test") && name !== "test_collection" && !name.includes("_test_") && name !== "large_payload_test" && name !== "concurrent_test";
2660
2979
  }
2661
- .content {
2662
- margin-bottom: 30px;
2980
+ );
2981
+ const settings = await this.getSettings();
2982
+ const selected = settings?.selected_collections || [];
2983
+ const dismissed = settings?.dismissed_collections || [];
2984
+ const notifications = [];
2985
+ for (const collection of collections2 || []) {
2986
+ const collectionId = String(collection.id);
2987
+ if (selected.includes(collectionId) || dismissed.includes(collectionId)) {
2988
+ continue;
2663
2989
  }
2664
- .button {
2665
- display: inline-block;
2666
- padding: 14px 32px;
2667
- background: linear-gradient(135deg, #0ea5e9 0%, #06b6d4 100%);
2668
- color: #ffffff !important;
2669
- text-decoration: none;
2670
- border-radius: 6px;
2671
- font-weight: 600;
2672
- text-align: center;
2673
- margin: 20px 0;
2990
+ const countStmt = this.db.prepare(
2991
+ "SELECT COUNT(*) as count FROM content WHERE collection_id = ?"
2992
+ );
2993
+ const countResult = await countStmt.bind(collectionId).first();
2994
+ const itemCount = countResult?.count || 0;
2995
+ notifications.push({
2996
+ collection: {
2997
+ id: collectionId,
2998
+ name: collection.name,
2999
+ display_name: collection.display_name,
3000
+ description: collection.description,
3001
+ item_count: itemCount,
3002
+ is_indexed: false,
3003
+ is_dismissed: false,
3004
+ is_new: true
3005
+ },
3006
+ message: `New collection "${collection.display_name}" with ${itemCount} items available for indexing`
3007
+ });
3008
+ }
3009
+ return notifications;
3010
+ } catch (error) {
3011
+ console.error("Error detecting new collections:", error);
3012
+ return [];
3013
+ }
3014
+ }
3015
+ /**
3016
+ * Get all collections with indexing status
3017
+ */
3018
+ async getAllCollections() {
3019
+ try {
3020
+ const collectionsStmt = this.db.prepare(
3021
+ "SELECT id, name, display_name, description FROM collections WHERE is_active = 1 ORDER BY display_name"
3022
+ );
3023
+ const { results: allCollections } = await collectionsStmt.all();
3024
+ console.log("[AISearchService.getAllCollections] Raw collections from DB:", allCollections?.length || 0);
3025
+ const firstCollection = allCollections?.[0];
3026
+ if (firstCollection) {
3027
+ console.log("[AISearchService.getAllCollections] Sample collection:", {
3028
+ id: firstCollection.id,
3029
+ name: firstCollection.name,
3030
+ display_name: firstCollection.display_name
3031
+ });
3032
+ }
3033
+ const collections2 = (allCollections || []).filter(
3034
+ (col) => col.id && col.name
3035
+ );
3036
+ console.log("[AISearchService.getAllCollections] After filtering test collections:", collections2.length);
3037
+ console.log("[AISearchService.getAllCollections] Remaining collections:", collections2.map((c) => c.name).join(", "));
3038
+ const settings = await this.getSettings();
3039
+ const selected = settings?.selected_collections || [];
3040
+ const dismissed = settings?.dismissed_collections || [];
3041
+ console.log("[AISearchService.getAllCollections] Settings:", {
3042
+ selected_count: selected.length,
3043
+ dismissed_count: dismissed.length,
3044
+ selected
3045
+ });
3046
+ const collectionInfos = [];
3047
+ for (const collection of collections2) {
3048
+ if (!collection.id || !collection.name) continue;
3049
+ const collectionId = String(collection.id);
3050
+ if (!collectionId) {
3051
+ console.warn("[AISearchService] Skipping invalid collection:", collection);
3052
+ continue;
2674
3053
  }
2675
- .button:hover {
2676
- opacity: 0.9;
3054
+ const countStmt = this.db.prepare(
3055
+ "SELECT COUNT(*) as count FROM content WHERE collection_id = ?"
3056
+ );
3057
+ const countResult = await countStmt.bind(collectionId).first();
3058
+ const itemCount = countResult?.count || 0;
3059
+ collectionInfos.push({
3060
+ id: collectionId,
3061
+ name: collection.name,
3062
+ display_name: collection.display_name || collection.name,
3063
+ description: collection.description,
3064
+ item_count: itemCount,
3065
+ is_indexed: selected.includes(collectionId),
3066
+ is_dismissed: dismissed.includes(collectionId),
3067
+ is_new: !selected.includes(collectionId) && !dismissed.includes(collectionId)
3068
+ });
3069
+ }
3070
+ console.log("[AISearchService.getAllCollections] Returning collectionInfos:", collectionInfos.length);
3071
+ const firstInfo = collectionInfos[0];
3072
+ if (collectionInfos.length > 0 && firstInfo) {
3073
+ console.log("[AISearchService.getAllCollections] First collectionInfo:", {
3074
+ id: firstInfo.id,
3075
+ name: firstInfo.name,
3076
+ display_name: firstInfo.display_name,
3077
+ item_count: firstInfo.item_count
3078
+ });
3079
+ }
3080
+ return collectionInfos;
3081
+ } catch (error) {
3082
+ console.error("[AISearchService] Error fetching collections:", error);
3083
+ return [];
3084
+ }
3085
+ }
3086
+ /**
3087
+ * Execute search query
3088
+ */
3089
+ async search(query) {
3090
+ const settings = await this.getSettings();
3091
+ if (!settings?.enabled) {
3092
+ return {
3093
+ results: [],
3094
+ total: 0,
3095
+ query_time_ms: 0,
3096
+ mode: query.mode
3097
+ };
3098
+ }
3099
+ if (query.mode === "ai" && settings.ai_mode_enabled && this.customRAG?.isAvailable()) {
3100
+ return this.searchAI(query, settings);
3101
+ }
3102
+ return this.searchKeyword(query, settings);
3103
+ }
3104
+ /**
3105
+ * AI-powered semantic search using Custom RAG
3106
+ */
3107
+ async searchAI(query, settings) {
3108
+ try {
3109
+ if (!this.customRAG) {
3110
+ console.warn("[AISearchService] CustomRAG not available, falling back to keyword search");
3111
+ return this.searchKeyword(query, settings);
3112
+ }
3113
+ const result = await this.customRAG.search(query, settings);
3114
+ return result;
3115
+ } catch (error) {
3116
+ console.error("[AISearchService] AI search error, falling back to keyword:", error);
3117
+ return this.searchKeyword(query, settings);
3118
+ }
3119
+ }
3120
+ /**
3121
+ * Traditional keyword search
3122
+ */
3123
+ async searchKeyword(query, settings) {
3124
+ const startTime = Date.now();
3125
+ try {
3126
+ const conditions = [];
3127
+ const params = [];
3128
+ if (query.query) {
3129
+ conditions.push("(c.title LIKE ? OR c.slug LIKE ? OR c.data LIKE ?)");
3130
+ const searchTerm = `%${query.query}%`;
3131
+ params.push(searchTerm, searchTerm, searchTerm);
3132
+ }
3133
+ if (query.filters?.collections && query.filters.collections.length > 0) {
3134
+ const placeholders = query.filters.collections.map(() => "?").join(",");
3135
+ conditions.push(`c.collection_id IN (${placeholders})`);
3136
+ params.push(...query.filters.collections);
3137
+ } else if (settings.selected_collections.length > 0) {
3138
+ const placeholders = settings.selected_collections.map(() => "?").join(",");
3139
+ conditions.push(`c.collection_id IN (${placeholders})`);
3140
+ params.push(...settings.selected_collections);
3141
+ }
3142
+ if (query.filters?.status && query.filters.status.length > 0) {
3143
+ const placeholders = query.filters.status.map(() => "?").join(",");
3144
+ conditions.push(`c.status IN (${placeholders})`);
3145
+ params.push(...query.filters.status);
3146
+ } else {
3147
+ conditions.push("c.status != 'deleted'");
3148
+ }
3149
+ if (query.filters?.dateRange) {
3150
+ const field = query.filters.dateRange.field || "created_at";
3151
+ if (query.filters.dateRange.start) {
3152
+ conditions.push(`c.${field} >= ?`);
3153
+ params.push(query.filters.dateRange.start.getTime());
2677
3154
  }
2678
- .expiry {
2679
- color: #ef4444;
2680
- font-size: 14px;
2681
- margin-top: 20px;
3155
+ if (query.filters.dateRange.end) {
3156
+ conditions.push(`c.${field} <= ?`);
3157
+ params.push(query.filters.dateRange.end.getTime());
2682
3158
  }
2683
- .footer {
2684
- margin-top: 40px;
2685
- padding-top: 20px;
2686
- border-top: 1px solid #e5e7eb;
2687
- font-size: 12px;
2688
- color: #6b7280;
2689
- text-align: center;
3159
+ }
3160
+ if (query.filters?.author) {
3161
+ conditions.push("c.author_id = ?");
3162
+ params.push(query.filters.author);
3163
+ }
3164
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
3165
+ const countStmt = this.db.prepare(`
3166
+ SELECT COUNT(*) as count
3167
+ FROM content c
3168
+ ${whereClause}
3169
+ `);
3170
+ const countResult = await countStmt.bind(...params).first();
3171
+ const total = countResult?.count || 0;
3172
+ const limit = query.limit || settings.results_limit;
3173
+ const offset = query.offset || 0;
3174
+ const resultsStmt = this.db.prepare(`
3175
+ SELECT
3176
+ c.id, c.title, c.slug, c.collection_id, c.status,
3177
+ c.created_at, c.updated_at, c.author_id, c.data,
3178
+ col.name as collection_name, col.display_name as collection_display_name,
3179
+ u.email as author_email
3180
+ FROM content c
3181
+ JOIN collections col ON c.collection_id = col.id
3182
+ LEFT JOIN users u ON c.author_id = u.id
3183
+ ${whereClause}
3184
+ ORDER BY c.updated_at DESC
3185
+ LIMIT ? OFFSET ?
3186
+ `);
3187
+ const { results } = await resultsStmt.bind(...params, limit, offset).all();
3188
+ const searchResults = (results || []).map((row) => ({
3189
+ id: String(row.id),
3190
+ title: row.title || "Untitled",
3191
+ slug: row.slug || "",
3192
+ collection_id: String(row.collection_id),
3193
+ collection_name: row.collection_display_name || row.collection_name,
3194
+ snippet: this.extractSnippet(row.data, query.query),
3195
+ status: row.status,
3196
+ created_at: Number(row.created_at),
3197
+ updated_at: Number(row.updated_at),
3198
+ author_name: row.author_email
3199
+ }));
3200
+ const queryTime = Date.now() - startTime;
3201
+ await this.logSearch(query.query, query.mode, searchResults.length);
3202
+ return {
3203
+ results: searchResults,
3204
+ total,
3205
+ query_time_ms: queryTime,
3206
+ mode: query.mode
3207
+ };
3208
+ } catch (error) {
3209
+ console.error("Keyword search error:", error);
3210
+ return {
3211
+ results: [],
3212
+ total: 0,
3213
+ query_time_ms: Date.now() - startTime,
3214
+ mode: query.mode
3215
+ };
3216
+ }
3217
+ }
3218
+ /**
3219
+ * Extract snippet from content data
3220
+ */
3221
+ extractSnippet(data, query) {
3222
+ try {
3223
+ const parsed = typeof data === "string" ? JSON.parse(data) : data;
3224
+ const text = JSON.stringify(parsed).toLowerCase();
3225
+ const queryLower = query.toLowerCase();
3226
+ const index = text.indexOf(queryLower);
3227
+ if (index === -1) {
3228
+ return JSON.stringify(parsed).substring(0, 200) + "...";
3229
+ }
3230
+ const start = Math.max(0, index - 50);
3231
+ const end = Math.min(text.length, index + query.length + 50);
3232
+ return text.substring(start, end) + "...";
3233
+ } catch {
3234
+ return data.substring(0, 200) + "...";
3235
+ }
3236
+ }
3237
+ /**
3238
+ * Get search suggestions (autocomplete)
3239
+ */
3240
+ async getSearchSuggestions(partial) {
3241
+ try {
3242
+ const settings = await this.getSettings();
3243
+ if (!settings?.autocomplete_enabled) {
3244
+ return [];
3245
+ }
3246
+ if (this.customRAG?.isAvailable()) {
3247
+ try {
3248
+ const aiSuggestions = await this.customRAG.getSuggestions(partial, 5);
3249
+ if (aiSuggestions.length > 0) {
3250
+ return aiSuggestions;
3251
+ }
3252
+ } catch (error) {
3253
+ console.error("[AISearchService] Error getting AI suggestions:", error);
2690
3254
  }
2691
- .security-note {
2692
- background: #fef3c7;
2693
- border-left: 4px solid #f59e0b;
2694
- padding: 12px 16px;
2695
- margin-top: 20px;
2696
- border-radius: 4px;
2697
- font-size: 14px;
3255
+ }
3256
+ const stmt = this.db.prepare(`
3257
+ SELECT DISTINCT query
3258
+ FROM ai_search_history
3259
+ WHERE query LIKE ?
3260
+ ORDER BY created_at DESC
3261
+ LIMIT 10
3262
+ `);
3263
+ const { results } = await stmt.bind(`%${partial}%`).all();
3264
+ return (results || []).map((r) => r.query);
3265
+ } catch (error) {
3266
+ console.error("Error getting suggestions:", error);
3267
+ return [];
3268
+ }
3269
+ }
3270
+ /**
3271
+ * Log search query to history
3272
+ */
3273
+ async logSearch(query, mode, resultsCount) {
3274
+ try {
3275
+ const stmt = this.db.prepare(`
3276
+ INSERT INTO ai_search_history (query, mode, results_count, created_at)
3277
+ VALUES (?, ?, ?, ?)
3278
+ `);
3279
+ await stmt.bind(query, mode, resultsCount, Date.now()).run();
3280
+ } catch (error) {
3281
+ console.error("Error logging search:", error);
3282
+ }
3283
+ }
3284
+ /**
3285
+ * Get search analytics
3286
+ */
3287
+ async getSearchAnalytics() {
3288
+ try {
3289
+ const totalStmt = this.db.prepare(`
3290
+ SELECT COUNT(*) as count
3291
+ FROM ai_search_history
3292
+ WHERE created_at >= ?
3293
+ `);
3294
+ const thirtyDaysAgo = Date.now() - 30 * 24 * 60 * 60 * 1e3;
3295
+ const totalResult = await totalStmt.bind(thirtyDaysAgo).first();
3296
+ const modeStmt = this.db.prepare(`
3297
+ SELECT mode, COUNT(*) as count
3298
+ FROM ai_search_history
3299
+ WHERE created_at >= ?
3300
+ GROUP BY mode
3301
+ `);
3302
+ const { results: modeResults } = await modeStmt.bind(thirtyDaysAgo).all();
3303
+ const aiCount = modeResults?.find((r) => r.mode === "ai")?.count || 0;
3304
+ const keywordCount = modeResults?.find((r) => r.mode === "keyword")?.count || 0;
3305
+ const popularStmt = this.db.prepare(`
3306
+ SELECT query, COUNT(*) as count
3307
+ FROM ai_search_history
3308
+ WHERE created_at >= ?
3309
+ GROUP BY query
3310
+ ORDER BY count DESC
3311
+ LIMIT 10
3312
+ `);
3313
+ const { results: popularResults } = await popularStmt.bind(thirtyDaysAgo).all();
3314
+ return {
3315
+ total_queries: totalResult?.count || 0,
3316
+ ai_queries: aiCount,
3317
+ keyword_queries: keywordCount,
3318
+ popular_queries: (popularResults || []).map((r) => ({
3319
+ query: r.query,
3320
+ count: r.count
3321
+ })),
3322
+ average_query_time: 0
3323
+ // TODO: Track query times
3324
+ };
3325
+ } catch (error) {
3326
+ console.error("Error getting analytics:", error);
3327
+ return {
3328
+ total_queries: 0,
3329
+ ai_queries: 0,
3330
+ keyword_queries: 0,
3331
+ popular_queries: [],
3332
+ average_query_time: 0
3333
+ };
3334
+ }
3335
+ }
3336
+ /**
3337
+ * Verify Custom RAG is available
3338
+ */
3339
+ verifyBinding() {
3340
+ return this.customRAG?.isAvailable() ?? false;
3341
+ }
3342
+ /**
3343
+ * Get Custom RAG service instance (for indexer)
3344
+ */
3345
+ getCustomRAG() {
3346
+ return this.customRAG;
3347
+ }
3348
+ };
3349
+
3350
+ // src/plugins/core-plugins/ai-search-plugin/services/indexer.ts
3351
+ var IndexManager = class {
3352
+ constructor(db, ai, vectorize) {
3353
+ this.db = db;
3354
+ this.ai = ai;
3355
+ this.vectorize = vectorize;
3356
+ if (this.ai && this.vectorize) {
3357
+ this.customRAG = new CustomRAGService(db, ai, vectorize);
3358
+ console.log("[IndexManager] Custom RAG initialized");
3359
+ }
3360
+ }
3361
+ customRAG;
3362
+ /**
3363
+ * Index all content items within a collection using Custom RAG
3364
+ */
3365
+ async indexCollection(collectionId) {
3366
+ try {
3367
+ const collectionStmt = this.db.prepare(
3368
+ "SELECT id, name, display_name FROM collections WHERE id = ?"
3369
+ );
3370
+ const collection = await collectionStmt.bind(collectionId).first();
3371
+ if (!collection) {
3372
+ throw new Error(`Collection ${collectionId} not found`);
3373
+ }
3374
+ await this.updateIndexStatus(collectionId, {
3375
+ collection_id: collectionId,
3376
+ collection_name: collection.display_name,
3377
+ total_items: 0,
3378
+ indexed_items: 0,
3379
+ status: "indexing"
3380
+ });
3381
+ if (this.customRAG?.isAvailable()) {
3382
+ console.log(`[IndexManager] Using Custom RAG to index collection ${collectionId}`);
3383
+ const result = await this.customRAG.indexCollection(collectionId);
3384
+ const finalStatus = {
3385
+ collection_id: collectionId,
3386
+ collection_name: collection.display_name,
3387
+ total_items: result.total_items,
3388
+ indexed_items: result.indexed_chunks,
3389
+ last_sync_at: Date.now(),
3390
+ status: result.errors > 0 ? "error" : "completed",
3391
+ error_message: result.errors > 0 ? `${result.errors} errors during indexing` : void 0
3392
+ };
3393
+ await this.updateIndexStatus(collectionId, finalStatus);
3394
+ return finalStatus;
3395
+ }
3396
+ console.warn(`[IndexManager] Custom RAG not available, skipping indexing for ${collectionId}`);
3397
+ const fallbackStatus = {
3398
+ collection_id: collectionId,
3399
+ collection_name: collection.display_name,
3400
+ total_items: 0,
3401
+ indexed_items: 0,
3402
+ last_sync_at: Date.now(),
3403
+ status: "completed",
3404
+ error_message: "Custom RAG not available - using keyword search only"
3405
+ };
3406
+ await this.updateIndexStatus(collectionId, fallbackStatus);
3407
+ return fallbackStatus;
3408
+ } catch (error) {
3409
+ console.error(`[IndexManager] Error indexing collection ${collectionId}:`, error);
3410
+ const errorStatus = {
3411
+ collection_id: collectionId,
3412
+ collection_name: "Unknown",
3413
+ total_items: 0,
3414
+ indexed_items: 0,
3415
+ status: "error",
3416
+ error_message: error instanceof Error ? error.message : String(error)
3417
+ };
3418
+ await this.updateIndexStatus(collectionId, errorStatus);
3419
+ return errorStatus;
3420
+ }
3421
+ }
3422
+ /**
3423
+ * Index a single content item
3424
+ */
3425
+ async indexContentItem(item, collectionId) {
3426
+ try {
3427
+ let parsedData = {};
3428
+ try {
3429
+ parsedData = typeof item.data === "string" ? JSON.parse(item.data) : item.data;
3430
+ } catch {
3431
+ parsedData = {};
3432
+ }
3433
+ const document = {
3434
+ id: `content_${item.id}`,
3435
+ title: item.title || "Untitled",
3436
+ slug: item.slug || "",
3437
+ content: this.extractSearchableText(parsedData),
3438
+ metadata: {
3439
+ collection_id: collectionId,
3440
+ collection_name: item.collection_name,
3441
+ collection_display_name: item.collection_display_name,
3442
+ status: item.status,
3443
+ created_at: item.created_at,
3444
+ updated_at: item.updated_at,
3445
+ author_id: item.author_id
2698
3446
  }
2699
- </style>
2700
- </head>
2701
- <body>
2702
- <div class="container">
2703
- <div class="header">
2704
- <h1>\u{1F517} Your Magic Link</h1>
3447
+ };
3448
+ console.log(`Indexed content item: ${item.id}`);
3449
+ } catch (error) {
3450
+ console.error(`Error indexing content item ${item.id}:`, error);
3451
+ throw error;
3452
+ }
3453
+ }
3454
+ /**
3455
+ * Extract searchable text from content data
3456
+ */
3457
+ extractSearchableText(data) {
3458
+ const parts = [];
3459
+ if (data.title) parts.push(String(data.title));
3460
+ if (data.name) parts.push(String(data.name));
3461
+ if (data.description) parts.push(String(data.description));
3462
+ if (data.content) parts.push(String(data.content));
3463
+ if (data.body) parts.push(String(data.body));
3464
+ if (data.text) parts.push(String(data.text));
3465
+ const extractStrings = (obj) => {
3466
+ if (typeof obj === "string") {
3467
+ parts.push(obj);
3468
+ } else if (Array.isArray(obj)) {
3469
+ obj.forEach(extractStrings);
3470
+ } else if (obj && typeof obj === "object") {
3471
+ Object.values(obj).forEach(extractStrings);
3472
+ }
3473
+ };
3474
+ extractStrings(data);
3475
+ return parts.join(" ");
3476
+ }
3477
+ /**
3478
+ * Update a single content item in the index
3479
+ */
3480
+ async updateIndex(collectionId, contentId) {
3481
+ try {
3482
+ const stmt = this.db.prepare(`
3483
+ SELECT
3484
+ c.id, c.title, c.slug, c.data, c.status,
3485
+ c.created_at, c.updated_at, c.author_id,
3486
+ col.name as collection_name, col.display_name as collection_display_name
3487
+ FROM content c
3488
+ JOIN collections col ON c.collection_id = col.id
3489
+ WHERE c.id = ? AND c.collection_id = ?
3490
+ `);
3491
+ const item = await stmt.bind(contentId, collectionId).first();
3492
+ if (!item) {
3493
+ throw new Error(`Content item ${contentId} not found`);
3494
+ }
3495
+ await this.indexContentItem(item, String(collectionId));
3496
+ const status = await this.getIndexStatus(String(collectionId));
3497
+ if (status) {
3498
+ await this.updateIndexStatus(String(collectionId), {
3499
+ ...status,
3500
+ last_sync_at: Date.now()
3501
+ });
3502
+ }
3503
+ } catch (error) {
3504
+ console.error(`Error updating index for content ${contentId}:`, error);
3505
+ throw error;
3506
+ }
3507
+ }
3508
+ /**
3509
+ * Remove a content item from the index using Custom RAG
3510
+ */
3511
+ async removeFromIndex(collectionId, contentId) {
3512
+ try {
3513
+ if (this.customRAG?.isAvailable()) {
3514
+ console.log(`[IndexManager] Removing content ${contentId} from index`);
3515
+ await this.customRAG.removeContentFromIndex(contentId);
3516
+ } else {
3517
+ console.warn(`[IndexManager] Custom RAG not available, skipping removal for ${contentId}`);
3518
+ }
3519
+ } catch (error) {
3520
+ console.error(`[IndexManager] Error removing content ${contentId} from index:`, error);
3521
+ throw error;
3522
+ }
3523
+ }
3524
+ /**
3525
+ * Get indexing status for a collection
3526
+ */
3527
+ async getIndexStatus(collectionId) {
3528
+ try {
3529
+ const stmt = this.db.prepare(
3530
+ "SELECT * FROM ai_search_index_meta WHERE collection_id = ?"
3531
+ );
3532
+ const result = await stmt.bind(collectionId).first();
3533
+ if (!result) {
3534
+ return null;
3535
+ }
3536
+ return {
3537
+ collection_id: String(result.collection_id),
3538
+ collection_name: result.collection_name,
3539
+ total_items: result.total_items,
3540
+ indexed_items: result.indexed_items,
3541
+ last_sync_at: result.last_sync_at,
3542
+ status: result.status,
3543
+ error_message: result.error_message
3544
+ };
3545
+ } catch (error) {
3546
+ console.error(`Error getting index status for collection ${collectionId}:`, error);
3547
+ return null;
3548
+ }
3549
+ }
3550
+ /**
3551
+ * Get indexing status for all collections
3552
+ */
3553
+ async getAllIndexStatus() {
3554
+ try {
3555
+ const stmt = this.db.prepare("SELECT * FROM ai_search_index_meta");
3556
+ const { results } = await stmt.all();
3557
+ const statusMap = {};
3558
+ for (const row of results || []) {
3559
+ const collectionId = String(row.collection_id);
3560
+ statusMap[collectionId] = {
3561
+ collection_id: collectionId,
3562
+ collection_name: row.collection_name,
3563
+ total_items: row.total_items,
3564
+ indexed_items: row.indexed_items,
3565
+ last_sync_at: row.last_sync_at,
3566
+ status: row.status,
3567
+ error_message: row.error_message
3568
+ };
3569
+ }
3570
+ return statusMap;
3571
+ } catch (error) {
3572
+ console.error("Error getting all index status:", error);
3573
+ return {};
3574
+ }
3575
+ }
3576
+ /**
3577
+ * Update index status in database
3578
+ */
3579
+ async updateIndexStatus(collectionId, status) {
3580
+ try {
3581
+ const checkStmt = this.db.prepare(
3582
+ "SELECT id FROM ai_search_index_meta WHERE collection_id = ?"
3583
+ );
3584
+ const existing = await checkStmt.bind(collectionId).first();
3585
+ if (existing) {
3586
+ const stmt = this.db.prepare(`
3587
+ UPDATE ai_search_index_meta
3588
+ SET collection_name = ?,
3589
+ total_items = ?,
3590
+ indexed_items = ?,
3591
+ last_sync_at = ?,
3592
+ status = ?,
3593
+ error_message = ?
3594
+ WHERE collection_id = ?
3595
+ `);
3596
+ await stmt.bind(
3597
+ status.collection_name,
3598
+ status.total_items,
3599
+ status.indexed_items,
3600
+ status.last_sync_at || null,
3601
+ status.status,
3602
+ status.error_message || null,
3603
+ String(collectionId)
3604
+ ).run();
3605
+ } else {
3606
+ const stmt = this.db.prepare(`
3607
+ INSERT INTO ai_search_index_meta (
3608
+ collection_id, collection_name, total_items, indexed_items,
3609
+ last_sync_at, status, error_message
3610
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
3611
+ `);
3612
+ await stmt.bind(
3613
+ String(status.collection_id),
3614
+ status.collection_name,
3615
+ status.total_items,
3616
+ status.indexed_items,
3617
+ status.last_sync_at || null,
3618
+ status.status,
3619
+ status.error_message || null
3620
+ ).run();
3621
+ }
3622
+ } catch (error) {
3623
+ console.error(`Error updating index status for collection ${collectionId}:`, error);
3624
+ throw error;
3625
+ }
3626
+ }
3627
+ /**
3628
+ * Sync all selected collections
3629
+ */
3630
+ async syncAll(selectedCollections) {
3631
+ for (const collectionId of selectedCollections) {
3632
+ try {
3633
+ await this.indexCollection(collectionId);
3634
+ } catch (error) {
3635
+ console.error(`Error syncing collection ${collectionId}:`, error);
3636
+ }
3637
+ }
3638
+ }
3639
+ };
3640
+
3641
+ // src/plugins/core-plugins/ai-search-plugin/components/settings-page.ts
3642
+ function renderSettingsPage(data) {
3643
+ const settings = data.settings || {
3644
+ enabled: false,
3645
+ ai_mode_enabled: true,
3646
+ selected_collections: [],
3647
+ dismissed_collections: [],
3648
+ autocomplete_enabled: true,
3649
+ cache_duration: 1,
3650
+ results_limit: 20,
3651
+ index_media: false
3652
+ };
3653
+ const selectedCollections = Array.isArray(settings.selected_collections) ? settings.selected_collections : [];
3654
+ const dismissedCollections = Array.isArray(settings.dismissed_collections) ? settings.dismissed_collections : [];
3655
+ const enabled = settings.enabled === true;
3656
+ const aiModeEnabled = settings.ai_mode_enabled !== false;
3657
+ const autocompleteEnabled = settings.autocomplete_enabled !== false;
3658
+ const indexMedia = settings.index_media === true;
3659
+ const selectedCollectionIds = new Set(selectedCollections.map((id) => String(id)));
3660
+ const dismissedCollectionIds = new Set(dismissedCollections.map((id) => String(id)));
3661
+ const collections2 = Array.isArray(data.collections) ? data.collections : [];
3662
+ console.log("[SettingsPage Template] Collections received:", collections2.length);
3663
+ if (collections2.length > 0) {
3664
+ console.log("[SettingsPage Template] First collection:", collections2[0]);
3665
+ }
3666
+ const content2 = `
3667
+ <div class="w-full px-4 sm:px-6 lg:px-8 py-6">
3668
+ <!-- Header with Back Button -->
3669
+ <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-6">
3670
+ <div>
3671
+ <h1 class="text-2xl/8 font-semibold text-zinc-950 dark:text-white sm:text-xl/8">\u{1F50D} AI Search Settings</h1>
3672
+ <p class="mt-2 text-sm/6 text-zinc-500 dark:text-zinc-400">
3673
+ Configure advanced search with Cloudflare AI Search. Select collections to index and manage search preferences.
3674
+ </p>
3675
+ </div>
3676
+ <div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
3677
+ <a href="/admin/plugins" class="inline-flex items-center justify-center rounded-lg bg-white dark:bg-zinc-800 px-3.5 py-2.5 text-sm font-semibold text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 hover:bg-zinc-50 dark:hover:bg-zinc-700 transition-colors shadow-sm">
3678
+ <svg class="-ml-0.5 mr-1.5 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
3679
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
3680
+ </svg>
3681
+ Back to Plugins
3682
+ </a>
3683
+ </div>
3684
+ </div>
3685
+
3686
+
3687
+ <!-- Main Settings Card -->
3688
+ <div class="rounded-xl bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10 p-6 mb-6">
3689
+ <form id="settingsForm" class="space-y-6">
3690
+ <!-- Enable Search Section -->
3691
+ <div>
3692
+ <h2 class="text-xl font-semibold text-zinc-950 dark:text-white mb-4">\u{1F50D} Search Settings</h2>
3693
+ <div class="space-y-3">
3694
+ <div class="flex items-center gap-3 p-4 border border-indigo-200 bg-indigo-50 dark:bg-indigo-900/20 rounded-lg">
3695
+ <input type="checkbox" id="enabled" name="enabled" ${enabled ? "checked" : ""} class="w-5 h-5 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500 cursor-pointer">
3696
+ <div class="flex-1">
3697
+ <label for="enabled" class="text-base font-semibold text-zinc-900 dark:text-white select-none cursor-pointer block">Enable AI Search</label>
3698
+ <p class="text-xs text-zinc-600 dark:text-zinc-400 mt-0.5">Turn on advanced search capabilities across your content</p>
3699
+ </div>
3700
+ </div>
3701
+
3702
+ <div class="flex items-center gap-3 p-4 border border-blue-200 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
3703
+ <input type="checkbox" id="ai_mode_enabled" name="ai_mode_enabled" ${aiModeEnabled ? "checked" : ""} class="w-5 h-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500 cursor-pointer">
3704
+ <div class="flex-1">
3705
+ <label for="ai_mode_enabled" class="text-base font-semibold text-zinc-900 dark:text-white select-none cursor-pointer block">\u{1F916} AI/Semantic Search</label>
3706
+ <p class="text-xs text-zinc-600 dark:text-zinc-400 mt-0.5">
3707
+ Enable natural language queries (requires Cloudflare Workers AI binding)
3708
+ <a href="https://developers.cloudflare.com/workers-ai/" target="_blank" class="text-blue-600 dark:text-blue-400 hover:underline ml-1">\u2192 Setup Guide</a>
3709
+ </p>
3710
+ <p class="text-xs text-amber-600 dark:text-amber-400 mt-1">
3711
+ \u26A0\uFE0F If AI binding unavailable, will fallback to keyword search
3712
+ </p>
3713
+ </div>
3714
+ </div>
3715
+ </div>
3716
+ </div>
3717
+
3718
+ <hr class="border-zinc-200 dark:border-zinc-800">
3719
+
3720
+ <!-- Collections Section -->
3721
+ <div>
3722
+ <div class="flex items-start justify-between mb-4">
3723
+ <div>
3724
+ <h2 class="text-xl font-semibold text-zinc-950 dark:text-white">\u{1F4DA} Collections to Index</h2>
3725
+ <p class="text-sm text-zinc-600 dark:text-zinc-400 mt-1">
3726
+ Select which content collections should be indexed and searchable. Only checked collections will be included in search results.
3727
+ </p>
3728
+ </div>
3729
+ </div>
3730
+ <div class="space-y-3 max-h-96 overflow-y-auto border-2 border-zinc-300 dark:border-zinc-700 rounded-lg p-4 bg-white dark:bg-zinc-800" id="collections-list">
3731
+ ${collections2.length === 0 ? '<p class="text-sm text-zinc-500 dark:text-zinc-400 p-4">No collections available. Create collections first.</p>' : collections2.map((collection) => {
3732
+ const collectionId = String(collection.id);
3733
+ const isChecked = selectedCollectionIds.has(collectionId);
3734
+ const isDismissed = dismissedCollectionIds.has(collectionId);
3735
+ const indexStatusMap = data.indexStatus || {};
3736
+ const status = indexStatusMap[collectionId];
3737
+ const isNew = collection.is_new === true && !isDismissed && !status;
3738
+ const statusBadge = status && isChecked ? `<span class="ml-2 px-2 py-1 text-xs rounded-full ${status.status === "completed" ? "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300" : status.status === "indexing" ? "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300" : status.status === "error" ? "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300" : "bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300"}">${status.status}</span>` : "";
3739
+ return `<div class="flex items-start gap-3 p-3 rounded-lg border border-zinc-200 dark:border-zinc-700 ${isNew ? "bg-blue-50 dark:bg-blue-900/10 border-blue-200 dark:border-blue-800" : "hover:bg-zinc-50 dark:hover:bg-zinc-800"}">
3740
+ <input
3741
+ type="checkbox"
3742
+ id="collection_${collectionId}"
3743
+ name="selected_collections"
3744
+ value="${collectionId}"
3745
+ ${isChecked ? "checked" : ""}
3746
+ class="mt-1 w-5 h-5 text-indigo-600 bg-white border-gray-300 rounded focus:ring-indigo-500 focus:ring-2 cursor-pointer"
3747
+ style="cursor: pointer; flex-shrink: 0;"
3748
+ />
3749
+ <div class="flex-1 min-w-0">
3750
+ <label for="collection_${collectionId}" class="text-sm font-medium text-zinc-950 dark:text-white select-none cursor-pointer flex items-center">
3751
+ ${collection.display_name || collection.name || "Unnamed Collection"}
3752
+ ${isNew ? '<span class="ml-2 px-2 py-0.5 text-xs rounded-full bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300">NEW</span>' : ""}
3753
+ ${statusBadge}
3754
+ </label>
3755
+ <p class="text-xs text-zinc-500 dark:text-zinc-400 mt-1">
3756
+ ${collection.description || collection.name || "No description"} \u2022 ${collection.item_count || 0} items
3757
+ ${status ? ` \u2022 ${status.indexed_items}/${status.total_items} indexed` : ""}
3758
+ </p>
3759
+ ${status && status.status === "indexing" ? `<div class="mt-2 w-full bg-gray-200 rounded-full h-2 dark:bg-gray-700">
3760
+ <div class="bg-blue-600 h-2 rounded-full" style="width: ${status.indexed_items / status.total_items * 100}%"></div>
3761
+ </div>` : ""}
3762
+ </div>
3763
+ ${isChecked ? `
3764
+ <button
3765
+ type="button"
3766
+ onclick="reindexCollection('${collectionId}')"
3767
+ class="px-3 py-1.5 text-xs font-medium text-white bg-indigo-600 hover:bg-indigo-700 rounded-md transition-colors flex items-center gap-1.5 whitespace-nowrap"
3768
+ ${status && status.status === "indexing" ? "disabled" : ""}
3769
+ >
3770
+ <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
3771
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
3772
+ </svg>
3773
+ Re-index
3774
+ </button>
3775
+ ` : ""}
3776
+ </div>`;
3777
+ }).join("")}
3778
+ </div>
3779
+ </div>
3780
+
3781
+ <hr class="border-zinc-200 dark:border-zinc-800">
3782
+
3783
+ <!-- Advanced Options -->
3784
+ <div>
3785
+ <h2 class="text-xl font-semibold text-zinc-950 dark:text-white mb-4">\u2699\uFE0F Advanced Options</h2>
3786
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
3787
+ <div class="flex items-start gap-3 p-3 border border-zinc-200 dark:border-zinc-700 rounded-lg">
3788
+ <input type="checkbox" id="autocomplete_enabled" name="autocomplete_enabled" ${autocompleteEnabled ? "checked" : ""} class="mt-0.5 w-5 h-5 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500 cursor-pointer">
3789
+ <div>
3790
+ <label for="autocomplete_enabled" class="text-sm font-medium text-zinc-950 dark:text-white select-none cursor-pointer block">Autocomplete Suggestions</label>
3791
+ <p class="text-xs text-zinc-500 dark:text-zinc-400 mt-0.5">Show search suggestions as users type</p>
3792
+ </div>
3793
+ </div>
3794
+
3795
+ <div class="flex items-start gap-3 p-3 border border-zinc-200 dark:border-zinc-700 rounded-lg">
3796
+ <input type="checkbox" id="index_media" name="index_media" ${indexMedia ? "checked" : ""} class="mt-0.5 w-5 h-5 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500 cursor-pointer">
3797
+ <div>
3798
+ <label for="index_media" class="text-sm font-medium text-zinc-950 dark:text-white select-none cursor-pointer block">Index Media Metadata</label>
3799
+ <p class="text-xs text-zinc-500 dark:text-zinc-400 mt-0.5">Include media files in search results</p>
3800
+ </div>
3801
+ </div>
3802
+
3803
+ <div>
3804
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Cache Duration (hours)</label>
3805
+ <input type="number" id="cache_duration" name="cache_duration" value="${settings.cache_duration || 1}" min="0" max="24" class="w-full rounded-lg bg-white dark:bg-white/5 px-3 py-2 text-sm text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 focus:ring-2 focus:ring-indigo-500">
3806
+ </div>
3807
+ <div>
3808
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Results Per Page</label>
3809
+ <input type="number" id="results_limit" name="results_limit" value="${settings.results_limit || 20}" min="10" max="100" class="w-full rounded-lg bg-white dark:bg-white/5 px-3 py-2 text-sm text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 focus:ring-2 focus:ring-indigo-500">
3810
+ </div>
3811
+ </div>
3812
+ </div>
3813
+
3814
+ <!-- Save Button -->
3815
+ <div class="flex items-center justify-between pt-4 border-t border-zinc-200 dark:border-zinc-800">
3816
+ <p class="text-xs text-zinc-500 dark:text-zinc-400">
3817
+ \u{1F4A1} Collections marked as <span class="px-1.5 py-0.5 text-xs font-medium rounded-full bg-blue-500 text-white">NEW</span> haven't been indexed yet
3818
+ </p>
3819
+ <button type="submit" class="inline-flex items-center justify-center rounded-lg bg-indigo-600 text-white px-6 py-2.5 text-sm font-semibold hover:bg-indigo-500 shadow-sm transition-colors">
3820
+ \u{1F4BE} Save Settings
3821
+ </button>
3822
+ </div>
3823
+ </form>
3824
+ </div>
3825
+
3826
+
3827
+ <!-- Search Analytics -->
3828
+ <div class="rounded-xl bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10 p-6">
3829
+ <h2 class="text-xl font-semibold text-zinc-950 dark:text-white mb-4">\u{1F4CA} Search Analytics</h2>
3830
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
3831
+ <div class="p-4 rounded-lg bg-zinc-50 dark:bg-zinc-800">
3832
+ <div class="text-sm text-zinc-500 dark:text-zinc-400">Total Queries</div>
3833
+ <div class="text-2xl font-bold text-zinc-950 dark:text-white mt-1">${data.analytics.total_queries}</div>
3834
+ </div>
3835
+ <div class="p-4 rounded-lg bg-zinc-50 dark:bg-zinc-800">
3836
+ <div class="text-sm text-zinc-500 dark:text-zinc-400">AI Queries</div>
3837
+ <div class="text-2xl font-bold text-blue-600 dark:text-blue-400 mt-1">${data.analytics.ai_queries}</div>
3838
+ </div>
3839
+ <div class="p-4 rounded-lg bg-zinc-50 dark:bg-zinc-800">
3840
+ <div class="text-sm text-zinc-500 dark:text-zinc-400">Keyword Queries</div>
3841
+ <div class="text-2xl font-bold text-indigo-600 dark:text-indigo-400 mt-1">${data.analytics.keyword_queries}</div>
3842
+ </div>
3843
+ </div>
3844
+ ${data.analytics.popular_queries.length > 0 ? `
3845
+ <div>
3846
+ <h3 class="text-sm font-semibold text-zinc-950 dark:text-white mb-2">Popular Searches</h3>
3847
+ <div class="space-y-1">
3848
+ ${data.analytics.popular_queries.map(
3849
+ (item) => `
3850
+ <div class="flex items-center justify-between text-sm">
3851
+ <span class="text-zinc-700 dark:text-zinc-300">"${item.query}"</span>
3852
+ <span class="text-zinc-500 dark:text-zinc-400">${item.count} times</span>
3853
+ </div>
3854
+ `
3855
+ ).join("")}
3856
+ </div>
3857
+ </div>
3858
+ ` : '<p class="text-sm text-zinc-500 dark:text-zinc-400">No search history yet.</p>'}
3859
+ </div>
3860
+
3861
+ <!-- Success Message -->
3862
+ <div id="msg" class="hidden fixed bottom-4 right-4 p-4 rounded-lg bg-green-50 text-green-900 border border-green-200 dark:bg-green-900/20 dark:text-green-100 dark:border-green-800 shadow-lg z-50">
3863
+ <div class="flex items-center gap-2">
3864
+ <span class="text-xl">\u2705</span>
3865
+ <span class="font-semibold">Settings Saved Successfully!</span>
3866
+ </div>
3867
+ </div>
3868
+ </div>
3869
+ <script>
3870
+ // Form submission with error handling
3871
+ document.getElementById('settingsForm').addEventListener('submit', async (e) => {
3872
+ e.preventDefault();
3873
+ console.log('[AI Search Client] Form submitted');
3874
+
3875
+ try {
3876
+ const btn = e.submitter;
3877
+ btn.innerText = 'Saving...';
3878
+ btn.disabled = true;
3879
+
3880
+ const formData = new FormData(e.target);
3881
+ const selectedCollections = Array.from(formData.getAll('selected_collections')).map(String);
3882
+
3883
+ const data = {
3884
+ enabled: document.getElementById('enabled').checked,
3885
+ ai_mode_enabled: document.getElementById('ai_mode_enabled').checked,
3886
+ selected_collections: selectedCollections,
3887
+ autocomplete_enabled: document.getElementById('autocomplete_enabled').checked,
3888
+ cache_duration: Number(formData.get('cache_duration')),
3889
+ results_limit: Number(formData.get('results_limit')),
3890
+ index_media: document.getElementById('index_media').checked,
3891
+ };
3892
+
3893
+ console.log('[AI Search Client] Sending data:', data);
3894
+ console.log('[AI Search Client] Selected collections:', selectedCollections);
3895
+
3896
+ const res = await fetch('/admin/plugins/ai-search', {
3897
+ method: 'POST',
3898
+ headers: {'Content-Type': 'application/json'},
3899
+ body: JSON.stringify(data)
3900
+ });
3901
+
3902
+ console.log('[AI Search Client] Response status:', res.status);
3903
+
3904
+ if (res.ok) {
3905
+ const result = await res.json();
3906
+ console.log('[AI Search Client] Save successful:', result);
3907
+ document.getElementById('msg').classList.remove('hidden');
3908
+ setTimeout(() => {
3909
+ document.getElementById('msg').classList.add('hidden');
3910
+ location.reload();
3911
+ }, 2000);
3912
+ } else {
3913
+ const error = await res.text();
3914
+ console.error('[AI Search Client] Save failed:', error);
3915
+ alert('Failed to save settings: ' + error);
3916
+ }
3917
+
3918
+ btn.innerText = 'Save Settings';
3919
+ btn.disabled = false;
3920
+ } catch (error) {
3921
+ console.error('[AI Search Client] Error:', error);
3922
+ alert('Error saving settings: ' + error.message);
3923
+ }
3924
+ });
3925
+
3926
+ // Add collection to index
3927
+ async function addCollectionToIndex(collectionId) {
3928
+ const form = document.getElementById('settingsForm');
3929
+ const checkbox = document.getElementById('collection_' + collectionId);
3930
+ if (checkbox) {
3931
+ checkbox.checked = true;
3932
+ form.dispatchEvent(new Event('submit'));
3933
+ }
3934
+ }
3935
+
3936
+ // Dismiss collection
3937
+ async function dismissCollection(collectionId) {
3938
+ const res = await fetch('/admin/plugins/ai-search', {
3939
+ method: 'POST',
3940
+ headers: {'Content-Type': 'application/json'},
3941
+ body: JSON.stringify({
3942
+ dismissed_collections: [collectionId]
3943
+ })
3944
+ });
3945
+ if (res.ok) {
3946
+ location.reload();
3947
+ }
3948
+ }
3949
+
3950
+ // Re-index collection
3951
+ async function reindexCollection(collectionId) {
3952
+ const res = await fetch('/admin/plugins/ai-search/api/reindex', {
3953
+ method: 'POST',
3954
+ headers: {'Content-Type': 'application/json'},
3955
+ body: JSON.stringify({ collection_id: collectionId })
3956
+ });
3957
+ if (res.ok) {
3958
+ alert('Re-indexing started. Page will refresh in a moment.');
3959
+ setTimeout(() => location.reload(), 2000);
3960
+ } else {
3961
+ alert('Failed to start re-indexing. Please try again.');
3962
+ }
3963
+ }
3964
+
3965
+ // Poll for index status updates
3966
+ setInterval(async () => {
3967
+ const res = await fetch('/admin/plugins/ai-search/api/status');
3968
+ if (res.ok) {
3969
+ const { data } = await res.json();
3970
+ // Update status indicators if needed
3971
+ // For now, just reload every 30 seconds if indexing is in progress
3972
+ const hasIndexing = Object.values(data).some((s) => s.status === 'indexing');
3973
+ if (hasIndexing) {
3974
+ location.reload();
3975
+ }
3976
+ }
3977
+ }, 30000);
3978
+ </script>
3979
+ `;
3980
+ return renderAdminLayout({
3981
+ title: "AI Search Settings",
3982
+ pageTitle: "AI Search Settings",
3983
+ currentPath: "/admin/plugins/ai-search/settings",
3984
+ user: data.user,
3985
+ content: content2
3986
+ });
3987
+ }
3988
+
3989
+ // src/plugins/core-plugins/ai-search-plugin/routes/admin.ts
3990
+ var adminRoutes = new Hono();
3991
+ adminRoutes.use("*", requireAuth());
3992
+ adminRoutes.get("/", async (c) => {
3993
+ try {
3994
+ const user = c.get("user");
3995
+ const db = c.env.DB;
3996
+ const ai = c.env.AI;
3997
+ const vectorize = c.env.VECTORIZE_INDEX;
3998
+ const service = new AISearchService(db, ai, vectorize);
3999
+ const indexer = new IndexManager(db, ai, vectorize);
4000
+ const settings = await service.getSettings();
4001
+ console.log("[AI Search Settings Route] Settings loaded:", !!settings);
4002
+ const collections2 = await service.getAllCollections();
4003
+ console.log("[AI Search Settings Route] Collections returned:", collections2.length);
4004
+ if (collections2.length === 0) {
4005
+ const directQuery = await db.prepare("SELECT id, name, display_name FROM collections WHERE is_active = 1").all();
4006
+ console.log("[AI Search Settings Route] Direct DB query found:", directQuery.results?.length || 0, "collections");
4007
+ if (directQuery.results && directQuery.results.length > 0) {
4008
+ console.log("[AI Search Settings Route] Sample from DB:", directQuery.results[0]);
4009
+ }
4010
+ } else if (collections2.length > 0 && collections2[0]) {
4011
+ console.log("[AI Search Settings Route] First collection:", {
4012
+ id: collections2[0].id,
4013
+ name: collections2[0].name,
4014
+ display_name: collections2[0].display_name
4015
+ });
4016
+ }
4017
+ const newCollections = await service.detectNewCollections();
4018
+ console.log("AI Search: New collections:", newCollections.length);
4019
+ const indexStatus = await indexer.getAllIndexStatus();
4020
+ console.log("AI Search: Index status:", Object.keys(indexStatus).length);
4021
+ const analytics = await service.getSearchAnalytics();
4022
+ return c.html(
4023
+ renderSettingsPage({
4024
+ settings,
4025
+ collections: collections2 || [],
4026
+ newCollections: newCollections || [],
4027
+ indexStatus: indexStatus || {},
4028
+ analytics,
4029
+ user: {
4030
+ name: user.email,
4031
+ email: user.email,
4032
+ role: user.role
4033
+ }
4034
+ })
4035
+ );
4036
+ } catch (error) {
4037
+ console.error("Error rendering AI Search settings:", error);
4038
+ return c.html(`<p>Error loading settings: ${error instanceof Error ? error.message : String(error)}</p>`, 500);
4039
+ }
4040
+ });
4041
+ adminRoutes.post("/", async (c) => {
4042
+ try {
4043
+ const db = c.env.DB;
4044
+ const ai = c.env.AI;
4045
+ const vectorize = c.env.VECTORIZE_INDEX;
4046
+ const service = new AISearchService(db, ai, vectorize);
4047
+ const indexer = new IndexManager(db, ai, vectorize);
4048
+ const body = await c.req.json();
4049
+ console.log("[AI Search POST] Received body:", JSON.stringify(body, null, 2));
4050
+ const currentSettings = await service.getSettings();
4051
+ console.log("[AI Search POST] Current settings selected_collections:", currentSettings?.selected_collections);
4052
+ const updatedSettings = {
4053
+ enabled: body.enabled !== void 0 ? Boolean(body.enabled) : currentSettings?.enabled,
4054
+ ai_mode_enabled: body.ai_mode_enabled !== void 0 ? Boolean(body.ai_mode_enabled) : currentSettings?.ai_mode_enabled,
4055
+ selected_collections: Array.isArray(body.selected_collections) ? body.selected_collections.map(String) : currentSettings?.selected_collections || [],
4056
+ dismissed_collections: Array.isArray(body.dismissed_collections) ? body.dismissed_collections.map(String) : currentSettings?.dismissed_collections || [],
4057
+ autocomplete_enabled: body.autocomplete_enabled !== void 0 ? Boolean(body.autocomplete_enabled) : currentSettings?.autocomplete_enabled,
4058
+ cache_duration: body.cache_duration ? Number(body.cache_duration) : currentSettings?.cache_duration,
4059
+ results_limit: body.results_limit ? Number(body.results_limit) : currentSettings?.results_limit,
4060
+ index_media: body.index_media !== void 0 ? Boolean(body.index_media) : currentSettings?.index_media
4061
+ };
4062
+ console.log("[AI Search POST] Updated settings selected_collections:", updatedSettings.selected_collections);
4063
+ const collectionsChanged = JSON.stringify(updatedSettings.selected_collections) !== JSON.stringify(currentSettings?.selected_collections || []);
4064
+ const saved = await service.updateSettings(updatedSettings);
4065
+ console.log("[AI Search POST] Settings saved, selected_collections:", saved.selected_collections);
4066
+ if (collectionsChanged && updatedSettings.selected_collections) {
4067
+ console.log("[AI Search POST] Collections changed, starting background indexing");
4068
+ c.executionCtx.waitUntil(
4069
+ indexer.syncAll(updatedSettings.selected_collections).then(() => console.log("[AI Search POST] Background indexing completed")).catch((error) => console.error("[AI Search POST] Background indexing error:", error))
4070
+ );
4071
+ }
4072
+ return c.json({ success: true, settings: saved });
4073
+ } catch (error) {
4074
+ console.error("Error updating AI Search settings:", error);
4075
+ return c.json({ error: "Failed to update settings" }, 500);
4076
+ }
4077
+ });
4078
+ adminRoutes.get("/api/settings", async (c) => {
4079
+ try {
4080
+ const db = c.env.DB;
4081
+ const ai = c.env.AI;
4082
+ const vectorize = c.env.VECTORIZE_INDEX;
4083
+ const service = new AISearchService(db, ai, vectorize);
4084
+ const settings = await service.getSettings();
4085
+ return c.json({ success: true, data: settings });
4086
+ } catch (error) {
4087
+ console.error("Error fetching settings:", error);
4088
+ return c.json({ error: "Failed to fetch settings" }, 500);
4089
+ }
4090
+ });
4091
+ adminRoutes.get("/api/new-collections", async (c) => {
4092
+ try {
4093
+ const db = c.env.DB;
4094
+ const ai = c.env.AI;
4095
+ const vectorize = c.env.VECTORIZE_INDEX;
4096
+ const service = new AISearchService(db, ai, vectorize);
4097
+ const notifications = await service.detectNewCollections();
4098
+ return c.json({ success: true, data: notifications });
4099
+ } catch (error) {
4100
+ console.error("Error detecting new collections:", error);
4101
+ return c.json({ error: "Failed to detect new collections" }, 500);
4102
+ }
4103
+ });
4104
+ adminRoutes.get("/api/status", async (c) => {
4105
+ try {
4106
+ const db = c.env.DB;
4107
+ const ai = c.env.AI;
4108
+ const vectorize = c.env.VECTORIZE_INDEX;
4109
+ const indexer = new IndexManager(db, ai, vectorize);
4110
+ const status = await indexer.getAllIndexStatus();
4111
+ return c.json({ success: true, data: status });
4112
+ } catch (error) {
4113
+ console.error("Error fetching index status:", error);
4114
+ return c.json({ error: "Failed to fetch status" }, 500);
4115
+ }
4116
+ });
4117
+ adminRoutes.post("/api/reindex", async (c) => {
4118
+ try {
4119
+ const db = c.env.DB;
4120
+ const ai = c.env.AI;
4121
+ const vectorize = c.env.VECTORIZE_INDEX;
4122
+ const indexer = new IndexManager(db, ai, vectorize);
4123
+ const body = await c.req.json();
4124
+ const collectionIdRaw = body.collection_id;
4125
+ const collectionId = collectionIdRaw ? String(collectionIdRaw) : "";
4126
+ if (!collectionId || collectionId === "undefined" || collectionId === "null") {
4127
+ return c.json({ error: "collection_id is required" }, 400);
4128
+ }
4129
+ c.executionCtx.waitUntil(
4130
+ indexer.indexCollection(collectionId).then(() => console.log(`[AI Search Reindex] Completed for collection ${collectionId}`)).catch((error) => console.error(`[AI Search Reindex] Error for collection ${collectionId}:`, error))
4131
+ );
4132
+ return c.json({ success: true, message: "Re-indexing started" });
4133
+ } catch (error) {
4134
+ console.error("Error starting re-index:", error);
4135
+ return c.json({ error: "Failed to start re-indexing" }, 500);
4136
+ }
4137
+ });
4138
+ var admin_default = adminRoutes;
4139
+ var apiRoutes = new Hono();
4140
+ apiRoutes.post("/", async (c) => {
4141
+ try {
4142
+ const db = c.env.DB;
4143
+ const ai = c.env.AI;
4144
+ const vectorize = c.env.VECTORIZE_INDEX;
4145
+ const service = new AISearchService(db, ai, vectorize);
4146
+ const body = await c.req.json();
4147
+ const query = {
4148
+ query: body.query || "",
4149
+ mode: body.mode || "keyword",
4150
+ filters: body.filters || {},
4151
+ limit: body.limit ? Number(body.limit) : void 0,
4152
+ offset: body.offset ? Number(body.offset) : void 0
4153
+ };
4154
+ if (query.filters?.dateRange) {
4155
+ if (typeof query.filters.dateRange.start === "string") {
4156
+ query.filters.dateRange.start = new Date(query.filters.dateRange.start);
4157
+ }
4158
+ if (typeof query.filters.dateRange.end === "string") {
4159
+ query.filters.dateRange.end = new Date(query.filters.dateRange.end);
4160
+ }
4161
+ }
4162
+ const results = await service.search(query);
4163
+ return c.json({
4164
+ success: true,
4165
+ data: results
4166
+ });
4167
+ } catch (error) {
4168
+ console.error("Search error:", error);
4169
+ return c.json(
4170
+ {
4171
+ success: false,
4172
+ error: "Search failed",
4173
+ message: error instanceof Error ? error.message : String(error)
4174
+ },
4175
+ 500
4176
+ );
4177
+ }
4178
+ });
4179
+ apiRoutes.get("/suggest", async (c) => {
4180
+ try {
4181
+ const db = c.env.DB;
4182
+ const ai = c.env.AI;
4183
+ const vectorize = c.env.VECTORIZE_INDEX;
4184
+ const service = new AISearchService(db, ai, vectorize);
4185
+ const query = c.req.query("q") || "";
4186
+ if (!query || query.length < 2) {
4187
+ return c.json({ success: true, data: [] });
4188
+ }
4189
+ const suggestions = await service.getSearchSuggestions(query);
4190
+ return c.json({
4191
+ success: true,
4192
+ data: suggestions
4193
+ });
4194
+ } catch (error) {
4195
+ console.error("Suggestions error:", error);
4196
+ return c.json(
4197
+ {
4198
+ success: false,
4199
+ error: "Failed to get suggestions"
4200
+ },
4201
+ 500
4202
+ );
4203
+ }
4204
+ });
4205
+ apiRoutes.get("/analytics", async (c) => {
4206
+ try {
4207
+ const db = c.env.DB;
4208
+ const ai = c.env.AI;
4209
+ const vectorize = c.env.VECTORIZE_INDEX;
4210
+ const service = new AISearchService(db, ai, vectorize);
4211
+ const analytics = await service.getSearchAnalytics();
4212
+ return c.json({
4213
+ success: true,
4214
+ data: analytics
4215
+ });
4216
+ } catch (error) {
4217
+ console.error("Analytics error:", error);
4218
+ return c.json(
4219
+ {
4220
+ success: false,
4221
+ error: "Failed to get analytics"
4222
+ },
4223
+ 500
4224
+ );
4225
+ }
4226
+ });
4227
+ var api_default2 = apiRoutes;
4228
+
4229
+ // src/plugins/core-plugins/ai-search-plugin/manifest.json
4230
+ var manifest_default = {
4231
+ name: "AI Search",
4232
+ description: "Advanced search with Cloudflare AI Search. Full-text search, semantic search, and advanced filtering across all content collections.",
4233
+ version: "1.0.0",
4234
+ author: "SonicJS"};
4235
+
4236
+ // src/plugins/core-plugins/ai-search-plugin/index.ts
4237
+ var aiSearchPlugin = new PluginBuilder({
4238
+ name: manifest_default.name,
4239
+ version: manifest_default.version,
4240
+ description: manifest_default.description,
4241
+ author: { name: manifest_default.author }
4242
+ }).metadata({
4243
+ description: manifest_default.description,
4244
+ author: { name: manifest_default.author }
4245
+ }).addService("aiSearch", AISearchService).addService("indexManager", IndexManager).addRoute("/admin/plugins/ai-search", admin_default).addRoute("/api/search", api_default2).build();
4246
+ var magicLinkRequestSchema = z.object({
4247
+ email: z.string().email("Valid email is required")
4248
+ });
4249
+ function createMagicLinkAuthPlugin() {
4250
+ const magicLinkRoutes = new Hono();
4251
+ magicLinkRoutes.post("/request", async (c) => {
4252
+ try {
4253
+ const body = await c.req.json();
4254
+ const validation = magicLinkRequestSchema.safeParse(body);
4255
+ if (!validation.success) {
4256
+ return c.json({
4257
+ error: "Validation failed",
4258
+ details: validation.error.issues
4259
+ }, 400);
4260
+ }
4261
+ const { email } = validation.data;
4262
+ const normalizedEmail = email.toLowerCase();
4263
+ const db = c.env.DB;
4264
+ const oneHourAgo = Date.now() - 60 * 60 * 1e3;
4265
+ const recentLinks = await db.prepare(`
4266
+ SELECT COUNT(*) as count
4267
+ FROM magic_links
4268
+ WHERE user_email = ? AND created_at > ?
4269
+ `).bind(normalizedEmail, oneHourAgo).first();
4270
+ const rateLimitPerHour = 5;
4271
+ if (recentLinks && recentLinks.count >= rateLimitPerHour) {
4272
+ return c.json({
4273
+ error: "Too many requests. Please try again later."
4274
+ }, 429);
4275
+ }
4276
+ const user = await db.prepare(`
4277
+ SELECT id, email, role, is_active
4278
+ FROM users
4279
+ WHERE email = ?
4280
+ `).bind(normalizedEmail).first();
4281
+ const allowNewUsers = false;
4282
+ if (!user && !allowNewUsers) {
4283
+ return c.json({
4284
+ message: "If an account exists for this email, you will receive a magic link shortly."
4285
+ });
4286
+ }
4287
+ if (user && !user.is_active) {
4288
+ return c.json({
4289
+ error: "This account has been deactivated."
4290
+ }, 403);
4291
+ }
4292
+ const token = crypto.randomUUID() + "-" + crypto.randomUUID();
4293
+ const tokenId = crypto.randomUUID();
4294
+ const linkExpiryMinutes = 15;
4295
+ const expiresAt = Date.now() + linkExpiryMinutes * 60 * 1e3;
4296
+ await db.prepare(`
4297
+ INSERT INTO magic_links (
4298
+ id, user_email, token, expires_at, used, created_at, ip_address, user_agent
4299
+ ) VALUES (?, ?, ?, ?, 0, ?, ?, ?)
4300
+ `).bind(
4301
+ tokenId,
4302
+ normalizedEmail,
4303
+ token,
4304
+ expiresAt,
4305
+ Date.now(),
4306
+ c.req.header("cf-connecting-ip") || c.req.header("x-forwarded-for") || "unknown",
4307
+ c.req.header("user-agent") || "unknown"
4308
+ ).run();
4309
+ const baseUrl = new URL(c.req.url).origin;
4310
+ const magicLink = `${baseUrl}/auth/magic-link/verify?token=${token}`;
4311
+ try {
4312
+ const emailPlugin2 = c.env.plugins?.get("email");
4313
+ if (emailPlugin2 && emailPlugin2.sendEmail) {
4314
+ await emailPlugin2.sendEmail({
4315
+ to: normalizedEmail,
4316
+ subject: "Your Magic Link to Sign In",
4317
+ html: renderMagicLinkEmail(magicLink, linkExpiryMinutes)
4318
+ });
4319
+ } else {
4320
+ console.error("Email plugin not available");
4321
+ console.log(`Magic link for ${normalizedEmail}: ${magicLink}`);
4322
+ }
4323
+ } catch (error) {
4324
+ console.error("Failed to send magic link email:", error);
4325
+ return c.json({
4326
+ error: "Failed to send email. Please try again later."
4327
+ }, 500);
4328
+ }
4329
+ return c.json({
4330
+ message: "If an account exists for this email, you will receive a magic link shortly.",
4331
+ // For development only - remove in production
4332
+ ...c.env.ENVIRONMENT === "development" && { dev_link: magicLink }
4333
+ });
4334
+ } catch (error) {
4335
+ console.error("Magic link request error:", error);
4336
+ return c.json({ error: "Failed to process request" }, 500);
4337
+ }
4338
+ });
4339
+ magicLinkRoutes.get("/verify", async (c) => {
4340
+ try {
4341
+ const token = c.req.query("token");
4342
+ if (!token) {
4343
+ return c.redirect("/auth/login?error=Invalid magic link");
4344
+ }
4345
+ const db = c.env.DB;
4346
+ const magicLink = await db.prepare(`
4347
+ SELECT * FROM magic_links
4348
+ WHERE token = ? AND used = 0
4349
+ `).bind(token).first();
4350
+ if (!magicLink) {
4351
+ return c.redirect("/auth/login?error=Invalid or expired magic link");
4352
+ }
4353
+ if (magicLink.expires_at < Date.now()) {
4354
+ return c.redirect("/auth/login?error=This magic link has expired");
4355
+ }
4356
+ let user = await db.prepare(`
4357
+ SELECT * FROM users WHERE email = ? AND is_active = 1
4358
+ `).bind(magicLink.user_email).first();
4359
+ const allowNewUsers = false;
4360
+ if (!user && allowNewUsers) {
4361
+ const userId = crypto.randomUUID();
4362
+ const username = magicLink.user_email.split("@")[0];
4363
+ const now = Date.now();
4364
+ await db.prepare(`
4365
+ INSERT INTO users (
4366
+ id, email, username, first_name, last_name,
4367
+ password_hash, role, is_active, created_at, updated_at
4368
+ ) VALUES (?, ?, ?, ?, ?, NULL, 'viewer', 1, ?, ?)
4369
+ `).bind(
4370
+ userId,
4371
+ magicLink.user_email,
4372
+ username,
4373
+ username,
4374
+ "",
4375
+ now,
4376
+ now
4377
+ ).run();
4378
+ user = {
4379
+ id: userId,
4380
+ email: magicLink.user_email,
4381
+ username,
4382
+ role: "viewer"
4383
+ };
4384
+ } else if (!user) {
4385
+ return c.redirect("/auth/login?error=No account found for this email");
4386
+ }
4387
+ await db.prepare(`
4388
+ UPDATE magic_links
4389
+ SET used = 1, used_at = ?
4390
+ WHERE id = ?
4391
+ `).bind(Date.now(), magicLink.id).run();
4392
+ const jwtToken = await AuthManager.generateToken(
4393
+ user.id,
4394
+ user.email,
4395
+ user.role
4396
+ );
4397
+ AuthManager.setAuthCookie(c, jwtToken);
4398
+ await db.prepare(`
4399
+ UPDATE users SET last_login_at = ? WHERE id = ?
4400
+ `).bind(Date.now(), user.id).run();
4401
+ return c.redirect("/admin/dashboard?message=Successfully signed in");
4402
+ } catch (error) {
4403
+ console.error("Magic link verification error:", error);
4404
+ return c.redirect("/auth/login?error=Authentication failed");
4405
+ }
4406
+ });
4407
+ return {
4408
+ name: "magic-link-auth",
4409
+ version: "1.0.0",
4410
+ description: "Passwordless authentication via email magic links",
4411
+ author: {
4412
+ name: "SonicJS Team",
4413
+ email: "team@sonicjs.com"
4414
+ },
4415
+ dependencies: ["email"],
4416
+ routes: [{
4417
+ path: "/auth/magic-link",
4418
+ handler: magicLinkRoutes,
4419
+ description: "Magic link authentication endpoints",
4420
+ requiresAuth: false
4421
+ }],
4422
+ async install(context) {
4423
+ console.log("Installing magic-link-auth plugin...");
4424
+ },
4425
+ async activate(context) {
4426
+ console.log("Magic link authentication activated");
4427
+ console.log("Users can now sign in via /auth/magic-link/request");
4428
+ },
4429
+ async deactivate(context) {
4430
+ console.log("Magic link authentication deactivated");
4431
+ },
4432
+ async uninstall(context) {
4433
+ console.log("Uninstalling magic-link-auth plugin...");
4434
+ }
4435
+ };
4436
+ }
4437
+ function renderMagicLinkEmail(magicLink, expiryMinutes) {
4438
+ return `
4439
+ <!DOCTYPE html>
4440
+ <html>
4441
+ <head>
4442
+ <meta charset="utf-8">
4443
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
4444
+ <title>Your Magic Link</title>
4445
+ <style>
4446
+ body {
4447
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
4448
+ line-height: 1.6;
4449
+ color: #333;
4450
+ max-width: 600px;
4451
+ margin: 0 auto;
4452
+ padding: 20px;
4453
+ }
4454
+ .container {
4455
+ background: #ffffff;
4456
+ border-radius: 8px;
4457
+ padding: 40px;
4458
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
4459
+ }
4460
+ .header {
4461
+ text-align: center;
4462
+ margin-bottom: 30px;
4463
+ }
4464
+ .header h1 {
4465
+ color: #0ea5e9;
4466
+ margin: 0;
4467
+ font-size: 24px;
4468
+ }
4469
+ .content {
4470
+ margin-bottom: 30px;
4471
+ }
4472
+ .button {
4473
+ display: inline-block;
4474
+ padding: 14px 32px;
4475
+ background: linear-gradient(135deg, #0ea5e9 0%, #06b6d4 100%);
4476
+ color: #ffffff !important;
4477
+ text-decoration: none;
4478
+ border-radius: 6px;
4479
+ font-weight: 600;
4480
+ text-align: center;
4481
+ margin: 20px 0;
4482
+ }
4483
+ .button:hover {
4484
+ opacity: 0.9;
4485
+ }
4486
+ .expiry {
4487
+ color: #ef4444;
4488
+ font-size: 14px;
4489
+ margin-top: 20px;
4490
+ }
4491
+ .footer {
4492
+ margin-top: 40px;
4493
+ padding-top: 20px;
4494
+ border-top: 1px solid #e5e7eb;
4495
+ font-size: 12px;
4496
+ color: #6b7280;
4497
+ text-align: center;
4498
+ }
4499
+ .security-note {
4500
+ background: #fef3c7;
4501
+ border-left: 4px solid #f59e0b;
4502
+ padding: 12px 16px;
4503
+ margin-top: 20px;
4504
+ border-radius: 4px;
4505
+ font-size: 14px;
4506
+ }
4507
+ </style>
4508
+ </head>
4509
+ <body>
4510
+ <div class="container">
4511
+ <div class="header">
4512
+ <h1>\u{1F517} Your Magic Link</h1>
4513
+ </div>
4514
+
4515
+ <div class="content">
4516
+ <p>Hello!</p>
4517
+ <p>You requested a magic link to sign in to your account. Click the button below to continue:</p>
4518
+
4519
+ <div style="text-align: center;">
4520
+ <a href="${magicLink}" class="button">Sign In</a>
4521
+ </div>
4522
+
4523
+ <p class="expiry">\u23F0 This link expires in ${expiryMinutes} minutes</p>
4524
+
4525
+ <div class="security-note">
4526
+ <strong>Security Notice:</strong> If you didn't request this link, you can safely ignore this email.
4527
+ Someone may have entered your email address by mistake.
4528
+ </div>
4529
+ </div>
4530
+
4531
+ <div class="footer">
4532
+ <p>This is an automated email from SonicJS.</p>
4533
+ <p>For security, this link can only be used once.</p>
4534
+ </div>
4535
+ </div>
4536
+ </body>
4537
+ </html>
4538
+ `;
4539
+ }
4540
+ createMagicLinkAuthPlugin();
4541
+
4542
+ // src/plugins/cache/services/cache-config.ts
4543
+ var CACHE_CONFIGS = {
4544
+ // Content (high read, low write)
4545
+ content: {
4546
+ ttl: 3600,
4547
+ // 1 hour
4548
+ kvEnabled: true,
4549
+ memoryEnabled: true,
4550
+ namespace: "content",
4551
+ invalidateOn: ["content.update", "content.delete", "content.publish"],
4552
+ version: "v1"
4553
+ },
4554
+ // User data (medium read, medium write)
4555
+ user: {
4556
+ ttl: 900,
4557
+ // 15 minutes
4558
+ kvEnabled: true,
4559
+ memoryEnabled: true,
4560
+ namespace: "user",
4561
+ invalidateOn: ["user.update", "user.delete", "auth.login"],
4562
+ version: "v1"
4563
+ },
4564
+ // Configuration (high read, very low write)
4565
+ config: {
4566
+ ttl: 7200,
4567
+ // 2 hours
4568
+ kvEnabled: true,
4569
+ memoryEnabled: true,
4570
+ namespace: "config",
4571
+ invalidateOn: ["config.update", "plugin.activate", "plugin.deactivate"],
4572
+ version: "v1"
4573
+ },
4574
+ // Media metadata (high read, low write)
4575
+ media: {
4576
+ ttl: 3600,
4577
+ // 1 hour
4578
+ kvEnabled: true,
4579
+ memoryEnabled: true,
4580
+ namespace: "media",
4581
+ invalidateOn: ["media.upload", "media.delete", "media.update"],
4582
+ version: "v1"
4583
+ },
4584
+ // API responses (very high read, low write)
4585
+ api: {
4586
+ ttl: 300,
4587
+ // 5 minutes
4588
+ kvEnabled: true,
4589
+ memoryEnabled: true,
4590
+ namespace: "api",
4591
+ invalidateOn: ["content.update", "content.publish"],
4592
+ version: "v1"
4593
+ },
4594
+ // Session data (very high read, medium write)
4595
+ session: {
4596
+ ttl: 1800,
4597
+ // 30 minutes
4598
+ kvEnabled: false,
4599
+ // Only in-memory for sessions
4600
+ memoryEnabled: true,
4601
+ namespace: "session",
4602
+ invalidateOn: ["auth.logout"],
4603
+ version: "v1"
4604
+ },
4605
+ // Plugin data
4606
+ plugin: {
4607
+ ttl: 3600,
4608
+ // 1 hour
4609
+ kvEnabled: true,
4610
+ memoryEnabled: true,
4611
+ namespace: "plugin",
4612
+ invalidateOn: ["plugin.activate", "plugin.deactivate", "plugin.update"],
4613
+ version: "v1"
4614
+ },
4615
+ // Collections/schema
4616
+ collection: {
4617
+ ttl: 7200,
4618
+ // 2 hours
4619
+ kvEnabled: true,
4620
+ memoryEnabled: true,
4621
+ namespace: "collection",
4622
+ invalidateOn: ["collection.update", "collection.delete"],
4623
+ version: "v1"
4624
+ }
4625
+ };
4626
+ function getCacheConfig(namespace) {
4627
+ return CACHE_CONFIGS[namespace] || {
4628
+ ttl: 3600,
4629
+ kvEnabled: true,
4630
+ memoryEnabled: true,
4631
+ namespace,
4632
+ invalidateOn: [],
4633
+ version: "v1"
4634
+ };
4635
+ }
4636
+ function generateCacheKey(namespace, type, identifier, version) {
4637
+ const v = version || getCacheConfig(namespace).version || "v1";
4638
+ return `${namespace}:${type}:${identifier}:${v}`;
4639
+ }
4640
+ function parseCacheKey(key) {
4641
+ const parts = key.split(":");
4642
+ if (parts.length !== 4) {
4643
+ return null;
4644
+ }
4645
+ return {
4646
+ namespace: parts[0] || "",
4647
+ type: parts[1] || "",
4648
+ identifier: parts[2] || "",
4649
+ version: parts[3] || ""
4650
+ };
4651
+ }
4652
+
4653
+ // src/plugins/cache/services/cache.ts
4654
+ var MemoryCache = class {
4655
+ cache = /* @__PURE__ */ new Map();
4656
+ maxSize = 50 * 1024 * 1024;
4657
+ // 50MB
4658
+ currentSize = 0;
4659
+ /**
4660
+ * Get item from memory cache
4661
+ */
4662
+ get(key) {
4663
+ const entry = this.cache.get(key);
4664
+ if (!entry) {
4665
+ return null;
4666
+ }
4667
+ if (Date.now() > entry.expiresAt) {
4668
+ this.delete(key);
4669
+ return null;
4670
+ }
4671
+ return entry.data;
4672
+ }
4673
+ /**
4674
+ * Set item in memory cache
4675
+ */
4676
+ set(key, value, ttl, version = "v1") {
4677
+ const now = Date.now();
4678
+ const entry = {
4679
+ data: value,
4680
+ timestamp: now,
4681
+ expiresAt: now + ttl * 1e3,
4682
+ version
4683
+ };
4684
+ const entrySize = JSON.stringify(entry).length * 2;
4685
+ if (this.currentSize + entrySize > this.maxSize) {
4686
+ this.evictLRU(entrySize);
4687
+ }
4688
+ if (this.cache.has(key)) {
4689
+ this.delete(key);
4690
+ }
4691
+ this.cache.set(key, entry);
4692
+ this.currentSize += entrySize;
4693
+ }
4694
+ /**
4695
+ * Delete item from memory cache
4696
+ */
4697
+ delete(key) {
4698
+ const entry = this.cache.get(key);
4699
+ if (entry) {
4700
+ const entrySize = JSON.stringify(entry).length * 2;
4701
+ this.currentSize -= entrySize;
4702
+ return this.cache.delete(key);
4703
+ }
4704
+ return false;
4705
+ }
4706
+ /**
4707
+ * Clear all items from memory cache
4708
+ */
4709
+ clear() {
4710
+ this.cache.clear();
4711
+ this.currentSize = 0;
4712
+ }
4713
+ /**
4714
+ * Get cache statistics
4715
+ */
4716
+ getStats() {
4717
+ return {
4718
+ size: this.currentSize,
4719
+ count: this.cache.size
4720
+ };
4721
+ }
4722
+ /**
4723
+ * Evict least recently used items to make space
4724
+ */
4725
+ evictLRU(neededSpace) {
4726
+ const entries = Array.from(this.cache.entries()).sort(
4727
+ (a, b) => a[1].timestamp - b[1].timestamp
4728
+ );
4729
+ let freedSpace = 0;
4730
+ for (const [key, entry] of entries) {
4731
+ if (freedSpace >= neededSpace) break;
4732
+ const entrySize = JSON.stringify(entry).length * 2;
4733
+ this.delete(key);
4734
+ freedSpace += entrySize;
4735
+ }
4736
+ }
4737
+ /**
4738
+ * Delete items matching a pattern
4739
+ */
4740
+ invalidatePattern(pattern) {
4741
+ const regex = new RegExp(
4742
+ "^" + pattern.replace(/\*/g, ".*").replace(/\?/g, ".") + "$"
4743
+ );
4744
+ let count = 0;
4745
+ for (const key of this.cache.keys()) {
4746
+ if (regex.test(key)) {
4747
+ this.delete(key);
4748
+ count++;
4749
+ }
4750
+ }
4751
+ return count;
4752
+ }
4753
+ };
4754
+ var CacheService = class {
4755
+ memoryCache;
4756
+ config;
4757
+ stats;
4758
+ kvNamespace;
4759
+ constructor(config, kvNamespace) {
4760
+ this.memoryCache = new MemoryCache();
4761
+ this.config = config;
4762
+ this.kvNamespace = kvNamespace;
4763
+ this.stats = {
4764
+ memoryHits: 0,
4765
+ memoryMisses: 0,
4766
+ kvHits: 0,
4767
+ kvMisses: 0,
4768
+ dbHits: 0,
4769
+ totalRequests: 0,
4770
+ hitRate: 0,
4771
+ memorySize: 0,
4772
+ entryCount: 0
4773
+ };
4774
+ }
4775
+ /**
4776
+ * Get value from cache (tries memory first, then KV)
4777
+ */
4778
+ async get(key) {
4779
+ this.stats.totalRequests++;
4780
+ if (this.config.memoryEnabled) {
4781
+ const memoryValue = this.memoryCache.get(key);
4782
+ if (memoryValue !== null) {
4783
+ this.stats.memoryHits++;
4784
+ this.updateHitRate();
4785
+ return memoryValue;
4786
+ }
4787
+ this.stats.memoryMisses++;
4788
+ }
4789
+ if (this.config.kvEnabled && this.kvNamespace) {
4790
+ try {
4791
+ const kvValue = await this.kvNamespace.get(key, "json");
4792
+ if (kvValue !== null) {
4793
+ this.stats.kvHits++;
4794
+ if (this.config.memoryEnabled) {
4795
+ this.memoryCache.set(key, kvValue, this.config.ttl, this.config.version);
4796
+ }
4797
+ this.updateHitRate();
4798
+ return kvValue;
4799
+ }
4800
+ this.stats.kvMisses++;
4801
+ } catch (error) {
4802
+ console.error("KV cache read error:", error);
4803
+ this.stats.kvMisses++;
4804
+ }
4805
+ }
4806
+ this.updateHitRate();
4807
+ return null;
4808
+ }
4809
+ /**
4810
+ * Get value from cache with source information
4811
+ */
4812
+ async getWithSource(key) {
4813
+ this.stats.totalRequests++;
4814
+ if (this.config.memoryEnabled) {
4815
+ const memoryValue = this.memoryCache.get(key);
4816
+ if (memoryValue !== null) {
4817
+ this.stats.memoryHits++;
4818
+ this.updateHitRate();
4819
+ const entry = await this.getEntry(key);
4820
+ return {
4821
+ data: memoryValue,
4822
+ source: "memory",
4823
+ hit: true,
4824
+ timestamp: entry?.timestamp,
4825
+ ttl: entry?.ttl
4826
+ };
4827
+ }
4828
+ this.stats.memoryMisses++;
4829
+ }
4830
+ if (this.config.kvEnabled && this.kvNamespace) {
4831
+ try {
4832
+ const kvValue = await this.kvNamespace.get(key, "json");
4833
+ if (kvValue !== null) {
4834
+ this.stats.kvHits++;
4835
+ if (this.config.memoryEnabled) {
4836
+ this.memoryCache.set(key, kvValue, this.config.ttl, this.config.version);
4837
+ }
4838
+ this.updateHitRate();
4839
+ return {
4840
+ data: kvValue,
4841
+ source: "kv",
4842
+ hit: true
4843
+ };
4844
+ }
4845
+ this.stats.kvMisses++;
4846
+ } catch (error) {
4847
+ console.error("KV cache read error:", error);
4848
+ this.stats.kvMisses++;
4849
+ }
4850
+ }
4851
+ this.updateHitRate();
4852
+ return {
4853
+ data: null,
4854
+ source: "miss",
4855
+ hit: false
4856
+ };
4857
+ }
4858
+ /**
4859
+ * Set value in cache (stores in both memory and KV)
4860
+ */
4861
+ async set(key, value, customConfig) {
4862
+ const config = { ...this.config, ...customConfig };
4863
+ if (config.memoryEnabled) {
4864
+ this.memoryCache.set(key, value, config.ttl, config.version);
4865
+ }
4866
+ if (config.kvEnabled && this.kvNamespace) {
4867
+ try {
4868
+ await this.kvNamespace.put(key, JSON.stringify(value), {
4869
+ expirationTtl: config.ttl
4870
+ });
4871
+ } catch (error) {
4872
+ console.error("KV cache write error:", error);
4873
+ }
4874
+ }
4875
+ }
4876
+ /**
4877
+ * Delete value from cache (removes from both memory and KV)
4878
+ */
4879
+ async delete(key) {
4880
+ if (this.config.memoryEnabled) {
4881
+ this.memoryCache.delete(key);
4882
+ }
4883
+ if (this.config.kvEnabled && this.kvNamespace) {
4884
+ try {
4885
+ await this.kvNamespace.delete(key);
4886
+ } catch (error) {
4887
+ console.error("KV cache delete error:", error);
4888
+ }
4889
+ }
4890
+ }
4891
+ /**
4892
+ * Clear all cache entries for this namespace
4893
+ */
4894
+ async clear() {
4895
+ if (this.config.memoryEnabled) {
4896
+ this.memoryCache.clear();
4897
+ }
4898
+ this.stats = {
4899
+ memoryHits: 0,
4900
+ memoryMisses: 0,
4901
+ kvHits: 0,
4902
+ kvMisses: 0,
4903
+ dbHits: 0,
4904
+ totalRequests: 0,
4905
+ hitRate: 0,
4906
+ memorySize: 0,
4907
+ entryCount: 0
4908
+ };
4909
+ }
4910
+ /**
4911
+ * Invalidate cache entries matching a pattern
4912
+ */
4913
+ async invalidate(pattern) {
4914
+ let count = 0;
4915
+ if (this.config.memoryEnabled) {
4916
+ count += this.memoryCache.invalidatePattern(pattern);
4917
+ }
4918
+ if (this.config.kvEnabled && this.kvNamespace) {
4919
+ try {
4920
+ const regex = new RegExp(
4921
+ "^" + pattern.replace(/\*/g, ".*").replace(/\?/g, ".") + "$"
4922
+ );
4923
+ const prefix = this.config.namespace + ":";
4924
+ const list = await this.kvNamespace.list({ prefix });
4925
+ for (const key of list.keys) {
4926
+ if (regex.test(key.name)) {
4927
+ await this.kvNamespace.delete(key.name);
4928
+ count++;
4929
+ }
4930
+ }
4931
+ } catch (error) {
4932
+ console.error("KV cache invalidation error:", error);
4933
+ }
4934
+ }
4935
+ return count;
4936
+ }
4937
+ /**
4938
+ * Invalidate cache entries matching a pattern (alias for invalidate)
4939
+ */
4940
+ async invalidatePattern(pattern) {
4941
+ return this.invalidate(pattern);
4942
+ }
4943
+ /**
4944
+ * Get cache statistics
4945
+ */
4946
+ getStats() {
4947
+ const memStats = this.memoryCache.getStats();
4948
+ return {
4949
+ ...this.stats,
4950
+ memorySize: memStats.size,
4951
+ entryCount: memStats.count
4952
+ };
4953
+ }
4954
+ /**
4955
+ * Update hit rate calculation
4956
+ */
4957
+ updateHitRate() {
4958
+ const totalHits = this.stats.memoryHits + this.stats.kvHits + this.stats.dbHits;
4959
+ this.stats.hitRate = this.stats.totalRequests > 0 ? totalHits / this.stats.totalRequests * 100 : 0;
4960
+ }
4961
+ /**
4962
+ * Generate a cache key using the configured namespace
4963
+ */
4964
+ generateKey(type, identifier) {
4965
+ return generateCacheKey(
4966
+ this.config.namespace,
4967
+ type,
4968
+ identifier,
4969
+ this.config.version
4970
+ );
4971
+ }
4972
+ /**
4973
+ * Warm cache with multiple entries
4974
+ */
4975
+ async warmCache(entries) {
4976
+ for (const entry of entries) {
4977
+ await this.set(entry.key, entry.value);
4978
+ }
4979
+ }
4980
+ /**
4981
+ * Check if a key exists in cache
4982
+ */
4983
+ async has(key) {
4984
+ const value = await this.get(key);
4985
+ return value !== null;
4986
+ }
4987
+ /**
4988
+ * Get multiple values at once
4989
+ */
4990
+ async getMany(keys) {
4991
+ const results = /* @__PURE__ */ new Map();
4992
+ for (const key of keys) {
4993
+ const value = await this.get(key);
4994
+ if (value !== null) {
4995
+ results.set(key, value);
4996
+ }
4997
+ }
4998
+ return results;
4999
+ }
5000
+ /**
5001
+ * Set multiple values at once
5002
+ */
5003
+ async setMany(entries, customConfig) {
5004
+ for (const entry of entries) {
5005
+ await this.set(entry.key, entry.value, customConfig);
5006
+ }
5007
+ }
5008
+ /**
5009
+ * Delete multiple keys at once
5010
+ */
5011
+ async deleteMany(keys) {
5012
+ for (const key of keys) {
5013
+ await this.delete(key);
5014
+ }
5015
+ }
5016
+ /**
5017
+ * Get or set pattern - fetch from cache or compute if not found
5018
+ */
5019
+ async getOrSet(key, fetcher, customConfig) {
5020
+ const cached = await this.get(key);
5021
+ if (cached !== null) {
5022
+ return cached;
5023
+ }
5024
+ const value = await fetcher();
5025
+ await this.set(key, value, customConfig);
5026
+ return value;
5027
+ }
5028
+ /**
5029
+ * List all cache keys with metadata
5030
+ */
5031
+ async listKeys() {
5032
+ const keys = [];
5033
+ if (this.config.memoryEnabled) {
5034
+ const cache = this.memoryCache.cache;
5035
+ for (const [key, entry] of cache.entries()) {
5036
+ const size = JSON.stringify(entry).length * 2;
5037
+ const age = Date.now() - entry.timestamp;
5038
+ keys.push({
5039
+ key,
5040
+ size,
5041
+ expiresAt: entry.expiresAt,
5042
+ age
5043
+ });
5044
+ }
5045
+ }
5046
+ return keys.sort((a, b) => a.age - b.age);
5047
+ }
5048
+ /**
5049
+ * Get cache entry with full metadata
5050
+ */
5051
+ async getEntry(key) {
5052
+ if (!this.config.memoryEnabled) {
5053
+ return null;
5054
+ }
5055
+ const cache = this.memoryCache.cache;
5056
+ const entry = cache.get(key);
5057
+ if (!entry) {
5058
+ return null;
5059
+ }
5060
+ if (Date.now() > entry.expiresAt) {
5061
+ await this.delete(key);
5062
+ return null;
5063
+ }
5064
+ const size = JSON.stringify(entry).length * 2;
5065
+ const ttl = Math.max(0, entry.expiresAt - Date.now()) / 1e3;
5066
+ return {
5067
+ data: entry.data,
5068
+ timestamp: entry.timestamp,
5069
+ expiresAt: entry.expiresAt,
5070
+ ttl,
5071
+ size
5072
+ };
5073
+ }
5074
+ };
5075
+ var cacheInstances = /* @__PURE__ */ new Map();
5076
+ var globalKVNamespace;
5077
+ function getCacheService(config, kvNamespace) {
5078
+ const key = config.namespace;
5079
+ if (!cacheInstances.has(key)) {
5080
+ const kv = globalKVNamespace;
5081
+ cacheInstances.set(key, new CacheService(config, kv));
5082
+ }
5083
+ return cacheInstances.get(key);
5084
+ }
5085
+ async function clearAllCaches() {
5086
+ for (const cache of cacheInstances.values()) {
5087
+ await cache.clear();
5088
+ }
5089
+ }
5090
+ function getAllCacheStats() {
5091
+ const stats = {};
5092
+ for (const [namespace, cache] of cacheInstances.entries()) {
5093
+ stats[namespace] = cache.getStats();
5094
+ }
5095
+ return stats;
5096
+ }
5097
+
5098
+ // src/plugins/cache/services/event-bus.ts
5099
+ var EventBus = class {
5100
+ subscriptions = /* @__PURE__ */ new Map();
5101
+ eventLog = [];
5102
+ maxLogSize = 100;
5103
+ /**
5104
+ * Subscribe to an event
5105
+ */
5106
+ on(event, handler) {
5107
+ if (!this.subscriptions.has(event)) {
5108
+ this.subscriptions.set(event, []);
5109
+ }
5110
+ this.subscriptions.get(event).push(handler);
5111
+ return () => {
5112
+ const handlers = this.subscriptions.get(event);
5113
+ if (handlers) {
5114
+ const index = handlers.indexOf(handler);
5115
+ if (index > -1) {
5116
+ handlers.splice(index, 1);
5117
+ }
5118
+ }
5119
+ };
5120
+ }
5121
+ /**
5122
+ * Emit an event to all subscribers
5123
+ */
5124
+ async emit(event, data) {
5125
+ this.logEvent(event, data);
5126
+ const handlers = this.subscriptions.get(event) || [];
5127
+ await Promise.all(
5128
+ handlers.map(async (handler) => {
5129
+ try {
5130
+ await handler(data);
5131
+ } catch (error) {
5132
+ console.error(`Error in event handler for ${event}:`, error);
5133
+ }
5134
+ })
5135
+ );
5136
+ const wildcardHandlers = this.subscriptions.get("*") || [];
5137
+ await Promise.all(
5138
+ wildcardHandlers.map(async (handler) => {
5139
+ try {
5140
+ await handler({ event, data });
5141
+ } catch (error) {
5142
+ console.error(`Error in wildcard event handler for ${event}:`, error);
5143
+ }
5144
+ })
5145
+ );
5146
+ }
5147
+ /**
5148
+ * Remove all subscribers for an event
5149
+ */
5150
+ off(event) {
5151
+ this.subscriptions.delete(event);
5152
+ }
5153
+ /**
5154
+ * Get all registered events
5155
+ */
5156
+ getEvents() {
5157
+ return Array.from(this.subscriptions.keys());
5158
+ }
5159
+ /**
5160
+ * Get subscriber count for an event
5161
+ */
5162
+ getSubscriberCount(event) {
5163
+ return this.subscriptions.get(event)?.length || 0;
5164
+ }
5165
+ /**
5166
+ * Log an event for debugging
5167
+ */
5168
+ logEvent(event, data) {
5169
+ this.eventLog.push({
5170
+ event,
5171
+ timestamp: Date.now(),
5172
+ data
5173
+ });
5174
+ if (this.eventLog.length > this.maxLogSize) {
5175
+ this.eventLog.shift();
5176
+ }
5177
+ }
5178
+ /**
5179
+ * Get recent event log
5180
+ */
5181
+ getEventLog(limit = 50) {
5182
+ return this.eventLog.slice(-limit);
5183
+ }
5184
+ /**
5185
+ * Clear event log
5186
+ */
5187
+ clearEventLog() {
5188
+ this.eventLog = [];
5189
+ }
5190
+ /**
5191
+ * Get statistics
5192
+ */
5193
+ getStats() {
5194
+ const eventCounts = {};
5195
+ for (const log of this.eventLog) {
5196
+ eventCounts[log.event] = (eventCounts[log.event] || 0) + 1;
5197
+ }
5198
+ return {
5199
+ totalEvents: this.eventLog.length,
5200
+ totalSubscriptions: this.subscriptions.size,
5201
+ eventCounts
5202
+ };
5203
+ }
5204
+ };
5205
+ var globalEventBus = null;
5206
+ function getEventBus() {
5207
+ if (!globalEventBus) {
5208
+ globalEventBus = new EventBus();
5209
+ }
5210
+ return globalEventBus;
5211
+ }
5212
+ function onEvent(event, handler) {
5213
+ const bus = getEventBus();
5214
+ return bus.on(event, handler);
5215
+ }
5216
+
5217
+ // src/plugins/cache/services/cache-invalidation.ts
5218
+ function setupCacheInvalidation() {
5219
+ getEventBus();
5220
+ setupContentInvalidation();
5221
+ setupUserInvalidation();
5222
+ setupConfigInvalidation();
5223
+ setupMediaInvalidation();
5224
+ setupAPIInvalidation();
5225
+ setupCollectionInvalidation();
5226
+ console.log("Cache invalidation listeners registered");
5227
+ }
5228
+ function setupContentInvalidation() {
5229
+ const config = CACHE_CONFIGS.content;
5230
+ if (!config) return;
5231
+ const contentCache = getCacheService(config);
5232
+ onEvent("content.create", async (_data) => {
5233
+ await contentCache.invalidate("content:*");
5234
+ console.log("Cache invalidated: content.create");
5235
+ });
5236
+ onEvent("content.update", async (data) => {
5237
+ if (data?.id) {
5238
+ await contentCache.delete(contentCache.generateKey("item", data.id));
5239
+ }
5240
+ await contentCache.invalidate("content:list:*");
5241
+ console.log("Cache invalidated: content.update", data?.id);
5242
+ });
5243
+ onEvent("content.delete", async (data) => {
5244
+ if (data?.id) {
5245
+ await contentCache.delete(contentCache.generateKey("item", data.id));
5246
+ }
5247
+ await contentCache.invalidate("content:*");
5248
+ console.log("Cache invalidated: content.delete", data?.id);
5249
+ });
5250
+ onEvent("content.publish", async (_data) => {
5251
+ await contentCache.invalidate("content:*");
5252
+ console.log("Cache invalidated: content.publish");
5253
+ });
5254
+ }
5255
+ function setupUserInvalidation() {
5256
+ const config = CACHE_CONFIGS.user;
5257
+ if (!config) return;
5258
+ const userCache = getCacheService(config);
5259
+ onEvent("user.update", async (data) => {
5260
+ if (data?.id) {
5261
+ await userCache.delete(userCache.generateKey("id", data.id));
5262
+ }
5263
+ if (data?.email) {
5264
+ await userCache.delete(userCache.generateKey("email", data.email));
5265
+ }
5266
+ console.log("Cache invalidated: user.update", data?.id);
5267
+ });
5268
+ onEvent("user.delete", async (data) => {
5269
+ if (data?.id) {
5270
+ await userCache.delete(userCache.generateKey("id", data.id));
5271
+ }
5272
+ if (data?.email) {
5273
+ await userCache.delete(userCache.generateKey("email", data.email));
5274
+ }
5275
+ console.log("Cache invalidated: user.delete", data?.id);
5276
+ });
5277
+ onEvent("auth.login", async (data) => {
5278
+ if (data?.userId) {
5279
+ await userCache.delete(userCache.generateKey("id", data.userId));
5280
+ }
5281
+ console.log("Cache invalidated: auth.login", data?.userId);
5282
+ });
5283
+ onEvent("auth.logout", async (data) => {
5284
+ const sessionConfig = CACHE_CONFIGS.session;
5285
+ if (sessionConfig) {
5286
+ const sessionCache = getCacheService(sessionConfig);
5287
+ if (data?.sessionId) {
5288
+ await sessionCache.delete(sessionCache.generateKey("session", data.sessionId));
5289
+ }
5290
+ }
5291
+ console.log("Cache invalidated: auth.logout");
5292
+ });
5293
+ }
5294
+ function setupConfigInvalidation() {
5295
+ const configConfig = CACHE_CONFIGS.config;
5296
+ if (!configConfig) return;
5297
+ const configCache = getCacheService(configConfig);
5298
+ onEvent("config.update", async (_data) => {
5299
+ await configCache.invalidate("config:*");
5300
+ console.log("Cache invalidated: config.update");
5301
+ });
5302
+ onEvent("plugin.activate", async (data) => {
5303
+ await configCache.invalidate("config:*");
5304
+ const pluginConfig = CACHE_CONFIGS.plugin;
5305
+ if (pluginConfig) {
5306
+ const pluginCache = getCacheService(pluginConfig);
5307
+ await pluginCache.invalidate("plugin:*");
5308
+ }
5309
+ console.log("Cache invalidated: plugin.activate", data?.pluginId);
5310
+ });
5311
+ onEvent("plugin.deactivate", async (data) => {
5312
+ await configCache.invalidate("config:*");
5313
+ const pluginConfig = CACHE_CONFIGS.plugin;
5314
+ if (pluginConfig) {
5315
+ const pluginCache = getCacheService(pluginConfig);
5316
+ await pluginCache.invalidate("plugin:*");
5317
+ }
5318
+ console.log("Cache invalidated: plugin.deactivate", data?.pluginId);
5319
+ });
5320
+ onEvent("plugin.update", async (data) => {
5321
+ const pluginConfig = CACHE_CONFIGS.plugin;
5322
+ if (!pluginConfig) return;
5323
+ const pluginCache = getCacheService(pluginConfig);
5324
+ await pluginCache.invalidate("plugin:*");
5325
+ console.log("Cache invalidated: plugin.update", data?.pluginId);
5326
+ });
5327
+ }
5328
+ function setupMediaInvalidation() {
5329
+ const config = CACHE_CONFIGS.media;
5330
+ if (!config) return;
5331
+ const mediaCache = getCacheService(config);
5332
+ onEvent("media.upload", async (_data) => {
5333
+ await mediaCache.invalidate("media:*");
5334
+ console.log("Cache invalidated: media.upload");
5335
+ });
5336
+ onEvent("media.delete", async (data) => {
5337
+ if (data?.id) {
5338
+ await mediaCache.delete(mediaCache.generateKey("item", data.id));
5339
+ }
5340
+ await mediaCache.invalidate("media:list:*");
5341
+ console.log("Cache invalidated: media.delete", data?.id);
5342
+ });
5343
+ onEvent("media.update", async (data) => {
5344
+ if (data?.id) {
5345
+ await mediaCache.delete(mediaCache.generateKey("item", data.id));
5346
+ }
5347
+ await mediaCache.invalidate("media:list:*");
5348
+ console.log("Cache invalidated: media.update", data?.id);
5349
+ });
5350
+ }
5351
+ function setupAPIInvalidation() {
5352
+ const config = CACHE_CONFIGS.api;
5353
+ if (!config) return;
5354
+ const apiCache = getCacheService(config);
5355
+ onEvent("content.update", async (_data) => {
5356
+ await apiCache.invalidate("api:*");
5357
+ console.log("Cache invalidated: api (content.update)");
5358
+ });
5359
+ onEvent("content.publish", async (_data) => {
5360
+ await apiCache.invalidate("api:*");
5361
+ console.log("Cache invalidated: api (content.publish)");
5362
+ });
5363
+ onEvent("content.create", async (_data) => {
5364
+ await apiCache.invalidate("api:*");
5365
+ console.log("Cache invalidated: api (content.create)");
5366
+ });
5367
+ onEvent("content.delete", async (_data) => {
5368
+ await apiCache.invalidate("api:*");
5369
+ console.log("Cache invalidated: api (content.delete)");
5370
+ });
5371
+ onEvent("collection.update", async (_data) => {
5372
+ await apiCache.invalidate("api:*");
5373
+ console.log("Cache invalidated: api (collection.update)");
5374
+ });
5375
+ }
5376
+ function setupCollectionInvalidation() {
5377
+ const config = CACHE_CONFIGS.collection;
5378
+ if (!config) return;
5379
+ const collectionCache = getCacheService(config);
5380
+ onEvent("collection.create", async (_data) => {
5381
+ await collectionCache.invalidate("collection:*");
5382
+ console.log("Cache invalidated: collection.create");
5383
+ });
5384
+ onEvent("collection.update", async (data) => {
5385
+ if (data?.id) {
5386
+ await collectionCache.delete(collectionCache.generateKey("item", data.id));
5387
+ }
5388
+ await collectionCache.invalidate("collection:*");
5389
+ console.log("Cache invalidated: collection.update", data?.id);
5390
+ });
5391
+ onEvent("collection.delete", async (data) => {
5392
+ await collectionCache.invalidate("collection:*");
5393
+ console.log("Cache invalidated: collection.delete", data?.id);
5394
+ });
5395
+ }
5396
+ function getCacheInvalidationStats() {
5397
+ const eventBus = getEventBus();
5398
+ return eventBus.getStats();
5399
+ }
5400
+ function getRecentInvalidations(limit = 50) {
5401
+ const eventBus = getEventBus();
5402
+ return eventBus.getEventLog(limit);
5403
+ }
5404
+
5405
+ // src/plugins/cache/services/cache-warming.ts
5406
+ async function warmCommonCaches(db) {
5407
+ let totalWarmed = 0;
5408
+ let totalErrors = 0;
5409
+ const details = [];
5410
+ try {
5411
+ const collectionCount = await warmCollections(db);
5412
+ totalWarmed += collectionCount;
5413
+ details.push({ namespace: "collection", count: collectionCount });
5414
+ const contentCount = await warmRecentContent(db);
5415
+ totalWarmed += contentCount;
5416
+ details.push({ namespace: "content", count: contentCount });
5417
+ const mediaCount = await warmRecentMedia(db);
5418
+ totalWarmed += mediaCount;
5419
+ details.push({ namespace: "media", count: mediaCount });
5420
+ } catch (error) {
5421
+ console.error("Error warming caches:", error);
5422
+ totalErrors++;
5423
+ }
5424
+ return {
5425
+ warmed: totalWarmed,
5426
+ errors: totalErrors,
5427
+ details
5428
+ };
5429
+ }
5430
+ async function warmCollections(db) {
5431
+ const config = CACHE_CONFIGS.collection;
5432
+ if (!config) return 0;
5433
+ const collectionCache = getCacheService(config);
5434
+ let count = 0;
5435
+ try {
5436
+ const stmt = db.prepare("SELECT * FROM collections WHERE is_active = 1");
5437
+ const { results } = await stmt.all();
5438
+ for (const collection of results) {
5439
+ const key = collectionCache.generateKey("item", collection.id);
5440
+ await collectionCache.set(key, collection);
5441
+ count++;
5442
+ }
5443
+ const listKey = collectionCache.generateKey("list", "all");
5444
+ await collectionCache.set(listKey, results);
5445
+ count++;
5446
+ } catch (error) {
5447
+ console.error("Error warming collections cache:", error);
5448
+ }
5449
+ return count;
5450
+ }
5451
+ async function warmRecentContent(db, limit = 50) {
5452
+ const config = CACHE_CONFIGS.content;
5453
+ if (!config) return 0;
5454
+ const contentCache = getCacheService(config);
5455
+ let count = 0;
5456
+ try {
5457
+ const stmt = db.prepare(`SELECT * FROM content ORDER BY created_at DESC LIMIT ${limit}`);
5458
+ const { results } = await stmt.all();
5459
+ for (const content2 of results) {
5460
+ const key = contentCache.generateKey("item", content2.id);
5461
+ await contentCache.set(key, content2);
5462
+ count++;
5463
+ }
5464
+ const listKey = contentCache.generateKey("list", "recent");
5465
+ await contentCache.set(listKey, results);
5466
+ count++;
5467
+ } catch (error) {
5468
+ console.error("Error warming content cache:", error);
5469
+ }
5470
+ return count;
5471
+ }
5472
+ async function warmRecentMedia(db, limit = 50) {
5473
+ const config = CACHE_CONFIGS.media;
5474
+ if (!config) return 0;
5475
+ const mediaCache = getCacheService(config);
5476
+ let count = 0;
5477
+ try {
5478
+ const stmt = db.prepare(`SELECT * FROM media WHERE deleted_at IS NULL ORDER BY uploaded_at DESC LIMIT ${limit}`);
5479
+ const { results } = await stmt.all();
5480
+ for (const media2 of results) {
5481
+ const key = mediaCache.generateKey("item", media2.id);
5482
+ await mediaCache.set(key, media2);
5483
+ count++;
5484
+ }
5485
+ const listKey = mediaCache.generateKey("list", "recent");
5486
+ await mediaCache.set(listKey, results);
5487
+ count++;
5488
+ } catch (error) {
5489
+ console.error("Error warming media cache:", error);
5490
+ }
5491
+ return count;
5492
+ }
5493
+ async function warmNamespace(namespace, entries) {
5494
+ const config = CACHE_CONFIGS[namespace];
5495
+ if (!config) {
5496
+ throw new Error(`Unknown namespace: ${namespace}`);
5497
+ }
5498
+ const cache = getCacheService(config);
5499
+ await cache.setMany(entries);
5500
+ return entries.length;
5501
+ }
5502
+
5503
+ // src/templates/pages/admin-cache.template.ts
5504
+ init_admin_layout_catalyst_template();
5505
+ function renderCacheDashboard(data) {
5506
+ const pageContent = `
5507
+ <div class="space-y-6">
5508
+ <!-- Header -->
5509
+ <div class="flex items-center justify-between">
5510
+ <div>
5511
+ <h1 class="text-2xl font-semibold text-zinc-950 dark:text-white">Cache System</h1>
5512
+ <p class="mt-1 text-sm text-zinc-600 dark:text-zinc-400">
5513
+ Monitor and manage cache performance across all namespaces
5514
+ </p>
5515
+ </div>
5516
+ <div class="flex gap-3">
5517
+ <button
5518
+ onclick="refreshStats()"
5519
+ class="inline-flex items-center gap-2 rounded-lg bg-white dark:bg-zinc-900 px-4 py-2 text-sm font-medium text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 hover:bg-zinc-50 dark:hover:bg-zinc-800"
5520
+ >
5521
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
5522
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
5523
+ </svg>
5524
+ Refresh
5525
+ </button>
5526
+ <button
5527
+ onclick="clearAllCaches()"
5528
+ class="inline-flex items-center gap-2 rounded-lg bg-red-600 dark:bg-red-500 px-4 py-2 text-sm font-medium text-white hover:bg-red-700 dark:hover:bg-red-600"
5529
+ >
5530
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
5531
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
5532
+ </svg>
5533
+ Clear All
5534
+ </button>
2705
5535
  </div>
5536
+ </div>
2706
5537
 
2707
- <div class="content">
2708
- <p>Hello!</p>
2709
- <p>You requested a magic link to sign in to your account. Click the button below to continue:</p>
5538
+ <!-- Overall Stats Cards -->
5539
+ <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
5540
+ ${renderStatCard("Total Requests", data.totals.requests.toLocaleString(), "lime", `
5541
+ <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
5542
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/>
5543
+ </svg>
5544
+ `)}
2710
5545
 
2711
- <div style="text-align: center;">
2712
- <a href="${magicLink}" class="button">Sign In</a>
2713
- </div>
5546
+ ${renderStatCard("Hit Rate", data.totals.hitRate + "%", "blue", `
5547
+ <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
5548
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
5549
+ </svg>
5550
+ `, parseFloat(data.totals.hitRate) > 70 ? "lime" : parseFloat(data.totals.hitRate) > 40 ? "amber" : "red")}
2714
5551
 
2715
- <p class="expiry">\u23F0 This link expires in ${expiryMinutes} minutes</p>
5552
+ ${renderStatCard("Memory Usage", formatBytes(data.totals.memorySize), "purple", `
5553
+ <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
5554
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"/>
5555
+ </svg>
5556
+ `)}
2716
5557
 
2717
- <div class="security-note">
2718
- <strong>Security Notice:</strong> If you didn't request this link, you can safely ignore this email.
2719
- Someone may have entered your email address by mistake.
5558
+ ${renderStatCard("Cached Entries", data.totals.entryCount.toLocaleString(), "sky", `
5559
+ <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
5560
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"/>
5561
+ </svg>
5562
+ `)}
5563
+ </div>
5564
+
5565
+ <!-- Namespace Statistics -->
5566
+ <div class="overflow-hidden rounded-xl bg-white dark:bg-zinc-900 ring-1 ring-zinc-950/5 dark:ring-white/10">
5567
+ <div class="px-6 py-4 border-b border-zinc-950/5 dark:border-white/10">
5568
+ <h2 class="text-lg font-semibold text-zinc-950 dark:text-white">Cache Namespaces</h2>
5569
+ </div>
5570
+ <div class="overflow-x-auto">
5571
+ <table class="min-w-full divide-y divide-zinc-950/5 dark:divide-white/10">
5572
+ <thead class="bg-zinc-50 dark:bg-zinc-800/50">
5573
+ <tr>
5574
+ <th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">
5575
+ Namespace
5576
+ </th>
5577
+ <th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">
5578
+ Requests
5579
+ </th>
5580
+ <th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">
5581
+ Hit Rate
5582
+ </th>
5583
+ <th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">
5584
+ Memory Hits
5585
+ </th>
5586
+ <th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">
5587
+ KV Hits
5588
+ </th>
5589
+ <th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">
5590
+ Entries
5591
+ </th>
5592
+ <th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">
5593
+ Size
5594
+ </th>
5595
+ <th class="px-6 py-3 text-right text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">
5596
+ Actions
5597
+ </th>
5598
+ </tr>
5599
+ </thead>
5600
+ <tbody class="divide-y divide-zinc-950/5 dark:divide-white/10">
5601
+ ${data.namespaces.map((namespace) => {
5602
+ const stat = data.stats[namespace];
5603
+ if (!stat) return "";
5604
+ return renderNamespaceRow(namespace, stat);
5605
+ }).join("")}
5606
+ </tbody>
5607
+ </table>
5608
+ </div>
5609
+ </div>
5610
+
5611
+ <!-- Performance Chart Placeholder -->
5612
+ <div class="overflow-hidden rounded-xl bg-white dark:bg-zinc-900 ring-1 ring-zinc-950/5 dark:ring-white/10">
5613
+ <div class="px-6 py-4 border-b border-zinc-950/5 dark:border-white/10">
5614
+ <h2 class="text-lg font-semibold text-zinc-950 dark:text-white">Performance Overview</h2>
5615
+ </div>
5616
+ <div class="p-6">
5617
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
5618
+ ${renderPerformanceMetric("Memory Cache", data.totals.hits, data.totals.misses)}
5619
+ ${renderHealthStatus(parseFloat(data.totals.hitRate))}
2720
5620
  </div>
2721
5621
  </div>
5622
+ </div>
5623
+ </div>
2722
5624
 
2723
- <div class="footer">
2724
- <p>This is an automated email from SonicJS.</p>
2725
- <p>For security, this link can only be used once.</p>
5625
+ <script>
5626
+ async function refreshStats() {
5627
+ window.location.reload()
5628
+ }
5629
+
5630
+ async function clearAllCaches() {
5631
+ showConfirmDialog('clear-all-cache-confirm')
5632
+ }
5633
+
5634
+ async function performClearAllCaches() {
5635
+ try {
5636
+ const response = await fetch('/admin/cache/clear', {
5637
+ method: 'POST'
5638
+ })
5639
+
5640
+ const result = await response.json()
5641
+ if (result.success) {
5642
+ alert('All caches cleared successfully')
5643
+ window.location.reload()
5644
+ } else {
5645
+ alert('Error clearing caches: ' + result.error)
5646
+ }
5647
+ } catch (error) {
5648
+ alert('Error clearing caches: ' + error.message)
5649
+ }
5650
+ }
5651
+
5652
+ let namespaceToDelete = null
5653
+
5654
+ async function clearNamespaceCache(namespace) {
5655
+ namespaceToDelete = namespace
5656
+ showConfirmDialog('clear-namespace-cache-confirm')
5657
+ }
5658
+
5659
+ async function performClearNamespaceCache() {
5660
+ if (!namespaceToDelete) return
5661
+
5662
+ try {
5663
+ const response = await fetch(\`/admin/cache/clear/\${namespaceToDelete}\`, {
5664
+ method: 'POST'
5665
+ })
5666
+
5667
+ const result = await response.json()
5668
+ if (result.success) {
5669
+ alert('Cache cleared successfully')
5670
+ window.location.reload()
5671
+ } else {
5672
+ alert('Error clearing cache: ' + result.error)
5673
+ }
5674
+ } catch (error) {
5675
+ alert('Error clearing cache: ' + error.message)
5676
+ } finally {
5677
+ namespaceToDelete = null
5678
+ }
5679
+ }
5680
+ </script>
5681
+
5682
+ <!-- Confirmation Dialogs -->
5683
+ ${renderConfirmationDialog({
5684
+ id: "clear-all-cache-confirm",
5685
+ title: "Clear All Cache",
5686
+ message: "Are you sure you want to clear all cache entries? This cannot be undone.",
5687
+ confirmText: "Clear All",
5688
+ cancelText: "Cancel",
5689
+ iconColor: "yellow",
5690
+ confirmClass: "bg-yellow-500 hover:bg-yellow-400",
5691
+ onConfirm: "performClearAllCaches()"
5692
+ })}
5693
+
5694
+ ${renderConfirmationDialog({
5695
+ id: "clear-namespace-cache-confirm",
5696
+ title: "Clear Namespace Cache",
5697
+ message: "Clear cache for this namespace?",
5698
+ confirmText: "Clear",
5699
+ cancelText: "Cancel",
5700
+ iconColor: "yellow",
5701
+ confirmClass: "bg-yellow-500 hover:bg-yellow-400",
5702
+ onConfirm: "performClearNamespaceCache()"
5703
+ })}
5704
+
5705
+ ${getConfirmationDialogScript()}
5706
+ `;
5707
+ const layoutData = {
5708
+ title: "Cache System",
5709
+ pageTitle: "Cache System",
5710
+ currentPath: "/admin/cache",
5711
+ user: data.user,
5712
+ version: data.version,
5713
+ content: pageContent
5714
+ };
5715
+ return renderAdminLayoutCatalyst(layoutData);
5716
+ }
5717
+ function renderStatCard(label, value, color, icon, colorOverride) {
5718
+ const finalColor = colorOverride || color;
5719
+ const colorClasses = {
5720
+ lime: "bg-lime-50 dark:bg-lime-500/10 text-lime-600 dark:text-lime-400 ring-lime-600/20 dark:ring-lime-500/20",
5721
+ blue: "bg-blue-50 dark:bg-blue-500/10 text-blue-600 dark:text-blue-400 ring-blue-600/20 dark:ring-blue-500/20",
5722
+ purple: "bg-purple-50 dark:bg-purple-500/10 text-purple-600 dark:text-purple-400 ring-purple-600/20 dark:ring-purple-500/20",
5723
+ sky: "bg-sky-50 dark:bg-sky-500/10 text-sky-600 dark:text-sky-400 ring-sky-600/20 dark:ring-sky-500/20",
5724
+ amber: "bg-amber-50 dark:bg-amber-500/10 text-amber-600 dark:text-amber-400 ring-amber-600/20 dark:ring-amber-500/20",
5725
+ red: "bg-red-50 dark:bg-red-500/10 text-red-600 dark:text-red-400 ring-red-600/20 dark:ring-red-500/20"
5726
+ };
5727
+ return `
5728
+ <div class="overflow-hidden rounded-xl bg-white dark:bg-zinc-900 ring-1 ring-zinc-950/5 dark:ring-white/10">
5729
+ <div class="p-6">
5730
+ <div class="flex items-center justify-between">
5731
+ <div class="flex items-center gap-3">
5732
+ <div class="rounded-lg p-2 ring-1 ring-inset ${colorClasses[finalColor]}">
5733
+ ${icon}
5734
+ </div>
5735
+ <div>
5736
+ <p class="text-sm text-zinc-600 dark:text-zinc-400">${label}</p>
5737
+ <p class="mt-1 text-2xl font-semibold text-zinc-950 dark:text-white">${value}</p>
5738
+ </div>
5739
+ </div>
2726
5740
  </div>
2727
5741
  </div>
2728
- </body>
2729
- </html>
5742
+ </div>
2730
5743
  `;
2731
5744
  }
2732
- createMagicLinkAuthPlugin();
5745
+ function renderNamespaceRow(namespace, stat) {
5746
+ const hitRate = stat.hitRate.toFixed(1);
5747
+ const hitRateColor = stat.hitRate > 70 ? "text-lime-600 dark:text-lime-400" : stat.hitRate > 40 ? "text-amber-600 dark:text-amber-400" : "text-red-600 dark:text-red-400";
5748
+ return `
5749
+ <tr class="hover:bg-zinc-50 dark:hover:bg-zinc-800/50">
5750
+ <td class="px-6 py-4 whitespace-nowrap">
5751
+ <span class="inline-flex items-center rounded-md px-2 py-1 text-xs font-medium bg-zinc-100 dark:bg-zinc-800 text-zinc-900 dark:text-zinc-100 ring-1 ring-inset ring-zinc-200 dark:ring-zinc-700">
5752
+ ${namespace}
5753
+ </span>
5754
+ </td>
5755
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-zinc-900 dark:text-zinc-100">
5756
+ ${stat.totalRequests.toLocaleString()}
5757
+ </td>
5758
+ <td class="px-6 py-4 whitespace-nowrap">
5759
+ <span class="text-sm font-medium ${hitRateColor}">
5760
+ ${hitRate}%
5761
+ </span>
5762
+ </td>
5763
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-zinc-600 dark:text-zinc-400">
5764
+ ${stat.memoryHits.toLocaleString()}
5765
+ </td>
5766
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-zinc-600 dark:text-zinc-400">
5767
+ ${stat.kvHits.toLocaleString()}
5768
+ </td>
5769
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-zinc-600 dark:text-zinc-400">
5770
+ ${stat.entryCount.toLocaleString()}
5771
+ </td>
5772
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-zinc-600 dark:text-zinc-400">
5773
+ ${formatBytes(stat.memorySize)}
5774
+ </td>
5775
+ <td class="px-6 py-4 whitespace-nowrap text-right text-sm">
5776
+ <button
5777
+ onclick="clearNamespaceCache('${namespace}')"
5778
+ class="text-red-600 dark:text-red-400 hover:text-red-700 dark:hover:text-red-300"
5779
+ >
5780
+ Clear
5781
+ </button>
5782
+ </td>
5783
+ </tr>
5784
+ `;
5785
+ }
5786
+ function renderPerformanceMetric(label, hits, misses) {
5787
+ const total = hits + misses;
5788
+ const hitPercentage = total > 0 ? hits / total * 100 : 0;
5789
+ return `
5790
+ <div>
5791
+ <h3 class="text-sm font-medium text-zinc-900 dark:text-zinc-100 mb-3">${label}</h3>
5792
+ <div class="space-y-2">
5793
+ <div class="flex items-center justify-between text-sm">
5794
+ <span class="text-zinc-600 dark:text-zinc-400">Hits</span>
5795
+ <span class="font-medium text-zinc-900 dark:text-zinc-100">${hits.toLocaleString()}</span>
5796
+ </div>
5797
+ <div class="flex items-center justify-between text-sm">
5798
+ <span class="text-zinc-600 dark:text-zinc-400">Misses</span>
5799
+ <span class="font-medium text-zinc-900 dark:text-zinc-100">${misses.toLocaleString()}</span>
5800
+ </div>
5801
+ <div class="mt-3">
5802
+ <div class="flex items-center justify-between text-sm mb-1">
5803
+ <span class="text-zinc-600 dark:text-zinc-400">Hit Rate</span>
5804
+ <span class="font-medium text-zinc-900 dark:text-zinc-100">${hitPercentage.toFixed(1)}%</span>
5805
+ </div>
5806
+ <div class="h-2 bg-zinc-200 dark:bg-zinc-700 rounded-full overflow-hidden">
5807
+ <div class="h-full bg-lime-500 dark:bg-lime-400" style="width: ${hitPercentage}%"></div>
5808
+ </div>
5809
+ </div>
5810
+ </div>
5811
+ </div>
5812
+ `;
5813
+ }
5814
+ function renderHealthStatus(hitRate) {
5815
+ const status = hitRate > 70 ? "healthy" : hitRate > 40 ? "warning" : "critical";
5816
+ const statusConfig = {
5817
+ healthy: {
5818
+ label: "Healthy",
5819
+ color: "lime",
5820
+ icon: `<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
5821
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
5822
+ </svg>`
5823
+ },
5824
+ warning: {
5825
+ label: "Needs Attention",
5826
+ color: "amber",
5827
+ icon: `<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
5828
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
5829
+ </svg>`
5830
+ },
5831
+ critical: {
5832
+ label: "Critical",
5833
+ color: "red",
5834
+ icon: `<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
5835
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
5836
+ </svg>`
5837
+ }
5838
+ };
5839
+ const config = statusConfig[status];
5840
+ const colorClasses = {
5841
+ lime: "bg-lime-50 dark:bg-lime-500/10 text-lime-600 dark:text-lime-400 ring-lime-600/20 dark:ring-lime-500/20",
5842
+ amber: "bg-amber-50 dark:bg-amber-500/10 text-amber-600 dark:text-amber-400 ring-amber-600/20 dark:ring-amber-500/20",
5843
+ red: "bg-red-50 dark:bg-red-500/10 text-red-600 dark:text-red-400 ring-red-600/20 dark:ring-red-500/20"
5844
+ };
5845
+ return `
5846
+ <div>
5847
+ <h3 class="text-sm font-medium text-zinc-900 dark:text-zinc-100 mb-3">System Health</h3>
5848
+ <div class="flex items-center gap-3 p-4 rounded-lg ring-1 ring-inset ${colorClasses[config.color]}">
5849
+ ${config.icon}
5850
+ <div>
5851
+ <p class="text-sm font-medium">${config.label}</p>
5852
+ <p class="text-xs mt-0.5 opacity-80">
5853
+ ${status === "healthy" ? "Cache is performing well" : status === "warning" ? "Consider increasing cache TTL or capacity" : "Cache hit rate is too low"}
5854
+ </p>
5855
+ </div>
5856
+ </div>
5857
+ </div>
5858
+ `;
5859
+ }
5860
+ function formatBytes(bytes) {
5861
+ if (bytes === 0) return "0 B";
5862
+ const k = 1024;
5863
+ const sizes = ["B", "KB", "MB", "GB"];
5864
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
5865
+ return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`;
5866
+ }
5867
+
5868
+ // src/plugins/cache/routes.ts
5869
+ var app = new Hono();
5870
+ app.get("/", async (c) => {
5871
+ const stats = getAllCacheStats();
5872
+ const user = c.get("user");
5873
+ let totalHits = 0;
5874
+ let totalMisses = 0;
5875
+ let totalSize = 0;
5876
+ let totalEntries = 0;
5877
+ Object.values(stats).forEach((stat) => {
5878
+ totalHits += stat.memoryHits + stat.kvHits;
5879
+ totalMisses += stat.memoryMisses + stat.kvMisses;
5880
+ totalSize += stat.memorySize;
5881
+ totalEntries += stat.entryCount;
5882
+ });
5883
+ const totalRequests = totalHits + totalMisses;
5884
+ const overallHitRate = totalRequests > 0 ? totalHits / totalRequests * 100 : 0;
5885
+ const dashboardData = {
5886
+ stats,
5887
+ totals: {
5888
+ hits: totalHits,
5889
+ misses: totalMisses,
5890
+ requests: totalRequests,
5891
+ hitRate: overallHitRate.toFixed(2),
5892
+ memorySize: totalSize,
5893
+ entryCount: totalEntries
5894
+ },
5895
+ namespaces: Object.keys(stats),
5896
+ user: user ? {
5897
+ name: user.email,
5898
+ email: user.email,
5899
+ role: user.role
5900
+ } : void 0,
5901
+ version: c.get("appVersion")
5902
+ };
5903
+ return c.html(renderCacheDashboard(dashboardData));
5904
+ });
5905
+ app.get("/stats", async (c) => {
5906
+ const stats = getAllCacheStats();
5907
+ return c.json({
5908
+ success: true,
5909
+ data: stats,
5910
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
5911
+ });
5912
+ });
5913
+ app.get("/stats/:namespace", async (c) => {
5914
+ const namespace = c.req.param("namespace");
5915
+ const config = CACHE_CONFIGS[namespace];
5916
+ if (!config) {
5917
+ return c.json({
5918
+ success: false,
5919
+ error: `Unknown namespace: ${namespace}`
5920
+ }, 404);
5921
+ }
5922
+ const cache = getCacheService(config);
5923
+ const stats = cache.getStats();
5924
+ return c.json({
5925
+ success: true,
5926
+ data: {
5927
+ namespace,
5928
+ config,
5929
+ stats
5930
+ },
5931
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
5932
+ });
5933
+ });
5934
+ app.post("/clear", async (c) => {
5935
+ await clearAllCaches();
5936
+ return c.json({
5937
+ success: true,
5938
+ message: "All cache entries cleared",
5939
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
5940
+ });
5941
+ });
5942
+ app.post("/clear/:namespace", async (c) => {
5943
+ const namespace = c.req.param("namespace");
5944
+ const config = CACHE_CONFIGS[namespace];
5945
+ if (!config) {
5946
+ return c.json({
5947
+ success: false,
5948
+ error: `Unknown namespace: ${namespace}`
5949
+ }, 404);
5950
+ }
5951
+ const cache = getCacheService(config);
5952
+ await cache.clear();
5953
+ return c.json({
5954
+ success: true,
5955
+ message: `Cache cleared for namespace: ${namespace}`,
5956
+ namespace,
5957
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
5958
+ });
5959
+ });
5960
+ app.post("/invalidate", async (c) => {
5961
+ const body = await c.req.json();
5962
+ const { pattern, namespace } = body;
5963
+ if (!pattern) {
5964
+ return c.json({
5965
+ success: false,
5966
+ error: "Pattern is required"
5967
+ }, 400);
5968
+ }
5969
+ let totalInvalidated = 0;
5970
+ if (namespace) {
5971
+ const config = CACHE_CONFIGS[namespace];
5972
+ if (!config) {
5973
+ return c.json({
5974
+ success: false,
5975
+ error: `Unknown namespace: ${namespace}`
5976
+ }, 404);
5977
+ }
5978
+ const cache = getCacheService(config);
5979
+ totalInvalidated = await cache.invalidate(pattern);
5980
+ } else {
5981
+ for (const config of Object.values(CACHE_CONFIGS)) {
5982
+ const cache = getCacheService(config);
5983
+ totalInvalidated += await cache.invalidate(pattern);
5984
+ }
5985
+ }
5986
+ return c.json({
5987
+ success: true,
5988
+ invalidated: totalInvalidated,
5989
+ pattern,
5990
+ namespace: namespace || "all",
5991
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
5992
+ });
5993
+ });
5994
+ app.get("/health", async (c) => {
5995
+ const stats = getAllCacheStats();
5996
+ const namespaces = Object.entries(stats);
5997
+ const healthChecks = namespaces.map(([name, stat]) => {
5998
+ const hitRate = stat.hitRate;
5999
+ const memoryUsage = stat.memorySize / (50 * 1024 * 1024);
6000
+ return {
6001
+ namespace: name,
6002
+ status: hitRate > 70 ? "healthy" : hitRate > 40 ? "warning" : "unhealthy",
6003
+ hitRate,
6004
+ memoryUsage: (memoryUsage * 100).toFixed(2) + "%",
6005
+ entryCount: stat.entryCount
6006
+ };
6007
+ });
6008
+ const overallStatus = healthChecks.every((h) => h.status === "healthy") ? "healthy" : healthChecks.some((h) => h.status === "unhealthy") ? "unhealthy" : "warning";
6009
+ return c.json({
6010
+ success: true,
6011
+ data: {
6012
+ status: overallStatus,
6013
+ namespaces: healthChecks,
6014
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
6015
+ }
6016
+ });
6017
+ });
6018
+ app.get("/browser", async (c) => {
6019
+ const namespace = c.req.query("namespace") || "all";
6020
+ const search = c.req.query("search") || "";
6021
+ const sortBy = c.req.query("sort") || "age";
6022
+ const limit = parseInt(c.req.query("limit") || "100");
6023
+ const entries = [];
6024
+ const namespaces = namespace === "all" ? Object.keys(CACHE_CONFIGS) : [namespace];
6025
+ for (const ns of namespaces) {
6026
+ const config = CACHE_CONFIGS[ns];
6027
+ if (!config) continue;
6028
+ const cache = getCacheService(config);
6029
+ const keys = await cache.listKeys();
6030
+ for (const keyInfo of keys) {
6031
+ if (search && !keyInfo.key.toLowerCase().includes(search.toLowerCase())) {
6032
+ continue;
6033
+ }
6034
+ const parsed = parseCacheKey(keyInfo.key);
6035
+ const ttl = Math.max(0, keyInfo.expiresAt - Date.now()) / 1e3;
6036
+ entries.push({
6037
+ namespace: ns,
6038
+ key: keyInfo.key,
6039
+ size: keyInfo.size,
6040
+ age: keyInfo.age,
6041
+ ttl,
6042
+ expiresAt: keyInfo.expiresAt,
6043
+ parsed
6044
+ });
6045
+ }
6046
+ }
6047
+ if (sortBy === "size") {
6048
+ entries.sort((a, b) => b.size - a.size);
6049
+ } else if (sortBy === "age") {
6050
+ entries.sort((a, b) => a.age - b.age);
6051
+ } else if (sortBy === "key") {
6052
+ entries.sort((a, b) => a.key.localeCompare(b.key));
6053
+ }
6054
+ const limitedEntries = entries.slice(0, limit);
6055
+ return c.json({
6056
+ success: true,
6057
+ data: {
6058
+ entries: limitedEntries,
6059
+ total: entries.length,
6060
+ showing: limitedEntries.length,
6061
+ namespace,
6062
+ search,
6063
+ sortBy
6064
+ },
6065
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
6066
+ });
6067
+ });
6068
+ app.get("/browser/:namespace/:key", async (c) => {
6069
+ const namespace = c.req.param("namespace");
6070
+ const key = decodeURIComponent(c.req.param("key"));
6071
+ const config = CACHE_CONFIGS[namespace];
6072
+ if (!config) {
6073
+ return c.json({
6074
+ success: false,
6075
+ error: `Unknown namespace: ${namespace}`
6076
+ }, 404);
6077
+ }
6078
+ const cache = getCacheService(config);
6079
+ const entry = await cache.getEntry(key);
6080
+ if (!entry) {
6081
+ return c.json({
6082
+ success: false,
6083
+ error: "Cache entry not found or expired"
6084
+ }, 404);
6085
+ }
6086
+ const parsed = parseCacheKey(key);
6087
+ return c.json({
6088
+ success: true,
6089
+ data: {
6090
+ key,
6091
+ namespace,
6092
+ parsed,
6093
+ ...entry,
6094
+ createdAt: new Date(entry.timestamp).toISOString(),
6095
+ expiresAt: new Date(entry.expiresAt).toISOString()
6096
+ },
6097
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
6098
+ });
6099
+ });
6100
+ app.get("/analytics", async (c) => {
6101
+ const stats = getAllCacheStats();
6102
+ const invalidationStats = getCacheInvalidationStats();
6103
+ const recentInvalidations = getRecentInvalidations(20);
6104
+ let totalHits = 0;
6105
+ let totalMisses = 0;
6106
+ let totalSize = 0;
6107
+ let totalEntries = 0;
6108
+ const namespacesAnalytics = [];
6109
+ for (const [namespace, stat] of Object.entries(stats)) {
6110
+ totalHits += stat.memoryHits + stat.kvHits;
6111
+ totalMisses += stat.memoryMisses + stat.kvMisses;
6112
+ totalSize += stat.memorySize;
6113
+ totalEntries += stat.entryCount;
6114
+ const totalRequests2 = stat.memoryHits + stat.kvHits + stat.memoryMisses + stat.kvMisses;
6115
+ const hitRate = totalRequests2 > 0 ? (stat.memoryHits + stat.kvHits) / totalRequests2 * 100 : 0;
6116
+ const avgEntrySize = stat.entryCount > 0 ? stat.memorySize / stat.entryCount : 0;
6117
+ namespacesAnalytics.push({
6118
+ namespace,
6119
+ hitRate: hitRate.toFixed(2),
6120
+ totalRequests: totalRequests2,
6121
+ memoryHitRate: totalRequests2 > 0 ? (stat.memoryHits / totalRequests2 * 100).toFixed(2) : "0",
6122
+ kvHitRate: totalRequests2 > 0 ? (stat.kvHits / totalRequests2 * 100).toFixed(2) : "0",
6123
+ avgEntrySize: Math.round(avgEntrySize),
6124
+ totalSize: stat.memorySize,
6125
+ entryCount: stat.entryCount,
6126
+ efficiency: totalRequests2 > 0 ? ((stat.memoryHits + stat.kvHits) / (stat.memoryHits + stat.kvHits + stat.dbHits + 1)).toFixed(2) : "0"
6127
+ });
6128
+ }
6129
+ namespacesAnalytics.sort((a, b) => parseFloat(b.hitRate) - parseFloat(a.hitRate));
6130
+ const totalRequests = totalHits + totalMisses;
6131
+ const overallHitRate = totalRequests > 0 ? totalHits / totalRequests * 100 : 0;
6132
+ const dbQueriesAvoided = totalHits;
6133
+ const timeSaved = dbQueriesAvoided * 48;
6134
+ const estimatedCostSavings = dbQueriesAvoided / 1e6 * 0.5;
6135
+ return c.json({
6136
+ success: true,
6137
+ data: {
6138
+ overview: {
6139
+ totalHits,
6140
+ totalMisses,
6141
+ totalRequests,
6142
+ overallHitRate: overallHitRate.toFixed(2),
6143
+ totalSize,
6144
+ totalEntries,
6145
+ avgEntrySize: totalEntries > 0 ? Math.round(totalSize / totalEntries) : 0
6146
+ },
6147
+ performance: {
6148
+ dbQueriesAvoided,
6149
+ timeSavedMs: timeSaved,
6150
+ timeSavedMinutes: (timeSaved / 1e3 / 60).toFixed(2),
6151
+ estimatedCostSavings: estimatedCostSavings.toFixed(4)
6152
+ },
6153
+ namespaces: namespacesAnalytics,
6154
+ invalidation: {
6155
+ ...invalidationStats,
6156
+ recent: recentInvalidations
6157
+ }
6158
+ },
6159
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
6160
+ });
6161
+ });
6162
+ app.get("/analytics/trends", async (c) => {
6163
+ const stats = getAllCacheStats();
6164
+ const dataPoint = {
6165
+ timestamp: Date.now(),
6166
+ stats: Object.entries(stats).map(([namespace, stat]) => ({
6167
+ namespace,
6168
+ hitRate: stat.hitRate,
6169
+ entryCount: stat.entryCount,
6170
+ memorySize: stat.memorySize,
6171
+ totalRequests: stat.totalRequests
6172
+ }))
6173
+ };
6174
+ return c.json({
6175
+ success: true,
6176
+ data: {
6177
+ trends: [dataPoint],
6178
+ note: "Historical trends require persistent storage. This returns current snapshot only."
6179
+ },
6180
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
6181
+ });
6182
+ });
6183
+ app.get("/analytics/top-keys", async (c) => {
6184
+ c.req.query("namespace") || "all";
6185
+ parseInt(c.req.query("limit") || "10");
6186
+ return c.json({
6187
+ success: true,
6188
+ data: {
6189
+ topKeys: [],
6190
+ note: "Top keys tracking requires per-key hit counting. Feature not yet implemented."
6191
+ },
6192
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
6193
+ });
6194
+ });
6195
+ app.post("/warm", async (c) => {
6196
+ try {
6197
+ const db = c.env.DB;
6198
+ const result = await warmCommonCaches(db);
6199
+ return c.json({
6200
+ success: true,
6201
+ message: "Cache warming completed",
6202
+ ...result,
6203
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
6204
+ });
6205
+ } catch (error) {
6206
+ console.error("Cache warming error:", error);
6207
+ return c.json({
6208
+ success: false,
6209
+ error: "Cache warming failed",
6210
+ details: error instanceof Error ? error.message : "Unknown error"
6211
+ }, 500);
6212
+ }
6213
+ });
6214
+ app.post("/warm/:namespace", async (c) => {
6215
+ try {
6216
+ const namespace = c.req.param("namespace");
6217
+ const body = await c.req.json();
6218
+ const { entries } = body;
6219
+ if (!entries || !Array.isArray(entries)) {
6220
+ return c.json({
6221
+ success: false,
6222
+ error: "Entries array is required"
6223
+ }, 400);
6224
+ }
6225
+ const count = await warmNamespace(namespace, entries);
6226
+ return c.json({
6227
+ success: true,
6228
+ message: `Warmed ${count} entries in namespace: ${namespace}`,
6229
+ namespace,
6230
+ count,
6231
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
6232
+ });
6233
+ } catch (error) {
6234
+ console.error("Namespace warming error:", error);
6235
+ return c.json({
6236
+ success: false,
6237
+ error: "Namespace warming failed",
6238
+ details: error instanceof Error ? error.message : "Unknown error"
6239
+ }, 500);
6240
+ }
6241
+ });
6242
+ var routes_default = app;
6243
+
6244
+ // src/plugins/cache/index.ts
6245
+ var CachePlugin = class {
6246
+ _context = null;
6247
+ /**
6248
+ * Get plugin routes
6249
+ */
6250
+ getRoutes() {
6251
+ return routes_default;
6252
+ }
6253
+ /**
6254
+ * Activate the cache plugin
6255
+ */
6256
+ async activate(context) {
6257
+ this._context = context;
6258
+ const settings = context.config || {};
6259
+ console.log("\u2705 Cache plugin activated", {
6260
+ memoryEnabled: settings.memoryEnabled ?? true,
6261
+ kvEnabled: settings.kvEnabled ?? false,
6262
+ defaultTTL: settings.defaultTTL ?? 3600
6263
+ });
6264
+ for (const [_namespace, config] of Object.entries(CACHE_CONFIGS)) {
6265
+ getCacheService({
6266
+ ...config,
6267
+ memoryEnabled: settings.memoryEnabled ?? config.memoryEnabled,
6268
+ kvEnabled: settings.kvEnabled ?? config.kvEnabled,
6269
+ ttl: settings.defaultTTL ?? config.ttl
6270
+ });
6271
+ }
6272
+ setupCacheInvalidation();
6273
+ }
6274
+ /**
6275
+ * Deactivate the cache plugin
6276
+ */
6277
+ async deactivate() {
6278
+ console.log("\u274C Cache plugin deactivated - clearing all caches");
6279
+ await clearAllCaches();
6280
+ this._context = null;
6281
+ }
6282
+ /**
6283
+ * Configure the cache plugin
6284
+ */
6285
+ async configure(settings) {
6286
+ console.log("\u2699\uFE0F Cache plugin configured", settings);
6287
+ for (const [_namespace, config] of Object.entries(CACHE_CONFIGS)) {
6288
+ getCacheService({
6289
+ ...config,
6290
+ memoryEnabled: settings.memoryEnabled ?? config.memoryEnabled,
6291
+ kvEnabled: settings.kvEnabled ?? config.kvEnabled,
6292
+ ttl: settings.defaultTTL ?? config.ttl
6293
+ });
6294
+ }
6295
+ }
6296
+ /**
6297
+ * Get cache statistics
6298
+ */
6299
+ async getStats(c) {
6300
+ const stats = getAllCacheStats();
6301
+ return c.json({
6302
+ success: true,
6303
+ data: stats,
6304
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
6305
+ });
6306
+ }
6307
+ /**
6308
+ * Clear all cache entries
6309
+ */
6310
+ async clearCache(c) {
6311
+ await clearAllCaches();
6312
+ return c.json({
6313
+ success: true,
6314
+ message: "All cache entries cleared",
6315
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
6316
+ });
6317
+ }
6318
+ /**
6319
+ * Invalidate cache entries matching pattern
6320
+ */
6321
+ async invalidatePattern(c) {
6322
+ const body = await c.req.json();
6323
+ const { pattern, namespace: _namespace } = body;
6324
+ if (!pattern) {
6325
+ return c.json({
6326
+ success: false,
6327
+ error: "Pattern is required"
6328
+ }, 400);
6329
+ }
6330
+ let totalInvalidated = 0;
6331
+ if (_namespace) {
6332
+ const cache = getCacheService(CACHE_CONFIGS[_namespace] || {
6333
+ ttl: 3600,
6334
+ kvEnabled: false,
6335
+ memoryEnabled: true,
6336
+ namespace: _namespace,
6337
+ invalidateOn: [],
6338
+ version: "v1"
6339
+ });
6340
+ totalInvalidated = await cache.invalidate(pattern);
6341
+ } else {
6342
+ for (const config of Object.values(CACHE_CONFIGS)) {
6343
+ const cache = getCacheService(config);
6344
+ totalInvalidated += await cache.invalidate(pattern);
6345
+ }
6346
+ }
6347
+ return c.json({
6348
+ success: true,
6349
+ invalidated: totalInvalidated,
6350
+ pattern,
6351
+ namespace: _namespace || "all",
6352
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
6353
+ });
6354
+ }
6355
+ };
6356
+ var plugin = new CachePlugin();
6357
+ var cache_default = plugin;
6358
+
6359
+ // src/assets/favicon.ts
6360
+ var faviconSvg = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
6361
+ <svg
6362
+ version="1.1"
6363
+ id="Layer_1"
6364
+ x="0px"
6365
+ y="0px"
6366
+ viewBox="380 1300 257.89001 278.8855"
6367
+ xml:space="preserve"
6368
+ width="257.89001"
6369
+ height="278.8855"
6370
+ xmlns="http://www.w3.org/2000/svg">
6371
+ <g
6372
+ id="g10"
6373
+ transform="translate(-383.935,-60.555509)">
6374
+ <g
6375
+ id="g9">
6376
+ <path
6377
+ fill="#f1f2f2"
6378
+ d="m 974.78,1398.211 c -5.016,6.574 -10.034,13.146 -15.048,19.721 -1.828,2.398 -3.657,4.796 -5.487,7.194 1.994,1.719 3.958,3.51 5.873,5.424 18.724,18.731 28.089,41.216 28.089,67.459 0,26.251 -9.366,48.658 -28.089,67.237 -18.731,18.579 -41.215,27.868 -67.459,27.868 -9.848,0 -19.156,-1.308 -27.923,-3.923 l -4.185,3.354 c -8.587,6.885 -17.154,13.796 -25.725,20.702 17.52,8.967 36.86,13.487 58.054,13.487 35.533,0 65.91,-12.608 91.124,-37.821 25.214,-25.215 37.821,-55.584 37.821,-91.125 0,-35.534 -12.607,-65.911 -37.821,-91.126 -3,-2.999 -6.078,-5.808 -9.224,-8.451 z"
6379
+ id="path2" />
6380
+ <path
6381
+ fill="#34d399"
6382
+ d="m 854.024,1585.195 20.001,-16.028 c 16.616,-13.507 33.04,-27.265 50.086,-40.251 1.13,-0.861 2.9,-1.686 2.003,-3.516 -0.843,-1.716 -2.481,-2.302 -4.484,-2.123 -8.514,0.765 -17.016,-0.538 -25.537,-0.353 -1.124,0.024 -2.768,0.221 -3.163,-1.25 -0.371,-1.369 1.088,-2.063 1.919,-2.894 6.26,-6.242 12.574,-12.43 18.816,-18.691 9.303,-9.327 18.565,-18.714 27.851,-28.066 1.848,-1.859 3.701,-3.713 5.549,-5.572 2.655,-2.661 5.309,-5.315 7.958,-7.982 0.574,-0.579 1.259,-1.141 1.246,-1.94 -0.004,-0.257 -0.078,-0.538 -0.254,-0.853 -0.556,-0.981 -1.441,-1.1 -2.469,-0.957 -0.658,0.096 -1.315,0.185 -1.973,0.275 -3.844,0.538 -7.689,1.076 -11.533,1.608 -3.641,0.505 -7.281,1.02 -10.922,1.529 -4.162,0.582 -8.324,1.158 -12.486,1.748 -1.142,0.161 -2.409,1.662 -3.354,0.508 -0.419,-0.508 -0.431,-1.028 -0.251,-1.531 0.269,-0.741 0.957,-1.441 1.387,-2.021 3.414,-4.58 6.882,-9.124 10.356,-13.662 1.74,-2.272 3.48,-4.544 5.214,-6.822 4.682,-6.141 9.369,-12.281 14.051,-18.422 0.09,-0.119 0.181,-0.237 0.271,-0.355 6.848,-8.98 13.7,-17.958 20.553,-26.936 0.488,-0.64 0.977,-1.28 1.465,-1.92 2.159,-2.828 4.315,-5.658 6.476,-8.486 4.197,-5.501 8.454,-10.954 12.67,-16.442 0.263,-0.347 0.538,-0.718 0.717,-1.106 0.269,-0.586 0.299,-1.196 -0.335,-1.776 -0.825,-0.753 -1.8,-0.15 -2.595,0.419 -0.67,0.472 -1.333,0.957 -1.955,1.489 -2.206,1.889 -4.401,3.797 -6.595,5.698 -3.958,3.438 -7.922,6.876 -11.976,10.194 -2.443,2.003 -4.865,4.028 -7.301,6.038 -18.689,-10.581 -39.53,-15.906 -62.549,-15.906 -35.54,0 -65.911,12.607 -91.125,37.82 -25.214,25.215 -37.821,55.592 -37.821,91.126 0,35.54 12.607,65.91 37.821,91.125 4.146,4.146 8.445,7.916 12.87,11.381 -9.015,11.14 -18.036,22.277 -27.034,33.429 -1.208,1.489 -3.755,3.151 -2.745,4.891 0.078,0.144 0.173,0.281 0.305,0.425 1.321,1.429 3.492,-1.303 4.933,-2.457 6.673,-5.333 13.333,-10.685 19.982,-16.042 3.707,-2.984 7.417,-5.965 11.124,-8.952 1.474,-1.188 2.951,-2.373 4.425,-3.561 6.41,-5.164 12.816,-10.333 19.238,-15.481 z m -56.472,-87.186 c 0,-26.243 9.29,-48.728 27.868,-67.459 18.579,-18.723 40.987,-28.089 67.238,-28.089 12.273,0 23.712,2.075 34.34,6.171 -3.37,2.905 -6.734,5.816 -10.069,8.762 -6.075,5.351 -12.365,10.469 -18.667,15.564 -4.179,3.378 -8.371,6.744 -12.514,10.164 -7.54,6.23 -15.037,12.52 -22.529,18.804 -7.091,5.955 -14.182,11.904 -21.19,17.949 -1.136,0.974 -3.055,1.907 -2.135,3.94 0.831,1.836 2.774,1.417 4.341,1.578 l 12.145,-0.599 14.151,-0.698 c 1.031,-0.102 2.192,-0.257 2.89,0.632 0.034,0.044 0.073,0.078 0.106,0.127 1.017,1.561 -0.67,2.105 -1.387,2.942 -6.308,7.318 -12.616,14.637 -18.978,21.907 -8.161,9.339 -16.353,18.649 -24.544,27.958 -2.146,2.433 -4.275,4.879 -6.422,7.312 -1.034,1.172 -2.129,2.272 -1.238,3.922 0.933,1.728 2.685,1.752 4.323,1.602 4.134,-0.367 8.263,-0.489 12.396,-0.492 0.242,0 0.485,-0.01 0.728,0 2.711,0.01 5.422,0.068 8.134,0.145 2.582,0.074 5.166,0.165 7.752,0.249 0.275,1.62 -0.879,2.356 -1.62,3.259 -1.333,1.626 -2.667,3.247 -4,4.867 -4.315,5.252 -8.62,10.514 -12.928,15.772 -3.562,-2.725 -7.007,-5.733 -10.324,-9.051 -18.577,-18.576 -27.867,-40.983 -27.867,-67.234 z"
6383
+ id="path9" />
6384
+ </g>
6385
+ </g>
6386
+ </svg>`;
2733
6387
 
2734
6388
  // src/app.ts
2735
6389
  function createSonicJSApp(config = {}) {
2736
- const app = new Hono();
6390
+ const app2 = new Hono();
2737
6391
  const appVersion = config.version || getCoreVersion();
2738
6392
  const appName = config.name || "SonicJS AI";
2739
- app.use("*", async (c, next) => {
6393
+ app2.use("*", async (c, next) => {
2740
6394
  c.set("appVersion", appVersion);
2741
6395
  await next();
2742
6396
  });
2743
- app.use("*", metricsMiddleware());
2744
- app.use("*", bootstrapMiddleware(config));
2745
- app.use("*", adminSetupMiddleware());
6397
+ app2.use("*", metricsMiddleware());
6398
+ app2.use("*", bootstrapMiddleware(config));
2746
6399
  if (config.middleware?.beforeAuth) {
2747
6400
  for (const middleware of config.middleware.beforeAuth) {
2748
- app.use("*", middleware);
6401
+ app2.use("*", middleware);
2749
6402
  }
2750
6403
  }
2751
- app.use("*", async (_c, next) => {
6404
+ app2.use("*", async (_c, next) => {
2752
6405
  await next();
2753
6406
  });
2754
- app.use("*", async (_c, next) => {
6407
+ app2.use("*", async (_c, next) => {
2755
6408
  await next();
2756
6409
  });
2757
6410
  if (config.middleware?.afterAuth) {
2758
6411
  for (const middleware of config.middleware.afterAuth) {
2759
- app.use("*", middleware);
2760
- }
2761
- }
2762
- app.route("/api", api_default);
2763
- app.route("/api/media", api_media_default);
2764
- app.route("/api/system", api_system_default);
2765
- app.route("/admin/api", admin_api_default);
2766
- app.route("/admin/dashboard", router);
2767
- app.route("/admin/collections", adminCollectionsRoutes);
2768
- app.route("/admin/settings", adminSettingsRoutes);
2769
- app.route("/admin/database-tools", createDatabaseToolsAdminRoutes());
2770
- app.route("/admin/seed-data", createSeedDataAdminRoutes());
2771
- app.route("/admin/content", admin_content_default);
2772
- app.route("/admin/media", adminMediaRoutes);
2773
- app.route("/admin/plugins", adminPluginRoutes);
2774
- app.route("/admin/logs", adminLogsRoutes);
2775
- app.route("/admin", userRoutes);
2776
- app.route("/auth", auth_default);
2777
- app.route("/", test_cleanup_default);
6412
+ app2.use("*", middleware);
6413
+ }
6414
+ }
6415
+ app2.route("/api", api_default);
6416
+ app2.route("/api/media", api_media_default);
6417
+ app2.route("/api/system", api_system_default);
6418
+ app2.route("/admin/api", admin_api_default);
6419
+ app2.route("/admin/dashboard", router);
6420
+ app2.route("/admin/collections", adminCollectionsRoutes);
6421
+ app2.route("/admin/settings", adminSettingsRoutes);
6422
+ app2.route("/admin/database-tools", createDatabaseToolsAdminRoutes());
6423
+ app2.route("/admin/seed-data", createSeedDataAdminRoutes());
6424
+ app2.route("/admin/content", admin_content_default);
6425
+ app2.route("/admin/media", adminMediaRoutes);
6426
+ if (aiSearchPlugin.routes && aiSearchPlugin.routes.length > 0) {
6427
+ for (const route of aiSearchPlugin.routes) {
6428
+ app2.route(route.path, route.handler);
6429
+ }
6430
+ }
6431
+ app2.route("/admin/cache", cache_default.getRoutes());
6432
+ app2.route("/admin/plugins", adminPluginRoutes);
6433
+ app2.route("/admin/logs", adminLogsRoutes);
6434
+ app2.route("/admin", userRoutes);
6435
+ app2.route("/auth", auth_default);
6436
+ app2.route("/", test_cleanup_default);
2778
6437
  if (emailPlugin.routes && emailPlugin.routes.length > 0) {
2779
6438
  for (const route of emailPlugin.routes) {
2780
- app.route(route.path, route.handler);
6439
+ app2.route(route.path, route.handler);
2781
6440
  }
2782
6441
  }
2783
6442
  if (otpLoginPlugin.routes && otpLoginPlugin.routes.length > 0) {
2784
6443
  for (const route of otpLoginPlugin.routes) {
2785
- app.route(route.path, route.handler);
6444
+ app2.route(route.path, route.handler);
2786
6445
  }
2787
6446
  }
2788
6447
  const magicLinkPlugin = createMagicLinkAuthPlugin();
2789
6448
  if (magicLinkPlugin.routes && magicLinkPlugin.routes.length > 0) {
2790
6449
  for (const route of magicLinkPlugin.routes) {
2791
- app.route(route.path, route.handler);
6450
+ app2.route(route.path, route.handler);
2792
6451
  }
2793
6452
  }
2794
- app.get("/files/*", async (c) => {
6453
+ app2.get("/favicon.svg", (c) => {
6454
+ return new Response(faviconSvg, {
6455
+ headers: {
6456
+ "Content-Type": "image/svg+xml",
6457
+ "Cache-Control": "public, max-age=31536000"
6458
+ }
6459
+ });
6460
+ });
6461
+ app2.get("/files/*", async (c) => {
2795
6462
  try {
2796
6463
  const url = new URL(c.req.url);
2797
6464
  const pathname = url.pathname;
@@ -2820,13 +6487,13 @@ function createSonicJSApp(config = {}) {
2820
6487
  });
2821
6488
  if (config.routes) {
2822
6489
  for (const route of config.routes) {
2823
- app.route(route.path, route.handler);
6490
+ app2.route(route.path, route.handler);
2824
6491
  }
2825
6492
  }
2826
- app.get("/", (c) => {
6493
+ app2.get("/", (c) => {
2827
6494
  return c.redirect("/auth/login");
2828
6495
  });
2829
- app.get("/health", (c) => {
6496
+ app2.get("/health", (c) => {
2830
6497
  return c.json({
2831
6498
  name: appName,
2832
6499
  version: appVersion,
@@ -2834,14 +6501,14 @@ function createSonicJSApp(config = {}) {
2834
6501
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
2835
6502
  });
2836
6503
  });
2837
- app.notFound((c) => {
6504
+ app2.notFound((c) => {
2838
6505
  return c.json({ error: "Not Found", status: 404 }, 404);
2839
6506
  });
2840
- app.onError((err, c) => {
6507
+ app2.onError((err, c) => {
2841
6508
  console.error(err);
2842
6509
  return c.json({ error: "Internal Server Error", status: 500 }, 500);
2843
6510
  });
2844
- return app;
6511
+ return app2;
2845
6512
  }
2846
6513
  function setupCoreMiddleware(_app) {
2847
6514
  console.warn("setupCoreMiddleware is deprecated. Use createSonicJSApp() instead.");