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

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 (224) hide show
  1. package/README.md +4 -3
  2. package/dist/admin-documents-form.template-KN7JF66Q.cjs +19 -0
  3. package/dist/{admin-layout-catalyst.template-UMTIN66R.js.map → admin-documents-form.template-KN7JF66Q.cjs.map} +1 -1
  4. package/dist/admin-documents-form.template-NLSI6Z42.js +6 -0
  5. package/dist/{admin-layout-catalyst.template-HFD37TY5.cjs.map → admin-documents-form.template-NLSI6Z42.js.map} +1 -1
  6. package/dist/admin-layout-catalyst.template-WHJGSWWD.js +7 -0
  7. package/dist/admin-layout-catalyst.template-WHJGSWWD.js.map +1 -0
  8. package/dist/admin-layout-catalyst.template-ZK5HD545.cjs +17 -0
  9. package/dist/admin-layout-catalyst.template-ZK5HD545.cjs.map +1 -0
  10. package/dist/app-Bo0X1OWX.d.ts +1268 -0
  11. package/dist/app-Do66yCcV.d.cts +1268 -0
  12. package/dist/cache-DDARE4QE.js +4 -0
  13. package/dist/cache-DDARE4QE.js.map +1 -0
  14. package/dist/cache-LVYS4BPL.cjs +33 -0
  15. package/dist/cache-LVYS4BPL.cjs.map +1 -0
  16. package/dist/chunk-2CB4KY7I.cjs +771 -0
  17. package/dist/chunk-2CB4KY7I.cjs.map +1 -0
  18. package/dist/{chunk-55RDMDOP.js → chunk-3TB6AT6X.js} +148 -55
  19. package/dist/chunk-3TB6AT6X.js.map +1 -0
  20. package/dist/{chunk-ON5ZMSU4.js → chunk-6JQOUUOB.js} +3 -3
  21. package/dist/chunk-6JQOUUOB.js.map +1 -0
  22. package/dist/chunk-6OUHGKFD.js +387 -0
  23. package/dist/chunk-6OUHGKFD.js.map +1 -0
  24. package/dist/{chunk-7A4CB7T3.cjs → chunk-AAWNRBRB.cjs} +509 -91
  25. package/dist/chunk-AAWNRBRB.cjs.map +1 -0
  26. package/dist/chunk-AI663NBO.js +821 -0
  27. package/dist/chunk-AI663NBO.js.map +1 -0
  28. package/dist/chunk-BDDABDAB.cjs +1149 -0
  29. package/dist/chunk-BDDABDAB.cjs.map +1 -0
  30. package/dist/chunk-BLMTL57B.js +767 -0
  31. package/dist/chunk-BLMTL57B.js.map +1 -0
  32. package/dist/chunk-DNQCEKUK.cjs +327 -0
  33. package/dist/chunk-DNQCEKUK.cjs.map +1 -0
  34. package/dist/chunk-DSA4UX5B.cjs +276 -0
  35. package/dist/chunk-DSA4UX5B.cjs.map +1 -0
  36. package/dist/chunk-EF2NQUIQ.js +323 -0
  37. package/dist/chunk-EF2NQUIQ.js.map +1 -0
  38. package/dist/chunk-GCDZZNIN.js +192 -0
  39. package/dist/chunk-GCDZZNIN.js.map +1 -0
  40. package/dist/{chunk-ABB34XUS.cjs → chunk-H2AXVCLS.cjs} +667 -19
  41. package/dist/chunk-H2AXVCLS.cjs.map +1 -0
  42. package/dist/{chunk-XWIA3HVX.js → chunk-HDWE5FRJ.js} +6 -1249
  43. package/dist/chunk-HDWE5FRJ.js.map +1 -0
  44. package/dist/chunk-HIKBY7MS.cjs +70 -0
  45. package/dist/chunk-HIKBY7MS.cjs.map +1 -0
  46. package/dist/chunk-IESEVHXL.js +66 -0
  47. package/dist/chunk-IESEVHXL.js.map +1 -0
  48. package/dist/chunk-IVPRUGTY.js +242 -0
  49. package/dist/chunk-IVPRUGTY.js.map +1 -0
  50. package/dist/{chunk-JZVHLLSI.cjs → chunk-IXUHXTHW.cjs} +2 -151
  51. package/dist/chunk-IXUHXTHW.cjs.map +1 -0
  52. package/dist/chunk-J6JTWD2A.cjs +100 -0
  53. package/dist/chunk-J6JTWD2A.cjs.map +1 -0
  54. package/dist/chunk-JEQ7FLOD.cjs +199 -0
  55. package/dist/chunk-JEQ7FLOD.cjs.map +1 -0
  56. package/dist/chunk-K25XHMM3.js +566 -0
  57. package/dist/chunk-K25XHMM3.js.map +1 -0
  58. package/dist/chunk-LRZIAW7U.cjs +158 -0
  59. package/dist/chunk-LRZIAW7U.cjs.map +1 -0
  60. package/dist/{chunk-OHYBNCVL.cjs → chunk-MVIZJOO5.cjs} +10 -1256
  61. package/dist/chunk-MVIZJOO5.cjs.map +1 -0
  62. package/dist/{chunk-UYJ6TJHX.cjs → chunk-NAVPFIG5.cjs} +148 -55
  63. package/dist/chunk-NAVPFIG5.cjs.map +1 -0
  64. package/dist/chunk-NLJVSER2.js +273 -0
  65. package/dist/chunk-NLJVSER2.js.map +1 -0
  66. package/dist/chunk-NMPEMSU4.js +154 -0
  67. package/dist/chunk-NMPEMSU4.js.map +1 -0
  68. package/dist/chunk-NUKJ54GA.cjs +245 -0
  69. package/dist/chunk-NUKJ54GA.cjs.map +1 -0
  70. package/dist/{chunk-E4YFJBM2.cjs → chunk-QAYFOER6.cjs} +621 -829
  71. package/dist/chunk-QAYFOER6.cjs.map +1 -0
  72. package/dist/{chunk-BU7SFHGP.js → chunk-QZGABF2M.js} +3 -149
  73. package/dist/chunk-QZGABF2M.js.map +1 -0
  74. package/dist/chunk-RNZFGN4R.js +88 -0
  75. package/dist/chunk-RNZFGN4R.js.map +1 -0
  76. package/dist/{chunk-4NPCDK6B.js → chunk-RZ6H7OZK.js} +505 -90
  77. package/dist/chunk-RZ6H7OZK.js.map +1 -0
  78. package/dist/{chunk-OCL3HMEG.js → chunk-VD2EA3WT.js} +7004 -9807
  79. package/dist/chunk-VD2EA3WT.js.map +1 -0
  80. package/dist/{chunk-R4FOLLFB.cjs → chunk-VXE42MYF.cjs} +8730 -11520
  81. package/dist/chunk-VXE42MYF.cjs.map +1 -0
  82. package/dist/{chunk-4ZSNJDLS.cjs → chunk-WULONYGB.cjs} +9 -9
  83. package/dist/chunk-WULONYGB.cjs.map +1 -0
  84. package/dist/chunk-XW56B23A.cjs +408 -0
  85. package/dist/chunk-XW56B23A.cjs.map +1 -0
  86. package/dist/chunk-YA3TJ65D.cjs +575 -0
  87. package/dist/chunk-YA3TJ65D.cjs.map +1 -0
  88. package/dist/{chunk-TFNTM3OA.js → chunk-YHSQVQXX.js} +645 -15
  89. package/dist/chunk-YHSQVQXX.js.map +1 -0
  90. package/dist/chunk-YP7GW2G5.cjs +866 -0
  91. package/dist/chunk-YP7GW2G5.cjs.map +1 -0
  92. package/dist/{chunk-QFWHAFEO.js → chunk-ZEZ245PW.js} +148 -858
  93. package/dist/chunk-ZEZ245PW.js.map +1 -0
  94. package/dist/{chunk-JZV22DEV.js → chunk-ZGGXCFR6.js} +611 -817
  95. package/dist/chunk-ZGGXCFR6.js.map +1 -0
  96. package/dist/{collection-config-B4PG-AaF.d.cts → collection-config-JgHOpFCG.d.cts} +30 -2
  97. package/dist/{collection-config-B4PG-AaF.d.ts → collection-config-JgHOpFCG.d.ts} +30 -2
  98. package/dist/config-HFXANXCC.js +6 -0
  99. package/dist/config-HFXANXCC.js.map +1 -0
  100. package/dist/config-ON6FNMYX.cjs +19 -0
  101. package/dist/config-ON6FNMYX.cjs.map +1 -0
  102. package/dist/define-plugin-BzNHc1ZI.d.ts +1321 -0
  103. package/dist/define-plugin-IWDKYaVm.d.cts +1321 -0
  104. package/dist/document-projection-TDWRJX3Z.cjs +13 -0
  105. package/dist/document-projection-TDWRJX3Z.cjs.map +1 -0
  106. package/dist/document-projection-YYMC6I4U.js +4 -0
  107. package/dist/document-projection-YYMC6I4U.js.map +1 -0
  108. package/dist/index.cjs +13734 -4328
  109. package/dist/index.cjs.map +1 -1
  110. package/dist/index.d.cts +329 -492
  111. package/dist/index.d.ts +329 -492
  112. package/dist/index.js +13385 -3998
  113. package/dist/index.js.map +1 -1
  114. package/dist/middleware.cjs +36 -32
  115. package/dist/middleware.d.cts +50 -7
  116. package/dist/middleware.d.ts +50 -7
  117. package/dist/middleware.js +7 -3
  118. package/dist/migrations-NJJWQUKK.cjs +13 -0
  119. package/dist/{migrations-566IIPS2.cjs.map → migrations-NJJWQUKK.cjs.map} +1 -1
  120. package/dist/migrations-WCAVBD7C.js +4 -0
  121. package/dist/{migrations-H5IXZNCO.js.map → migrations-WCAVBD7C.js.map} +1 -1
  122. package/dist/{plugin-bootstrap-DfVerYV4.d.cts → plugin-bootstrap-B8ThJU21.d.cts} +4315 -1661
  123. package/dist/{plugin-bootstrap-P_ciLp_C.d.ts → plugin-bootstrap-qu8hJgUt.d.ts} +4315 -1661
  124. package/dist/plugins.cjs +171 -12
  125. package/dist/plugins.d.cts +36 -2
  126. package/dist/plugins.d.ts +36 -2
  127. package/dist/plugins.js +5 -2
  128. package/dist/rbac-O73MFKDA.js +5 -0
  129. package/dist/rbac-O73MFKDA.js.map +1 -0
  130. package/dist/rbac-VONLJJKB.cjs +14 -0
  131. package/dist/rbac-VONLJJKB.cjs.map +1 -0
  132. package/dist/routes.cjs +41 -45
  133. package/dist/routes.d.cts +56 -146
  134. package/dist/routes.d.ts +56 -146
  135. package/dist/routes.js +17 -9
  136. package/dist/services.cjs +39 -72
  137. package/dist/services.d.cts +79 -54
  138. package/dist/services.d.ts +79 -54
  139. package/dist/services.js +6 -3
  140. package/dist/templates.cjs +17 -29
  141. package/dist/templates.d.cts +1 -66
  142. package/dist/templates.d.ts +1 -66
  143. package/dist/templates.js +3 -3
  144. package/dist/types-Dea1eNxU.d.cts +286 -0
  145. package/dist/types-Dea1eNxU.d.ts +286 -0
  146. package/dist/types.d.cts +1 -1
  147. package/dist/types.d.ts +1 -1
  148. package/dist/utils.cjs +18 -17
  149. package/dist/utils.d.cts +1 -1
  150. package/dist/utils.d.ts +1 -1
  151. package/dist/utils.js +2 -1
  152. package/migrations/0001_core.sql +184 -0
  153. package/migrations/0002_documents.sql +163 -0
  154. package/package.json +12 -7
  155. package/dist/admin-layout-catalyst.template-HFD37TY5.cjs +0 -17
  156. package/dist/admin-layout-catalyst.template-UMTIN66R.js +0 -7
  157. package/dist/app-C9esKLmh.d.cts +0 -112
  158. package/dist/app-C9esKLmh.d.ts +0 -112
  159. package/dist/chunk-4NPCDK6B.js.map +0 -1
  160. package/dist/chunk-4ZSNJDLS.cjs.map +0 -1
  161. package/dist/chunk-55RDMDOP.js.map +0 -1
  162. package/dist/chunk-635JAMSE.cjs +0 -653
  163. package/dist/chunk-635JAMSE.cjs.map +0 -1
  164. package/dist/chunk-7A4CB7T3.cjs.map +0 -1
  165. package/dist/chunk-ABB34XUS.cjs.map +0 -1
  166. package/dist/chunk-BU7SFHGP.js.map +0 -1
  167. package/dist/chunk-E4YFJBM2.cjs.map +0 -1
  168. package/dist/chunk-EXNEW5US.js +0 -648
  169. package/dist/chunk-EXNEW5US.js.map +0 -1
  170. package/dist/chunk-JZV22DEV.js.map +0 -1
  171. package/dist/chunk-JZVHLLSI.cjs.map +0 -1
  172. package/dist/chunk-OCL3HMEG.js.map +0 -1
  173. package/dist/chunk-OHYBNCVL.cjs.map +0 -1
  174. package/dist/chunk-ON5ZMSU4.js.map +0 -1
  175. package/dist/chunk-QFWHAFEO.js.map +0 -1
  176. package/dist/chunk-R4FOLLFB.cjs.map +0 -1
  177. package/dist/chunk-RLMUFFUD.cjs +0 -2219
  178. package/dist/chunk-RLMUFFUD.cjs.map +0 -1
  179. package/dist/chunk-TFNTM3OA.js.map +0 -1
  180. package/dist/chunk-UYJ6TJHX.cjs.map +0 -1
  181. package/dist/chunk-WAEQXGCX.cjs +0 -1898
  182. package/dist/chunk-WAEQXGCX.cjs.map +0 -1
  183. package/dist/chunk-XWIA3HVX.js.map +0 -1
  184. package/dist/chunk-ZYAYUIZE.js +0 -2217
  185. package/dist/chunk-ZYAYUIZE.js.map +0 -1
  186. package/dist/migrations-566IIPS2.cjs +0 -13
  187. package/dist/migrations-H5IXZNCO.js +0 -4
  188. package/dist/plugin-manager-BoM3Q7o7.d.cts +0 -328
  189. package/dist/plugin-manager-Efx9RyDX.d.ts +0 -328
  190. package/migrations/001_initial_schema.sql +0 -170
  191. package/migrations/002_faq_plugin.sql +0 -86
  192. package/migrations/003_stage5_enhancements.sql +0 -121
  193. package/migrations/004_stage6_user_management.sql +0 -183
  194. package/migrations/005_stage7_workflow_automation.sql +0 -294
  195. package/migrations/006_plugin_system.sql +0 -155
  196. package/migrations/007_demo_login_plugin.sql +0 -23
  197. package/migrations/008_fix_slug_validation.sql +0 -22
  198. package/migrations/009_system_logging.sql +0 -57
  199. package/migrations/011_config_managed_collections.sql +0 -15
  200. package/migrations/012_testimonials_plugin.sql +0 -80
  201. package/migrations/013_code_examples_plugin.sql +0 -177
  202. package/migrations/014_fix_plugin_registry.sql +0 -88
  203. package/migrations/015_add_remaining_plugins.sql +0 -89
  204. package/migrations/016_remove_duplicate_cache_plugin.sql +0 -17
  205. package/migrations/017_auth_configurable_fields.sql +0 -49
  206. package/migrations/018_settings_table.sql +0 -23
  207. package/migrations/019_remove_blog_posts_collection.sql +0 -15
  208. package/migrations/020_add_email_plugin.sql +0 -22
  209. package/migrations/021_add_magic_link_auth_plugin.sql +0 -42
  210. package/migrations/022_add_tinymce_plugin.sql +0 -25
  211. package/migrations/023_add_easy_mdx_plugin.sql +0 -25
  212. package/migrations/024_add_quill_editor_plugin.sql +0 -25
  213. package/migrations/025_add_easymde_plugin.sql +0 -25
  214. package/migrations/026_add_otp_login.sql +0 -42
  215. package/migrations/027_fix_slug_field_type.sql +0 -18
  216. package/migrations/028_fix_slug_field_type_in_schemas.sql +0 -30
  217. package/migrations/029_add_forms_system.sql +0 -184
  218. package/migrations/030_add_turnstile_to_forms.sql +0 -14
  219. package/migrations/031_ai_search_plugin.sql +0 -45
  220. package/migrations/032_user_profiles.sql +0 -37
  221. package/migrations/033_form_content_integration.sql +0 -19
  222. package/migrations/034_security_audit_plugin.sql +0 -27
  223. package/migrations/035_user_profiles_data_column.sql +0 -16
  224. package/migrations/036_analytics_events.sql +0 -22
@@ -1,4 +1,123 @@
1
+ import { escapeHtml } from './chunk-TQABQWOP.js';
2
+ import { getCookie } from 'hono/cookie';
3
+
4
+ // src/services/collection-registry.ts
5
+ var CollectionRegistry = class {
6
+ byName = /* @__PURE__ */ new Map();
7
+ bySlug = /* @__PURE__ */ new Map();
8
+ /**
9
+ * Replace the registry contents with the given configs. Idempotent —
10
+ * calling with the same configs twice yields the same state.
11
+ */
12
+ register(configs) {
13
+ this.byName.clear();
14
+ this.bySlug.clear();
15
+ for (const config of configs) {
16
+ if (!config.name) continue;
17
+ const record = {
18
+ ...config,
19
+ id: config.name,
20
+ slug: config.slug ?? config.name.replace(/_/g, "-"),
21
+ managed: config.managed !== void 0 ? config.managed : true,
22
+ isActive: config.isActive !== void 0 ? config.isActive : true
23
+ };
24
+ this.byName.set(record.name, record);
25
+ this.bySlug.set(record.slug, record);
26
+ }
27
+ }
28
+ /** All registered collections (including inactive). */
29
+ list() {
30
+ return Array.from(this.byName.values());
31
+ }
32
+ /** Active collections only. */
33
+ listActive() {
34
+ return this.list().filter((c) => c.isActive !== false);
35
+ }
36
+ getByName(name) {
37
+ return this.byName.get(name);
38
+ }
39
+ /** For code-defined collections, id === name. */
40
+ getById(id) {
41
+ return this.byName.get(id);
42
+ }
43
+ /** Look up by the URL slug (set in CollectionConfig.slug). Falls back to getByName if needed. */
44
+ getBySlug(slug) {
45
+ return this.bySlug.get(slug);
46
+ }
47
+ /** Resolve a path segment to a record — tries slug first, then name. */
48
+ getBySlugOrName(slugOrName) {
49
+ return this.bySlug.get(slugOrName) ?? this.byName.get(slugOrName);
50
+ }
51
+ isActive(name) {
52
+ const record = this.byName.get(name);
53
+ return record?.isActive !== false && record !== void 0;
54
+ }
55
+ size() {
56
+ return this.byName.size;
57
+ }
58
+ /** Test helper — wipe state. */
59
+ clear() {
60
+ this.byName.clear();
61
+ this.bySlug.clear();
62
+ }
63
+ };
64
+ function collectionRecordToRow(record) {
65
+ return {
66
+ id: record.id,
67
+ name: record.name,
68
+ display_name: record.displayName,
69
+ description: record.description ?? null,
70
+ schema: record.schema,
71
+ is_active: record.isActive === false ? 0 : 1,
72
+ managed: record.managed === false ? 0 : 1,
73
+ source_type: "code",
74
+ source_id: null,
75
+ created_at: 0,
76
+ updated_at: 0
77
+ };
78
+ }
79
+ var instance = null;
80
+ function getCollectionRegistry() {
81
+ if (!instance) {
82
+ instance = new CollectionRegistry();
83
+ }
84
+ return instance;
85
+ }
86
+ function resetCollectionRegistry() {
87
+ instance = null;
88
+ }
89
+
1
90
  // src/services/collection-loader.ts
91
+ function isCodeCollectionInternal(cfg) {
92
+ return cfg.internal === true;
93
+ }
94
+ function isDbDocTypeInternal(source) {
95
+ return source === "system" || source === "plugin";
96
+ }
97
+ async function getVisibleCollections(db) {
98
+ const codeRegistry = getCollectionRegistry().list();
99
+ const codeMap = /* @__PURE__ */ new Map();
100
+ for (const cfg of codeRegistry) {
101
+ if (!isCodeCollectionInternal(cfg)) {
102
+ codeMap.set(cfg.name, { name: cfg.name, displayName: cfg.displayName });
103
+ }
104
+ }
105
+ let dbRows = [];
106
+ try {
107
+ const { results } = await db.prepare(
108
+ "SELECT name, display_name, source FROM document_types WHERE is_active = 1 ORDER BY display_name"
109
+ ).all();
110
+ dbRows = results ?? [];
111
+ } catch {
112
+ }
113
+ const merged = new Map(codeMap);
114
+ for (const row of dbRows) {
115
+ if (!isDbDocTypeInternal(row.source) && !merged.has(row.name)) {
116
+ merged.set(row.name, { name: String(row.name), displayName: String(row.display_name) });
117
+ }
118
+ }
119
+ return Array.from(merged.values());
120
+ }
2
121
  var registeredCollections = [];
3
122
  function registerCollections(collections) {
4
123
  for (const config of collections) {
@@ -127,621 +246,295 @@ function validateCollectionConfig(config) {
127
246
  errors
128
247
  };
129
248
  }
130
-
131
- // src/services/collection-sync.ts
132
- async function syncCollections(db) {
133
- console.log("\u{1F504} Starting collection sync...");
134
- const results = [];
135
- const configs = await loadCollectionConfigs();
136
- if (configs.length === 0) {
137
- console.log("\u26A0\uFE0F No collection configurations found");
138
- return results;
139
- }
140
- for (const config of configs) {
141
- const result = await syncCollection(db, config);
142
- results.push(result);
143
- }
144
- const created = results.filter((r) => r.status === "created").length;
145
- const updated = results.filter((r) => r.status === "updated").length;
146
- const unchanged = results.filter((r) => r.status === "unchanged").length;
147
- const errors = results.filter((r) => r.status === "error").length;
148
- console.log(`\u2705 Collection sync complete: ${created} created, ${updated} updated, ${unchanged} unchanged, ${errors} errors`);
149
- return results;
249
+ var TENANT_COOKIE = "sonicjs-tenant";
250
+ var TENANT_SWITCHER_MARKER = "<!-- TENANT_SWITCHER -->";
251
+ var MULTI_TENANT_PLUGIN_ID = "multi-tenant";
252
+ var CACHE_TTL_MS = 3e4;
253
+ var DEFAULT_SETTINGS = {
254
+ headerName: "X-Tenant-Id",
255
+ subdomainResolution: false,
256
+ rootDomain: ""
257
+ };
258
+ var cache = null;
259
+ function invalidateTenantCache() {
260
+ cache = null;
150
261
  }
151
- async function syncCollection(db, config) {
262
+ async function loadTenantState(db) {
263
+ const now = Date.now();
264
+ if (cache && now - cache.fetchedAt < CACHE_TTL_MS) return cache;
265
+ let pluginActive = false;
266
+ let settings = DEFAULT_SETTINGS;
267
+ const tenants = /* @__PURE__ */ new Map();
268
+ const domains = /* @__PURE__ */ new Map();
152
269
  try {
153
- const validation = validateCollectionConfig(config);
154
- if (!validation.valid) {
155
- return {
156
- name: config.name,
157
- status: "error",
158
- error: `Validation failed: ${validation.errors.join(", ")}`
159
- };
160
- }
161
- const existingStmt = db.prepare("SELECT * FROM collections WHERE name = ?");
162
- const existing = await existingStmt.bind(config.name).first();
163
- const now = Date.now();
164
- const collectionId = existing?.id || `col-${config.name}-${crypto.randomUUID().slice(0, 8)}`;
165
- const schemaJson = JSON.stringify(config.schema);
166
- const isActive = config.isActive !== false ? 1 : 0;
167
- const managed = config.managed !== false ? 1 : 0;
168
- if (!existing) {
169
- const insertStmt = db.prepare(`
170
- INSERT INTO collections (id, name, display_name, description, schema, is_active, managed, created_at, updated_at)
171
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
172
- `);
173
- await insertStmt.bind(
174
- collectionId,
175
- config.name,
176
- config.displayName,
177
- config.description || null,
178
- schemaJson,
179
- isActive,
180
- managed,
181
- now,
182
- now
183
- ).run();
184
- console.log(` \u2713 Created collection: ${config.name}`);
185
- return {
186
- name: config.name,
187
- status: "created",
188
- message: `Created collection "${config.displayName}"`
189
- };
190
- } else {
191
- const existingSchema = existing.schema ? JSON.stringify(existing.schema) : "{}";
192
- const existingDisplayName = existing.display_name;
193
- const existingDescription = existing.description;
194
- const existingIsActive = existing.is_active;
195
- const existingManaged = existing.managed;
196
- const needsUpdate = schemaJson !== existingSchema || config.displayName !== existingDisplayName || (config.description || null) !== existingDescription || isActive !== existingIsActive || managed !== existingManaged;
197
- if (!needsUpdate) {
198
- return {
199
- name: config.name,
200
- status: "unchanged",
201
- message: `Collection "${config.displayName}" is up to date`
270
+ const pluginRow = await db.prepare(
271
+ `SELECT data FROM documents
272
+ WHERE type_id = 'plugin' AND tenant_id = 'default' AND slug = ?
273
+ AND q_plugin_status = 'active' AND is_current_draft = 1 AND deleted_at IS NULL`
274
+ ).bind(MULTI_TENANT_PLUGIN_ID).first();
275
+ if (pluginRow) {
276
+ pluginActive = true;
277
+ try {
278
+ const data = typeof pluginRow.data === "string" ? JSON.parse(pluginRow.data) : pluginRow.data ?? {};
279
+ const s = data.settings ?? {};
280
+ settings = {
281
+ headerName: typeof s.headerName === "string" && s.headerName.trim() !== "" ? s.headerName.trim() : DEFAULT_SETTINGS.headerName,
282
+ subdomainResolution: s.subdomainResolution === true,
283
+ rootDomain: typeof s.rootDomain === "string" ? s.rootDomain.trim().toLowerCase() : ""
202
284
  };
285
+ } catch {
286
+ settings = DEFAULT_SETTINGS;
203
287
  }
204
- const updateStmt = db.prepare(`
205
- UPDATE collections
206
- SET display_name = ?, description = ?, schema = ?, is_active = ?, managed = ?, updated_at = ?
207
- WHERE name = ?
208
- `);
209
- await updateStmt.bind(
210
- config.displayName,
211
- config.description || null,
212
- schemaJson,
213
- isActive,
214
- managed,
215
- now,
216
- config.name
217
- ).run();
218
- console.log(` \u2713 Updated collection: ${config.name}`);
219
- return {
220
- name: config.name,
221
- status: "updated",
222
- message: `Updated collection "${config.displayName}"`
223
- };
224
- }
225
- } catch (error) {
226
- console.error(` \u2717 Error syncing collection ${config.name}:`, error);
227
- return {
228
- name: config.name,
229
- status: "error",
230
- error: error instanceof Error ? error.message : "Unknown error"
231
- };
232
- }
233
- }
234
- async function isCollectionManaged(db, collectionName) {
235
- try {
236
- const stmt = db.prepare("SELECT managed FROM collections WHERE name = ?");
237
- const result = await stmt.bind(collectionName).first();
238
- return result?.managed === 1;
239
- } catch (error) {
240
- console.error(`Error checking if collection is managed:`, error);
241
- return false;
242
- }
243
- }
244
- async function getManagedCollections(db) {
245
- try {
246
- const stmt = db.prepare("SELECT name FROM collections WHERE managed = 1");
247
- const { results } = await stmt.all();
248
- return (results || []).map((row) => row.name);
249
- } catch (error) {
250
- console.error("Error getting managed collections:", error);
251
- return [];
252
- }
253
- }
254
- async function cleanupRemovedCollections(db) {
255
- try {
256
- const configs = await loadCollectionConfigs();
257
- const configNames = new Set(configs.map((c) => c.name));
258
- const managedCollections = await getManagedCollections(db);
259
- const removed = [];
260
- for (const managedName of managedCollections) {
261
- if (!configNames.has(managedName)) {
262
- const updateStmt = db.prepare(`
263
- UPDATE collections
264
- SET is_active = 0, updated_at = ?
265
- WHERE name = ? AND managed = 1
266
- `);
267
- await updateStmt.bind(Date.now(), managedName).run();
268
- removed.push(managedName);
269
- console.log(` \u26A0\uFE0F Deactivated removed collection: ${managedName}`);
270
- }
271
- }
272
- return removed;
273
- } catch (error) {
274
- console.error("Error cleaning up removed collections:", error);
275
- return [];
276
- }
277
- }
278
- async function fullCollectionSync(db) {
279
- const results = await syncCollections(db);
280
- const removed = await cleanupRemovedCollections(db);
281
- return { results, removed };
282
- }
283
-
284
- // src/services/form-collection-sync.ts
285
- var SYSTEM_FORM_USER_ID = "system-form-submission";
286
- function mapFormioTypeToSchemaType(component) {
287
- switch (component.type) {
288
- case "textfield":
289
- case "textarea":
290
- case "password":
291
- case "phoneNumber":
292
- case "url":
293
- return { type: "string", title: component.label || component.key };
294
- case "email":
295
- return { type: "string", format: "email", title: component.label || component.key };
296
- case "number":
297
- case "currency":
298
- return { type: "number", title: component.label || component.key };
299
- case "checkbox":
300
- return { type: "boolean", title: component.label || component.key };
301
- case "select":
302
- case "radio": {
303
- const enumValues = (component.data?.values || component.values || []).map((v) => v.value);
304
- const enumLabels = (component.data?.values || component.values || []).map((v) => v.label);
305
- return {
306
- type: "select",
307
- title: component.label || component.key,
308
- enum: enumValues,
309
- enumLabels
310
- };
311
- }
312
- case "selectboxes":
313
- return { type: "object", title: component.label || component.key };
314
- case "datetime":
315
- case "day":
316
- case "time":
317
- return { type: "string", format: "date-time", title: component.label || component.key };
318
- case "file":
319
- case "signature":
320
- return { type: "string", title: component.label || component.key };
321
- case "address":
322
- return { type: "object", title: component.label || component.key };
323
- case "hidden":
324
- return { type: "string", title: component.label || component.key };
325
- default:
326
- return { type: "string", title: component.label || component.key };
327
- }
328
- }
329
- function extractFieldComponents(components) {
330
- const fields = [];
331
- if (!components) return fields;
332
- for (const comp of components) {
333
- if (comp.type === "panel" || comp.type === "fieldset" || comp.type === "well" || comp.type === "tabs") {
334
- if (comp.components) {
335
- fields.push(...extractFieldComponents(comp.components));
336
- }
337
- continue;
338
- }
339
- if (comp.type === "columns" && comp.columns) {
340
- for (const col of comp.columns) {
341
- if (col.components) {
342
- fields.push(...extractFieldComponents(col.components));
288
+ const { results } = await db.prepare(
289
+ `SELECT slug, name, status, domain FROM auth_tenant`
290
+ ).all();
291
+ for (const row of results ?? []) {
292
+ const status = row.status === "inactive" ? "inactive" : "active";
293
+ tenants.set(row.slug, { name: row.name || row.slug, status });
294
+ if (row.domain && status === "active") {
295
+ domains.set(String(row.domain).toLowerCase(), row.slug);
343
296
  }
344
297
  }
345
- continue;
346
- }
347
- if (comp.type === "table" && comp.rows) {
348
- for (const row of comp.rows) {
349
- if (Array.isArray(row)) {
350
- for (const cell of row) {
351
- if (cell.components) {
352
- fields.push(...extractFieldComponents(cell.components));
353
- }
354
- }
355
- }
356
- }
357
- continue;
358
- }
359
- if (comp.type === "button" || comp.type === "htmlelement" || comp.type === "content") {
360
- continue;
361
- }
362
- if (comp.type === "turnstile") {
363
- continue;
364
- }
365
- if (comp.key) {
366
- fields.push(comp);
367
- }
368
- if (comp.components) {
369
- fields.push(...extractFieldComponents(comp.components));
298
+ if (!tenants.has("default")) tenants.set("default", { name: "Default", status: "active" });
370
299
  }
300
+ } catch {
301
+ pluginActive = false;
371
302
  }
372
- return fields;
303
+ cache = { pluginActive, settings, tenants, domains, fetchedAt: now };
304
+ return cache;
373
305
  }
374
- function deriveCollectionSchemaFromFormio(formioSchema) {
375
- const components = formioSchema?.components || [];
376
- const fieldComponents = extractFieldComponents(components);
377
- const properties = {
378
- // Always include a title field for the content item
379
- title: { type: "string", title: "Title", required: true }
380
- };
381
- const required = ["title"];
382
- for (const comp of fieldComponents) {
383
- const key = comp.key;
384
- if (!key || key === "submit" || key === "title") continue;
385
- const fieldDef = mapFormioTypeToSchemaType(comp);
386
- if (comp.validate?.required) {
387
- fieldDef.required = true;
388
- required.push(key);
389
- }
390
- properties[key] = fieldDef;
391
- }
392
- return { type: "object", properties, required };
306
+ function isActiveTenant(state, slug) {
307
+ if (!slug) return false;
308
+ const entry = state.tenants.get(slug);
309
+ return !!entry && entry.status === "active";
393
310
  }
394
- function deriveSubmissionTitle(data, formDisplayName) {
395
- const candidates = ["name", "fullName", "full_name", "firstName", "first_name"];
396
- for (const key of candidates) {
397
- if (data[key] && typeof data[key] === "string" && data[key].trim()) {
398
- if (key === "firstName" || key === "first_name") {
399
- const last = data["lastName"] || data["last_name"] || data["lastname"] || "";
400
- if (last) return `${data[key].trim()} ${last.trim()}`;
401
- }
402
- return data[key].trim();
311
+ function resolveTenantSlug(state, req, opts) {
312
+ if (!state.pluginActive) return "default";
313
+ const full = state;
314
+ const enforce = opts?.enforceMembership === true;
315
+ const member = (slug) => !enforce || slug === "default" || (opts?.memberSlugs?.has(slug) ?? false);
316
+ const accept = (slug) => isActiveTenant(full, slug) && member(slug);
317
+ const header = req.header?.trim().toLowerCase();
318
+ if (accept(header)) return header;
319
+ const cookie = req.cookie?.trim().toLowerCase();
320
+ if (accept(cookie)) return cookie;
321
+ const host = req.host?.split(":")[0]?.toLowerCase() ?? "";
322
+ if (host) {
323
+ const bySlug = state.domains.get(host);
324
+ if (accept(bySlug)) return bySlug;
325
+ const { subdomainResolution, rootDomain } = state.settings;
326
+ if (subdomainResolution && rootDomain && host.endsWith(`.${rootDomain}`)) {
327
+ const sub = host.slice(0, -(rootDomain.length + 1));
328
+ if (sub && !sub.includes(".") && accept(sub)) return sub;
403
329
  }
404
330
  }
405
- if (data.email && typeof data.email === "string" && data.email.trim()) {
406
- return data.email.trim();
407
- }
408
- if (data.subject && typeof data.subject === "string" && data.subject.trim()) {
409
- return data.subject.trim();
410
- }
411
- const dateStr = (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", {
412
- year: "numeric",
413
- month: "short",
414
- day: "numeric",
415
- hour: "2-digit",
416
- minute: "2-digit"
417
- });
418
- return `${formDisplayName} - ${dateStr}`;
419
- }
420
- function mapFormStatusToContentStatus(formStatus) {
421
- switch (formStatus) {
422
- case "pending":
423
- return "published";
424
- case "reviewed":
425
- return "published";
426
- case "approved":
427
- return "published";
428
- case "rejected":
429
- return "archived";
430
- case "spam":
431
- return "deleted";
432
- default:
433
- return "published";
434
- }
435
- }
436
- async function syncFormCollection(db, form) {
437
- const collectionName = `form_${form.name}`;
438
- const displayName = `${form.display_name} (Form)`;
439
- const formioSchema = typeof form.formio_schema === "string" ? JSON.parse(form.formio_schema) : form.formio_schema;
440
- const schema = deriveCollectionSchemaFromFormio(formioSchema);
441
- const schemaJson = JSON.stringify(schema);
442
- const now = Date.now();
443
- const isActive = form.is_active ? 1 : 0;
444
- const existing = await db.prepare(
445
- "SELECT id, schema, display_name, description, is_active FROM collections WHERE source_type = ? AND source_id = ?"
446
- ).bind("form", form.id).first();
447
- if (!existing) {
448
- const collectionId = `col-form-${form.name}-${crypto.randomUUID().slice(0, 8)}`;
449
- await db.prepare(`
450
- INSERT INTO collections (id, name, display_name, description, schema, is_active, managed, source_type, source_id, created_at, updated_at)
451
- VALUES (?, ?, ?, ?, ?, ?, 1, 'form', ?, ?, ?)
452
- `).bind(
453
- collectionId,
454
- collectionName,
455
- displayName,
456
- form.description || null,
457
- schemaJson,
458
- isActive,
459
- form.id,
460
- now,
461
- now
462
- ).run();
463
- console.log(`[FormSync] Created shadow collection: ${collectionName}`);
464
- return { collectionId, status: "created" };
465
- }
466
- const existingSchema = existing.schema ? JSON.stringify(typeof existing.schema === "string" ? JSON.parse(existing.schema) : existing.schema) : "{}";
467
- const needsUpdate = schemaJson !== existingSchema || displayName !== existing.display_name || (form.description || null) !== existing.description || isActive !== existing.is_active;
468
- if (!needsUpdate) {
469
- return { collectionId: existing.id, status: "unchanged" };
470
- }
471
- await db.prepare(`
472
- UPDATE collections SET display_name = ?, description = ?, schema = ?, is_active = ?, updated_at = ?
473
- WHERE id = ?
474
- `).bind(
475
- displayName,
476
- form.description || null,
477
- schemaJson,
478
- isActive,
479
- now,
480
- existing.id
481
- ).run();
482
- console.log(`[FormSync] Updated shadow collection: ${collectionName}`);
483
- return { collectionId: existing.id, status: "updated" };
331
+ return "default";
484
332
  }
485
- async function syncAllFormCollections(db) {
333
+ async function loadMemberRoles(db, userId) {
486
334
  try {
487
- const tableCheck = await db.prepare(
488
- "SELECT name FROM sqlite_master WHERE type='table' AND name='forms'"
489
- ).first();
490
- if (!tableCheck) {
491
- console.log("[FormSync] Forms table does not exist, skipping form sync");
492
- return;
493
- }
494
- const { results: forms } = await db.prepare(
495
- "SELECT id, name, display_name, description, formio_schema, is_active FROM forms"
496
- ).all();
497
- if (!forms || forms.length === 0) {
498
- console.log("[FormSync] No forms found, skipping");
499
- return;
500
- }
501
- let created = 0;
502
- let updated = 0;
503
- for (const form of forms) {
504
- try {
505
- const result = await syncFormCollection(db, form);
506
- if (result.status === "created") created++;
507
- if (result.status === "updated") updated++;
508
- await backfillFormSubmissions(db, form.id, result.collectionId);
509
- } catch (error) {
510
- console.error(`[FormSync] Error syncing form ${form.name}:`, error);
511
- }
512
- }
513
- console.log(`[FormSync] Sync complete: ${created} created, ${updated} updated out of ${forms.length} forms`);
514
- } catch (error) {
515
- console.error("[FormSync] Error syncing form collections:", error);
335
+ const { results } = await db.prepare(`
336
+ SELECT t.slug, m.role FROM auth_tenant_member m
337
+ JOIN auth_tenant t ON t.id = m.tenant_id
338
+ WHERE m.user_id = ?
339
+ `).bind(userId).all();
340
+ return new Map((results ?? []).map((r) => [r.slug, r.role || "viewer"]));
341
+ } catch {
342
+ return /* @__PURE__ */ new Map();
516
343
  }
517
344
  }
518
- async function createContentFromSubmission(db, submissionData, form, submissionId, metadata = {}) {
519
- try {
520
- let collection = await db.prepare(
521
- "SELECT id FROM collections WHERE source_type = ? AND source_id = ?"
522
- ).bind("form", form.id).first();
523
- if (!collection) {
524
- console.warn(`[FormSync] No shadow collection found for form ${form.name}, attempting to create...`);
525
- try {
526
- const fullForm = await db.prepare(
527
- "SELECT id, name, display_name, description, formio_schema, is_active FROM forms WHERE id = ?"
528
- ).bind(form.id).first();
529
- if (fullForm) {
530
- const schema = typeof fullForm.formio_schema === "string" ? JSON.parse(fullForm.formio_schema) : fullForm.formio_schema;
531
- const result = await syncFormCollection(db, {
532
- id: fullForm.id,
533
- name: fullForm.name,
534
- display_name: fullForm.display_name,
535
- description: fullForm.description,
536
- formio_schema: schema,
537
- is_active: fullForm.is_active ?? 1
538
- });
539
- collection = await db.prepare(
540
- "SELECT id FROM collections WHERE source_type = ? AND source_id = ?"
541
- ).bind("form", form.id).first();
542
- console.log(`[FormSync] On-the-fly sync result: ${result.status}, collectionId: ${result.collectionId}`);
543
- }
544
- } catch (syncErr) {
545
- console.error("[FormSync] On-the-fly shadow collection creation failed:", syncErr);
546
- }
547
- if (!collection) {
548
- console.error(`[FormSync] Still no shadow collection for form ${form.name} after recovery attempt`);
549
- return null;
550
- }
551
- }
552
- const contentId = crypto.randomUUID();
553
- const now = Date.now();
554
- const title = deriveSubmissionTitle(submissionData, form.display_name);
555
- const slug = `submission-${submissionId.slice(0, 8)}`;
556
- const contentData = {
557
- title,
558
- ...submissionData,
559
- _submission_metadata: {
560
- submissionId,
561
- formId: form.id,
562
- formName: form.name,
563
- email: metadata.userEmail || submissionData.email || null,
564
- ipAddress: metadata.ipAddress || null,
565
- userAgent: metadata.userAgent || null,
566
- submittedAt: now
567
- }
568
- };
569
- const authorId = metadata.userId || SYSTEM_FORM_USER_ID;
570
- if (authorId === SYSTEM_FORM_USER_ID) {
571
- const systemUser = await db.prepare("SELECT id FROM users WHERE id = ?").bind(SYSTEM_FORM_USER_ID).first();
572
- if (!systemUser) {
573
- console.log("[FormSync] System form user missing, creating...");
574
- const sysNow = Date.now();
575
- await db.prepare(`
576
- INSERT OR IGNORE INTO users (id, email, username, first_name, last_name, password_hash, role, is_active, created_at, updated_at)
577
- VALUES (?, ?, ?, ?, ?, NULL, 'viewer', 0, ?, ?)
578
- `).bind(SYSTEM_FORM_USER_ID, "system-forms@sonicjs.internal", "system-forms", "Form", "Submission", sysNow, sysNow).run();
579
- }
345
+ function tenantMiddleware() {
346
+ return async (c, next) => {
347
+ const db = c.env?.DB;
348
+ if (!db) {
349
+ c.set("tenantId", "default");
350
+ return next();
580
351
  }
581
- console.log(`[FormSync] Inserting content: id=${contentId}, collection=${collection.id}, slug=${slug}, title=${title}, author=${authorId}`);
582
- await db.prepare(`
583
- INSERT INTO content (id, collection_id, slug, title, data, status, author_id, created_at, updated_at)
584
- VALUES (?, ?, ?, ?, ?, 'published', ?, ?, ?)
585
- `).bind(
586
- contentId,
587
- collection.id,
588
- slug,
589
- title,
590
- JSON.stringify(contentData),
591
- authorId,
592
- now,
593
- now
594
- ).run();
595
- await db.prepare(
596
- "UPDATE form_submissions SET content_id = ? WHERE id = ?"
597
- ).bind(contentId, submissionId).run();
598
- console.log(`[FormSync] Content created successfully: ${contentId}`);
599
- return contentId;
600
- } catch (error) {
601
- console.error("[FormSync] Error creating content from submission:", error);
602
- return null;
603
- }
604
- }
605
- async function backfillFormSubmissions(db, formId, collectionId) {
606
- try {
607
- const { results: submissions } = await db.prepare(
608
- "SELECT id, submission_data, user_email, ip_address, user_agent, user_id, submitted_at FROM form_submissions WHERE form_id = ? AND content_id IS NULL"
609
- ).bind(formId).all();
610
- if (!submissions || submissions.length === 0) {
611
- return 0;
352
+ const state = await loadTenantState(db);
353
+ const user = c.get("user");
354
+ let memberSlugs;
355
+ let memberRoles;
356
+ const enforceMembership = !!(user?.userId && state.pluginActive && !user.isSuperAdmin);
357
+ if (user?.userId && state.pluginActive && !user.isSuperAdmin) {
358
+ memberRoles = await loadMemberRoles(db, user.userId);
359
+ memberSlugs = new Set(memberRoles.keys());
612
360
  }
613
- const form = await db.prepare(
614
- "SELECT id, name, display_name FROM forms WHERE id = ?"
615
- ).bind(formId).first();
616
- if (!form) return 0;
617
- let count = 0;
618
- for (const sub of submissions) {
619
- try {
620
- const submissionData = typeof sub.submission_data === "string" ? JSON.parse(sub.submission_data) : sub.submission_data;
621
- const contentId = await createContentFromSubmission(
622
- db,
623
- submissionData,
624
- { id: form.id, name: form.name, display_name: form.display_name },
625
- sub.id,
626
- {
627
- ipAddress: sub.ip_address,
628
- userAgent: sub.user_agent,
629
- userEmail: sub.user_email,
630
- userId: sub.user_id
631
- }
632
- );
633
- if (contentId) count++;
634
- } catch (error) {
635
- console.error(`[FormSync] Error backfilling submission ${sub.id}:`, error);
636
- }
361
+ const tenantId = resolveTenantSlug(
362
+ state,
363
+ {
364
+ header: c.req.header(state.settings.headerName),
365
+ cookie: getCookie(c, TENANT_COOKIE),
366
+ host: c.req.header("host")
367
+ },
368
+ { memberSlugs, enforceMembership }
369
+ );
370
+ c.set("tenantId", tenantId);
371
+ if (user?.userId) {
372
+ const role = tenantId === "default" || user.isSuperAdmin ? user.role : memberRoles?.get(tenantId) ?? user.role;
373
+ c.set("tenantRole", role);
637
374
  }
638
- if (count > 0) {
639
- console.log(`[FormSync] Backfilled ${count} submissions for form ${formId}`);
375
+ await next();
376
+ const path = new URL(c.req.url).pathname;
377
+ if (!path.startsWith("/admin")) return;
378
+ if (!c.res.headers.get("content-type")?.includes("text/html")) return;
379
+ const status = c.res.status;
380
+ const headers = new Headers(c.res.headers);
381
+ const html = await c.res.text();
382
+ if (html.includes(TENANT_SWITCHER_MARKER)) {
383
+ const replacement = state.pluginActive ? renderTenantSwitcher(state, tenantId, enforceMembership ? memberSlugs : void 0) : "";
384
+ c.res = new Response(html.split(TENANT_SWITCHER_MARKER).join(replacement), { status, headers });
385
+ } else {
386
+ c.res = new Response(html, { status, headers });
640
387
  }
641
- return count;
642
- } catch (error) {
643
- console.error("[FormSync] Error backfilling submissions:", error);
644
- return 0;
645
- }
388
+ };
389
+ }
390
+ function renderTenantSwitcher(state, currentTenantId, memberSlugs) {
391
+ 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));
392
+ const options = active.map(
393
+ ([slug, t]) => `<option value="${escapeHtml(slug)}" ${slug === currentTenantId ? "selected" : ""}>${escapeHtml(t.name)}</option>`
394
+ ).join("");
395
+ return `
396
+ <div class="border-b border-zinc-950/5 px-4 py-3 dark:border-white/5" data-tenant-switcher>
397
+ <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">
398
+ <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>
399
+ Tenant
400
+ </label>
401
+ <form method="POST" action="/admin/tenants/switch" data-tenant-switcher-form>
402
+ <select
403
+ id="tenant-switcher-select"
404
+ name="tenant"
405
+ onchange="this.form.requestSubmit ? this.form.requestSubmit() : this.form.submit()"
406
+ 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"
407
+ >${options}</select>
408
+ <input type="hidden" name="redirect" value="" data-tenant-switcher-redirect>
409
+ </form>
410
+ <script>
411
+ (function () {
412
+ var r = document.querySelector('[data-tenant-switcher-redirect]');
413
+ if (r) r.value = window.location.pathname + window.location.search;
414
+ })();
415
+ </script>
416
+ </div>`;
646
417
  }
647
418
 
648
419
  // src/services/plugin-service.ts
420
+ var TENANT = "default";
421
+ var TYPE_ID = "plugin";
649
422
  var PluginService = class {
650
423
  constructor(db) {
651
424
  this.db = db;
652
425
  }
653
426
  async getAllPlugins() {
654
- await this.ensureAllPluginsExist();
655
- const stmt = this.db.prepare(`
656
- SELECT * FROM plugins
657
- ORDER BY is_core DESC, display_name ASC
658
- `);
659
- const { results } = await stmt.all();
660
- return (results || []).map(this.mapPluginFromDb);
661
- }
662
- /**
663
- * Ensure all plugins from the registry exist in the database
664
- * Auto-installs any newly detected plugins with inactive status
665
- *
666
- * Note: This method should be overridden or configured with a plugin registry
667
- * in the consuming application
668
- */
669
- async ensureAllPluginsExist() {
670
- console.log("[PluginService] ensureAllPluginsExist - requires PLUGIN_REGISTRY configuration");
427
+ const { results } = await this.db.prepare(`
428
+ SELECT * FROM documents
429
+ WHERE type_id = ? AND tenant_id = ? AND is_current_draft = 1 AND deleted_at IS NULL
430
+ ORDER BY json_extract(data, '$.isCore') DESC, title ASC
431
+ `).bind(TYPE_ID, TENANT).all();
432
+ return (results || []).map(mapDocumentToPlugin);
671
433
  }
672
434
  async getPlugin(pluginId) {
673
- const stmt = this.db.prepare("SELECT * FROM plugins WHERE id = ?");
674
- const plugin = await stmt.bind(pluginId).first();
675
- if (!plugin) return null;
676
- return this.mapPluginFromDb(plugin);
435
+ const row = await this.db.prepare(`
436
+ SELECT * FROM documents
437
+ WHERE slug = ? AND type_id = ? AND tenant_id = ? AND is_current_draft = 1 AND deleted_at IS NULL
438
+ `).bind(pluginId, TYPE_ID, TENANT).first();
439
+ if (!row) return null;
440
+ return mapDocumentToPlugin(row);
677
441
  }
678
442
  async getPluginByName(name) {
679
- const stmt = this.db.prepare("SELECT * FROM plugins WHERE name = ?");
680
- const plugin = await stmt.bind(name).first();
681
- if (!plugin) return null;
682
- return this.mapPluginFromDb(plugin);
443
+ return this.getPlugin(name);
683
444
  }
684
445
  async getPluginStats() {
685
- const stmt = this.db.prepare(`
686
- SELECT
446
+ const stats = await this.db.prepare(`
447
+ SELECT
687
448
  COUNT(*) as total,
688
- COUNT(CASE WHEN status = 'active' THEN 1 END) as active,
689
- COUNT(CASE WHEN status = 'inactive' THEN 1 END) as inactive,
690
- COUNT(CASE WHEN status = 'error' THEN 1 END) as errors
691
- FROM plugins
692
- `);
693
- const stats = await stmt.first();
449
+ COUNT(CASE WHEN q_plugin_status = 'active' THEN 1 END) as active,
450
+ COUNT(CASE WHEN q_plugin_status = 'inactive' THEN 1 END) as inactive,
451
+ COUNT(CASE WHEN q_plugin_status = 'error' THEN 1 END) as errors
452
+ FROM documents
453
+ WHERE type_id = ? AND tenant_id = ? AND is_current_draft = 1 AND deleted_at IS NULL
454
+ `).bind(TYPE_ID, TENANT).first();
694
455
  return {
695
- total: stats.total || 0,
696
- active: stats.active || 0,
697
- inactive: stats.inactive || 0,
698
- errors: stats.errors || 0,
456
+ total: stats?.total || 0,
457
+ active: stats?.active || 0,
458
+ inactive: stats?.inactive || 0,
459
+ errors: stats?.errors || 0,
699
460
  uninstalled: 0
700
461
  };
701
462
  }
702
463
  async installPlugin(pluginData) {
703
- const id = pluginData.id || `plugin-${Date.now()}`;
464
+ const slug = pluginData.id || pluginData.name || `plugin-${Date.now()}`;
465
+ const docId = crypto.randomUUID();
704
466
  const now = Math.floor(Date.now() / 1e3);
705
- const stmt = this.db.prepare(`
706
- INSERT INTO plugins (
707
- id, name, display_name, description, version, author, category, icon,
708
- status, is_core, settings, permissions, dependencies, download_count,
709
- rating, installed_at, last_updated
710
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
711
- `);
712
- await stmt.bind(
713
- id,
714
- pluginData.name || id,
467
+ const data = JSON.stringify({
468
+ name: slug,
469
+ displayName: pluginData.display_name || "Unnamed Plugin",
470
+ description: pluginData.description || "",
471
+ version: pluginData.version || "1.0.0",
472
+ author: pluginData.author || "Unknown",
473
+ category: pluginData.category || "utilities",
474
+ icon: pluginData.icon || "\u{1F50C}",
475
+ status: "active",
476
+ isCore: pluginData.is_core || false,
477
+ settings: pluginData.settings || {},
478
+ permissions: pluginData.permissions || [],
479
+ dependencies: pluginData.dependencies || [],
480
+ downloadCount: pluginData.download_count || 0,
481
+ rating: pluginData.rating || 0
482
+ });
483
+ await this.db.prepare(`
484
+ INSERT INTO documents (
485
+ id, root_id, type_id, version_number, is_current_draft, is_published, status,
486
+ parent_root_id, slug, title, tenant_id, locale, translation_group_id,
487
+ data, metadata, created_at, updated_at
488
+ ) VALUES (
489
+ ?, ?, ?, 1, 1, 1, 'published',
490
+ '', ?, ?, ?, 'default', '',
491
+ ?, '{}', ?, ?
492
+ )
493
+ `).bind(
494
+ docId,
495
+ docId,
496
+ TYPE_ID,
497
+ slug,
715
498
  pluginData.display_name || "Unnamed Plugin",
716
- pluginData.description || "",
717
- pluginData.version || "1.0.0",
718
- pluginData.author || "Unknown",
719
- pluginData.category || "utilities",
720
- pluginData.icon || "\u{1F50C}",
721
- "inactive",
722
- pluginData.is_core || false,
723
- JSON.stringify(pluginData.settings || {}),
724
- JSON.stringify(pluginData.permissions || []),
725
- JSON.stringify(pluginData.dependencies || []),
726
- pluginData.download_count || 0,
727
- pluginData.rating || 0,
499
+ TENANT,
500
+ data,
728
501
  now,
729
502
  now
730
503
  ).run();
731
- await this.logActivity(id, "installed", null, { version: pluginData.version });
732
- const installed = await this.getPlugin(id);
504
+ await this.logActivity(slug, "installed", null, { version: pluginData.version });
505
+ await this.logActivity(slug, "activated", null);
506
+ const installed = await this.getPlugin(slug);
733
507
  if (!installed) throw new Error("Failed to install plugin");
734
508
  return installed;
735
509
  }
510
+ /**
511
+ * Ensure a definePlugin-registered plugin exists in the DB with active status.
512
+ * No-op if already present. Used by admin routes to auto-register SDK plugins
513
+ * that have never been explicitly installed.
514
+ */
515
+ async ensurePlugin(id, data) {
516
+ const existing = await this.getPlugin(id);
517
+ if (existing) return existing;
518
+ return this.installPlugin({
519
+ id,
520
+ name: id,
521
+ display_name: data.displayName || id,
522
+ description: data.description || "",
523
+ author: data.author || "",
524
+ version: data.version || "1.0.0"
525
+ });
526
+ }
736
527
  async uninstallPlugin(pluginId) {
737
528
  const plugin = await this.getPlugin(pluginId);
738
529
  if (!plugin) throw new Error("Plugin not found");
739
530
  if (plugin.is_core) throw new Error("Cannot uninstall core plugins");
740
- if (plugin.status === "active") {
741
- await this.deactivatePlugin(pluginId);
742
- }
743
- const stmt = this.db.prepare("DELETE FROM plugins WHERE id = ?");
744
- await stmt.bind(pluginId).run();
531
+ if (plugin.status === "active") await this.deactivatePlugin(pluginId);
532
+ const now = Math.floor(Date.now() / 1e3);
533
+ await this.db.prepare(`
534
+ UPDATE documents
535
+ SET deleted_at = ?, updated_at = ?, is_current_draft = 0, is_published = 0
536
+ WHERE slug = ? AND type_id = ? AND tenant_id = ? AND is_current_draft = 1
537
+ `).bind(now, now, pluginId, TYPE_ID, TENANT).run();
745
538
  await this.logActivity(pluginId, "uninstalled", null, { name: plugin.name });
746
539
  }
747
540
  async activatePlugin(pluginId) {
@@ -751,100 +544,128 @@ var PluginService = class {
751
544
  await this.checkDependencies(plugin.dependencies);
752
545
  }
753
546
  const now = Math.floor(Date.now() / 1e3);
754
- const stmt = this.db.prepare(`
755
- UPDATE plugins
756
- SET status = 'active', activated_at = ?, error_message = NULL
757
- WHERE id = ?
758
- `);
759
- await stmt.bind(now, pluginId).run();
547
+ await this.db.prepare(`
548
+ UPDATE documents
549
+ SET data = json_set(data, '$.status', 'active', '$.activatedAt', ?, '$.errorMessage', null),
550
+ updated_at = ?
551
+ WHERE slug = ? AND type_id = ? AND tenant_id = ? AND is_current_draft = 1 AND deleted_at IS NULL
552
+ `).bind(now, now, pluginId, TYPE_ID, TENANT).run();
553
+ await this.db.prepare(`UPDATE plugins SET status = 'active' WHERE id = ?`).bind(pluginId).run().catch(() => {
554
+ });
555
+ invalidateTenantCache();
760
556
  await this.logActivity(pluginId, "activated", null);
761
557
  }
762
558
  async deactivatePlugin(pluginId) {
763
559
  const plugin = await this.getPlugin(pluginId);
764
560
  if (!plugin) throw new Error("Plugin not found");
765
561
  await this.checkDependents(plugin.name);
766
- const stmt = this.db.prepare(`
767
- UPDATE plugins
768
- SET status = 'inactive', activated_at = NULL
769
- WHERE id = ?
770
- `);
771
- await stmt.bind(pluginId).run();
562
+ const now = Math.floor(Date.now() / 1e3);
563
+ await this.db.prepare(`
564
+ UPDATE documents
565
+ SET data = json_set(data, '$.status', 'inactive', '$.activatedAt', null),
566
+ updated_at = ?
567
+ WHERE slug = ? AND type_id = ? AND tenant_id = ? AND is_current_draft = 1 AND deleted_at IS NULL
568
+ `).bind(now, pluginId, TYPE_ID, TENANT).run();
569
+ await this.db.prepare(`UPDATE plugins SET status = 'inactive' WHERE id = ?`).bind(pluginId).run().catch(() => {
570
+ });
571
+ invalidateTenantCache();
772
572
  await this.logActivity(pluginId, "deactivated", null);
773
573
  }
774
574
  async updatePluginSettings(pluginId, settings) {
775
575
  const plugin = await this.getPlugin(pluginId);
776
576
  if (!plugin) throw new Error("Plugin not found");
777
- const stmt = this.db.prepare(`
778
- UPDATE plugins
779
- SET settings = ?, updated_at = unixepoch()
780
- WHERE id = ?
781
- `);
782
- await stmt.bind(JSON.stringify(settings), pluginId).run();
577
+ const now = Math.floor(Date.now() / 1e3);
578
+ await this.db.prepare(`
579
+ UPDATE documents
580
+ SET data = json_set(data, '$.settings', json(?)), updated_at = ?
581
+ WHERE slug = ? AND type_id = ? AND tenant_id = ? AND is_current_draft = 1 AND deleted_at IS NULL
582
+ `).bind(JSON.stringify(settings), now, pluginId, TYPE_ID, TENANT).run();
583
+ invalidateTenantCache();
783
584
  await this.logActivity(pluginId, "settings_updated", null);
784
585
  }
586
+ async updatePluginVersion(pluginId, patch) {
587
+ const now = Math.floor(Date.now() / 1e3);
588
+ await this.db.prepare(`
589
+ UPDATE documents
590
+ SET data = json_set(data,
591
+ '$.version', ?,
592
+ '$.description', ?,
593
+ '$.permissions', json(?),
594
+ '$.settings', json(?)
595
+ ),
596
+ updated_at = ?
597
+ WHERE slug = ? AND type_id = ? AND tenant_id = ? AND is_current_draft = 1 AND deleted_at IS NULL
598
+ `).bind(
599
+ patch.version,
600
+ patch.description,
601
+ JSON.stringify(patch.permissions),
602
+ JSON.stringify(patch.settings || {}),
603
+ now,
604
+ pluginId,
605
+ TYPE_ID,
606
+ TENANT
607
+ ).run();
608
+ }
785
609
  async setPluginError(pluginId, error) {
786
- const stmt = this.db.prepare(`
787
- UPDATE plugins
788
- SET status = 'error', error_message = ?
789
- WHERE id = ?
790
- `);
791
- await stmt.bind(error, pluginId).run();
610
+ const now = Math.floor(Date.now() / 1e3);
611
+ await this.db.prepare(`
612
+ UPDATE documents
613
+ SET data = json_set(data, '$.status', 'error', '$.errorMessage', ?),
614
+ updated_at = ?
615
+ WHERE slug = ? AND type_id = ? AND tenant_id = ? AND is_current_draft = 1 AND deleted_at IS NULL
616
+ `).bind(error, now, pluginId, TYPE_ID, TENANT).run();
792
617
  await this.logActivity(pluginId, "error", null, { error });
793
618
  }
794
619
  async getPluginActivity(pluginId, limit = 10) {
795
- const stmt = this.db.prepare(`
796
- SELECT * FROM plugin_activity_log
797
- WHERE plugin_id = ?
798
- ORDER BY timestamp DESC
799
- LIMIT ?
800
- `);
801
- const { results } = await stmt.bind(pluginId, limit).all();
802
- return (results || []).map((row) => ({
803
- id: row.id,
804
- action: row.action,
805
- userId: row.user_id,
806
- details: row.details ? JSON.parse(row.details) : null,
807
- timestamp: row.timestamp
808
- }));
620
+ try {
621
+ const { results } = await this.db.prepare(`
622
+ SELECT id, data, created_at FROM documents
623
+ WHERE type_id = 'plugin_activity'
624
+ AND tenant_id = ?
625
+ AND is_current_draft = 1
626
+ AND deleted_at IS NULL
627
+ AND json_extract(data, '$.pluginId') = ?
628
+ ORDER BY created_at DESC
629
+ LIMIT ?
630
+ `).bind(TENANT, pluginId, limit).all();
631
+ return (results || []).map((row) => {
632
+ const d = typeof row.data === "string" ? JSON.parse(row.data) : row.data || {};
633
+ return {
634
+ id: row.id,
635
+ action: d.action,
636
+ userId: d.userId || null,
637
+ details: d.details || null,
638
+ timestamp: row.created_at
639
+ };
640
+ });
641
+ } catch {
642
+ return [];
643
+ }
809
644
  }
810
645
  async registerHook(pluginId, hookName, handlerName, priority = 10) {
811
646
  const id = `hook-${Date.now()}`;
812
- const stmt = this.db.prepare(`
647
+ await this.db.prepare(`
813
648
  INSERT INTO plugin_hooks (id, plugin_id, hook_name, handler_name, priority)
814
649
  VALUES (?, ?, ?, ?, ?)
815
- `);
816
- await stmt.bind(id, pluginId, hookName, handlerName, priority).run();
650
+ `).bind(id, pluginId, hookName, handlerName, priority).run();
817
651
  }
818
652
  async registerRoute(pluginId, path, method, handlerName, middleware) {
819
653
  const id = `route-${Date.now()}`;
820
- const stmt = this.db.prepare(`
654
+ await this.db.prepare(`
821
655
  INSERT INTO plugin_routes (id, plugin_id, path, method, handler_name, middleware)
822
656
  VALUES (?, ?, ?, ?, ?, ?)
823
- `);
824
- await stmt.bind(
825
- id,
826
- pluginId,
827
- path,
828
- method,
829
- handlerName,
830
- JSON.stringify(middleware || [])
831
- ).run();
657
+ `).bind(id, pluginId, path, method, handlerName, JSON.stringify(middleware || [])).run();
832
658
  }
833
659
  async getPluginHooks(pluginId) {
834
- const stmt = this.db.prepare(`
835
- SELECT * FROM plugin_hooks
836
- WHERE plugin_id = ? AND is_active = TRUE
837
- ORDER BY priority ASC
838
- `);
839
- const { results } = await stmt.bind(pluginId).all();
660
+ const { results } = await this.db.prepare(`
661
+ SELECT * FROM plugin_hooks WHERE plugin_id = ? AND is_active = TRUE ORDER BY priority ASC
662
+ `).bind(pluginId).all();
840
663
  return results || [];
841
664
  }
842
665
  async getPluginRoutes(pluginId) {
843
- const stmt = this.db.prepare(`
844
- SELECT * FROM plugin_routes
845
- WHERE plugin_id = ? AND is_active = TRUE
846
- `);
847
- const { results } = await stmt.bind(pluginId).all();
666
+ const { results } = await this.db.prepare(`
667
+ SELECT * FROM plugin_routes WHERE plugin_id = ? AND is_active = TRUE
668
+ `).bind(pluginId).all();
848
669
  return results || [];
849
670
  }
850
671
  async checkDependencies(dependencies) {
@@ -856,55 +677,61 @@ var PluginService = class {
856
677
  }
857
678
  }
858
679
  async checkDependents(pluginName) {
859
- const stmt = this.db.prepare(`
860
- SELECT id, display_name FROM plugins
861
- WHERE status = 'active'
862
- AND dependencies LIKE ?
863
- `);
864
- const { results } = await stmt.bind(`%"${pluginName}"%`).all();
680
+ const { results } = await this.db.prepare(`
681
+ SELECT slug, title FROM documents
682
+ WHERE type_id = ? AND tenant_id = ? AND is_current_draft = 1 AND deleted_at IS NULL
683
+ AND q_plugin_status = 'active'
684
+ AND json_extract(data, '$.dependencies') LIKE ?
685
+ `).bind(TYPE_ID, TENANT, `%"${pluginName}"%`).all();
865
686
  if (results && results.length > 0) {
866
- const names = results.map((p) => p.display_name).join(", ");
687
+ const names = results.map((p) => p.title || p.slug).join(", ");
867
688
  throw new Error(`Cannot deactivate. The following plugins depend on this one: ${names}`);
868
689
  }
869
690
  }
870
691
  async logActivity(pluginId, action, userId, details) {
871
- const id = `activity-${Date.now()}`;
872
- const stmt = this.db.prepare(`
873
- INSERT INTO plugin_activity_log (id, plugin_id, action, user_id, details)
874
- VALUES (?, ?, ?, ?, ?)
875
- `);
876
- await stmt.bind(
877
- id,
878
- pluginId,
879
- action,
880
- userId,
881
- details ? JSON.stringify(details) : null
882
- ).run();
883
- }
884
- mapPluginFromDb(row) {
885
- return {
886
- id: row.id,
887
- name: row.name,
888
- display_name: row.display_name,
889
- description: row.description,
890
- version: row.version,
891
- author: row.author,
892
- category: row.category,
893
- icon: row.icon,
894
- status: row.status,
895
- is_core: row.is_core === 1,
896
- settings: row.settings ? JSON.parse(row.settings) : void 0,
897
- permissions: row.permissions ? JSON.parse(row.permissions) : void 0,
898
- dependencies: row.dependencies ? JSON.parse(row.dependencies) : void 0,
899
- download_count: row.download_count || 0,
900
- rating: row.rating || 0,
901
- installed_at: row.installed_at,
902
- activated_at: row.activated_at,
903
- last_updated: row.last_updated,
904
- error_message: row.error_message
905
- };
692
+ try {
693
+ const docId = crypto.randomUUID();
694
+ const now = Math.floor(Date.now() / 1e3);
695
+ const data = JSON.stringify({ pluginId, action, userId, details: details || null });
696
+ await this.db.prepare(`
697
+ INSERT INTO documents (
698
+ id, root_id, type_id, version_number, is_current_draft, is_published, status,
699
+ parent_root_id, slug, title, tenant_id, locale, translation_group_id,
700
+ data, metadata, created_at, updated_at
701
+ ) VALUES (
702
+ ?, ?, 'plugin_activity', 1, 1, 1, 'published',
703
+ '', ?, ?, 'default', 'default', '',
704
+ ?, '{}', ?, ?
705
+ )
706
+ `).bind(docId, docId, docId, action, data, now, now).run();
707
+ } catch {
708
+ }
906
709
  }
907
710
  };
711
+ function mapDocumentToPlugin(row) {
712
+ const data = typeof row.data === "string" ? JSON.parse(row.data) : row.data || {};
713
+ return {
714
+ id: row.slug || data.name || row.root_id,
715
+ name: data.name || row.slug || "",
716
+ display_name: data.displayName || row.title || "",
717
+ description: data.description || "",
718
+ version: data.version || "1.0.0",
719
+ author: data.author || "Unknown",
720
+ category: data.category || "utilities",
721
+ icon: data.icon || "\u{1F50C}",
722
+ status: data.status || "inactive",
723
+ is_core: data.isCore === true || data.isCore === 1,
724
+ settings: data.settings,
725
+ permissions: data.permissions,
726
+ dependencies: data.dependencies,
727
+ download_count: data.downloadCount || 0,
728
+ rating: data.rating || 0,
729
+ installed_at: row.created_at,
730
+ activated_at: data.activatedAt || void 0,
731
+ last_updated: row.updated_at,
732
+ error_message: data.errorMessage || void 0
733
+ };
734
+ }
908
735
 
909
736
  // src/plugins/manifest-registry.ts
910
737
  var PLUGIN_REGISTRY = {
@@ -941,23 +768,6 @@ var PLUGIN_REGISTRY = {
941
768
  "order": 50
942
769
  }
943
770
  },
944
- "code-examples-plugin": {
945
- "id": "code-examples-plugin",
946
- "codeName": "code-examples-plugin",
947
- "displayName": "Code Examples",
948
- "description": "Code snippets and examples library with syntax highlighting and categorization",
949
- "version": "1.0.0-beta.1",
950
- "author": "SonicJS Team",
951
- "category": "content",
952
- "iconEmoji": "\u{1F4BB}",
953
- "is_core": false,
954
- "permissions": [
955
- "code-examples:manage"
956
- ],
957
- "dependencies": [],
958
- "defaultSettings": {},
959
- "adminMenu": null
960
- },
961
771
  "core-analytics": {
962
772
  "id": "core-analytics",
963
773
  "codeName": "core-analytics",
@@ -1011,12 +821,6 @@ var PLUGIN_REGISTRY = {
1011
821
  "label": "Password",
1012
822
  "type": "password"
1013
823
  },
1014
- "username": {
1015
- "required": true,
1016
- "minLength": 3,
1017
- "label": "Username",
1018
- "type": "text"
1019
- },
1020
824
  "firstName": {
1021
825
  "required": true,
1022
826
  "minLength": 1,
@@ -1032,7 +836,6 @@ var PLUGIN_REGISTRY = {
1032
836
  },
1033
837
  "validation": {
1034
838
  "emailFormat": true,
1035
- "allowDuplicateUsernames": false,
1036
839
  "passwordRequirements": {
1037
840
  "requireUppercase": false,
1038
841
  "requireLowercase": false,
@@ -1058,6 +861,7 @@ var PLUGIN_REGISTRY = {
1058
861
  "category": "system",
1059
862
  "iconEmoji": "\u26A1",
1060
863
  "is_core": true,
864
+ "defaultActive": true,
1061
865
  "permissions": [
1062
866
  "cache.view",
1063
867
  "cache.clear",
@@ -1070,7 +874,12 @@ var PLUGIN_REGISTRY = {
1070
874
  "enableDatabaseCache": true,
1071
875
  "defaultTTL": 3600
1072
876
  },
1073
- "adminMenu": null
877
+ "adminMenu": {
878
+ "label": "Cache",
879
+ "icon": "server",
880
+ "path": "/admin/cache",
881
+ "order": 60
882
+ }
1074
883
  },
1075
884
  "core-media": {
1076
885
  "id": "core-media",
@@ -1104,7 +913,7 @@ var PLUGIN_REGISTRY = {
1104
913
  "author": "SonicJS Team",
1105
914
  "category": "development",
1106
915
  "iconEmoji": "\u{1F5C4}\uFE0F",
1107
- "is_core": false,
916
+ "is_core": true,
1108
917
  "permissions": [
1109
918
  "database:admin"
1110
919
  ],
@@ -1138,32 +947,6 @@ var PLUGIN_REGISTRY = {
1138
947
  },
1139
948
  "adminMenu": null
1140
949
  },
1141
- "design": {
1142
- "id": "design",
1143
- "codeName": "design",
1144
- "displayName": "Design System",
1145
- "description": "Design system management including themes, components, and UI customization. Provides a visual interface for managing design tokens, typography, colors, and component library.",
1146
- "version": "1.0.0-beta.1",
1147
- "author": "SonicJS",
1148
- "category": "utilities",
1149
- "iconEmoji": "\u{1F3A8}",
1150
- "is_core": false,
1151
- "permissions": [
1152
- "design.view",
1153
- "design.edit"
1154
- ],
1155
- "dependencies": [],
1156
- "defaultSettings": {
1157
- "defaultTheme": "light",
1158
- "customCSS": ""
1159
- },
1160
- "adminMenu": {
1161
- "label": "Design",
1162
- "icon": "palette",
1163
- "path": "/admin/design",
1164
- "order": 80
1165
- }
1166
- },
1167
950
  "easy-mdx": {
1168
951
  "id": "easy-mdx",
1169
952
  "codeName": "easy-mdx",
@@ -1188,12 +971,12 @@ var PLUGIN_REGISTRY = {
1188
971
  "id": "email",
1189
972
  "codeName": "email",
1190
973
  "displayName": "Email",
1191
- "description": "Send transactional emails using Resend",
1192
- "version": "1.0.0-beta.1",
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",
1193
976
  "author": "SonicJS Team",
1194
977
  "category": "utilities",
1195
978
  "iconEmoji": "\u{1F4E7}",
1196
- "is_core": false,
979
+ "is_core": true,
1197
980
  "permissions": [
1198
981
  "email:manage",
1199
982
  "email:send",
@@ -1201,19 +984,45 @@ var PLUGIN_REGISTRY = {
1201
984
  ],
1202
985
  "dependencies": [],
1203
986
  "defaultSettings": {
1204
- "apiKey": "",
987
+ "provider": "cloudflare",
988
+ "resendApiKey": "",
1205
989
  "fromEmail": "",
1206
990
  "fromName": "",
1207
991
  "replyTo": "",
1208
- "logoUrl": ""
992
+ "logoUrl": "",
993
+ "cfAccountId": "",
994
+ "cfEmailApiToken": ""
1209
995
  },
1210
996
  "adminMenu": {
1211
997
  "label": "Email",
1212
998
  "icon": "envelope",
1213
- "path": "/admin/plugins/email/settings",
999
+ "path": "/admin/plugins/email",
1214
1000
  "order": 80
1215
1001
  }
1216
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
+ },
1217
1026
  "global-variables": {
1218
1027
  "id": "global-variables",
1219
1028
  "codeName": "global-variables",
@@ -1263,6 +1072,31 @@ var PLUGIN_REGISTRY = {
1263
1072
  "order": 90
1264
1073
  }
1265
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
+ },
1266
1100
  "magic-link-auth": {
1267
1101
  "id": "magic-link-auth",
1268
1102
  "codeName": "magic-link-auth",
@@ -1284,6 +1118,33 @@ var PLUGIN_REGISTRY = {
1284
1118
  },
1285
1119
  "adminMenu": null
1286
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
+ },
1287
1148
  "oauth-providers": {
1288
1149
  "id": "oauth-providers",
1289
1150
  "codeName": "oauth-providers",
@@ -1440,27 +1301,6 @@ var PLUGIN_REGISTRY = {
1440
1301
  "order": 85
1441
1302
  }
1442
1303
  },
1443
- "seed-data": {
1444
- "id": "seed-data",
1445
- "codeName": "seed-data",
1446
- "displayName": "Seed Data Generator",
1447
- "description": "Development tool for generating sample data and testing content. Useful for demos and development environments",
1448
- "version": "1.0.0-beta.1",
1449
- "author": "SonicJS Team",
1450
- "category": "development",
1451
- "iconEmoji": "\u{1F331}",
1452
- "is_core": false,
1453
- "permissions": [
1454
- "seed-data:generate"
1455
- ],
1456
- "dependencies": [],
1457
- "defaultSettings": {
1458
- "userCount": 20,
1459
- "contentCount": 200,
1460
- "defaultPassword": "password123"
1461
- },
1462
- "adminMenu": null
1463
- },
1464
1304
  "shortcodes": {
1465
1305
  "id": "shortcodes",
1466
1306
  "codeName": "shortcodes",
@@ -1513,23 +1353,6 @@ var PLUGIN_REGISTRY = {
1513
1353
  "order": 90
1514
1354
  }
1515
1355
  },
1516
- "testimonials-plugin": {
1517
- "id": "testimonials-plugin",
1518
- "codeName": "testimonials-plugin",
1519
- "displayName": "Testimonials",
1520
- "description": "Customer testimonials and reviews management with display widgets and ratings",
1521
- "version": "1.0.0-beta.1",
1522
- "author": "SonicJS Team",
1523
- "category": "content",
1524
- "iconEmoji": "\u{1F4AC}",
1525
- "is_core": false,
1526
- "permissions": [
1527
- "testimonials:manage"
1528
- ],
1529
- "dependencies": [],
1530
- "defaultSettings": {},
1531
- "adminMenu": null
1532
- },
1533
1356
  "tinymce-plugin": {
1534
1357
  "id": "tinymce-plugin",
1535
1358
  "codeName": "tinymce-plugin",
@@ -1598,27 +1421,19 @@ var PLUGIN_REGISTRY = {
1598
1421
  "defaultSettings": {},
1599
1422
  "adminMenu": null
1600
1423
  },
1601
- "workflow-plugin": {
1602
- "id": "workflow-plugin",
1603
- "codeName": "workflow-plugin",
1604
- "displayName": "Workflow Engine",
1605
- "description": "Content workflow and approval system with customizable states, transitions, and review processes",
1606
- "version": "1.0.0-beta.1",
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",
1607
1430
  "author": "SonicJS Team",
1608
1431
  "category": "content",
1609
- "iconEmoji": "\u{1F504}",
1610
- "is_core": false,
1611
- "permissions": [
1612
- "workflow:manage",
1613
- "workflow:approve"
1614
- ],
1432
+ "iconEmoji": "\u{1F551}",
1433
+ "is_core": true,
1434
+ "permissions": [],
1615
1435
  "dependencies": [],
1616
- "defaultSettings": {
1617
- "enableApprovalChains": true,
1618
- "enableScheduling": true,
1619
- "enableAutomation": true,
1620
- "enableNotifications": true
1621
- },
1436
+ "defaultSettings": {},
1622
1437
  "adminMenu": null
1623
1438
  }
1624
1439
  };
@@ -1633,17 +1448,8 @@ function findPluginByCodeName(codeName) {
1633
1448
  // src/services/plugin-bootstrap.ts
1634
1449
  var BOOTSTRAP_PLUGIN_IDS = [
1635
1450
  "core-auth",
1636
- "core-media",
1637
- "database-tools",
1638
- "seed-data",
1639
- "core-cache",
1640
- "workflow-plugin",
1641
- "easy-mdx",
1642
- "ai-search",
1643
- "oauth-providers",
1644
- "global-variables",
1645
- "user-profiles",
1646
- "stripe"
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)
1647
1453
  ];
1648
1454
  function registryToCorePlugin(entry) {
1649
1455
  return {
@@ -1668,9 +1474,10 @@ var PluginBootstrapService = class {
1668
1474
  pluginService;
1669
1475
  /**
1670
1476
  * Core plugins derived from the auto-generated plugin registry.
1671
- * Only plugins listed in BOOTSTRAP_PLUGIN_IDS are included.
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.
1672
1479
  */
1673
- CORE_PLUGINS = BOOTSTRAP_PLUGIN_IDS.filter((id) => PLUGIN_REGISTRY[id] !== void 0).map((id) => registryToCorePlugin(PLUGIN_REGISTRY[id]));
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]));
1674
1481
  /**
1675
1482
  * Bootstrap all core plugins - install them if they don't exist
1676
1483
  */
@@ -1731,28 +1538,15 @@ var PluginBootstrapService = class {
1731
1538
  }
1732
1539
  }
1733
1540
  /**
1734
- * Update an existing plugin
1541
+ * Update an existing plugin's version/description/permissions/settings
1735
1542
  */
1736
1543
  async updatePlugin(plugin) {
1737
- const now = Math.floor(Date.now() / 1e3);
1738
- const stmt = this.db.prepare(`
1739
- UPDATE plugins
1740
- SET
1741
- version = ?,
1742
- description = ?,
1743
- permissions = ?,
1744
- settings = ?,
1745
- last_updated = ?
1746
- WHERE id = ?
1747
- `);
1748
- await stmt.bind(
1749
- plugin.version,
1750
- plugin.description,
1751
- JSON.stringify(plugin.permissions),
1752
- JSON.stringify(plugin.settings || {}),
1753
- now,
1754
- plugin.id
1755
- ).run();
1544
+ await this.pluginService.updatePluginVersion(plugin.id, {
1545
+ version: plugin.version,
1546
+ description: plugin.description,
1547
+ permissions: plugin.permissions,
1548
+ settings: plugin.settings || {}
1549
+ });
1756
1550
  }
1757
1551
  /**
1758
1552
  * Check if bootstrap is needed (first run detection)
@@ -1778,6 +1572,6 @@ var PluginBootstrapService = class {
1778
1572
  }
1779
1573
  };
1780
1574
 
1781
- export { PLUGIN_REGISTRY, PluginBootstrapService, PluginService, backfillFormSubmissions, cleanupRemovedCollections, createContentFromSubmission, deriveCollectionSchemaFromFormio, deriveSubmissionTitle, findPluginByCodeName, fullCollectionSync, getAvailableCollectionNames, getManagedCollections, isCollectionManaged, loadCollectionConfig, loadCollectionConfigs, mapFormStatusToContentStatus, registerCollections, syncAllFormCollections, syncCollection, syncCollections, syncFormCollection, validateCollectionConfig };
1782
- //# sourceMappingURL=chunk-JZV22DEV.js.map
1783
- //# sourceMappingURL=chunk-JZV22DEV.js.map
1575
+ export { CollectionRegistry, MULTI_TENANT_PLUGIN_ID, PLUGIN_REGISTRY, PluginBootstrapService, PluginService, TENANT_COOKIE, collectionRecordToRow, findPluginByCodeName, getAvailableCollectionNames, getCollectionRegistry, getVisibleCollections, invalidateTenantCache, isCodeCollectionInternal, isDbDocTypeInternal, loadCollectionConfig, loadCollectionConfigs, registerCollections, resetCollectionRegistry, tenantMiddleware, validateCollectionConfig };
1576
+ //# sourceMappingURL=chunk-ZGGXCFR6.js.map
1577
+ //# sourceMappingURL=chunk-ZGGXCFR6.js.map