@sonicjs-cms/core 2.15.0 → 2.16.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 (48) hide show
  1. package/dist/{chunk-26HYU7MX.cjs → chunk-6ENX7QSA.cjs} +114 -114
  2. package/dist/{chunk-26HYU7MX.cjs.map → chunk-6ENX7QSA.cjs.map} +1 -1
  3. package/dist/{chunk-UFPT5KCQ.cjs → chunk-CZ6BVQZX.cjs} +19 -9
  4. package/dist/chunk-CZ6BVQZX.cjs.map +1 -0
  5. package/dist/{chunk-G7XSN72O.js → chunk-INSDRCG3.js} +2 -2
  6. package/dist/{chunk-G7XSN72O.js.map → chunk-INSDRCG3.js.map} +1 -1
  7. package/dist/{chunk-EWXV2KG2.js → chunk-MVSCB4E3.js} +3 -3
  8. package/dist/{chunk-EWXV2KG2.js.map → chunk-MVSCB4E3.js.map} +1 -1
  9. package/dist/{chunk-RVD7PLMU.cjs → chunk-OCLUXJ7E.cjs} +9 -2
  10. package/dist/chunk-OCLUXJ7E.cjs.map +1 -0
  11. package/dist/{chunk-43AB4EH4.cjs → chunk-Q5VFZUXV.cjs} +2 -2
  12. package/dist/{chunk-43AB4EH4.cjs.map → chunk-Q5VFZUXV.cjs.map} +1 -1
  13. package/dist/{chunk-5SOFMH66.js → chunk-VFQUULAV.js} +9 -2
  14. package/dist/chunk-VFQUULAV.js.map +1 -0
  15. package/dist/{chunk-7MMD5WMK.js → chunk-WLSIUKNM.js} +9 -9
  16. package/dist/{chunk-7MMD5WMK.js.map → chunk-WLSIUKNM.js.map} +1 -1
  17. package/dist/{chunk-2BL2A62D.js → chunk-Y5EH32F5.js} +15 -5
  18. package/dist/chunk-Y5EH32F5.js.map +1 -0
  19. package/dist/{chunk-VUISYUHY.cjs → chunk-YQW2GCJ3.cjs} +3 -3
  20. package/dist/{chunk-VUISYUHY.cjs.map → chunk-YQW2GCJ3.cjs.map} +1 -1
  21. package/dist/index.cjs +812 -134
  22. package/dist/index.cjs.map +1 -1
  23. package/dist/index.d.cts +2 -2
  24. package/dist/index.d.ts +2 -2
  25. package/dist/index.js +690 -12
  26. package/dist/index.js.map +1 -1
  27. package/dist/middleware.cjs +29 -29
  28. package/dist/middleware.js +3 -3
  29. package/dist/migrations-7HQ7LYAL.js +4 -0
  30. package/dist/{migrations-YB52SLW7.js.map → migrations-7HQ7LYAL.js.map} +1 -1
  31. package/dist/migrations-SVQTT7NV.cjs +13 -0
  32. package/dist/{migrations-APFGYCB6.cjs.map → migrations-SVQTT7NV.cjs.map} +1 -1
  33. package/dist/routes.cjs +28 -28
  34. package/dist/routes.js +5 -5
  35. package/dist/services.cjs +23 -23
  36. package/dist/services.d.cts +1 -1
  37. package/dist/services.d.ts +1 -1
  38. package/dist/services.js +2 -2
  39. package/dist/utils.cjs +11 -11
  40. package/dist/utils.js +1 -1
  41. package/migrations/036_analytics_events.sql +22 -0
  42. package/package.json +1 -1
  43. package/dist/chunk-2BL2A62D.js.map +0 -1
  44. package/dist/chunk-5SOFMH66.js.map +0 -1
  45. package/dist/chunk-RVD7PLMU.cjs.map +0 -1
  46. package/dist/chunk-UFPT5KCQ.cjs.map +0 -1
  47. package/dist/migrations-APFGYCB6.cjs +0 -13
  48. package/dist/migrations-YB52SLW7.js +0 -4
package/dist/index.js CHANGED
@@ -1,21 +1,21 @@
1
- import { renderConfirmationDialog, getConfirmationDialogScript, api_default, api_media_default, api_system_default, admin_api_default, router, adminCollectionsRoutes, adminFormsRoutes, adminSettingsRoutes, public_forms_default, router2, admin_content_default, adminMediaRoutes, userProfilesPlugin, adminPluginRoutes, adminLogsRoutes, userRoutes, auth_default, test_cleanup_default } from './chunk-7MMD5WMK.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, createUserProfilesPlugin, defineUserProfile, getUserProfileConfig, userProfilesPlugin } from './chunk-7MMD5WMK.js';
1
+ import { renderConfirmationDialog, getConfirmationDialogScript, api_default, api_media_default, api_system_default, admin_api_default, router, adminCollectionsRoutes, adminFormsRoutes, adminSettingsRoutes, public_forms_default, router2, admin_content_default, adminMediaRoutes, userProfilesPlugin, adminPluginRoutes, adminLogsRoutes, userRoutes, auth_default, test_cleanup_default } from './chunk-WLSIUKNM.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, createUserProfilesPlugin, defineUserProfile, getUserProfileConfig, userProfilesPlugin } from './chunk-WLSIUKNM.js';
3
3
  import { SettingsService, setAppInstance, schema_exports } from './chunk-TBJY2FF7.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-TBJY2FF7.js';
5
- import { requireAuth, AuthManager, metricsMiddleware, bootstrapMiddleware, securityHeadersMiddleware, csrfProtection } from './chunk-2BL2A62D.js';
6
- export { AuthManager, PermissionManager, bootstrapMiddleware, cacheHeaders, compressionMiddleware, detailedLoggingMiddleware, getActivePlugins, isPluginActive, logActivity, loggingMiddleware, optionalAuth, performanceLoggingMiddleware, requireActivePlugin, requireActivePlugins, requireAnyPermission, requireAuth, requirePermission, requireRole, securityHeadersMiddleware as securityHeaders, securityLoggingMiddleware } from './chunk-2BL2A62D.js';
7
- import { PluginService, PLUGIN_REGISTRY } from './chunk-G7XSN72O.js';
8
- export { PluginBootstrapService, PluginService as PluginServiceClass, backfillFormSubmissions, cleanupRemovedCollections, createContentFromSubmission, deriveCollectionSchemaFromFormio, deriveSubmissionTitle, fullCollectionSync, getAvailableCollectionNames, getManagedCollections, isCollectionManaged, loadCollectionConfig, loadCollectionConfigs, mapFormStatusToContentStatus, registerCollections, syncAllFormCollections, syncCollection, syncCollections, syncFormCollection, validateCollectionConfig } from './chunk-G7XSN72O.js';
9
- export { MigrationService } from './chunk-5SOFMH66.js';
5
+ import { requireAuth, AuthManager, metricsMiddleware, bootstrapMiddleware, securityHeadersMiddleware, csrfProtection } from './chunk-Y5EH32F5.js';
6
+ export { AuthManager, PermissionManager, bootstrapMiddleware, cacheHeaders, compressionMiddleware, detailedLoggingMiddleware, getActivePlugins, isPluginActive, logActivity, loggingMiddleware, optionalAuth, performanceLoggingMiddleware, requireActivePlugin, requireActivePlugins, requireAnyPermission, requireAuth, requirePermission, requireRole, securityHeadersMiddleware as securityHeaders, securityLoggingMiddleware } from './chunk-Y5EH32F5.js';
7
+ import { PluginService, PLUGIN_REGISTRY } from './chunk-INSDRCG3.js';
8
+ export { PluginBootstrapService, PluginService as PluginServiceClass, backfillFormSubmissions, cleanupRemovedCollections, createContentFromSubmission, deriveCollectionSchemaFromFormio, deriveSubmissionTitle, fullCollectionSync, getAvailableCollectionNames, getManagedCollections, isCollectionManaged, loadCollectionConfig, loadCollectionConfigs, mapFormStatusToContentStatus, registerCollections, syncAllFormCollections, syncCollection, syncCollections, syncFormCollection, validateCollectionConfig } from './chunk-INSDRCG3.js';
9
+ export { MigrationService } from './chunk-VFQUULAV.js';
10
10
  export { renderFilterBar } from './chunk-ON5ZMSU4.js';
11
11
  import { renderAdminLayout } from './chunk-XWIA3HVX.js';
12
12
  export { getConfirmationDialogScript, renderAlert, renderConfirmationDialog, renderForm, renderFormField, renderPagination, renderTable } from './chunk-XWIA3HVX.js';
13
13
  import { init_admin_layout_catalyst_template, renderAdminLayoutCatalyst } from './chunk-55RDMDOP.js';
14
14
  export { HookSystemImpl, HookUtils, PluginManager as PluginManagerClass, PluginRegistryImpl, PluginValidator as PluginValidatorClass, ScopedHookSystem as ScopedHookSystemClass } from './chunk-TFNTM3OA.js';
15
- import { PluginBuilder } from './chunk-EXNEW5US.js';
15
+ import { PluginBuilder, PluginHelpers } from './chunk-EXNEW5US.js';
16
16
  export { PluginBuilder, PluginHelpers } from './chunk-EXNEW5US.js';
17
- import { package_default, getCoreVersion } from './chunk-EWXV2KG2.js';
18
- export { QueryFilterBuilder, SONICJS_VERSION, TemplateRenderer, buildQuery, getCoreVersion, renderTemplate, templateRenderer } from './chunk-EWXV2KG2.js';
17
+ import { package_default, getCoreVersion } from './chunk-MVSCB4E3.js';
18
+ export { QueryFilterBuilder, SONICJS_VERSION, TemplateRenderer, buildQuery, getCoreVersion, renderTemplate, templateRenderer } from './chunk-MVSCB4E3.js';
19
19
  import './chunk-X7ZAEI5S.js';
20
20
  export { metricsTracker } from './chunk-FICTAGD4.js';
21
21
  export { escapeHtml, sanitizeInput, sanitizeObject } from './chunk-TQABQWOP.js';
@@ -5705,7 +5705,7 @@ var BruteForceDetector = class {
5705
5705
  }
5706
5706
  settings;
5707
5707
  async recordFailedAttempt(ip, email) {
5708
- if (!this.settings.enabled) {
5708
+ if (!this.settings.enabled || !this.kv) {
5709
5709
  return { ipCount: 0, emailCount: 0, shouldLockIP: false, shouldLockEmail: false, isSuspicious: false };
5710
5710
  }
5711
5711
  const windowMs = this.settings.windowMinutes * 60 * 1e3;
@@ -5722,7 +5722,7 @@ var BruteForceDetector = class {
5722
5722
  return { ipCount, emailCount, shouldLockIP, shouldLockEmail, isSuspicious };
5723
5723
  }
5724
5724
  async isLocked(ip, email) {
5725
- if (!this.settings.enabled) {
5725
+ if (!this.settings.enabled || !this.kv) {
5726
5726
  return { locked: false };
5727
5727
  }
5728
5728
  const ipLocked = await this.kv.get(`${LOCK_PREFIX}ip:${ip}`);
@@ -5736,6 +5736,7 @@ var BruteForceDetector = class {
5736
5736
  return { locked: false };
5737
5737
  }
5738
5738
  async lockIP(ip) {
5739
+ if (!this.kv) return;
5739
5740
  const ttl = this.settings.lockoutDurationMinutes * 60;
5740
5741
  await this.kv.put(`${LOCK_PREFIX}ip:${ip}`, JSON.stringify({
5741
5742
  lockedAt: Date.now(),
@@ -5743,6 +5744,7 @@ var BruteForceDetector = class {
5743
5744
  }), { expirationTtl: ttl });
5744
5745
  }
5745
5746
  async lockEmail(email) {
5747
+ if (!this.kv) return;
5746
5748
  const ttl = this.settings.lockoutDurationMinutes * 60;
5747
5749
  await this.kv.put(`${LOCK_PREFIX}email:${email}`, JSON.stringify({
5748
5750
  lockedAt: Date.now(),
@@ -5750,12 +5752,15 @@ var BruteForceDetector = class {
5750
5752
  }), { expirationTtl: ttl });
5751
5753
  }
5752
5754
  async unlockIP(ip) {
5755
+ if (!this.kv) return;
5753
5756
  await this.kv.delete(`${LOCK_PREFIX}ip:${ip}`);
5754
5757
  }
5755
5758
  async unlockEmail(email) {
5759
+ if (!this.kv) return;
5756
5760
  await this.kv.delete(`${LOCK_PREFIX}email:${email}`);
5757
5761
  }
5758
5762
  async getActiveLockouts() {
5763
+ if (!this.kv) return [];
5759
5764
  const ipLocks = await this.kv.list({ prefix: `${LOCK_PREFIX}ip:` });
5760
5765
  const emailLocks = await this.kv.list({ prefix: `${LOCK_PREFIX}email:` });
5761
5766
  const lockouts = [];
@@ -5786,6 +5791,7 @@ var BruteForceDetector = class {
5786
5791
  return lockouts;
5787
5792
  }
5788
5793
  async releaseLockout(key) {
5794
+ if (!this.kv) return;
5789
5795
  await this.kv.delete(key);
5790
5796
  }
5791
5797
  isAboveAlertThreshold(count) {
@@ -7762,6 +7768,672 @@ function pluginMenuMiddleware() {
7762
7768
  };
7763
7769
  }
7764
7770
 
7771
+ // src/plugins/types.ts
7772
+ var HOOKS2 = {
7773
+ // Request lifecycle
7774
+ REQUEST_START: "request:start",
7775
+ REQUEST_END: "request:end",
7776
+ USER_LOGIN: "user:login"};
7777
+ init_admin_layout_catalyst_template();
7778
+ var adminRoutes4 = new Hono();
7779
+ adminRoutes4.use("*", requireAuth());
7780
+ adminRoutes4.use("*", async (c, next) => {
7781
+ const user = c.get("user");
7782
+ if (user?.role !== "admin") {
7783
+ return c.text("Access denied", 403);
7784
+ }
7785
+ return next();
7786
+ });
7787
+ adminRoutes4.get("/", async (c) => {
7788
+ const user = c.get("user");
7789
+ const db = c.env.DB;
7790
+ let totalRequests = 0;
7791
+ let uniqueIPs = 0;
7792
+ let avgDuration = 0;
7793
+ let errorCount = 0;
7794
+ let topPages = [];
7795
+ let recentActivity = [];
7796
+ try {
7797
+ const now = Math.floor(Date.now() / 1e3);
7798
+ const dayAgo = now - 86400;
7799
+ const [requestsResult, ipsResult, durationResult, errorsResult, pagesResult, activityResult] = await Promise.all([
7800
+ db.prepare("SELECT COUNT(*) as count FROM system_logs WHERE category = ? AND created_at > ?").bind("api", dayAgo).first(),
7801
+ db.prepare("SELECT COUNT(DISTINCT ip_address) as count FROM system_logs WHERE category = ? AND created_at > ?").bind("api", dayAgo).first(),
7802
+ db.prepare("SELECT AVG(duration) as avg FROM system_logs WHERE category = ? AND created_at > ? AND duration IS NOT NULL").bind("api", dayAgo).first(),
7803
+ db.prepare("SELECT COUNT(*) as count FROM system_logs WHERE level IN (?, ?) AND created_at > ?").bind("error", "fatal", dayAgo).first(),
7804
+ db.prepare("SELECT url, COUNT(*) as views FROM system_logs WHERE category = ? AND created_at > ? AND url IS NOT NULL GROUP BY url ORDER BY views DESC LIMIT 10").bind("api", dayAgo).all(),
7805
+ db.prepare("SELECT url, method, status_code, duration, created_at FROM system_logs WHERE category = ? ORDER BY created_at DESC LIMIT 20").bind("api").all()
7806
+ ]);
7807
+ totalRequests = requestsResult?.count || 0;
7808
+ uniqueIPs = ipsResult?.count || 0;
7809
+ avgDuration = Math.round(durationResult?.avg || 0);
7810
+ errorCount = errorsResult?.count || 0;
7811
+ topPages = (pagesResult.results || []).map((r) => ({ path: r.url, views: r.views }));
7812
+ recentActivity = activityResult.results || [];
7813
+ } catch {
7814
+ }
7815
+ const content2 = `
7816
+ <div class="space-y-8">
7817
+ <div>
7818
+ <h1 class="text-2xl font-semibold text-zinc-950 dark:text-white">Analytics Dashboard</h1>
7819
+ <p class="mt-1 text-sm text-zinc-500 dark:text-zinc-400">Last 24 hours overview from system logs</p>
7820
+ </div>
7821
+
7822
+ <!-- Stats Cards -->
7823
+ <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
7824
+ <div class="rounded-lg bg-white dark:bg-zinc-800 p-6 ring-1 ring-zinc-950/5 dark:ring-white/10">
7825
+ <p class="text-sm font-medium text-zinc-500 dark:text-zinc-400">Total Requests</p>
7826
+ <p class="mt-2 text-3xl font-semibold text-zinc-950 dark:text-white">${totalRequests.toLocaleString()}</p>
7827
+ </div>
7828
+ <div class="rounded-lg bg-white dark:bg-zinc-800 p-6 ring-1 ring-zinc-950/5 dark:ring-white/10">
7829
+ <p class="text-sm font-medium text-zinc-500 dark:text-zinc-400">Unique Visitors</p>
7830
+ <p class="mt-2 text-3xl font-semibold text-zinc-950 dark:text-white">${uniqueIPs.toLocaleString()}</p>
7831
+ </div>
7832
+ <div class="rounded-lg bg-white dark:bg-zinc-800 p-6 ring-1 ring-zinc-950/5 dark:ring-white/10">
7833
+ <p class="text-sm font-medium text-zinc-500 dark:text-zinc-400">Avg Response Time</p>
7834
+ <p class="mt-2 text-3xl font-semibold text-zinc-950 dark:text-white">${avgDuration}ms</p>
7835
+ </div>
7836
+ <div class="rounded-lg bg-white dark:bg-zinc-800 p-6 ring-1 ring-zinc-950/5 dark:ring-white/10">
7837
+ <p class="text-sm font-medium text-zinc-500 dark:text-zinc-400">Errors</p>
7838
+ <p class="mt-2 text-3xl font-semibold ${errorCount > 0 ? "text-red-600 dark:text-red-400" : "text-zinc-950 dark:text-white"}">${errorCount.toLocaleString()}</p>
7839
+ </div>
7840
+ </div>
7841
+
7842
+ <!-- Top Pages -->
7843
+ <div class="rounded-lg bg-white dark:bg-zinc-800 ring-1 ring-zinc-950/5 dark:ring-white/10">
7844
+ <div class="px-6 py-4 border-b border-zinc-950/5 dark:border-white/10">
7845
+ <h2 class="text-lg font-semibold text-zinc-950 dark:text-white">Top Pages</h2>
7846
+ </div>
7847
+ <div class="divide-y divide-zinc-950/5 dark:divide-white/10">
7848
+ ${topPages.length > 0 ? topPages.map((p) => `
7849
+ <div class="flex items-center justify-between px-6 py-3">
7850
+ <span class="text-sm text-zinc-700 dark:text-zinc-300 font-mono truncate">${escapeHtml3(p.path)}</span>
7851
+ <span class="text-sm font-medium text-zinc-500 dark:text-zinc-400">${p.views}</span>
7852
+ </div>
7853
+ `).join("") : `
7854
+ <div class="px-6 py-8 text-center text-sm text-zinc-500 dark:text-zinc-400">
7855
+ No page views recorded yet. Analytics data will appear once requests are logged.
7856
+ </div>
7857
+ `}
7858
+ </div>
7859
+ </div>
7860
+
7861
+ <!-- Recent Activity -->
7862
+ <div class="rounded-lg bg-white dark:bg-zinc-800 ring-1 ring-zinc-950/5 dark:ring-white/10">
7863
+ <div class="px-6 py-4 border-b border-zinc-950/5 dark:border-white/10">
7864
+ <h2 class="text-lg font-semibold text-zinc-950 dark:text-white">Recent Activity</h2>
7865
+ </div>
7866
+ <div class="overflow-x-auto">
7867
+ <table class="w-full text-sm">
7868
+ <thead class="bg-zinc-50 dark:bg-zinc-800/50">
7869
+ <tr>
7870
+ <th class="px-6 py-2 text-left font-medium text-zinc-500 dark:text-zinc-400">Path</th>
7871
+ <th class="px-6 py-2 text-left font-medium text-zinc-500 dark:text-zinc-400">Method</th>
7872
+ <th class="px-6 py-2 text-left font-medium text-zinc-500 dark:text-zinc-400">Status</th>
7873
+ <th class="px-6 py-2 text-left font-medium text-zinc-500 dark:text-zinc-400">Duration</th>
7874
+ </tr>
7875
+ </thead>
7876
+ <tbody class="divide-y divide-zinc-950/5 dark:divide-white/10">
7877
+ ${recentActivity.length > 0 ? recentActivity.map((a) => `
7878
+ <tr>
7879
+ <td class="px-6 py-2 font-mono text-zinc-700 dark:text-zinc-300 truncate max-w-xs">${escapeHtml3(a.url || "")}</td>
7880
+ <td class="px-6 py-2"><span class="inline-flex items-center rounded px-1.5 py-0.5 text-xs font-medium ${a.method === "GET" ? "bg-green-50 text-green-700 dark:bg-green-900/30 dark:text-green-400" : "bg-blue-50 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"}">${a.method || ""}</span></td>
7881
+ <td class="px-6 py-2"><span class="inline-flex items-center rounded px-1.5 py-0.5 text-xs font-medium ${(a.status_code || 0) >= 400 ? "bg-red-50 text-red-700 dark:bg-red-900/30 dark:text-red-400" : "bg-green-50 text-green-700 dark:bg-green-900/30 dark:text-green-400"}">${a.status_code || ""}</span></td>
7882
+ <td class="px-6 py-2 text-zinc-500 dark:text-zinc-400">${a.duration || 0}ms</td>
7883
+ </tr>
7884
+ `).join("") : `
7885
+ <tr>
7886
+ <td colspan="4" class="px-6 py-8 text-center text-zinc-500 dark:text-zinc-400">No activity recorded yet.</td>
7887
+ </tr>
7888
+ `}
7889
+ </tbody>
7890
+ </table>
7891
+ </div>
7892
+ </div>
7893
+ </div>
7894
+ `;
7895
+ return c.html(renderAdminLayoutCatalyst({
7896
+ title: "Analytics",
7897
+ pageTitle: "Analytics Dashboard",
7898
+ currentPath: "/admin/analytics",
7899
+ version: c.get("appVersion"),
7900
+ user: user ? {
7901
+ name: user.email.split("@")[0] || "Admin",
7902
+ email: user.email,
7903
+ role: user.role
7904
+ } : void 0,
7905
+ content: content2,
7906
+ dynamicMenuItems: c.get("pluginMenuItems")
7907
+ }));
7908
+ });
7909
+ function escapeHtml3(str) {
7910
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
7911
+ }
7912
+
7913
+ // src/plugins/core-plugins/analytics/index.ts
7914
+ function createAnalyticsPlugin() {
7915
+ const builder = PluginBuilder.create({
7916
+ name: "core-analytics",
7917
+ version: "1.0.0-beta.1",
7918
+ description: "Core analytics tracking and reporting plugin"
7919
+ });
7920
+ builder.metadata({
7921
+ author: {
7922
+ name: "SonicJS Team",
7923
+ email: "team@sonicjs.com"
7924
+ },
7925
+ license: "MIT",
7926
+ compatibility: "^0.1.0",
7927
+ dependencies: ["core-auth"]
7928
+ // Requires auth for admin access
7929
+ });
7930
+ const analyticsAPI = new Hono();
7931
+ analyticsAPI.get("/stats", async (c) => {
7932
+ const timeRange = c.req.query("range") || "7d";
7933
+ c.req.query("metric") || "all";
7934
+ return c.json({
7935
+ message: "Analytics stats",
7936
+ data: {
7937
+ pageviews: 12500,
7938
+ uniqueVisitors: 3200,
7939
+ sessions: 4800,
7940
+ avgSessionDuration: 245,
7941
+ bounceRate: 0.35,
7942
+ topPages: [
7943
+ { path: "/", views: 3200 },
7944
+ { path: "/about", views: 1800 },
7945
+ { path: "/contact", views: 950 }
7946
+ ],
7947
+ timeRange
7948
+ }
7949
+ });
7950
+ });
7951
+ analyticsAPI.post("/track", async (c) => {
7952
+ const event = await c.req.json();
7953
+ console.info("Analytics event tracked:", event);
7954
+ return c.json({
7955
+ message: "Event tracked successfully",
7956
+ eventId: `event-${Date.now()}`
7957
+ });
7958
+ });
7959
+ analyticsAPI.get("/reports", async (c) => {
7960
+ const reportType = c.req.query("type") || "traffic";
7961
+ const startDate = c.req.query("start");
7962
+ const endDate = c.req.query("end");
7963
+ return c.json({
7964
+ message: "Analytics report",
7965
+ data: {
7966
+ reportType,
7967
+ dateRange: { start: startDate, end: endDate },
7968
+ data: []
7969
+ }
7970
+ });
7971
+ });
7972
+ analyticsAPI.get("/realtime", async (c) => {
7973
+ return c.json({
7974
+ message: "Real-time analytics",
7975
+ data: {
7976
+ activeUsers: 23,
7977
+ activePages: [
7978
+ { path: "/", users: 8 },
7979
+ { path: "/blog", users: 5 },
7980
+ { path: "/products", users: 4 }
7981
+ ],
7982
+ recentEvents: []
7983
+ }
7984
+ });
7985
+ });
7986
+ builder.addRoute("/api/analytics", analyticsAPI, {
7987
+ description: "Analytics tracking and reporting API",
7988
+ requiresAuth: true,
7989
+ roles: ["admin", "analytics:read"],
7990
+ priority: 3
7991
+ });
7992
+ builder.addRoute("/admin/analytics", adminRoutes4, {
7993
+ description: "Analytics admin dashboard",
7994
+ requiresAuth: true,
7995
+ priority: 50
7996
+ });
7997
+ builder.addSingleMiddleware("analytics-tracker", async (c, next) => {
7998
+ const start = Date.now();
7999
+ const path = c.req.path;
8000
+ const method = c.req.method;
8001
+ const userAgent = c.req.header("user-agent");
8002
+ const referer = c.req.header("referer");
8003
+ const ip = c.req.header("CF-Connecting-IP") || c.req.header("x-forwarded-for");
8004
+ await next();
8005
+ const duration = Date.now() - start;
8006
+ const status = c.res.status;
8007
+ const analyticsData = {
8008
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
8009
+ path,
8010
+ method,
8011
+ status,
8012
+ duration,
8013
+ userAgent,
8014
+ referer,
8015
+ ip,
8016
+ responseSize: c.res.headers.get("content-length") || 0
8017
+ };
8018
+ console.debug("Analytics tracking:", analyticsData);
8019
+ }, {
8020
+ description: "Track page views and request analytics",
8021
+ global: true,
8022
+ priority: 99
8023
+ // Run last to capture response data
8024
+ });
8025
+ builder.addService("analyticsService", {
8026
+ trackEvent: async (event) => {
8027
+ console.info("Tracking event:", event);
8028
+ return { eventId: `event-${Date.now()}` };
8029
+ },
8030
+ trackPageView: async (data) => {
8031
+ console.info("Tracking pageview:", data.path);
8032
+ return { viewId: `view-${Date.now()}` };
8033
+ },
8034
+ getStats: async (_timeRange) => {
8035
+ return {
8036
+ pageviews: 12500,
8037
+ sessions: 4800,
8038
+ uniqueVisitors: 3200
8039
+ };
8040
+ },
8041
+ generateReport: async (type, options) => {
8042
+ console.info(`Generating ${type} report with options:`, options);
8043
+ return { reportId: `report-${Date.now()}` };
8044
+ }
8045
+ }, {
8046
+ description: "Core analytics tracking service",
8047
+ singleton: true
8048
+ });
8049
+ const pageViewSchema = PluginHelpers.createSchema([
8050
+ { name: "path", type: "string", optional: false },
8051
+ { name: "title", type: "string", optional: true },
8052
+ { name: "referrer", type: "string", optional: true },
8053
+ { name: "userAgent", type: "string", optional: true },
8054
+ { name: "ipAddress", type: "string", optional: true },
8055
+ { name: "sessionId", type: "string", optional: true },
8056
+ { name: "userId", type: "number", optional: true },
8057
+ { name: "duration", type: "number", optional: true }
8058
+ ]);
8059
+ const eventSchema = PluginHelpers.createSchema([
8060
+ { name: "eventType", type: "string", optional: false },
8061
+ { name: "eventName", type: "string", optional: false },
8062
+ { name: "eventData", type: "object", optional: true },
8063
+ { name: "path", type: "string", optional: true },
8064
+ { name: "sessionId", type: "string", optional: true },
8065
+ { name: "userId", type: "number", optional: true }
8066
+ ]);
8067
+ const pageViewMigration = PluginHelpers.createMigration("analytics_pageviews", [
8068
+ { name: "id", type: "INTEGER", primaryKey: true },
8069
+ { name: "path", type: "TEXT", nullable: false },
8070
+ { name: "title", type: "TEXT", nullable: true },
8071
+ { name: "referrer", type: "TEXT", nullable: true },
8072
+ { name: "user_agent", type: "TEXT", nullable: true },
8073
+ { name: "ip_address", type: "TEXT", nullable: true },
8074
+ { name: "session_id", type: "TEXT", nullable: true },
8075
+ { name: "user_id", type: "INTEGER", nullable: true },
8076
+ { name: "duration", type: "INTEGER", nullable: true }
8077
+ ]);
8078
+ const eventMigration = PluginHelpers.createMigration("analytics_events", [
8079
+ { name: "id", type: "INTEGER", primaryKey: true },
8080
+ { name: "event_type", type: "TEXT", nullable: false },
8081
+ { name: "event_name", type: "TEXT", nullable: false },
8082
+ { name: "event_data", type: "TEXT", nullable: true },
8083
+ { name: "path", type: "TEXT", nullable: true },
8084
+ { name: "session_id", type: "TEXT", nullable: true },
8085
+ { name: "user_id", type: "INTEGER", nullable: true }
8086
+ ]);
8087
+ builder.addModel("PageView", {
8088
+ tableName: "analytics_pageviews",
8089
+ schema: pageViewSchema,
8090
+ migrations: [pageViewMigration]
8091
+ });
8092
+ builder.addModel("AnalyticsEvent", {
8093
+ tableName: "analytics_events",
8094
+ schema: eventSchema,
8095
+ migrations: [eventMigration]
8096
+ });
8097
+ builder.addHook(HOOKS2.REQUEST_START, async (data, _context) => {
8098
+ data.analytics = {
8099
+ startTime: Date.now(),
8100
+ sessionId: `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
8101
+ };
8102
+ return data;
8103
+ }, {
8104
+ priority: 1,
8105
+ description: "Initialize analytics tracking for requests"
8106
+ });
8107
+ builder.addHook(HOOKS2.REQUEST_END, async (data, _context) => {
8108
+ if (data.analytics) {
8109
+ const duration = Date.now() - data.analytics.startTime;
8110
+ console.debug(`Request completed in ${duration}ms`);
8111
+ }
8112
+ return data;
8113
+ }, {
8114
+ priority: 1,
8115
+ description: "Complete analytics tracking for requests"
8116
+ });
8117
+ builder.addHook(HOOKS2.USER_LOGIN, async (data, context) => {
8118
+ await context.services?.analyticsService?.trackEvent({
8119
+ eventType: "auth",
8120
+ eventName: "user_login",
8121
+ userId: data.userId,
8122
+ eventData: { loginMethod: data.method }
8123
+ });
8124
+ return data;
8125
+ }, {
8126
+ priority: 8,
8127
+ description: "Track user login events"
8128
+ });
8129
+ builder.addHook("content:view", async (data, context) => {
8130
+ await context.services?.analyticsService?.trackEvent({
8131
+ eventType: "content",
8132
+ eventName: "content_view",
8133
+ eventData: {
8134
+ contentId: data.id,
8135
+ contentType: data.type,
8136
+ title: data.title
8137
+ }
8138
+ });
8139
+ return data;
8140
+ }, {
8141
+ priority: 8,
8142
+ description: "Track content view events"
8143
+ });
8144
+ builder.addAdminPage(
8145
+ "/analytics",
8146
+ "Analytics Dashboard",
8147
+ "AnalyticsDashboardView",
8148
+ {
8149
+ description: "View analytics overview and key metrics",
8150
+ permissions: ["admin", "analytics:read"],
8151
+ icon: "chart-bar"
8152
+ }
8153
+ );
8154
+ builder.addAdminPage(
8155
+ "/analytics/reports",
8156
+ "Analytics Reports",
8157
+ "AnalyticsReportsView",
8158
+ {
8159
+ description: "Generate and view detailed analytics reports",
8160
+ permissions: ["admin", "analytics:read"],
8161
+ icon: "document-report"
8162
+ }
8163
+ );
8164
+ builder.addAdminPage(
8165
+ "/analytics/realtime",
8166
+ "Real-time Analytics",
8167
+ "AnalyticsRealtimeView",
8168
+ {
8169
+ description: "View real-time visitor activity",
8170
+ permissions: ["admin", "analytics:read"],
8171
+ icon: "lightning-bolt"
8172
+ }
8173
+ );
8174
+ builder.addAdminPage(
8175
+ "/analytics/settings",
8176
+ "Analytics Settings",
8177
+ "AnalyticsSettingsView",
8178
+ {
8179
+ description: "Configure analytics tracking and data collection",
8180
+ permissions: ["admin", "analytics:configure"],
8181
+ icon: "cog"
8182
+ }
8183
+ );
8184
+ builder.addMenuItem("Analytics", "/admin/analytics", {
8185
+ icon: "chart-bar",
8186
+ order: 40,
8187
+ permissions: ["admin", "analytics:read"]
8188
+ });
8189
+ builder.addMenuItem("Dashboard", "/admin/analytics", {
8190
+ icon: "chart-bar",
8191
+ parent: "Analytics",
8192
+ order: 1,
8193
+ permissions: ["admin", "analytics:read"]
8194
+ });
8195
+ builder.addMenuItem("Reports", "/admin/analytics/reports", {
8196
+ icon: "document-report",
8197
+ parent: "Analytics",
8198
+ order: 2,
8199
+ permissions: ["admin", "analytics:read"]
8200
+ });
8201
+ builder.addMenuItem("Real-time", "/admin/analytics/realtime", {
8202
+ icon: "lightning-bolt",
8203
+ parent: "Analytics",
8204
+ order: 3,
8205
+ permissions: ["admin", "analytics:read"]
8206
+ });
8207
+ builder.addMenuItem("Settings", "/admin/analytics/settings", {
8208
+ icon: "cog",
8209
+ parent: "Analytics",
8210
+ order: 4,
8211
+ permissions: ["admin", "analytics:configure"]
8212
+ });
8213
+ builder.lifecycle({
8214
+ install: async () => {
8215
+ console.info("Installing analytics plugin...");
8216
+ },
8217
+ activate: async () => {
8218
+ console.info("Activating analytics plugin...");
8219
+ },
8220
+ deactivate: async () => {
8221
+ console.info("Deactivating analytics plugin...");
8222
+ },
8223
+ configure: async (config) => {
8224
+ console.info("Configuring analytics plugin...", config);
8225
+ }
8226
+ });
8227
+ return builder.build();
8228
+ }
8229
+ var analyticsPlugin = createAnalyticsPlugin();
8230
+
8231
+ // src/plugins/core-plugins/analytics/services/event-tracking-service.ts
8232
+ var EventTrackingService = class {
8233
+ constructor(db) {
8234
+ this.db = db;
8235
+ }
8236
+ async trackEvent(input) {
8237
+ const id = crypto.randomUUID();
8238
+ const category = input.category || "user-activity";
8239
+ await this.db.prepare(`
8240
+ INSERT INTO analytics_events (id, event, category, properties, user_id, session_id, ip_address, user_agent, path)
8241
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
8242
+ `).bind(
8243
+ id,
8244
+ input.event,
8245
+ category,
8246
+ input.properties ? JSON.stringify(input.properties) : null,
8247
+ input.userId || null,
8248
+ input.sessionId || null,
8249
+ input.ipAddress || null,
8250
+ input.userAgent || null,
8251
+ input.path || null
8252
+ ).run();
8253
+ return id;
8254
+ }
8255
+ async trackBatch(events) {
8256
+ const ids = [];
8257
+ const stmts = events.map((input) => {
8258
+ const id = crypto.randomUUID();
8259
+ ids.push(id);
8260
+ const category = input.category || "user-activity";
8261
+ return this.db.prepare(`
8262
+ INSERT INTO analytics_events (id, event, category, properties, user_id, session_id, ip_address, user_agent, path)
8263
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
8264
+ `).bind(
8265
+ id,
8266
+ input.event,
8267
+ category,
8268
+ input.properties ? JSON.stringify(input.properties) : null,
8269
+ input.userId || null,
8270
+ input.sessionId || null,
8271
+ input.ipAddress || null,
8272
+ input.userAgent || null,
8273
+ input.path || null
8274
+ );
8275
+ });
8276
+ await this.db.batch(stmts);
8277
+ return ids;
8278
+ }
8279
+ async queryEvents(filters = {}) {
8280
+ const conditions = [];
8281
+ const params = [];
8282
+ if (filters.event) {
8283
+ conditions.push("event = ?");
8284
+ params.push(filters.event);
8285
+ }
8286
+ if (filters.category) {
8287
+ conditions.push("category = ?");
8288
+ params.push(filters.category);
8289
+ }
8290
+ if (filters.userId) {
8291
+ conditions.push("user_id = ?");
8292
+ params.push(filters.userId);
8293
+ }
8294
+ if (filters.sessionId) {
8295
+ conditions.push("session_id = ?");
8296
+ params.push(filters.sessionId);
8297
+ }
8298
+ if (filters.startDate) {
8299
+ conditions.push("created_at >= ?");
8300
+ params.push(filters.startDate);
8301
+ }
8302
+ if (filters.endDate) {
8303
+ conditions.push("created_at <= ?");
8304
+ params.push(filters.endDate);
8305
+ }
8306
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
8307
+ const limit = filters.limit || 50;
8308
+ const offset = filters.offset || 0;
8309
+ const [countResult, eventsResult] = await Promise.all([
8310
+ this.db.prepare(`SELECT COUNT(*) as total FROM analytics_events ${where}`).bind(...params).first(),
8311
+ this.db.prepare(`SELECT * FROM analytics_events ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`).bind(...params, limit, offset).all()
8312
+ ]);
8313
+ const events = (eventsResult.results || []).map((e) => ({
8314
+ ...e,
8315
+ properties: e.properties ? JSON.parse(e.properties) : null
8316
+ }));
8317
+ return { events, total: countResult?.total || 0 };
8318
+ }
8319
+ async getStats(startDate, endDate) {
8320
+ const conditions = [];
8321
+ const params = [];
8322
+ if (startDate) {
8323
+ conditions.push("created_at >= ?");
8324
+ params.push(startDate);
8325
+ }
8326
+ if (endDate) {
8327
+ conditions.push("created_at <= ?");
8328
+ params.push(endDate);
8329
+ }
8330
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
8331
+ const [totals, topEvents] = await Promise.all([
8332
+ this.db.prepare(`
8333
+ SELECT
8334
+ COUNT(*) as total_events,
8335
+ COUNT(DISTINCT user_id) as unique_users,
8336
+ COUNT(DISTINCT session_id) as unique_sessions
8337
+ FROM analytics_events ${where}
8338
+ `).bind(...params).first(),
8339
+ this.db.prepare(`
8340
+ SELECT event, COUNT(*) as count
8341
+ FROM analytics_events ${where}
8342
+ GROUP BY event ORDER BY count DESC LIMIT 20
8343
+ `).bind(...params).all()
8344
+ ]);
8345
+ return {
8346
+ totalEvents: totals?.total_events || 0,
8347
+ uniqueUsers: totals?.unique_users || 0,
8348
+ uniqueSessions: totals?.unique_sessions || 0,
8349
+ topEvents: (topEvents.results || []).map((r) => ({ event: r.event, count: r.count }))
8350
+ };
8351
+ }
8352
+ };
8353
+
8354
+ // src/plugins/core-plugins/analytics/routes/api.ts
8355
+ var apiRoutes4 = new Hono();
8356
+ apiRoutes4.post("/", async (c) => {
8357
+ const db = c.env.DB;
8358
+ const service = new EventTrackingService(db);
8359
+ const ip = c.req.header("cf-connecting-ip") || c.req.header("x-forwarded-for")?.split(",")[0]?.trim() || "unknown";
8360
+ const userAgent = c.req.header("user-agent") || "";
8361
+ const user = c.get("user");
8362
+ let body;
8363
+ try {
8364
+ body = await c.req.json();
8365
+ } catch {
8366
+ return c.json({ error: "Invalid JSON body" }, 400);
8367
+ }
8368
+ if (Array.isArray(body)) {
8369
+ if (body.length > 100) {
8370
+ return c.json({ error: "Batch size limit is 100 events" }, 400);
8371
+ }
8372
+ const events = body.map((e) => ({
8373
+ event: e.event,
8374
+ category: e.category || "user-activity",
8375
+ properties: e.properties,
8376
+ userId: user?.userId || e.userId,
8377
+ sessionId: e.sessionId,
8378
+ ipAddress: ip,
8379
+ userAgent,
8380
+ path: e.path
8381
+ }));
8382
+ const invalid = events.find((e) => !e.event || typeof e.event !== "string");
8383
+ if (invalid) {
8384
+ return c.json({ error: 'Each event must have an "event" string field' }, 400);
8385
+ }
8386
+ const ids = await service.trackBatch(events);
8387
+ return c.json({ success: true, eventIds: ids, count: ids.length });
8388
+ }
8389
+ if (!body.event || typeof body.event !== "string") {
8390
+ return c.json({ error: '"event" field is required and must be a string' }, 400);
8391
+ }
8392
+ const eventId = await service.trackEvent({
8393
+ event: body.event,
8394
+ category: body.category || "user-activity",
8395
+ properties: body.properties,
8396
+ userId: user?.userId || body.userId,
8397
+ sessionId: body.sessionId,
8398
+ ipAddress: ip,
8399
+ userAgent,
8400
+ path: body.path
8401
+ });
8402
+ return c.json({ success: true, eventId });
8403
+ });
8404
+ apiRoutes4.get("/", async (c) => {
8405
+ const user = c.get("user");
8406
+ if (!user || user.role !== "admin") {
8407
+ return c.json({ error: "Admin access required" }, 403);
8408
+ }
8409
+ const db = c.env.DB;
8410
+ const service = new EventTrackingService(db);
8411
+ const filters = {
8412
+ event: c.req.query("event") || void 0,
8413
+ category: c.req.query("category") || void 0,
8414
+ userId: c.req.query("userId") || void 0,
8415
+ sessionId: c.req.query("sessionId") || void 0,
8416
+ startDate: c.req.query("startDate") ? parseInt(c.req.query("startDate")) : void 0,
8417
+ endDate: c.req.query("endDate") ? parseInt(c.req.query("endDate")) : void 0,
8418
+ limit: c.req.query("limit") ? parseInt(c.req.query("limit")) : 50,
8419
+ offset: c.req.query("offset") ? parseInt(c.req.query("offset")) : 0
8420
+ };
8421
+ const result = await service.queryEvents(filters);
8422
+ return c.json(result);
8423
+ });
8424
+ apiRoutes4.get("/stats", async (c) => {
8425
+ const user = c.get("user");
8426
+ if (!user || user.role !== "admin") {
8427
+ return c.json({ error: "Admin access required" }, 403);
8428
+ }
8429
+ const db = c.env.DB;
8430
+ const service = new EventTrackingService(db);
8431
+ const startDate = c.req.query("startDate") ? parseInt(c.req.query("startDate")) : void 0;
8432
+ const endDate = c.req.query("endDate") ? parseInt(c.req.query("endDate")) : void 0;
8433
+ const stats = await service.getStats(startDate, endDate);
8434
+ return c.json(stats);
8435
+ });
8436
+
7765
8437
  // src/plugins/cache/services/cache-config.ts
7766
8438
  var CACHE_CONFIGS = {
7767
8439
  // Content (high read, low write)
@@ -9677,6 +10349,12 @@ function createSonicJSApp(config = {}) {
9677
10349
  app2.route(route.path, route.handler);
9678
10350
  }
9679
10351
  }
10352
+ if (analyticsPlugin.routes && analyticsPlugin.routes.length > 0) {
10353
+ for (const route of analyticsPlugin.routes) {
10354
+ app2.route(route.path, route.handler);
10355
+ }
10356
+ }
10357
+ app2.route("/api/events", apiRoutes4);
9680
10358
  if (stripePlugin.routes && stripePlugin.routes.length > 0) {
9681
10359
  for (const route of stripePlugin.routes) {
9682
10360
  app2.route(route.path, route.handler);