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