@sonicjs-cms/core 2.19.0 → 3.0.0-beta.11

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 (230) hide show
  1. package/README.md +52 -52
  2. package/dist/admin-documents-form.template-DDSH6ROU.js +6 -0
  3. package/dist/{admin-layout-catalyst.template-UMTIN66R.js.map → admin-documents-form.template-DDSH6ROU.js.map} +1 -1
  4. package/dist/admin-documents-form.template-LSZKGA5J.cjs +19 -0
  5. package/dist/{admin-layout-catalyst.template-HFD37TY5.cjs.map → admin-documents-form.template-LSZKGA5J.cjs.map} +1 -1
  6. package/dist/{filter-bar.template-DlVYMk-T.d.cts → admin-layout-catalyst.template-DrwDUfsE.d.cts} +25 -1
  7. package/dist/{filter-bar.template-DlVYMk-T.d.ts → admin-layout-catalyst.template-DrwDUfsE.d.ts} +25 -1
  8. package/dist/admin-layout-catalyst.template-KDHKVLXR.cjs +21 -0
  9. package/dist/admin-layout-catalyst.template-KDHKVLXR.cjs.map +1 -0
  10. package/dist/admin-layout-catalyst.template-YQ4EMF2J.js +7 -0
  11. package/dist/admin-layout-catalyst.template-YQ4EMF2J.js.map +1 -0
  12. package/dist/app-Bo0X1OWX.d.ts +1268 -0
  13. package/dist/app-Do66yCcV.d.cts +1268 -0
  14. package/dist/cache-DDARE4QE.js +4 -0
  15. package/dist/cache-DDARE4QE.js.map +1 -0
  16. package/dist/cache-LVYS4BPL.cjs +33 -0
  17. package/dist/cache-LVYS4BPL.cjs.map +1 -0
  18. package/dist/chunk-2CB4KY7I.cjs +771 -0
  19. package/dist/chunk-2CB4KY7I.cjs.map +1 -0
  20. package/dist/{chunk-4NPCDK6B.js → chunk-3PU4WVU6.js} +557 -90
  21. package/dist/chunk-3PU4WVU6.js.map +1 -0
  22. package/dist/chunk-4BTBSXMR.cjs +912 -0
  23. package/dist/chunk-4BTBSXMR.cjs.map +1 -0
  24. package/dist/{chunk-55RDMDOP.js → chunk-5V62WT6M.js} +181 -57
  25. package/dist/chunk-5V62WT6M.js.map +1 -0
  26. package/dist/chunk-6H66MSSL.js +273 -0
  27. package/dist/chunk-6H66MSSL.js.map +1 -0
  28. package/dist/chunk-AI663NBO.js +821 -0
  29. package/dist/chunk-AI663NBO.js.map +1 -0
  30. package/dist/chunk-BLMTL57B.js +767 -0
  31. package/dist/chunk-BLMTL57B.js.map +1 -0
  32. package/dist/{chunk-4ZSNJDLS.cjs → chunk-CRGUD4KC.cjs} +9 -9
  33. package/dist/chunk-CRGUD4KC.cjs.map +1 -0
  34. package/dist/chunk-GCDZZNIN.js +192 -0
  35. package/dist/chunk-GCDZZNIN.js.map +1 -0
  36. package/dist/chunk-HIKBY7MS.cjs +70 -0
  37. package/dist/chunk-HIKBY7MS.cjs.map +1 -0
  38. package/dist/chunk-HPAJKZAQ.js +387 -0
  39. package/dist/chunk-HPAJKZAQ.js.map +1 -0
  40. package/dist/chunk-IESEVHXL.js +66 -0
  41. package/dist/chunk-IESEVHXL.js.map +1 -0
  42. package/dist/chunk-IVPRUGTY.js +242 -0
  43. package/dist/chunk-IVPRUGTY.js.map +1 -0
  44. package/dist/{chunk-JZVHLLSI.cjs → chunk-IXUHXTHW.cjs} +2 -151
  45. package/dist/chunk-IXUHXTHW.cjs.map +1 -0
  46. package/dist/chunk-J6JTWD2A.cjs +100 -0
  47. package/dist/chunk-J6JTWD2A.cjs.map +1 -0
  48. package/dist/chunk-JEQ7FLOD.cjs +199 -0
  49. package/dist/chunk-JEQ7FLOD.cjs.map +1 -0
  50. package/dist/{chunk-ON5ZMSU4.js → chunk-JQISFW6U.js} +3 -3
  51. package/dist/chunk-JQISFW6U.js.map +1 -0
  52. package/dist/chunk-K25XHMM3.js +566 -0
  53. package/dist/chunk-K25XHMM3.js.map +1 -0
  54. package/dist/{chunk-R4FOLLFB.cjs → chunk-K342JMA3.cjs} +8730 -11520
  55. package/dist/chunk-K342JMA3.cjs.map +1 -0
  56. package/dist/{chunk-UYJ6TJHX.cjs → chunk-K623Q6WD.cjs} +181 -56
  57. package/dist/chunk-K623Q6WD.cjs.map +1 -0
  58. package/dist/chunk-KV3CM5RK.cjs +158 -0
  59. package/dist/chunk-KV3CM5RK.cjs.map +1 -0
  60. package/dist/{chunk-ABB34XUS.cjs → chunk-MKKGA3C4.cjs} +667 -19
  61. package/dist/chunk-MKKGA3C4.cjs.map +1 -0
  62. package/dist/chunk-N32OWET6.cjs +327 -0
  63. package/dist/chunk-N32OWET6.cjs.map +1 -0
  64. package/dist/chunk-NUKJ54GA.cjs +245 -0
  65. package/dist/chunk-NUKJ54GA.cjs.map +1 -0
  66. package/dist/{chunk-XWIA3HVX.js → chunk-OBA2RYZN.js} +6 -1249
  67. package/dist/chunk-OBA2RYZN.js.map +1 -0
  68. package/dist/chunk-ORF4CT74.cjs +276 -0
  69. package/dist/chunk-ORF4CT74.cjs.map +1 -0
  70. package/dist/{chunk-TFNTM3OA.js → chunk-PDYRDYXI.js} +645 -15
  71. package/dist/chunk-PDYRDYXI.js.map +1 -0
  72. package/dist/{chunk-OHYBNCVL.cjs → chunk-PXNTCCPE.cjs} +10 -1256
  73. package/dist/chunk-PXNTCCPE.cjs.map +1 -0
  74. package/dist/{chunk-E4YFJBM2.cjs → chunk-QJNKSFDJ.cjs} +876 -829
  75. package/dist/chunk-QJNKSFDJ.cjs.map +1 -0
  76. package/dist/chunk-QLFTG3QJ.js +1828 -0
  77. package/dist/chunk-QLFTG3QJ.js.map +1 -0
  78. package/dist/{chunk-BU7SFHGP.js → chunk-QZGABF2M.js} +3 -149
  79. package/dist/chunk-QZGABF2M.js.map +1 -0
  80. package/dist/chunk-RMRJGMDE.js +323 -0
  81. package/dist/chunk-RMRJGMDE.js.map +1 -0
  82. package/dist/chunk-RNZFGN4R.js +88 -0
  83. package/dist/chunk-RNZFGN4R.js.map +1 -0
  84. package/dist/chunk-RQ6N3FTV.js +900 -0
  85. package/dist/chunk-RQ6N3FTV.js.map +1 -0
  86. package/dist/{chunk-OCL3HMEG.js → chunk-SXLVXD2X.js} +7004 -9807
  87. package/dist/chunk-SXLVXD2X.js.map +1 -0
  88. package/dist/chunk-UHRHZXVR.cjs +408 -0
  89. package/dist/chunk-UHRHZXVR.cjs.map +1 -0
  90. package/dist/chunk-YA3TJ65D.cjs +575 -0
  91. package/dist/chunk-YA3TJ65D.cjs.map +1 -0
  92. package/dist/{chunk-7A4CB7T3.cjs → chunk-YJEBDJDV.cjs} +561 -91
  93. package/dist/chunk-YJEBDJDV.cjs.map +1 -0
  94. package/dist/chunk-YP7GW2G5.cjs +866 -0
  95. package/dist/chunk-YP7GW2G5.cjs.map +1 -0
  96. package/dist/chunk-ZUEIQFE5.js +154 -0
  97. package/dist/chunk-ZUEIQFE5.js.map +1 -0
  98. package/dist/{collection-config-B4PG-AaF.d.cts → collection-config-JgHOpFCG.d.cts} +30 -2
  99. package/dist/{collection-config-B4PG-AaF.d.ts → collection-config-JgHOpFCG.d.ts} +30 -2
  100. package/dist/config-HFXANXCC.js +6 -0
  101. package/dist/config-HFXANXCC.js.map +1 -0
  102. package/dist/config-ON6FNMYX.cjs +19 -0
  103. package/dist/config-ON6FNMYX.cjs.map +1 -0
  104. package/dist/define-plugin-BzNHc1ZI.d.ts +1321 -0
  105. package/dist/define-plugin-IWDKYaVm.d.cts +1321 -0
  106. package/dist/document-projection-TDWRJX3Z.cjs +13 -0
  107. package/dist/document-projection-TDWRJX3Z.cjs.map +1 -0
  108. package/dist/document-projection-YYMC6I4U.js +4 -0
  109. package/dist/document-projection-YYMC6I4U.js.map +1 -0
  110. package/dist/index.cjs +13739 -4328
  111. package/dist/index.cjs.map +1 -1
  112. package/dist/index.d.cts +331 -493
  113. package/dist/index.d.ts +331 -493
  114. package/dist/index.js +13456 -4067
  115. package/dist/index.js.map +1 -1
  116. package/dist/middleware.cjs +38 -32
  117. package/dist/middleware.d.cts +50 -7
  118. package/dist/middleware.d.ts +50 -7
  119. package/dist/middleware.js +9 -3
  120. package/dist/migrations-XQLBY7E5.js +4 -0
  121. package/dist/{migrations-H5IXZNCO.js.map → migrations-XQLBY7E5.js.map} +1 -1
  122. package/dist/migrations-ZXJEUTFA.cjs +13 -0
  123. package/dist/{migrations-566IIPS2.cjs.map → migrations-ZXJEUTFA.cjs.map} +1 -1
  124. package/dist/{plugin-bootstrap-DfVerYV4.d.cts → plugin-bootstrap-B8ThJU21.d.cts} +4315 -1661
  125. package/dist/{plugin-bootstrap-P_ciLp_C.d.ts → plugin-bootstrap-qu8hJgUt.d.ts} +4315 -1661
  126. package/dist/plugins.cjs +171 -12
  127. package/dist/plugins.d.cts +36 -2
  128. package/dist/plugins.d.ts +36 -2
  129. package/dist/plugins.js +5 -2
  130. package/dist/rbac-O73MFKDA.js +5 -0
  131. package/dist/rbac-O73MFKDA.js.map +1 -0
  132. package/dist/rbac-VONLJJKB.cjs +14 -0
  133. package/dist/rbac-VONLJJKB.cjs.map +1 -0
  134. package/dist/routes.cjs +42 -46
  135. package/dist/routes.d.cts +56 -146
  136. package/dist/routes.d.ts +56 -146
  137. package/dist/routes.js +18 -10
  138. package/dist/services.cjs +43 -76
  139. package/dist/services.d.cts +93 -55
  140. package/dist/services.d.ts +93 -55
  141. package/dist/services.js +6 -3
  142. package/dist/{telemetry-B9vIV4wh.d.cts → telemetry-Cku1ax74.d.cts} +1 -1
  143. package/dist/{telemetry-B9vIV4wh.d.ts → telemetry-Cku1ax74.d.ts} +1 -1
  144. package/dist/templates.cjs +17 -29
  145. package/dist/templates.d.cts +2 -89
  146. package/dist/templates.d.ts +2 -89
  147. package/dist/templates.js +3 -3
  148. package/dist/types-Dea1eNxU.d.cts +286 -0
  149. package/dist/types-Dea1eNxU.d.ts +286 -0
  150. package/dist/types.d.cts +2 -2
  151. package/dist/types.d.ts +2 -2
  152. package/dist/utils.cjs +21 -20
  153. package/dist/utils.d.cts +2 -2
  154. package/dist/utils.d.ts +2 -2
  155. package/dist/utils.js +3 -2
  156. package/migrations/0001_core.sql +184 -0
  157. package/migrations/0002_documents.sql +163 -0
  158. package/package.json +12 -7
  159. package/dist/admin-layout-catalyst.template-HFD37TY5.cjs +0 -17
  160. package/dist/admin-layout-catalyst.template-UMTIN66R.js +0 -7
  161. package/dist/app-C9esKLmh.d.cts +0 -112
  162. package/dist/app-C9esKLmh.d.ts +0 -112
  163. package/dist/chunk-4NPCDK6B.js.map +0 -1
  164. package/dist/chunk-4ZSNJDLS.cjs.map +0 -1
  165. package/dist/chunk-55RDMDOP.js.map +0 -1
  166. package/dist/chunk-635JAMSE.cjs +0 -653
  167. package/dist/chunk-635JAMSE.cjs.map +0 -1
  168. package/dist/chunk-7A4CB7T3.cjs.map +0 -1
  169. package/dist/chunk-ABB34XUS.cjs.map +0 -1
  170. package/dist/chunk-BU7SFHGP.js.map +0 -1
  171. package/dist/chunk-E4YFJBM2.cjs.map +0 -1
  172. package/dist/chunk-EXNEW5US.js +0 -648
  173. package/dist/chunk-EXNEW5US.js.map +0 -1
  174. package/dist/chunk-JZV22DEV.js +0 -1783
  175. package/dist/chunk-JZV22DEV.js.map +0 -1
  176. package/dist/chunk-JZVHLLSI.cjs.map +0 -1
  177. package/dist/chunk-OCL3HMEG.js.map +0 -1
  178. package/dist/chunk-OHYBNCVL.cjs.map +0 -1
  179. package/dist/chunk-ON5ZMSU4.js.map +0 -1
  180. package/dist/chunk-QFWHAFEO.js +0 -1843
  181. package/dist/chunk-QFWHAFEO.js.map +0 -1
  182. package/dist/chunk-R4FOLLFB.cjs.map +0 -1
  183. package/dist/chunk-RLMUFFUD.cjs +0 -2219
  184. package/dist/chunk-RLMUFFUD.cjs.map +0 -1
  185. package/dist/chunk-TFNTM3OA.js.map +0 -1
  186. package/dist/chunk-UYJ6TJHX.cjs.map +0 -1
  187. package/dist/chunk-WAEQXGCX.cjs +0 -1898
  188. package/dist/chunk-WAEQXGCX.cjs.map +0 -1
  189. package/dist/chunk-XWIA3HVX.js.map +0 -1
  190. package/dist/chunk-ZYAYUIZE.js +0 -2217
  191. package/dist/chunk-ZYAYUIZE.js.map +0 -1
  192. package/dist/migrations-566IIPS2.cjs +0 -13
  193. package/dist/migrations-H5IXZNCO.js +0 -4
  194. package/dist/plugin-manager-BoM3Q7o7.d.cts +0 -328
  195. package/dist/plugin-manager-Efx9RyDX.d.ts +0 -328
  196. package/migrations/001_initial_schema.sql +0 -170
  197. package/migrations/002_faq_plugin.sql +0 -86
  198. package/migrations/003_stage5_enhancements.sql +0 -121
  199. package/migrations/004_stage6_user_management.sql +0 -183
  200. package/migrations/005_stage7_workflow_automation.sql +0 -294
  201. package/migrations/006_plugin_system.sql +0 -155
  202. package/migrations/007_demo_login_plugin.sql +0 -23
  203. package/migrations/008_fix_slug_validation.sql +0 -22
  204. package/migrations/009_system_logging.sql +0 -57
  205. package/migrations/011_config_managed_collections.sql +0 -15
  206. package/migrations/012_testimonials_plugin.sql +0 -80
  207. package/migrations/013_code_examples_plugin.sql +0 -177
  208. package/migrations/014_fix_plugin_registry.sql +0 -88
  209. package/migrations/015_add_remaining_plugins.sql +0 -89
  210. package/migrations/016_remove_duplicate_cache_plugin.sql +0 -17
  211. package/migrations/017_auth_configurable_fields.sql +0 -49
  212. package/migrations/018_settings_table.sql +0 -23
  213. package/migrations/019_remove_blog_posts_collection.sql +0 -15
  214. package/migrations/020_add_email_plugin.sql +0 -22
  215. package/migrations/021_add_magic_link_auth_plugin.sql +0 -42
  216. package/migrations/022_add_tinymce_plugin.sql +0 -25
  217. package/migrations/023_add_easy_mdx_plugin.sql +0 -25
  218. package/migrations/024_add_quill_editor_plugin.sql +0 -25
  219. package/migrations/025_add_easymde_plugin.sql +0 -25
  220. package/migrations/026_add_otp_login.sql +0 -42
  221. package/migrations/027_fix_slug_field_type.sql +0 -18
  222. package/migrations/028_fix_slug_field_type_in_schemas.sql +0 -30
  223. package/migrations/029_add_forms_system.sql +0 -184
  224. package/migrations/030_add_turnstile_to_forms.sql +0 -14
  225. package/migrations/031_ai_search_plugin.sql +0 -45
  226. package/migrations/032_user_profiles.sql +0 -37
  227. package/migrations/033_form_content_integration.sql +0 -19
  228. package/migrations/034_security_audit_plugin.sql +0 -27
  229. package/migrations/035_user_profiles_data_column.sql +0 -16
  230. package/migrations/036_analytics_events.sql +0 -22
@@ -0,0 +1,1828 @@
1
+ import { getTelemetryConfig, sanitizeErrorMessage, sanitizeRoute, generateInstallationId, generateProjectId } from './chunk-X7ZAEI5S.js';
2
+ import { escapeHtml } from './chunk-TQABQWOP.js';
3
+ import { getCookie } from 'hono/cookie';
4
+
5
+ // src/services/collection-registry.ts
6
+ var CollectionRegistry = class {
7
+ byName = /* @__PURE__ */ new Map();
8
+ bySlug = /* @__PURE__ */ new Map();
9
+ /**
10
+ * Replace the registry contents with the given configs. Idempotent —
11
+ * calling with the same configs twice yields the same state.
12
+ */
13
+ register(configs) {
14
+ this.byName.clear();
15
+ this.bySlug.clear();
16
+ for (const config of configs) {
17
+ if (!config.name) continue;
18
+ const record = {
19
+ ...config,
20
+ id: config.name,
21
+ slug: config.slug ?? config.name.replace(/_/g, "-"),
22
+ managed: config.managed !== void 0 ? config.managed : true,
23
+ isActive: config.isActive !== void 0 ? config.isActive : true
24
+ };
25
+ this.byName.set(record.name, record);
26
+ this.bySlug.set(record.slug, record);
27
+ }
28
+ }
29
+ /** All registered collections (including inactive). */
30
+ list() {
31
+ return Array.from(this.byName.values());
32
+ }
33
+ /** Active collections only. */
34
+ listActive() {
35
+ return this.list().filter((c) => c.isActive !== false);
36
+ }
37
+ getByName(name) {
38
+ return this.byName.get(name);
39
+ }
40
+ /** For code-defined collections, id === name. */
41
+ getById(id) {
42
+ return this.byName.get(id);
43
+ }
44
+ /** Look up by the URL slug (set in CollectionConfig.slug). Falls back to getByName if needed. */
45
+ getBySlug(slug) {
46
+ return this.bySlug.get(slug);
47
+ }
48
+ /** Resolve a path segment to a record — tries slug first, then name. */
49
+ getBySlugOrName(slugOrName) {
50
+ return this.bySlug.get(slugOrName) ?? this.byName.get(slugOrName);
51
+ }
52
+ isActive(name) {
53
+ const record = this.byName.get(name);
54
+ return record?.isActive !== false && record !== void 0;
55
+ }
56
+ size() {
57
+ return this.byName.size;
58
+ }
59
+ /** Test helper — wipe state. */
60
+ clear() {
61
+ this.byName.clear();
62
+ this.bySlug.clear();
63
+ }
64
+ };
65
+ function collectionRecordToRow(record) {
66
+ return {
67
+ id: record.id,
68
+ name: record.name,
69
+ display_name: record.displayName,
70
+ description: record.description ?? null,
71
+ schema: record.schema,
72
+ is_active: record.isActive === false ? 0 : 1,
73
+ managed: record.managed === false ? 0 : 1,
74
+ source_type: "code",
75
+ source_id: null,
76
+ created_at: 0,
77
+ updated_at: 0
78
+ };
79
+ }
80
+ var instance = null;
81
+ function getCollectionRegistry() {
82
+ if (!instance) {
83
+ instance = new CollectionRegistry();
84
+ }
85
+ return instance;
86
+ }
87
+ function resetCollectionRegistry() {
88
+ instance = null;
89
+ }
90
+
91
+ // src/services/collection-loader.ts
92
+ function isCodeCollectionInternal(cfg) {
93
+ return cfg.internal === true;
94
+ }
95
+ function isDbDocTypeInternal(source) {
96
+ return source === "system" || source === "plugin";
97
+ }
98
+ async function getVisibleCollections(db) {
99
+ const codeRegistry = getCollectionRegistry().list();
100
+ const codeMap = /* @__PURE__ */ new Map();
101
+ for (const cfg of codeRegistry) {
102
+ if (!isCodeCollectionInternal(cfg)) {
103
+ codeMap.set(cfg.name, { name: cfg.name, displayName: cfg.displayName });
104
+ }
105
+ }
106
+ let dbRows = [];
107
+ try {
108
+ const { results } = await db.prepare(
109
+ "SELECT name, display_name, source FROM document_types WHERE is_active = 1 ORDER BY display_name"
110
+ ).all();
111
+ dbRows = results ?? [];
112
+ } catch {
113
+ }
114
+ const merged = new Map(codeMap);
115
+ for (const row of dbRows) {
116
+ if (!isDbDocTypeInternal(row.source) && !merged.has(row.name)) {
117
+ merged.set(row.name, { name: String(row.name), displayName: String(row.display_name) });
118
+ }
119
+ }
120
+ return Array.from(merged.values());
121
+ }
122
+ var registeredCollections = [];
123
+ function registerCollections(collections) {
124
+ for (const config of collections) {
125
+ if (!config.name || !config.displayName || !config.schema) {
126
+ console.error(`Invalid collection config: missing required fields`, config);
127
+ continue;
128
+ }
129
+ const normalizedConfig = {
130
+ ...config,
131
+ managed: config.managed !== void 0 ? config.managed : true,
132
+ isActive: config.isActive !== void 0 ? config.isActive : true
133
+ };
134
+ registeredCollections.push(normalizedConfig);
135
+ console.log(`\u2713 Registered collection: ${config.name}`);
136
+ }
137
+ }
138
+ async function loadCollectionConfigs() {
139
+ const collections = [...registeredCollections];
140
+ if (registeredCollections.length > 0) {
141
+ console.log(`\u{1F4E6} Found ${registeredCollections.length} registered collection(s) from application`);
142
+ } else {
143
+ console.log(`\u26A0\uFE0F No collections registered. Make sure to call registerCollections() in your app's index.ts`);
144
+ console.log(` Example: import myCollection from './collections/my-collection.collection'`);
145
+ console.log(` registerCollections([myCollection])`);
146
+ }
147
+ try {
148
+ const modules = import.meta.glob?.("../collections/*.collection.ts", { eager: true }) || {};
149
+ let coreCollectionCount = 0;
150
+ for (const [path, module] of Object.entries(modules)) {
151
+ try {
152
+ const configModule = module;
153
+ if (!configModule.default) {
154
+ console.warn(`Collection file ${path} does not export a default config`);
155
+ continue;
156
+ }
157
+ const config = configModule.default;
158
+ if (!config.name || !config.displayName || !config.schema) {
159
+ console.error(`Invalid collection config in ${path}: missing required fields`);
160
+ continue;
161
+ }
162
+ const normalizedConfig = {
163
+ ...config,
164
+ managed: config.managed !== void 0 ? config.managed : true,
165
+ isActive: config.isActive !== void 0 ? config.isActive : true
166
+ };
167
+ collections.push(normalizedConfig);
168
+ coreCollectionCount++;
169
+ console.log(`\u2713 Loaded core collection: ${config.name}`);
170
+ } catch (error) {
171
+ console.error(`Error loading collection from ${path}:`, error);
172
+ }
173
+ }
174
+ console.log(`\u{1F4CA} Collection summary: ${collections.length} total (${registeredCollections.length} from app, ${coreCollectionCount} from core)`);
175
+ return collections;
176
+ } catch (error) {
177
+ console.error("Error loading collection configurations:", error);
178
+ return collections;
179
+ }
180
+ }
181
+ async function loadCollectionConfig(name) {
182
+ try {
183
+ console.warn("loadCollectionConfig requires implementation in consuming application");
184
+ return null;
185
+ } catch (error) {
186
+ console.error(`Error loading collection ${name}:`, error);
187
+ return null;
188
+ }
189
+ }
190
+ async function getAvailableCollectionNames() {
191
+ try {
192
+ const modules = import.meta.glob?.("../collections/*.collection.ts") || {};
193
+ const names = [];
194
+ for (const path of Object.keys(modules)) {
195
+ const match = path.match(/\/([^/]+)\.collection\.ts$/);
196
+ if (match && match[1]) {
197
+ names.push(match[1]);
198
+ }
199
+ }
200
+ return names;
201
+ } catch (error) {
202
+ console.error("Error getting collection names:", error);
203
+ return [];
204
+ }
205
+ }
206
+ function validateCollectionConfig(config) {
207
+ const errors = [];
208
+ if (!config.name) {
209
+ errors.push("Collection name is required");
210
+ } else if (!/^[a-z0-9_-]+$/.test(config.name)) {
211
+ errors.push("Collection name must contain only lowercase letters, numbers, underscores, and hyphens");
212
+ }
213
+ if (!config.displayName) {
214
+ errors.push("Display name is required");
215
+ }
216
+ if (!config.schema) {
217
+ errors.push("Schema is required");
218
+ } else {
219
+ if (config.schema.type !== "object") {
220
+ errors.push('Schema type must be "object"');
221
+ }
222
+ if (!config.schema.properties || typeof config.schema.properties !== "object") {
223
+ errors.push("Schema must have properties");
224
+ }
225
+ for (const [fieldName, fieldConfig] of Object.entries(config.schema.properties || {})) {
226
+ if (!fieldConfig.type) {
227
+ errors.push(`Field "${fieldName}" is missing type`);
228
+ }
229
+ if (fieldConfig.type === "reference" && !fieldConfig.collection) {
230
+ errors.push(`Reference field "${fieldName}" is missing collection property`);
231
+ }
232
+ const layoutValue = fieldConfig.objectLayout;
233
+ if (layoutValue !== void 0) {
234
+ if (fieldConfig.type !== "object") {
235
+ errors.push(`Field "${fieldName}" uses objectLayout but is not an object field`);
236
+ } else if (!["nested", "flat"].includes(layoutValue)) {
237
+ errors.push(`Object field "${fieldName}" has invalid objectLayout. Use "nested" or "flat"`);
238
+ }
239
+ }
240
+ if (["select", "multiselect", "radio"].includes(fieldConfig.type) && !fieldConfig.enum) {
241
+ errors.push(`Select field "${fieldName}" is missing enum options`);
242
+ }
243
+ }
244
+ }
245
+ return {
246
+ valid: errors.length === 0,
247
+ errors
248
+ };
249
+ }
250
+ var TENANT_COOKIE = "sonicjs-tenant";
251
+ var TENANT_SWITCHER_MARKER = "<!-- TENANT_SWITCHER -->";
252
+ var MULTI_TENANT_PLUGIN_ID = "multi-tenant";
253
+ var CACHE_TTL_MS = 3e4;
254
+ var DEFAULT_SETTINGS = {
255
+ headerName: "X-Tenant-Id",
256
+ subdomainResolution: false,
257
+ rootDomain: ""
258
+ };
259
+ var cache = null;
260
+ function invalidateTenantCache() {
261
+ cache = null;
262
+ }
263
+ async function loadTenantState(db) {
264
+ const now = Date.now();
265
+ if (cache && now - cache.fetchedAt < CACHE_TTL_MS) return cache;
266
+ let pluginActive = false;
267
+ let settings = DEFAULT_SETTINGS;
268
+ const tenants = /* @__PURE__ */ new Map();
269
+ const domains = /* @__PURE__ */ new Map();
270
+ try {
271
+ const pluginRow = await db.prepare(
272
+ `SELECT data FROM documents
273
+ WHERE type_id = 'plugin' AND tenant_id = 'default' AND slug = ?
274
+ AND q_plugin_status = 'active' AND is_current_draft = 1 AND deleted_at IS NULL`
275
+ ).bind(MULTI_TENANT_PLUGIN_ID).first();
276
+ if (pluginRow) {
277
+ pluginActive = true;
278
+ try {
279
+ const data = typeof pluginRow.data === "string" ? JSON.parse(pluginRow.data) : pluginRow.data ?? {};
280
+ const s = data.settings ?? {};
281
+ settings = {
282
+ headerName: typeof s.headerName === "string" && s.headerName.trim() !== "" ? s.headerName.trim() : DEFAULT_SETTINGS.headerName,
283
+ subdomainResolution: s.subdomainResolution === true,
284
+ rootDomain: typeof s.rootDomain === "string" ? s.rootDomain.trim().toLowerCase() : ""
285
+ };
286
+ } catch {
287
+ settings = DEFAULT_SETTINGS;
288
+ }
289
+ const { results } = await db.prepare(
290
+ `SELECT slug, name, status, domain FROM auth_tenant`
291
+ ).all();
292
+ for (const row of results ?? []) {
293
+ const status = row.status === "inactive" ? "inactive" : "active";
294
+ tenants.set(row.slug, { name: row.name || row.slug, status });
295
+ if (row.domain && status === "active") {
296
+ domains.set(String(row.domain).toLowerCase(), row.slug);
297
+ }
298
+ }
299
+ if (!tenants.has("default")) tenants.set("default", { name: "Default", status: "active" });
300
+ }
301
+ } catch {
302
+ pluginActive = false;
303
+ }
304
+ cache = { pluginActive, settings, tenants, domains, fetchedAt: now };
305
+ return cache;
306
+ }
307
+ function isActiveTenant(state, slug) {
308
+ if (!slug) return false;
309
+ const entry = state.tenants.get(slug);
310
+ return !!entry && entry.status === "active";
311
+ }
312
+ function resolveTenantSlug(state, req, opts) {
313
+ if (!state.pluginActive) return "default";
314
+ const full = state;
315
+ const enforce = opts?.enforceMembership === true;
316
+ const member = (slug) => !enforce || slug === "default" || (opts?.memberSlugs?.has(slug) ?? false);
317
+ const accept = (slug) => isActiveTenant(full, slug) && member(slug);
318
+ const header = req.header?.trim().toLowerCase();
319
+ if (accept(header)) return header;
320
+ const cookie = req.cookie?.trim().toLowerCase();
321
+ if (accept(cookie)) return cookie;
322
+ const host = req.host?.split(":")[0]?.toLowerCase() ?? "";
323
+ if (host) {
324
+ const bySlug = state.domains.get(host);
325
+ if (accept(bySlug)) return bySlug;
326
+ const { subdomainResolution, rootDomain } = state.settings;
327
+ if (subdomainResolution && rootDomain && host.endsWith(`.${rootDomain}`)) {
328
+ const sub = host.slice(0, -(rootDomain.length + 1));
329
+ if (sub && !sub.includes(".") && accept(sub)) return sub;
330
+ }
331
+ }
332
+ return "default";
333
+ }
334
+ async function loadMemberRoles(db, userId) {
335
+ try {
336
+ const { results } = await db.prepare(`
337
+ SELECT t.slug, m.role FROM auth_tenant_member m
338
+ JOIN auth_tenant t ON t.id = m.tenant_id
339
+ WHERE m.user_id = ?
340
+ `).bind(userId).all();
341
+ return new Map((results ?? []).map((r) => [r.slug, r.role || "viewer"]));
342
+ } catch {
343
+ return /* @__PURE__ */ new Map();
344
+ }
345
+ }
346
+ function tenantMiddleware() {
347
+ return async (c, next) => {
348
+ const db = c.env?.DB;
349
+ if (!db) {
350
+ c.set("tenantId", "default");
351
+ return next();
352
+ }
353
+ const state = await loadTenantState(db);
354
+ const user = c.get("user");
355
+ let memberSlugs;
356
+ let memberRoles;
357
+ const enforceMembership = !!(user?.userId && state.pluginActive && !user.isSuperAdmin);
358
+ if (user?.userId && state.pluginActive && !user.isSuperAdmin) {
359
+ memberRoles = await loadMemberRoles(db, user.userId);
360
+ memberSlugs = new Set(memberRoles.keys());
361
+ }
362
+ const tenantId = resolveTenantSlug(
363
+ state,
364
+ {
365
+ header: c.req.header(state.settings.headerName),
366
+ cookie: getCookie(c, TENANT_COOKIE),
367
+ host: c.req.header("host")
368
+ },
369
+ { memberSlugs, enforceMembership }
370
+ );
371
+ c.set("tenantId", tenantId);
372
+ if (user?.userId) {
373
+ const role = tenantId === "default" || user.isSuperAdmin ? user.role : memberRoles?.get(tenantId) ?? user.role;
374
+ c.set("tenantRole", role);
375
+ }
376
+ await next();
377
+ const path = new URL(c.req.url).pathname;
378
+ if (!path.startsWith("/admin")) return;
379
+ if (!c.res.headers.get("content-type")?.includes("text/html")) return;
380
+ const status = c.res.status;
381
+ const headers = new Headers(c.res.headers);
382
+ const html = await c.res.text();
383
+ if (html.includes(TENANT_SWITCHER_MARKER)) {
384
+ const replacement = state.pluginActive ? renderTenantSwitcher(state, tenantId, enforceMembership ? memberSlugs : void 0) : "";
385
+ c.res = new Response(html.split(TENANT_SWITCHER_MARKER).join(replacement), { status, headers });
386
+ } else {
387
+ c.res = new Response(html, { status, headers });
388
+ }
389
+ };
390
+ }
391
+ function renderTenantSwitcher(state, currentTenantId, memberSlugs) {
392
+ const active = [...state.tenants.entries()].filter(([slug, t]) => t.status === "active" && (!memberSlugs || slug === "default" || memberSlugs.has(slug))).sort(([a], [b]) => a === "default" ? -1 : b === "default" ? 1 : a.localeCompare(b));
393
+ const options = active.map(
394
+ ([slug, t]) => `<option value="${escapeHtml(slug)}" ${slug === currentTenantId ? "selected" : ""}>${escapeHtml(t.name)}</option>`
395
+ ).join("");
396
+ return `
397
+ <div class="border-b border-zinc-950/5 px-4 py-3 dark:border-white/5" data-tenant-switcher>
398
+ <label for="tenant-switcher-select" class="mb-1 flex items-center gap-1.5 text-xs/5 font-medium text-zinc-500 dark:text-zinc-400">
399
+ <svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.75 21h16.5M4.5 3h15M5.25 3v18m13.5-18v18M9 6.75h1.5m-1.5 3h1.5m-1.5 3h1.5m3-6H15m-1.5 3H15m-1.5 3H15M9 21v-3.375c0-.621.504-1.125 1.125-1.125h3.75c.621 0 1.125.504 1.125 1.125V21"/></svg>
400
+ Tenant
401
+ </label>
402
+ <form method="POST" action="/admin/tenants/switch" data-tenant-switcher-form>
403
+ <select
404
+ id="tenant-switcher-select"
405
+ name="tenant"
406
+ onchange="this.form.requestSubmit ? this.form.requestSubmit() : this.form.submit()"
407
+ class="w-full rounded-lg border border-zinc-950/10 bg-white px-2 py-1.5 text-sm/5 text-zinc-950 dark:border-white/10 dark:bg-zinc-800 dark:text-white"
408
+ >${options}</select>
409
+ <input type="hidden" name="redirect" value="" data-tenant-switcher-redirect>
410
+ </form>
411
+ <script>
412
+ (function () {
413
+ var r = document.querySelector('[data-tenant-switcher-redirect]');
414
+ if (r) r.value = window.location.pathname + window.location.search;
415
+ })();
416
+ </script>
417
+ </div>`;
418
+ }
419
+
420
+ // src/services/plugin-service.ts
421
+ var TENANT = "default";
422
+ var TYPE_ID = "plugin";
423
+ var PluginService = class {
424
+ constructor(db) {
425
+ this.db = db;
426
+ }
427
+ async getAllPlugins() {
428
+ const { results } = await this.db.prepare(`
429
+ SELECT * FROM documents
430
+ WHERE type_id = ? AND tenant_id = ? AND is_current_draft = 1 AND deleted_at IS NULL
431
+ ORDER BY json_extract(data, '$.isCore') DESC, title ASC
432
+ `).bind(TYPE_ID, TENANT).all();
433
+ return (results || []).map(mapDocumentToPlugin);
434
+ }
435
+ async getPlugin(pluginId) {
436
+ const row = await this.db.prepare(`
437
+ SELECT * FROM documents
438
+ WHERE slug = ? AND type_id = ? AND tenant_id = ? AND is_current_draft = 1 AND deleted_at IS NULL
439
+ `).bind(pluginId, TYPE_ID, TENANT).first();
440
+ if (!row) return null;
441
+ return mapDocumentToPlugin(row);
442
+ }
443
+ async getPluginByName(name) {
444
+ return this.getPlugin(name);
445
+ }
446
+ async getPluginStats() {
447
+ const stats = await this.db.prepare(`
448
+ SELECT
449
+ COUNT(*) as total,
450
+ COUNT(CASE WHEN q_plugin_status = 'active' THEN 1 END) as active,
451
+ COUNT(CASE WHEN q_plugin_status = 'inactive' THEN 1 END) as inactive,
452
+ COUNT(CASE WHEN q_plugin_status = 'error' THEN 1 END) as errors
453
+ FROM documents
454
+ WHERE type_id = ? AND tenant_id = ? AND is_current_draft = 1 AND deleted_at IS NULL
455
+ `).bind(TYPE_ID, TENANT).first();
456
+ return {
457
+ total: stats?.total || 0,
458
+ active: stats?.active || 0,
459
+ inactive: stats?.inactive || 0,
460
+ errors: stats?.errors || 0,
461
+ uninstalled: 0
462
+ };
463
+ }
464
+ async installPlugin(pluginData) {
465
+ const slug = pluginData.id || pluginData.name || `plugin-${Date.now()}`;
466
+ const docId = crypto.randomUUID();
467
+ const now = Math.floor(Date.now() / 1e3);
468
+ const data = JSON.stringify({
469
+ name: slug,
470
+ displayName: pluginData.display_name || "Unnamed Plugin",
471
+ description: pluginData.description || "",
472
+ version: pluginData.version || "1.0.0",
473
+ author: pluginData.author || "Unknown",
474
+ category: pluginData.category || "utilities",
475
+ icon: pluginData.icon || "\u{1F50C}",
476
+ status: "active",
477
+ isCore: pluginData.is_core || false,
478
+ settings: pluginData.settings || {},
479
+ permissions: pluginData.permissions || [],
480
+ dependencies: pluginData.dependencies || [],
481
+ downloadCount: pluginData.download_count || 0,
482
+ rating: pluginData.rating || 0
483
+ });
484
+ await this.db.prepare(`
485
+ INSERT INTO documents (
486
+ id, root_id, type_id, version_number, is_current_draft, is_published, status,
487
+ parent_root_id, slug, title, tenant_id, locale, translation_group_id,
488
+ data, metadata, created_at, updated_at
489
+ ) VALUES (
490
+ ?, ?, ?, 1, 1, 1, 'published',
491
+ '', ?, ?, ?, 'default', '',
492
+ ?, '{}', ?, ?
493
+ )
494
+ `).bind(
495
+ docId,
496
+ docId,
497
+ TYPE_ID,
498
+ slug,
499
+ pluginData.display_name || "Unnamed Plugin",
500
+ TENANT,
501
+ data,
502
+ now,
503
+ now
504
+ ).run();
505
+ await this.logActivity(slug, "installed", null, { version: pluginData.version });
506
+ await this.logActivity(slug, "activated", null);
507
+ const installed = await this.getPlugin(slug);
508
+ if (!installed) throw new Error("Failed to install plugin");
509
+ return installed;
510
+ }
511
+ /**
512
+ * Ensure a definePlugin-registered plugin exists in the DB with active status.
513
+ * No-op if already present. Used by admin routes to auto-register SDK plugins
514
+ * that have never been explicitly installed.
515
+ */
516
+ async ensurePlugin(id, data) {
517
+ const existing = await this.getPlugin(id);
518
+ if (existing) return existing;
519
+ return this.installPlugin({
520
+ id,
521
+ name: id,
522
+ display_name: data.displayName || id,
523
+ description: data.description || "",
524
+ author: data.author || "",
525
+ version: data.version || "1.0.0"
526
+ });
527
+ }
528
+ async uninstallPlugin(pluginId) {
529
+ const plugin = await this.getPlugin(pluginId);
530
+ if (!plugin) throw new Error("Plugin not found");
531
+ if (plugin.is_core) throw new Error("Cannot uninstall core plugins");
532
+ if (plugin.status === "active") await this.deactivatePlugin(pluginId);
533
+ const now = Math.floor(Date.now() / 1e3);
534
+ await this.db.prepare(`
535
+ UPDATE documents
536
+ SET deleted_at = ?, updated_at = ?, is_current_draft = 0, is_published = 0
537
+ WHERE slug = ? AND type_id = ? AND tenant_id = ? AND is_current_draft = 1
538
+ `).bind(now, now, pluginId, TYPE_ID, TENANT).run();
539
+ await this.logActivity(pluginId, "uninstalled", null, { name: plugin.name });
540
+ }
541
+ async activatePlugin(pluginId) {
542
+ const plugin = await this.getPlugin(pluginId);
543
+ if (!plugin) throw new Error("Plugin not found");
544
+ if (plugin.dependencies && plugin.dependencies.length > 0) {
545
+ await this.checkDependencies(plugin.dependencies);
546
+ }
547
+ const now = Math.floor(Date.now() / 1e3);
548
+ await this.db.prepare(`
549
+ UPDATE documents
550
+ SET data = json_set(data, '$.status', 'active', '$.activatedAt', ?, '$.errorMessage', null),
551
+ updated_at = ?
552
+ WHERE slug = ? AND type_id = ? AND tenant_id = ? AND is_current_draft = 1 AND deleted_at IS NULL
553
+ `).bind(now, now, pluginId, TYPE_ID, TENANT).run();
554
+ await this.db.prepare(`UPDATE plugins SET status = 'active' WHERE id = ?`).bind(pluginId).run().catch(() => {
555
+ });
556
+ invalidateTenantCache();
557
+ await this.logActivity(pluginId, "activated", null);
558
+ }
559
+ async deactivatePlugin(pluginId) {
560
+ const plugin = await this.getPlugin(pluginId);
561
+ if (!plugin) throw new Error("Plugin not found");
562
+ await this.checkDependents(plugin.name);
563
+ const now = Math.floor(Date.now() / 1e3);
564
+ await this.db.prepare(`
565
+ UPDATE documents
566
+ SET data = json_set(data, '$.status', 'inactive', '$.activatedAt', null),
567
+ updated_at = ?
568
+ WHERE slug = ? AND type_id = ? AND tenant_id = ? AND is_current_draft = 1 AND deleted_at IS NULL
569
+ `).bind(now, pluginId, TYPE_ID, TENANT).run();
570
+ await this.db.prepare(`UPDATE plugins SET status = 'inactive' WHERE id = ?`).bind(pluginId).run().catch(() => {
571
+ });
572
+ invalidateTenantCache();
573
+ await this.logActivity(pluginId, "deactivated", null);
574
+ }
575
+ async updatePluginSettings(pluginId, settings) {
576
+ const plugin = await this.getPlugin(pluginId);
577
+ if (!plugin) throw new Error("Plugin not found");
578
+ const now = Math.floor(Date.now() / 1e3);
579
+ await this.db.prepare(`
580
+ UPDATE documents
581
+ SET data = json_set(data, '$.settings', json(?)), updated_at = ?
582
+ WHERE slug = ? AND type_id = ? AND tenant_id = ? AND is_current_draft = 1 AND deleted_at IS NULL
583
+ `).bind(JSON.stringify(settings), now, pluginId, TYPE_ID, TENANT).run();
584
+ invalidateTenantCache();
585
+ await this.logActivity(pluginId, "settings_updated", null);
586
+ }
587
+ async updatePluginVersion(pluginId, patch) {
588
+ const now = Math.floor(Date.now() / 1e3);
589
+ await this.db.prepare(`
590
+ UPDATE documents
591
+ SET data = json_set(data,
592
+ '$.version', ?,
593
+ '$.description', ?,
594
+ '$.permissions', json(?),
595
+ '$.settings', json(?)
596
+ ),
597
+ updated_at = ?
598
+ WHERE slug = ? AND type_id = ? AND tenant_id = ? AND is_current_draft = 1 AND deleted_at IS NULL
599
+ `).bind(
600
+ patch.version,
601
+ patch.description,
602
+ JSON.stringify(patch.permissions),
603
+ JSON.stringify(patch.settings || {}),
604
+ now,
605
+ pluginId,
606
+ TYPE_ID,
607
+ TENANT
608
+ ).run();
609
+ }
610
+ async setPluginError(pluginId, error) {
611
+ const now = Math.floor(Date.now() / 1e3);
612
+ await this.db.prepare(`
613
+ UPDATE documents
614
+ SET data = json_set(data, '$.status', 'error', '$.errorMessage', ?),
615
+ updated_at = ?
616
+ WHERE slug = ? AND type_id = ? AND tenant_id = ? AND is_current_draft = 1 AND deleted_at IS NULL
617
+ `).bind(error, now, pluginId, TYPE_ID, TENANT).run();
618
+ await this.logActivity(pluginId, "error", null, { error });
619
+ }
620
+ async getPluginActivity(pluginId, limit = 10) {
621
+ try {
622
+ const { results } = await this.db.prepare(`
623
+ SELECT id, data, created_at FROM documents
624
+ WHERE type_id = 'plugin_activity'
625
+ AND tenant_id = ?
626
+ AND is_current_draft = 1
627
+ AND deleted_at IS NULL
628
+ AND json_extract(data, '$.pluginId') = ?
629
+ ORDER BY created_at DESC
630
+ LIMIT ?
631
+ `).bind(TENANT, pluginId, limit).all();
632
+ return (results || []).map((row) => {
633
+ const d = typeof row.data === "string" ? JSON.parse(row.data) : row.data || {};
634
+ return {
635
+ id: row.id,
636
+ action: d.action,
637
+ userId: d.userId || null,
638
+ details: d.details || null,
639
+ timestamp: row.created_at
640
+ };
641
+ });
642
+ } catch {
643
+ return [];
644
+ }
645
+ }
646
+ async registerHook(pluginId, hookName, handlerName, priority = 10) {
647
+ const id = `hook-${Date.now()}`;
648
+ await this.db.prepare(`
649
+ INSERT INTO plugin_hooks (id, plugin_id, hook_name, handler_name, priority)
650
+ VALUES (?, ?, ?, ?, ?)
651
+ `).bind(id, pluginId, hookName, handlerName, priority).run();
652
+ }
653
+ async registerRoute(pluginId, path, method, handlerName, middleware) {
654
+ const id = `route-${Date.now()}`;
655
+ await this.db.prepare(`
656
+ INSERT INTO plugin_routes (id, plugin_id, path, method, handler_name, middleware)
657
+ VALUES (?, ?, ?, ?, ?, ?)
658
+ `).bind(id, pluginId, path, method, handlerName, JSON.stringify(middleware || [])).run();
659
+ }
660
+ async getPluginHooks(pluginId) {
661
+ const { results } = await this.db.prepare(`
662
+ SELECT * FROM plugin_hooks WHERE plugin_id = ? AND is_active = TRUE ORDER BY priority ASC
663
+ `).bind(pluginId).all();
664
+ return results || [];
665
+ }
666
+ async getPluginRoutes(pluginId) {
667
+ const { results } = await this.db.prepare(`
668
+ SELECT * FROM plugin_routes WHERE plugin_id = ? AND is_active = TRUE
669
+ `).bind(pluginId).all();
670
+ return results || [];
671
+ }
672
+ async checkDependencies(dependencies) {
673
+ for (const dep of dependencies) {
674
+ const plugin = await this.getPluginByName(dep);
675
+ if (!plugin || plugin.status !== "active") {
676
+ throw new Error(`Required dependency '${dep}' is not active`);
677
+ }
678
+ }
679
+ }
680
+ async checkDependents(pluginName) {
681
+ const { results } = await this.db.prepare(`
682
+ SELECT slug, title FROM documents
683
+ WHERE type_id = ? AND tenant_id = ? AND is_current_draft = 1 AND deleted_at IS NULL
684
+ AND q_plugin_status = 'active'
685
+ AND json_extract(data, '$.dependencies') LIKE ?
686
+ `).bind(TYPE_ID, TENANT, `%"${pluginName}"%`).all();
687
+ if (results && results.length > 0) {
688
+ const names = results.map((p) => p.title || p.slug).join(", ");
689
+ throw new Error(`Cannot deactivate. The following plugins depend on this one: ${names}`);
690
+ }
691
+ }
692
+ async logActivity(pluginId, action, userId, details) {
693
+ try {
694
+ const docId = crypto.randomUUID();
695
+ const now = Math.floor(Date.now() / 1e3);
696
+ const data = JSON.stringify({ pluginId, action, userId, details: details || null });
697
+ await this.db.prepare(`
698
+ INSERT INTO documents (
699
+ id, root_id, type_id, version_number, is_current_draft, is_published, status,
700
+ parent_root_id, slug, title, tenant_id, locale, translation_group_id,
701
+ data, metadata, created_at, updated_at
702
+ ) VALUES (
703
+ ?, ?, 'plugin_activity', 1, 1, 1, 'published',
704
+ '', ?, ?, 'default', 'default', '',
705
+ ?, '{}', ?, ?
706
+ )
707
+ `).bind(docId, docId, docId, action, data, now, now).run();
708
+ } catch {
709
+ }
710
+ }
711
+ };
712
+ function mapDocumentToPlugin(row) {
713
+ const data = typeof row.data === "string" ? JSON.parse(row.data) : row.data || {};
714
+ return {
715
+ id: row.slug || data.name || row.root_id,
716
+ name: data.name || row.slug || "",
717
+ display_name: data.displayName || row.title || "",
718
+ description: data.description || "",
719
+ version: data.version || "1.0.0",
720
+ author: data.author || "Unknown",
721
+ category: data.category || "utilities",
722
+ icon: data.icon || "\u{1F50C}",
723
+ status: data.status || "inactive",
724
+ is_core: data.isCore === true || data.isCore === 1,
725
+ settings: data.settings,
726
+ permissions: data.permissions,
727
+ dependencies: data.dependencies,
728
+ download_count: data.downloadCount || 0,
729
+ rating: data.rating || 0,
730
+ installed_at: row.created_at,
731
+ activated_at: data.activatedAt || void 0,
732
+ last_updated: row.updated_at,
733
+ error_message: data.errorMessage || void 0
734
+ };
735
+ }
736
+
737
+ // src/plugins/manifest-registry.ts
738
+ var PLUGIN_REGISTRY = {
739
+ "ai-search": {
740
+ "id": "ai-search",
741
+ "codeName": "ai-search-plugin",
742
+ "displayName": "AI Search",
743
+ "description": "Advanced search with Cloudflare AI Search. Full-text search, semantic search, and advanced filtering across all content collections.",
744
+ "version": "1.0.0",
745
+ "author": "SonicJS",
746
+ "category": "content",
747
+ "iconEmoji": "\u{1F50D}",
748
+ "is_core": true,
749
+ "permissions": [
750
+ "settings:write",
751
+ "admin:access",
752
+ "content:read"
753
+ ],
754
+ "dependencies": [],
755
+ "defaultSettings": {
756
+ "enabled": true,
757
+ "ai_mode_enabled": true,
758
+ "selected_collections": [],
759
+ "dismissed_collections": [],
760
+ "autocomplete_enabled": true,
761
+ "cache_duration": 1,
762
+ "results_limit": 20,
763
+ "index_media": false
764
+ },
765
+ "adminMenu": {
766
+ "label": "AI Search",
767
+ "icon": "magnifying-glass",
768
+ "path": "/admin/plugins/ai-search",
769
+ "order": 50
770
+ }
771
+ },
772
+ "core-analytics": {
773
+ "id": "core-analytics",
774
+ "codeName": "core-analytics",
775
+ "displayName": "Analytics & Insights",
776
+ "description": "Core analytics system for tracking page views, user behavior, and content performance. Provides dashboards and reports with real-time metrics",
777
+ "version": "1.0.0-beta.1",
778
+ "author": "SonicJS Team",
779
+ "category": "seo",
780
+ "iconEmoji": "\u{1F4CA}",
781
+ "is_core": true,
782
+ "permissions": [
783
+ "analytics:view",
784
+ "analytics:export"
785
+ ],
786
+ "dependencies": [],
787
+ "defaultSettings": {},
788
+ "adminMenu": {
789
+ "label": "Analytics",
790
+ "icon": "chart-bar",
791
+ "path": "/admin/analytics",
792
+ "order": 50
793
+ }
794
+ },
795
+ "core-auth": {
796
+ "id": "core-auth",
797
+ "codeName": "core-auth",
798
+ "displayName": "Authentication System",
799
+ "description": "Core authentication and user management system with role-based access control, session management, and security features",
800
+ "version": "1.0.0-beta.1",
801
+ "author": "SonicJS Team",
802
+ "category": "security",
803
+ "iconEmoji": "\u{1F510}",
804
+ "is_core": true,
805
+ "permissions": [
806
+ "manage:users",
807
+ "manage:roles",
808
+ "manage:permissions"
809
+ ],
810
+ "dependencies": [],
811
+ "defaultSettings": {
812
+ "requiredFields": {
813
+ "email": {
814
+ "required": true,
815
+ "minLength": 5,
816
+ "label": "Email",
817
+ "type": "email"
818
+ },
819
+ "password": {
820
+ "required": true,
821
+ "minLength": 8,
822
+ "label": "Password",
823
+ "type": "password"
824
+ },
825
+ "firstName": {
826
+ "required": true,
827
+ "minLength": 1,
828
+ "label": "First Name",
829
+ "type": "text"
830
+ },
831
+ "lastName": {
832
+ "required": true,
833
+ "minLength": 1,
834
+ "label": "Last Name",
835
+ "type": "text"
836
+ }
837
+ },
838
+ "validation": {
839
+ "emailFormat": true,
840
+ "passwordRequirements": {
841
+ "requireUppercase": false,
842
+ "requireLowercase": false,
843
+ "requireNumbers": false,
844
+ "requireSpecialChars": false
845
+ }
846
+ },
847
+ "registration": {
848
+ "enabled": true,
849
+ "requireEmailVerification": false,
850
+ "defaultRole": "viewer"
851
+ }
852
+ },
853
+ "adminMenu": null
854
+ },
855
+ "core-cache": {
856
+ "id": "core-cache",
857
+ "codeName": "core-cache",
858
+ "displayName": "Cache System",
859
+ "description": "Three-tiered caching system with in-memory and KV storage. Provides automatic caching for content, users, media, and API responses with configurable TTL and invalidation patterns.",
860
+ "version": "1.0.0-beta.1",
861
+ "author": "SonicJS",
862
+ "category": "system",
863
+ "iconEmoji": "\u26A1",
864
+ "is_core": true,
865
+ "permissions": [
866
+ "cache.view",
867
+ "cache.clear",
868
+ "cache.invalidate"
869
+ ],
870
+ "dependencies": [],
871
+ "defaultSettings": {
872
+ "enableMemoryCache": true,
873
+ "enableKVCache": true,
874
+ "enableDatabaseCache": true,
875
+ "defaultTTL": 3600
876
+ },
877
+ "adminMenu": {
878
+ "label": "Cache",
879
+ "icon": "server",
880
+ "path": "/admin/cache",
881
+ "order": 60
882
+ }
883
+ },
884
+ "core-media": {
885
+ "id": "core-media",
886
+ "codeName": "core-media",
887
+ "displayName": "Media Manager",
888
+ "description": "Core media upload and management system with support for images, videos, and documents. Includes automatic optimization, thumbnail generation, and cloud storage integration",
889
+ "version": "1.0.0-beta.1",
890
+ "author": "SonicJS Team",
891
+ "category": "media",
892
+ "iconEmoji": "\u{1F4F8}",
893
+ "is_core": true,
894
+ "permissions": [
895
+ "manage:media",
896
+ "upload:files"
897
+ ],
898
+ "dependencies": [],
899
+ "defaultSettings": {},
900
+ "adminMenu": {
901
+ "label": "Media",
902
+ "icon": "image",
903
+ "path": "/admin/media",
904
+ "order": 30
905
+ }
906
+ },
907
+ "database-tools": {
908
+ "id": "database-tools",
909
+ "codeName": "database-tools",
910
+ "displayName": "Database Tools",
911
+ "description": "Database management and administration tools including migrations, backups, and query execution",
912
+ "version": "1.0.0-beta.1",
913
+ "author": "SonicJS Team",
914
+ "category": "development",
915
+ "iconEmoji": "\u{1F5C4}\uFE0F",
916
+ "is_core": true,
917
+ "permissions": [
918
+ "database:admin"
919
+ ],
920
+ "dependencies": [],
921
+ "defaultSettings": {
922
+ "enableTruncate": true,
923
+ "enableBackup": true,
924
+ "enableValidation": true,
925
+ "requireConfirmation": true
926
+ },
927
+ "adminMenu": null
928
+ },
929
+ "demo-login-plugin": {
930
+ "id": "demo-login-plugin",
931
+ "codeName": "demo-login-plugin",
932
+ "displayName": "Demo Login",
933
+ "description": "Quick demo login functionality for testing and demonstrations",
934
+ "version": "1.0.0-beta.1",
935
+ "author": "SonicJS Team",
936
+ "category": "utilities",
937
+ "iconEmoji": "\u{1F3AF}",
938
+ "is_core": false,
939
+ "permissions": [],
940
+ "dependencies": [
941
+ "core-auth"
942
+ ],
943
+ "defaultSettings": {
944
+ "enableNotice": true,
945
+ "demoEmail": "admin@sonicjs.com",
946
+ "demoPassword": "sonicjs!"
947
+ },
948
+ "adminMenu": null
949
+ },
950
+ "easy-mdx": {
951
+ "id": "easy-mdx",
952
+ "codeName": "easy-mdx",
953
+ "displayName": "EasyMDE Markdown Editor",
954
+ "description": "Lightweight markdown editor with live preview. Provides a simple and efficient editor with markdown support for richtext fields.",
955
+ "version": "1.0.0",
956
+ "author": "SonicJS Team",
957
+ "category": "editor",
958
+ "iconEmoji": "\u{1F4DD}",
959
+ "is_core": false,
960
+ "permissions": [],
961
+ "dependencies": [],
962
+ "defaultSettings": {
963
+ "defaultHeight": 400,
964
+ "theme": "dark",
965
+ "toolbar": "full",
966
+ "placeholder": "Start writing your content..."
967
+ },
968
+ "adminMenu": null
969
+ },
970
+ "email": {
971
+ "id": "email",
972
+ "codeName": "email",
973
+ "displayName": "Email",
974
+ "description": "Send transactional emails via Cloudflare Email Service. Subscribes to auth lifecycle events (welcome, password reset, password changed) and runs a 5-minute reconciliation cron against the CF GraphQL Activity Log.",
975
+ "version": "1.0.0",
976
+ "author": "SonicJS Team",
977
+ "category": "utilities",
978
+ "iconEmoji": "\u{1F4E7}",
979
+ "is_core": true,
980
+ "permissions": [
981
+ "email:manage",
982
+ "email:send",
983
+ "email:view-logs"
984
+ ],
985
+ "dependencies": [],
986
+ "defaultSettings": {
987
+ "provider": "cloudflare",
988
+ "resendApiKey": "",
989
+ "fromEmail": "",
990
+ "fromName": "",
991
+ "replyTo": "",
992
+ "logoUrl": "",
993
+ "cfAccountId": "",
994
+ "cfEmailApiToken": ""
995
+ },
996
+ "adminMenu": {
997
+ "label": "Email",
998
+ "icon": "envelope",
999
+ "path": "/admin/plugins/email",
1000
+ "order": 80
1001
+ }
1002
+ },
1003
+ "forms": {
1004
+ "id": "forms",
1005
+ "codeName": "forms",
1006
+ "displayName": "Forms",
1007
+ "description": "Form builder with Form.io integration, Turnstile CAPTCHA support, and submission management",
1008
+ "version": "1.0.0",
1009
+ "author": "SonicJS Team",
1010
+ "category": "content",
1011
+ "iconEmoji": "\u{1F4CB}",
1012
+ "is_core": true,
1013
+ "permissions": [
1014
+ "forms:manage",
1015
+ "forms:view"
1016
+ ],
1017
+ "dependencies": [],
1018
+ "defaultSettings": {},
1019
+ "adminMenu": {
1020
+ "label": "Forms",
1021
+ "icon": "document-text",
1022
+ "path": "/admin/forms",
1023
+ "order": 30
1024
+ }
1025
+ },
1026
+ "global-variables": {
1027
+ "id": "global-variables",
1028
+ "codeName": "global-variables",
1029
+ "displayName": "Global Variables",
1030
+ "description": "Dynamic content variables with inline token support. Manage key-value variables via admin UI and use {variable_key} syntax in rich text fields for server-side resolution. Includes full CRUD admin page.",
1031
+ "version": "1.1.0",
1032
+ "author": "SonicJS Community",
1033
+ "category": "content",
1034
+ "iconEmoji": "\u{1F524}",
1035
+ "is_core": false,
1036
+ "permissions": [
1037
+ "global-variables:manage",
1038
+ "global-variables:view"
1039
+ ],
1040
+ "dependencies": [],
1041
+ "defaultSettings": {
1042
+ "enableResolution": true,
1043
+ "cacheEnabled": true,
1044
+ "cacheTTL": 300
1045
+ },
1046
+ "adminMenu": {
1047
+ "label": "Global Variables",
1048
+ "icon": "variable",
1049
+ "path": "/admin/global-variables",
1050
+ "order": 45
1051
+ }
1052
+ },
1053
+ "hello-world": {
1054
+ "id": "hello-world",
1055
+ "codeName": "hello-world",
1056
+ "displayName": "Hello World",
1057
+ "description": "A simple Hello World plugin demonstration",
1058
+ "version": "1.0.0-beta.1",
1059
+ "author": "SonicJS Team",
1060
+ "category": "utilities",
1061
+ "iconEmoji": "\u{1F44B}",
1062
+ "is_core": false,
1063
+ "permissions": [
1064
+ "hello-world:view"
1065
+ ],
1066
+ "dependencies": [],
1067
+ "defaultSettings": {},
1068
+ "adminMenu": {
1069
+ "label": "Hello World",
1070
+ "icon": "hand-raised",
1071
+ "path": "/admin/hello-world",
1072
+ "order": 90
1073
+ }
1074
+ },
1075
+ "lexical-editor": {
1076
+ "id": "lexical-editor",
1077
+ "codeName": "lexical-editor",
1078
+ "displayName": "Lexical Rich Text Editor",
1079
+ "description": "Lexical editor integration for rich text editing. Default rich text editor for SonicJS \u2014 on by default for greenfield installs.",
1080
+ "version": "1.0.0",
1081
+ "author": "SonicJS Team",
1082
+ "category": "editor",
1083
+ "iconEmoji": "\u{1F4DD}",
1084
+ "is_core": true,
1085
+ "defaultActive": true,
1086
+ "permissions": [],
1087
+ "dependencies": [],
1088
+ "defaultSettings": {
1089
+ "defaultHeight": 300,
1090
+ "defaultToolbar": "standard",
1091
+ "placeholder": "Enter content..."
1092
+ },
1093
+ "adminMenu": {
1094
+ "label": "Lexical Editor",
1095
+ "icon": "pencil-square",
1096
+ "path": "/admin/plugins/lexical-editor",
1097
+ "order": 80
1098
+ }
1099
+ },
1100
+ "magic-link-auth": {
1101
+ "id": "magic-link-auth",
1102
+ "codeName": "magic-link-auth",
1103
+ "displayName": "Magic Link Authentication",
1104
+ "description": "Passwordless authentication via email magic links. Users receive a secure one-time link to sign in without entering a password.",
1105
+ "version": "1.0.0",
1106
+ "author": "SonicJS Team",
1107
+ "category": "security",
1108
+ "iconEmoji": "\u{1F517}",
1109
+ "is_core": false,
1110
+ "permissions": [],
1111
+ "dependencies": [
1112
+ "email"
1113
+ ],
1114
+ "defaultSettings": {
1115
+ "linkExpiryMinutes": 15,
1116
+ "rateLimitPerHour": 5,
1117
+ "allowNewUsers": true
1118
+ },
1119
+ "adminMenu": null
1120
+ },
1121
+ "multi-tenant": {
1122
+ "id": "multi-tenant",
1123
+ "codeName": "multi-tenant",
1124
+ "displayName": "Multi-Tenant",
1125
+ "description": "Multi-tenancy for the document model: tenant registry, per-request tenant resolution (header, admin switcher cookie, or domain), and tenant-scoped content isolation. Off by default.",
1126
+ "version": "1.0.0",
1127
+ "author": "SonicJS Team",
1128
+ "category": "utilities",
1129
+ "iconEmoji": "\u{1F3E2}",
1130
+ "is_core": false,
1131
+ "permissions": [
1132
+ "tenants.manage",
1133
+ "tenants.view"
1134
+ ],
1135
+ "dependencies": [],
1136
+ "defaultSettings": {
1137
+ "headerName": "X-Tenant-Id",
1138
+ "subdomainResolution": false,
1139
+ "rootDomain": ""
1140
+ },
1141
+ "adminMenu": {
1142
+ "label": "Tenants",
1143
+ "icon": "building-office",
1144
+ "path": "/admin/tenants",
1145
+ "order": 80
1146
+ }
1147
+ },
1148
+ "oauth-providers": {
1149
+ "id": "oauth-providers",
1150
+ "codeName": "oauth-providers",
1151
+ "displayName": "OAuth Providers",
1152
+ "description": "OAuth2/OIDC social login with GitHub, Google, and more",
1153
+ "version": "1.0.0-beta.1",
1154
+ "author": "SonicJS Team",
1155
+ "category": "authentication",
1156
+ "iconEmoji": "\u{1F511}",
1157
+ "is_core": true,
1158
+ "permissions": [],
1159
+ "dependencies": [],
1160
+ "defaultSettings": {
1161
+ "providers": {
1162
+ "github": {
1163
+ "clientId": "",
1164
+ "clientSecret": "",
1165
+ "enabled": false
1166
+ },
1167
+ "google": {
1168
+ "clientId": "",
1169
+ "clientSecret": "",
1170
+ "enabled": false
1171
+ }
1172
+ }
1173
+ },
1174
+ "adminMenu": null
1175
+ },
1176
+ "otp-login": {
1177
+ "id": "otp-login",
1178
+ "codeName": "otp-login",
1179
+ "displayName": "OTP Login",
1180
+ "description": "Passwordless authentication via email one-time codes",
1181
+ "version": "1.0.0-beta.1",
1182
+ "author": "SonicJS Team",
1183
+ "category": "security",
1184
+ "iconEmoji": "\u{1F522}",
1185
+ "is_core": false,
1186
+ "permissions": [
1187
+ "otp:manage",
1188
+ "otp:request",
1189
+ "otp:verify"
1190
+ ],
1191
+ "dependencies": [
1192
+ "email"
1193
+ ],
1194
+ "defaultSettings": {
1195
+ "codeLength": 6,
1196
+ "codeExpiryMinutes": 10,
1197
+ "maxAttempts": 3,
1198
+ "rateLimitPerHour": 5,
1199
+ "allowNewUserRegistration": false,
1200
+ "logoUrl": "",
1201
+ "logoWidth": 150,
1202
+ "logoBorderWidth": 0,
1203
+ "logoBorderColor": "#ffffff",
1204
+ "loginUrl": "",
1205
+ "loginButtonText": ""
1206
+ },
1207
+ "adminMenu": {
1208
+ "label": "OTP Login",
1209
+ "icon": "key",
1210
+ "path": "/admin/plugins/otp-login/settings",
1211
+ "order": 85
1212
+ }
1213
+ },
1214
+ "quill-editor": {
1215
+ "id": "quill-editor",
1216
+ "codeName": "quill-editor",
1217
+ "displayName": "Quill Rich Text Editor",
1218
+ "description": "Quill WYSIWYG editor integration for rich text editing. Lightweight, modern editor with customizable toolbars and dark mode support.",
1219
+ "version": "1.0.0",
1220
+ "author": "SonicJS Team",
1221
+ "category": "editor",
1222
+ "iconEmoji": "\u270D\uFE0F",
1223
+ "is_core": true,
1224
+ "permissions": [],
1225
+ "dependencies": [],
1226
+ "defaultSettings": {
1227
+ "version": "2.0.2",
1228
+ "defaultHeight": 300,
1229
+ "defaultToolbar": "full",
1230
+ "theme": "snow"
1231
+ },
1232
+ "adminMenu": null
1233
+ },
1234
+ "redirect-management": {
1235
+ "id": "redirect-management",
1236
+ "codeName": "redirect-management",
1237
+ "displayName": "Redirect Management",
1238
+ "description": "URL redirect management with exact, partial, and regex matching",
1239
+ "version": "1.0.0",
1240
+ "author": "ahaas",
1241
+ "category": "utilities",
1242
+ "iconEmoji": "\u21AA\uFE0F",
1243
+ "is_core": false,
1244
+ "permissions": [
1245
+ "redirect.manage",
1246
+ "redirect.view"
1247
+ ],
1248
+ "dependencies": [],
1249
+ "defaultSettings": {
1250
+ "enabled": true,
1251
+ "autoOffloadEnabled": false
1252
+ },
1253
+ "adminMenu": {
1254
+ "label": "Redirects",
1255
+ "icon": "arrow-right",
1256
+ "path": "/admin/redirects",
1257
+ "order": 85
1258
+ }
1259
+ },
1260
+ "security-audit": {
1261
+ "id": "security-audit",
1262
+ "codeName": "security-audit",
1263
+ "displayName": "Security Audit",
1264
+ "description": "Security event logging, brute-force detection, and analytics dashboard. Monitors login attempts, registrations, lockouts, and suspicious activity.",
1265
+ "version": "1.0.0-beta.1",
1266
+ "author": "SonicJS Team",
1267
+ "category": "security",
1268
+ "iconEmoji": "\u{1F6E1}\uFE0F",
1269
+ "is_core": false,
1270
+ "permissions": [
1271
+ "security-audit:view",
1272
+ "security-audit:manage"
1273
+ ],
1274
+ "dependencies": [],
1275
+ "defaultSettings": {
1276
+ "retention": {
1277
+ "daysToKeep": 90,
1278
+ "maxEvents": 1e5,
1279
+ "autoPurge": true
1280
+ },
1281
+ "bruteForce": {
1282
+ "enabled": true,
1283
+ "maxFailedAttemptsPerIP": 10,
1284
+ "maxFailedAttemptsPerEmail": 5,
1285
+ "windowMinutes": 15,
1286
+ "lockoutDurationMinutes": 30,
1287
+ "alertThreshold": 20
1288
+ },
1289
+ "logging": {
1290
+ "logSuccessfulLogins": true,
1291
+ "logLogouts": true,
1292
+ "logRegistrations": true,
1293
+ "logPasswordResets": true,
1294
+ "logPermissionDenied": true
1295
+ }
1296
+ },
1297
+ "adminMenu": {
1298
+ "label": "Security Audit",
1299
+ "icon": "shield-check",
1300
+ "path": "/admin/plugins/security-audit",
1301
+ "order": 85
1302
+ }
1303
+ },
1304
+ "shortcodes": {
1305
+ "id": "shortcodes",
1306
+ "codeName": "shortcodes",
1307
+ "displayName": "Shortcodes",
1308
+ "description": 'Registered shortcode functions for dynamic content. Use [[shortcode_name param="value"]] syntax in rich text fields for server-side resolution. Includes handler registry, CRUD admin, and live preview.',
1309
+ "version": "1.0.0",
1310
+ "author": "SonicJS Community",
1311
+ "category": "content",
1312
+ "iconEmoji": "",
1313
+ "is_core": false,
1314
+ "permissions": [
1315
+ "shortcodes:manage",
1316
+ "shortcodes:view"
1317
+ ],
1318
+ "dependencies": [],
1319
+ "defaultSettings": {},
1320
+ "adminMenu": {
1321
+ "label": "Shortcodes",
1322
+ "icon": "bolt",
1323
+ "path": "/admin/shortcodes",
1324
+ "order": 46
1325
+ }
1326
+ },
1327
+ "stripe": {
1328
+ "id": "stripe",
1329
+ "codeName": "stripe",
1330
+ "displayName": "Stripe Subscriptions",
1331
+ "description": "Stripe subscription management with webhook handling, checkout sessions, and subscription gating",
1332
+ "version": "1.0.0-beta.1",
1333
+ "author": "SonicJS Team",
1334
+ "category": "payments",
1335
+ "iconEmoji": "\u{1F4B3}",
1336
+ "is_core": true,
1337
+ "permissions": [
1338
+ "stripe:manage",
1339
+ "stripe:view"
1340
+ ],
1341
+ "dependencies": [],
1342
+ "defaultSettings": {
1343
+ "stripeSecretKey": "",
1344
+ "stripeWebhookSecret": "",
1345
+ "stripePriceId": "",
1346
+ "successUrl": "/admin/dashboard",
1347
+ "cancelUrl": "/admin/dashboard"
1348
+ },
1349
+ "adminMenu": {
1350
+ "label": "Stripe",
1351
+ "icon": "credit-card",
1352
+ "path": "/admin/plugins/stripe",
1353
+ "order": 90
1354
+ }
1355
+ },
1356
+ "tinymce-plugin": {
1357
+ "id": "tinymce-plugin",
1358
+ "codeName": "tinymce-plugin",
1359
+ "displayName": "TinyMCE Rich Text Editor",
1360
+ "description": "Powerful WYSIWYG rich text editor for content creation. Provides a full-featured editor with formatting, media embedding, and customizable toolbars for richtext fields.",
1361
+ "version": "1.0.0",
1362
+ "author": "SonicJS Team",
1363
+ "category": "editor",
1364
+ "iconEmoji": "\u{1F4DD}",
1365
+ "is_core": false,
1366
+ "permissions": [],
1367
+ "dependencies": [],
1368
+ "defaultSettings": {
1369
+ "apiKey": "no-api-key",
1370
+ "defaultHeight": 300,
1371
+ "defaultToolbar": "full",
1372
+ "skin": "oxide-dark"
1373
+ },
1374
+ "adminMenu": null
1375
+ },
1376
+ "turnstile": {
1377
+ "id": "turnstile",
1378
+ "codeName": "turnstile-plugin",
1379
+ "displayName": "Cloudflare Turnstile",
1380
+ "description": "CAPTCHA-free bot protection using Cloudflare Turnstile. Provides reusable verification for any form.",
1381
+ "version": "1.0.0",
1382
+ "author": "SonicJS",
1383
+ "category": "security",
1384
+ "iconEmoji": "\u{1F6E1}\uFE0F",
1385
+ "is_core": true,
1386
+ "permissions": [
1387
+ "settings:write",
1388
+ "admin:access"
1389
+ ],
1390
+ "dependencies": [],
1391
+ "defaultSettings": {
1392
+ "siteKey": "",
1393
+ "secretKey": "",
1394
+ "theme": "auto",
1395
+ "size": "normal",
1396
+ "mode": "managed",
1397
+ "appearance": "always",
1398
+ "preClearanceEnabled": false,
1399
+ "preClearanceLevel": "managed",
1400
+ "enabled": false
1401
+ },
1402
+ "adminMenu": {
1403
+ "label": "Turnstile",
1404
+ "icon": "shield-check",
1405
+ "path": "/admin/plugins/turnstile/settings",
1406
+ "order": 100
1407
+ }
1408
+ },
1409
+ "user-profiles": {
1410
+ "id": "user-profiles",
1411
+ "codeName": "user-profiles",
1412
+ "displayName": "User Profiles",
1413
+ "description": "Configurable custom profile fields for users",
1414
+ "version": "1.0.0-beta.1",
1415
+ "author": "SonicJS Team",
1416
+ "category": "users",
1417
+ "iconEmoji": "\u{1F464}",
1418
+ "is_core": true,
1419
+ "permissions": [],
1420
+ "dependencies": [],
1421
+ "defaultSettings": {},
1422
+ "adminMenu": null
1423
+ },
1424
+ "versioning": {
1425
+ "id": "versioning",
1426
+ "codeName": "versioning",
1427
+ "displayName": "Versioning",
1428
+ "description": "View and restore content version history for types with versioning enabled.",
1429
+ "version": "1.0.0",
1430
+ "author": "SonicJS Team",
1431
+ "category": "content",
1432
+ "iconEmoji": "\u{1F551}",
1433
+ "is_core": true,
1434
+ "permissions": [],
1435
+ "dependencies": [],
1436
+ "defaultSettings": {},
1437
+ "adminMenu": null
1438
+ }
1439
+ };
1440
+ var ALL_PLUGIN_IDS = Object.keys(PLUGIN_REGISTRY);
1441
+ ALL_PLUGIN_IDS.filter(
1442
+ (id) => PLUGIN_REGISTRY[id]?.adminMenu !== null
1443
+ );
1444
+ function findPluginByCodeName(codeName) {
1445
+ return Object.values(PLUGIN_REGISTRY).find((p) => p.codeName === codeName) || PLUGIN_REGISTRY[codeName];
1446
+ }
1447
+
1448
+ // src/services/plugin-bootstrap.ts
1449
+ var BOOTSTRAP_PLUGIN_IDS = [
1450
+ "core-auth",
1451
+ // Collect any registry entries marked defaultActive (e.g. lexical-editor)
1452
+ ...Object.values(PLUGIN_REGISTRY).filter((e) => e.defaultActive === true && e.id !== "core-auth").map((e) => e.id)
1453
+ ];
1454
+ function registryToCorePlugin(entry) {
1455
+ return {
1456
+ id: entry.id,
1457
+ name: entry.codeName,
1458
+ display_name: entry.displayName,
1459
+ description: entry.description,
1460
+ version: entry.version,
1461
+ author: entry.author,
1462
+ category: entry.category,
1463
+ icon: entry.iconEmoji,
1464
+ permissions: entry.permissions,
1465
+ dependencies: entry.dependencies,
1466
+ settings: entry.defaultSettings
1467
+ };
1468
+ }
1469
+ var PluginBootstrapService = class {
1470
+ constructor(db) {
1471
+ this.db = db;
1472
+ this.pluginService = new PluginService(db);
1473
+ }
1474
+ pluginService;
1475
+ /**
1476
+ * Core plugins derived from the auto-generated plugin registry.
1477
+ * Only plugins listed in BOOTSTRAP_PLUGIN_IDS AND marked is_core=true are auto-installed.
1478
+ * Non-core plugins are available in the registry but not bootstrapped.
1479
+ */
1480
+ CORE_PLUGINS = BOOTSTRAP_PLUGIN_IDS.filter((id) => PLUGIN_REGISTRY[id] !== void 0 && PLUGIN_REGISTRY[id].is_core === true).map((id) => registryToCorePlugin(PLUGIN_REGISTRY[id]));
1481
+ /**
1482
+ * Bootstrap all core plugins - install them if they don't exist
1483
+ */
1484
+ async bootstrapCorePlugins() {
1485
+ console.log("[PluginBootstrap] Starting core plugin bootstrap process...");
1486
+ try {
1487
+ for (const corePlugin of this.CORE_PLUGINS) {
1488
+ await this.ensurePluginInstalled(corePlugin);
1489
+ }
1490
+ console.log(
1491
+ "[PluginBootstrap] Core plugin bootstrap completed successfully"
1492
+ );
1493
+ } catch (error) {
1494
+ console.error("[PluginBootstrap] Error during plugin bootstrap:", error);
1495
+ throw error;
1496
+ }
1497
+ }
1498
+ /**
1499
+ * Ensure a specific plugin is installed
1500
+ */
1501
+ async ensurePluginInstalled(plugin) {
1502
+ try {
1503
+ const existingPlugin = await this.pluginService.getPlugin(plugin.id);
1504
+ if (existingPlugin) {
1505
+ console.log(
1506
+ `[PluginBootstrap] Plugin already installed: ${plugin.display_name} (status: ${existingPlugin.status})`
1507
+ );
1508
+ if (existingPlugin.version !== plugin.version) {
1509
+ console.log(
1510
+ `[PluginBootstrap] Updating plugin version: ${plugin.display_name} from ${existingPlugin.version} to ${plugin.version}`
1511
+ );
1512
+ await this.updatePlugin(plugin);
1513
+ }
1514
+ if (plugin.id === "core-auth" && existingPlugin.status !== "active") {
1515
+ console.log(
1516
+ `[PluginBootstrap] Core-auth plugin is inactive, activating it now...`
1517
+ );
1518
+ await this.pluginService.activatePlugin(plugin.id);
1519
+ }
1520
+ } else {
1521
+ console.log(
1522
+ `[PluginBootstrap] Installing plugin: ${plugin.display_name}`
1523
+ );
1524
+ await this.pluginService.installPlugin({
1525
+ ...plugin,
1526
+ is_core: plugin.name.startsWith("core-")
1527
+ });
1528
+ console.log(
1529
+ `[PluginBootstrap] Activating newly installed plugin: ${plugin.display_name}`
1530
+ );
1531
+ await this.pluginService.activatePlugin(plugin.id);
1532
+ }
1533
+ } catch (error) {
1534
+ console.error(
1535
+ `[PluginBootstrap] Error ensuring plugin ${plugin.display_name}:`,
1536
+ error
1537
+ );
1538
+ }
1539
+ }
1540
+ /**
1541
+ * Update an existing plugin's version/description/permissions/settings
1542
+ */
1543
+ async updatePlugin(plugin) {
1544
+ await this.pluginService.updatePluginVersion(plugin.id, {
1545
+ version: plugin.version,
1546
+ description: plugin.description,
1547
+ permissions: plugin.permissions,
1548
+ settings: plugin.settings || {}
1549
+ });
1550
+ }
1551
+ /**
1552
+ * Check if bootstrap is needed (first run detection)
1553
+ */
1554
+ async isBootstrapNeeded() {
1555
+ try {
1556
+ for (const corePlugin of this.CORE_PLUGINS.filter(
1557
+ (p) => p.name.startsWith("core-")
1558
+ )) {
1559
+ const exists = await this.pluginService.getPlugin(corePlugin.id);
1560
+ if (!exists) {
1561
+ return true;
1562
+ }
1563
+ }
1564
+ return false;
1565
+ } catch (error) {
1566
+ console.error(
1567
+ "[PluginBootstrap] Error checking bootstrap status:",
1568
+ error
1569
+ );
1570
+ return true;
1571
+ }
1572
+ }
1573
+ };
1574
+
1575
+ // src/services/telemetry-service.ts
1576
+ var TelemetryService = class {
1577
+ config;
1578
+ identity = null;
1579
+ enabled = true;
1580
+ eventQueue = [];
1581
+ isInitialized = false;
1582
+ constructor(config) {
1583
+ this.config = {
1584
+ ...getTelemetryConfig(),
1585
+ ...config
1586
+ };
1587
+ this.enabled = this.config.enabled;
1588
+ }
1589
+ /**
1590
+ * Initialize the telemetry service
1591
+ */
1592
+ async initialize(identity) {
1593
+ if (!this.enabled) {
1594
+ if (this.config.debug) {
1595
+ console.log("[Telemetry] Disabled via configuration");
1596
+ }
1597
+ return;
1598
+ }
1599
+ try {
1600
+ this.identity = identity;
1601
+ if (this.config.debug) {
1602
+ console.log("[Telemetry] Initialized with installation ID:", identity.installationId);
1603
+ }
1604
+ this.isInitialized = true;
1605
+ await this.flushQueue();
1606
+ } catch (error) {
1607
+ if (this.config.debug) {
1608
+ console.error("[Telemetry] Initialization failed:", error);
1609
+ }
1610
+ this.enabled = false;
1611
+ }
1612
+ }
1613
+ /**
1614
+ * Track a telemetry event
1615
+ */
1616
+ async track(event, properties) {
1617
+ if (!this.enabled) return;
1618
+ try {
1619
+ const sanitizedProps = this.sanitizeProperties(properties);
1620
+ const enrichedProps = {
1621
+ ...sanitizedProps,
1622
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1623
+ version: this.getVersion()
1624
+ };
1625
+ if (!this.isInitialized) {
1626
+ this.eventQueue.push({ event, properties: enrichedProps });
1627
+ if (this.config.debug) {
1628
+ console.log("[Telemetry] Queued event:", event, enrichedProps);
1629
+ }
1630
+ return;
1631
+ }
1632
+ if (this.identity && this.config.host) {
1633
+ const payload = {
1634
+ data: {
1635
+ installation_id: this.identity.installationId,
1636
+ event_type: event,
1637
+ properties: enrichedProps,
1638
+ timestamp: enrichedProps.timestamp
1639
+ }
1640
+ };
1641
+ fetch(`${this.config.host}/v1/events`, {
1642
+ method: "POST",
1643
+ headers: { "Content-Type": "application/json" },
1644
+ body: JSON.stringify(payload)
1645
+ }).catch(() => {
1646
+ });
1647
+ if (this.config.debug) {
1648
+ console.log("[Telemetry] Tracked event:", event, enrichedProps);
1649
+ }
1650
+ } else if (this.config.debug) {
1651
+ console.log("[Telemetry] Event (no endpoint):", event, enrichedProps);
1652
+ }
1653
+ } catch (error) {
1654
+ if (this.config.debug) {
1655
+ console.error("[Telemetry] Failed to track event:", error);
1656
+ }
1657
+ }
1658
+ }
1659
+ /**
1660
+ * Track installation started
1661
+ */
1662
+ async trackInstallationStarted(properties) {
1663
+ await this.track("installation_started", properties);
1664
+ }
1665
+ /**
1666
+ * Track installation completed
1667
+ */
1668
+ async trackInstallationCompleted(properties) {
1669
+ await this.track("installation_completed", properties);
1670
+ }
1671
+ /**
1672
+ * Track installation failed
1673
+ */
1674
+ async trackInstallationFailed(error, properties) {
1675
+ await this.track("installation_failed", {
1676
+ ...properties,
1677
+ errorType: sanitizeErrorMessage(error)
1678
+ });
1679
+ }
1680
+ /**
1681
+ * Track dev server started
1682
+ */
1683
+ async trackDevServerStarted(properties) {
1684
+ await this.track("dev_server_started", properties);
1685
+ }
1686
+ /**
1687
+ * Track page view in admin UI
1688
+ */
1689
+ async trackPageView(route, properties) {
1690
+ await this.track("page_viewed", {
1691
+ ...properties,
1692
+ route: sanitizeRoute(route)
1693
+ });
1694
+ }
1695
+ /**
1696
+ * Track error (sanitized)
1697
+ */
1698
+ async trackError(error, properties) {
1699
+ await this.track("error_occurred", {
1700
+ ...properties,
1701
+ errorType: sanitizeErrorMessage(error)
1702
+ });
1703
+ }
1704
+ /**
1705
+ * Track plugin activation
1706
+ */
1707
+ async trackPluginActivated(properties) {
1708
+ await this.track("plugin_activated", properties);
1709
+ }
1710
+ /**
1711
+ * Track migration run
1712
+ */
1713
+ async trackMigrationRun(properties) {
1714
+ await this.track("migration_run", properties);
1715
+ }
1716
+ /**
1717
+ * Track project snapshot — fired on bootstrap to capture runtime project shape.
1718
+ * Arrays serialized as JSON strings (sanitizeProperties strips objects/arrays).
1719
+ */
1720
+ async trackProjectSnapshot(data) {
1721
+ if (!this.identity) {
1722
+ this.identity = { installationId: data.installation_id };
1723
+ this.isInitialized = true;
1724
+ }
1725
+ await this.track("project_snapshot", {
1726
+ installation_id: data.installation_id,
1727
+ collection_names: JSON.stringify(data.collection_names),
1728
+ collection_counts: JSON.stringify(data.collection_counts),
1729
+ active_plugins: JSON.stringify(data.active_plugins),
1730
+ field_type_histogram: JSON.stringify(data.field_type_histogram),
1731
+ doc_total: data.doc_total,
1732
+ sonicjs_version: data.sonicjs_version
1733
+ });
1734
+ }
1735
+ /**
1736
+ * Flush queued events
1737
+ */
1738
+ async flushQueue() {
1739
+ if (this.eventQueue.length === 0) return;
1740
+ const queue = [...this.eventQueue];
1741
+ this.eventQueue = [];
1742
+ for (const { event, properties } of queue) {
1743
+ await this.track(event, properties);
1744
+ }
1745
+ }
1746
+ /**
1747
+ * Sanitize properties to ensure no PII
1748
+ */
1749
+ sanitizeProperties(properties) {
1750
+ if (!properties) return {};
1751
+ const sanitized = {};
1752
+ for (const [key, value] of Object.entries(properties)) {
1753
+ if (value === void 0) continue;
1754
+ if (key === "route" && typeof value === "string") {
1755
+ sanitized[key] = sanitizeRoute(value);
1756
+ continue;
1757
+ }
1758
+ if (key.toLowerCase().includes("error") && typeof value === "string") {
1759
+ sanitized[key] = sanitizeErrorMessage(value);
1760
+ continue;
1761
+ }
1762
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
1763
+ sanitized[key] = value;
1764
+ }
1765
+ }
1766
+ return sanitized;
1767
+ }
1768
+ /**
1769
+ * Get SonicJS version
1770
+ */
1771
+ getVersion() {
1772
+ try {
1773
+ if (typeof process !== "undefined" && process.env) {
1774
+ return process.env.SONICJS_VERSION || "2.0.0";
1775
+ }
1776
+ return "2.0.0";
1777
+ } catch {
1778
+ return "unknown";
1779
+ }
1780
+ }
1781
+ /**
1782
+ * Shutdown the telemetry service (no-op for fetch-based telemetry)
1783
+ */
1784
+ async shutdown() {
1785
+ }
1786
+ /**
1787
+ * Enable telemetry
1788
+ */
1789
+ enable() {
1790
+ this.enabled = true;
1791
+ }
1792
+ /**
1793
+ * Disable telemetry
1794
+ */
1795
+ disable() {
1796
+ this.enabled = false;
1797
+ }
1798
+ /**
1799
+ * Check if telemetry is enabled
1800
+ */
1801
+ isEnabled() {
1802
+ return this.enabled;
1803
+ }
1804
+ };
1805
+ var telemetryInstance = null;
1806
+ function getTelemetryService(config) {
1807
+ if (!telemetryInstance) {
1808
+ telemetryInstance = new TelemetryService(config);
1809
+ }
1810
+ return telemetryInstance;
1811
+ }
1812
+ async function initTelemetry(identity, config) {
1813
+ const service = getTelemetryService(config);
1814
+ await service.initialize(identity);
1815
+ return service;
1816
+ }
1817
+ function createInstallationIdentity(projectName) {
1818
+ const installationId = generateInstallationId();
1819
+ const identity = { installationId };
1820
+ if (projectName) {
1821
+ identity.projectId = generateProjectId(projectName);
1822
+ }
1823
+ return identity;
1824
+ }
1825
+
1826
+ export { CollectionRegistry, MULTI_TENANT_PLUGIN_ID, PLUGIN_REGISTRY, PluginBootstrapService, PluginService, TENANT_COOKIE, TelemetryService, collectionRecordToRow, createInstallationIdentity, findPluginByCodeName, getAvailableCollectionNames, getCollectionRegistry, getTelemetryService, getVisibleCollections, initTelemetry, invalidateTenantCache, isCodeCollectionInternal, isDbDocTypeInternal, loadCollectionConfig, loadCollectionConfigs, registerCollections, resetCollectionRegistry, tenantMiddleware, validateCollectionConfig };
1827
+ //# sourceMappingURL=chunk-QLFTG3QJ.js.map
1828
+ //# sourceMappingURL=chunk-QLFTG3QJ.js.map