@sonicjs-cms/core 2.18.1 → 3.0.0-beta.10

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 (232) hide show
  1. package/README.md +52 -52
  2. package/dist/admin-documents-form.template-DDSH6ROU.js +6 -0
  3. package/dist/{admin-layout-catalyst.template-UMTIN66R.js.map → admin-documents-form.template-DDSH6ROU.js.map} +1 -1
  4. package/dist/admin-documents-form.template-LSZKGA5J.cjs +19 -0
  5. package/dist/{admin-layout-catalyst.template-HFD37TY5.cjs.map → admin-documents-form.template-LSZKGA5J.cjs.map} +1 -1
  6. package/dist/{filter-bar.template-DlVYMk-T.d.cts → admin-layout-catalyst.template-DrwDUfsE.d.cts} +25 -1
  7. package/dist/{filter-bar.template-DlVYMk-T.d.ts → admin-layout-catalyst.template-DrwDUfsE.d.ts} +25 -1
  8. package/dist/admin-layout-catalyst.template-KDHKVLXR.cjs +21 -0
  9. package/dist/admin-layout-catalyst.template-KDHKVLXR.cjs.map +1 -0
  10. package/dist/admin-layout-catalyst.template-YQ4EMF2J.js +7 -0
  11. package/dist/admin-layout-catalyst.template-YQ4EMF2J.js.map +1 -0
  12. package/dist/app-Bo0X1OWX.d.ts +1268 -0
  13. package/dist/app-Do66yCcV.d.cts +1268 -0
  14. package/dist/cache-DDARE4QE.js +4 -0
  15. package/dist/cache-DDARE4QE.js.map +1 -0
  16. package/dist/cache-LVYS4BPL.cjs +33 -0
  17. package/dist/cache-LVYS4BPL.cjs.map +1 -0
  18. package/dist/chunk-2CB4KY7I.cjs +771 -0
  19. package/dist/chunk-2CB4KY7I.cjs.map +1 -0
  20. package/dist/{chunk-ABB34XUS.cjs → chunk-3KYKEXV7.cjs} +667 -19
  21. package/dist/chunk-3KYKEXV7.cjs.map +1 -0
  22. package/dist/chunk-4BTBSXMR.cjs +912 -0
  23. package/dist/chunk-4BTBSXMR.cjs.map +1 -0
  24. package/dist/{chunk-55RDMDOP.js → chunk-5V62WT6M.js} +181 -57
  25. package/dist/chunk-5V62WT6M.js.map +1 -0
  26. package/dist/{chunk-XXDFQERJ.js → chunk-6OC6MF3C.js} +7192 -9806
  27. package/dist/chunk-6OC6MF3C.js.map +1 -0
  28. package/dist/chunk-AI663NBO.js +821 -0
  29. package/dist/chunk-AI663NBO.js.map +1 -0
  30. package/dist/chunk-ALDRXTUO.js +273 -0
  31. package/dist/chunk-ALDRXTUO.js.map +1 -0
  32. package/dist/{chunk-TFNTM3OA.js → chunk-ATUPB6MN.js} +645 -15
  33. package/dist/chunk-ATUPB6MN.js.map +1 -0
  34. package/dist/chunk-BLMTL57B.js +767 -0
  35. package/dist/chunk-BLMTL57B.js.map +1 -0
  36. package/dist/{chunk-4ZSNJDLS.cjs → chunk-CRGUD4KC.cjs} +9 -9
  37. package/dist/chunk-CRGUD4KC.cjs.map +1 -0
  38. package/dist/chunk-F67UK75A.cjs +158 -0
  39. package/dist/chunk-F67UK75A.cjs.map +1 -0
  40. package/dist/chunk-GCDZZNIN.js +192 -0
  41. package/dist/chunk-GCDZZNIN.js.map +1 -0
  42. package/dist/chunk-HIKBY7MS.cjs +70 -0
  43. package/dist/chunk-HIKBY7MS.cjs.map +1 -0
  44. package/dist/chunk-IDCZBF35.js +1186 -0
  45. package/dist/chunk-IDCZBF35.js.map +1 -0
  46. package/dist/chunk-IESEVHXL.js +66 -0
  47. package/dist/chunk-IESEVHXL.js.map +1 -0
  48. package/dist/chunk-IGADDMXH.js +387 -0
  49. package/dist/chunk-IGADDMXH.js.map +1 -0
  50. package/dist/chunk-IHTXB7AT.cjs +276 -0
  51. package/dist/chunk-IHTXB7AT.cjs.map +1 -0
  52. package/dist/chunk-IVPRUGTY.js +242 -0
  53. package/dist/chunk-IVPRUGTY.js.map +1 -0
  54. package/dist/{chunk-SQ6FNXU2.cjs → chunk-IXUHXTHW.cjs} +2 -151
  55. package/dist/chunk-IXUHXTHW.cjs.map +1 -0
  56. package/dist/chunk-J6JTWD2A.cjs +100 -0
  57. package/dist/chunk-J6JTWD2A.cjs.map +1 -0
  58. package/dist/chunk-JEQ7FLOD.cjs +199 -0
  59. package/dist/chunk-JEQ7FLOD.cjs.map +1 -0
  60. package/dist/{chunk-ON5ZMSU4.js → chunk-JQISFW6U.js} +3 -3
  61. package/dist/chunk-JQISFW6U.js.map +1 -0
  62. package/dist/chunk-K25XHMM3.js +566 -0
  63. package/dist/chunk-K25XHMM3.js.map +1 -0
  64. package/dist/{chunk-UYJ6TJHX.cjs → chunk-K623Q6WD.cjs} +181 -56
  65. package/dist/chunk-K623Q6WD.cjs.map +1 -0
  66. package/dist/chunk-MUNO67TT.cjs +1219 -0
  67. package/dist/chunk-MUNO67TT.cjs.map +1 -0
  68. package/dist/chunk-N32OWET6.cjs +327 -0
  69. package/dist/chunk-N32OWET6.cjs.map +1 -0
  70. package/dist/chunk-NUKJ54GA.cjs +245 -0
  71. package/dist/chunk-NUKJ54GA.cjs.map +1 -0
  72. package/dist/{chunk-XWIA3HVX.js → chunk-OBA2RYZN.js} +6 -1249
  73. package/dist/chunk-OBA2RYZN.js.map +1 -0
  74. package/dist/chunk-PMGOBS6X.cjs +408 -0
  75. package/dist/chunk-PMGOBS6X.cjs.map +1 -0
  76. package/dist/{chunk-OHYBNCVL.cjs → chunk-PXNTCCPE.cjs} +10 -1256
  77. package/dist/chunk-PXNTCCPE.cjs.map +1 -0
  78. package/dist/chunk-PYVFXCSD.js +1828 -0
  79. package/dist/chunk-PYVFXCSD.js.map +1 -0
  80. package/dist/{chunk-MGFRZO24.js → chunk-QZGABF2M.js} +3 -149
  81. package/dist/chunk-QZGABF2M.js.map +1 -0
  82. package/dist/{chunk-T3Q5V33G.cjs → chunk-R4ILO3W6.cjs} +876 -829
  83. package/dist/chunk-R4ILO3W6.cjs.map +1 -0
  84. package/dist/chunk-RMRJGMDE.js +323 -0
  85. package/dist/chunk-RMRJGMDE.js.map +1 -0
  86. package/dist/chunk-RNZFGN4R.js +88 -0
  87. package/dist/chunk-RNZFGN4R.js.map +1 -0
  88. package/dist/chunk-RQ6N3FTV.js +900 -0
  89. package/dist/chunk-RQ6N3FTV.js.map +1 -0
  90. package/dist/{chunk-SXXTQETM.cjs → chunk-TO6EY4P7.cjs} +8722 -11323
  91. package/dist/chunk-TO6EY4P7.cjs.map +1 -0
  92. package/dist/chunk-V464XBYS.js +154 -0
  93. package/dist/chunk-V464XBYS.js.map +1 -0
  94. package/dist/chunk-YA3TJ65D.cjs +575 -0
  95. package/dist/chunk-YA3TJ65D.cjs.map +1 -0
  96. package/dist/chunk-YP7GW2G5.cjs +866 -0
  97. package/dist/chunk-YP7GW2G5.cjs.map +1 -0
  98. package/dist/{collection-config-B4PG-AaF.d.cts → collection-config-JgHOpFCG.d.cts} +30 -2
  99. package/dist/{collection-config-B4PG-AaF.d.ts → collection-config-JgHOpFCG.d.ts} +30 -2
  100. package/dist/config-HFXANXCC.js +6 -0
  101. package/dist/config-HFXANXCC.js.map +1 -0
  102. package/dist/config-ON6FNMYX.cjs +19 -0
  103. package/dist/config-ON6FNMYX.cjs.map +1 -0
  104. package/dist/define-plugin-BzNHc1ZI.d.ts +1321 -0
  105. package/dist/define-plugin-IWDKYaVm.d.cts +1321 -0
  106. package/dist/document-projection-TDWRJX3Z.cjs +13 -0
  107. package/dist/document-projection-TDWRJX3Z.cjs.map +1 -0
  108. package/dist/document-projection-YYMC6I4U.js +4 -0
  109. package/dist/document-projection-YYMC6I4U.js.map +1 -0
  110. package/dist/index.cjs +13737 -4327
  111. package/dist/index.cjs.map +1 -1
  112. package/dist/index.d.cts +331 -493
  113. package/dist/index.d.ts +331 -493
  114. package/dist/index.js +13456 -4068
  115. package/dist/index.js.map +1 -1
  116. package/dist/middleware.cjs +38 -32
  117. package/dist/middleware.d.cts +69 -7
  118. package/dist/middleware.d.ts +69 -7
  119. package/dist/middleware.js +9 -3
  120. package/dist/migrations-2XHQEGOQ.cjs +13 -0
  121. package/dist/{migrations-IYNTWDC6.cjs.map → migrations-2XHQEGOQ.cjs.map} +1 -1
  122. package/dist/migrations-PE3CDVSM.js +4 -0
  123. package/dist/{migrations-R337UD46.js.map → migrations-PE3CDVSM.js.map} +1 -1
  124. package/dist/{plugin-bootstrap-DfVerYV4.d.cts → plugin-bootstrap-B8ThJU21.d.cts} +4315 -1661
  125. package/dist/{plugin-bootstrap-P_ciLp_C.d.ts → plugin-bootstrap-qu8hJgUt.d.ts} +4315 -1661
  126. package/dist/plugins.cjs +171 -12
  127. package/dist/plugins.d.cts +36 -2
  128. package/dist/plugins.d.ts +36 -2
  129. package/dist/plugins.js +5 -2
  130. package/dist/rbac-O73MFKDA.js +5 -0
  131. package/dist/rbac-O73MFKDA.js.map +1 -0
  132. package/dist/rbac-VONLJJKB.cjs +14 -0
  133. package/dist/rbac-VONLJJKB.cjs.map +1 -0
  134. package/dist/routes.cjs +42 -46
  135. package/dist/routes.d.cts +56 -146
  136. package/dist/routes.d.ts +56 -146
  137. package/dist/routes.js +18 -10
  138. package/dist/services.cjs +43 -76
  139. package/dist/services.d.cts +93 -55
  140. package/dist/services.d.ts +93 -55
  141. package/dist/services.js +6 -3
  142. package/dist/{telemetry-B9vIV4wh.d.cts → telemetry-Cku1ax74.d.cts} +1 -1
  143. package/dist/{telemetry-B9vIV4wh.d.ts → telemetry-Cku1ax74.d.ts} +1 -1
  144. package/dist/templates.cjs +17 -29
  145. package/dist/templates.d.cts +2 -89
  146. package/dist/templates.d.ts +2 -89
  147. package/dist/templates.js +3 -3
  148. package/dist/types-Dea1eNxU.d.cts +286 -0
  149. package/dist/types-Dea1eNxU.d.ts +286 -0
  150. package/dist/types.d.cts +2 -2
  151. package/dist/types.d.ts +2 -2
  152. package/dist/utils.cjs +21 -20
  153. package/dist/utils.d.cts +2 -2
  154. package/dist/utils.d.ts +2 -2
  155. package/dist/utils.js +3 -2
  156. package/migrations/0001_core.sql +184 -0
  157. package/migrations/0002_documents.sql +163 -0
  158. package/package.json +12 -7
  159. package/dist/admin-layout-catalyst.template-HFD37TY5.cjs +0 -17
  160. package/dist/admin-layout-catalyst.template-UMTIN66R.js +0 -7
  161. package/dist/app-C9esKLmh.d.cts +0 -112
  162. package/dist/app-C9esKLmh.d.ts +0 -112
  163. package/dist/chunk-4R3NOOL3.js +0 -2217
  164. package/dist/chunk-4R3NOOL3.js.map +0 -1
  165. package/dist/chunk-4ZSNJDLS.cjs.map +0 -1
  166. package/dist/chunk-55RDMDOP.js.map +0 -1
  167. package/dist/chunk-635JAMSE.cjs +0 -653
  168. package/dist/chunk-635JAMSE.cjs.map +0 -1
  169. package/dist/chunk-ABB34XUS.cjs.map +0 -1
  170. package/dist/chunk-C54YUA23.cjs +0 -2219
  171. package/dist/chunk-C54YUA23.cjs.map +0 -1
  172. package/dist/chunk-DSUJ5YQH.cjs +0 -722
  173. package/dist/chunk-DSUJ5YQH.cjs.map +0 -1
  174. package/dist/chunk-EW5NOBVU.js +0 -1783
  175. package/dist/chunk-EW5NOBVU.js.map +0 -1
  176. package/dist/chunk-EXNEW5US.js +0 -648
  177. package/dist/chunk-EXNEW5US.js.map +0 -1
  178. package/dist/chunk-I2H5NGJQ.js +0 -692
  179. package/dist/chunk-I2H5NGJQ.js.map +0 -1
  180. package/dist/chunk-MGFRZO24.js.map +0 -1
  181. package/dist/chunk-OHYBNCVL.cjs.map +0 -1
  182. package/dist/chunk-ON5ZMSU4.js.map +0 -1
  183. package/dist/chunk-QFWHAFEO.js +0 -1843
  184. package/dist/chunk-QFWHAFEO.js.map +0 -1
  185. package/dist/chunk-SQ6FNXU2.cjs.map +0 -1
  186. package/dist/chunk-SXXTQETM.cjs.map +0 -1
  187. package/dist/chunk-T3Q5V33G.cjs.map +0 -1
  188. package/dist/chunk-TFNTM3OA.js.map +0 -1
  189. package/dist/chunk-UYJ6TJHX.cjs.map +0 -1
  190. package/dist/chunk-WAEQXGCX.cjs +0 -1898
  191. package/dist/chunk-WAEQXGCX.cjs.map +0 -1
  192. package/dist/chunk-XWIA3HVX.js.map +0 -1
  193. package/dist/chunk-XXDFQERJ.js.map +0 -1
  194. package/dist/migrations-IYNTWDC6.cjs +0 -13
  195. package/dist/migrations-R337UD46.js +0 -4
  196. package/dist/plugin-manager-BoM3Q7o7.d.cts +0 -328
  197. package/dist/plugin-manager-Efx9RyDX.d.ts +0 -328
  198. package/migrations/001_initial_schema.sql +0 -170
  199. package/migrations/002_faq_plugin.sql +0 -86
  200. package/migrations/003_stage5_enhancements.sql +0 -121
  201. package/migrations/004_stage6_user_management.sql +0 -183
  202. package/migrations/005_stage7_workflow_automation.sql +0 -294
  203. package/migrations/006_plugin_system.sql +0 -155
  204. package/migrations/007_demo_login_plugin.sql +0 -23
  205. package/migrations/008_fix_slug_validation.sql +0 -22
  206. package/migrations/009_system_logging.sql +0 -57
  207. package/migrations/011_config_managed_collections.sql +0 -15
  208. package/migrations/012_testimonials_plugin.sql +0 -80
  209. package/migrations/013_code_examples_plugin.sql +0 -177
  210. package/migrations/014_fix_plugin_registry.sql +0 -88
  211. package/migrations/015_add_remaining_plugins.sql +0 -89
  212. package/migrations/016_remove_duplicate_cache_plugin.sql +0 -17
  213. package/migrations/017_auth_configurable_fields.sql +0 -49
  214. package/migrations/018_settings_table.sql +0 -23
  215. package/migrations/019_remove_blog_posts_collection.sql +0 -15
  216. package/migrations/020_add_email_plugin.sql +0 -22
  217. package/migrations/021_add_magic_link_auth_plugin.sql +0 -42
  218. package/migrations/022_add_tinymce_plugin.sql +0 -25
  219. package/migrations/023_add_easy_mdx_plugin.sql +0 -25
  220. package/migrations/024_add_quill_editor_plugin.sql +0 -25
  221. package/migrations/025_add_easymde_plugin.sql +0 -25
  222. package/migrations/026_add_otp_login.sql +0 -42
  223. package/migrations/027_fix_slug_field_type.sql +0 -18
  224. package/migrations/028_fix_slug_field_type_in_schemas.sql +0 -30
  225. package/migrations/029_add_forms_system.sql +0 -184
  226. package/migrations/030_add_turnstile_to_forms.sql +0 -14
  227. package/migrations/031_ai_search_plugin.sql +0 -45
  228. package/migrations/032_user_profiles.sql +0 -37
  229. package/migrations/033_form_content_integration.sql +0 -19
  230. package/migrations/034_security_audit_plugin.sql +0 -27
  231. package/migrations/035_user_profiles_data_column.sql +0 -16
  232. package/migrations/036_analytics_events.sql +0 -22
@@ -0,0 +1,1186 @@
1
+ import { RbacService } from './chunk-BLMTL57B.js';
2
+ import { loadCollectionConfigs, getCollectionRegistry, PluginBootstrapService, getTelemetryService } from './chunk-PYVFXCSD.js';
3
+ import { ensureScalarSchema, MigrationService } from './chunk-ALDRXTUO.js';
4
+ import { init_admin_layout_catalyst_template, setBranchLabel } from './chunk-5V62WT6M.js';
5
+ import { hasHookSystem, getHookSystem } from './chunk-RNZFGN4R.js';
6
+ import { metricsTracker } from './chunk-FICTAGD4.js';
7
+ import { z } from 'zod';
8
+ import { sign, verify } from 'hono/jwt';
9
+ import { getCookie, setCookie } from 'hono/cookie';
10
+
11
+ // src/services/document-type-registry.ts
12
+ function rowToDocumentType(row) {
13
+ return {
14
+ id: row.id,
15
+ name: row.name,
16
+ displayName: row.display_name,
17
+ description: row.description,
18
+ schema: JSON.parse(row.schema),
19
+ queryableFields: JSON.parse(row.queryable_fields),
20
+ settings: JSON.parse(row.settings),
21
+ pluginId: row.plugin_id,
22
+ source: row.source,
23
+ schemaVersion: row.schema_version,
24
+ isSystem: row.is_system === 1,
25
+ isActive: row.is_active === 1,
26
+ isAuth: row.is_auth === 1,
27
+ createdAt: row.created_at,
28
+ updatedAt: row.updated_at
29
+ };
30
+ }
31
+ var DocumentTypeRegistry = class {
32
+ constructor(db) {
33
+ this.db = db;
34
+ }
35
+ cache = /* @__PURE__ */ new Map();
36
+ // Register or update a document type. Idempotent: bumps schema_version only when schema changes.
37
+ async register(def) {
38
+ const now = Math.floor(Date.now() / 1e3);
39
+ const existing = await this.findById(def.id);
40
+ const schemaJson = JSON.stringify({ queryableFields: def.queryableFields ?? [], settings: def.settings ?? {} });
41
+ const queryableJson = JSON.stringify(def.queryableFields ?? []);
42
+ const settingsJson = JSON.stringify(def.settings ?? {});
43
+ if (existing) {
44
+ const schemaChanged = schemaJson !== JSON.stringify(existing.schema);
45
+ const newVersion = schemaChanged ? existing.schemaVersion + 1 : existing.schemaVersion;
46
+ await this.db.prepare(
47
+ `UPDATE document_types SET
48
+ display_name = ?,
49
+ description = ?,
50
+ schema = ?,
51
+ queryable_fields = ?,
52
+ settings = ?,
53
+ plugin_id = ?,
54
+ schema_version = ?,
55
+ is_active = 1,
56
+ is_auth = ?,
57
+ updated_at = ?
58
+ WHERE id = ?`
59
+ ).bind(
60
+ def.displayName,
61
+ def.description ?? null,
62
+ schemaJson,
63
+ queryableJson,
64
+ settingsJson,
65
+ def.pluginId ?? null,
66
+ newVersion,
67
+ def.isAuth ? 1 : 0,
68
+ now,
69
+ def.id
70
+ ).run();
71
+ await ensureScalarSchema(this.db, def.id, def.queryableFields ?? []);
72
+ const updated = await this.findById(def.id);
73
+ this.cache.set(def.id, updated);
74
+ return updated;
75
+ }
76
+ await this.db.prepare(
77
+ `INSERT INTO document_types (id, name, display_name, description, schema, queryable_fields, settings, plugin_id, source, schema_version, is_system, is_active, is_auth, created_at, updated_at)
78
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 0, 1, ?, ?, ?)`
79
+ ).bind(
80
+ def.id,
81
+ def.name ?? def.id,
82
+ def.displayName,
83
+ def.description ?? null,
84
+ schemaJson,
85
+ queryableJson,
86
+ settingsJson,
87
+ def.pluginId ?? null,
88
+ def.source ?? "code",
89
+ def.isAuth ? 1 : 0,
90
+ now,
91
+ now
92
+ ).run();
93
+ await ensureScalarSchema(this.db, def.id, def.queryableFields ?? []);
94
+ const created = await this.findById(def.id);
95
+ this.cache.set(def.id, created);
96
+ return created;
97
+ }
98
+ async findById(id) {
99
+ if (this.cache.has(id)) return this.cache.get(id);
100
+ const row = await this.db.prepare("SELECT * FROM document_types WHERE id = ?").bind(id).first();
101
+ if (!row) return null;
102
+ const dt = rowToDocumentType(row);
103
+ this.cache.set(id, dt);
104
+ return dt;
105
+ }
106
+ async findAll(activeOnly = true) {
107
+ const sql = activeOnly ? "SELECT * FROM document_types WHERE is_active = 1 ORDER BY name" : "SELECT * FROM document_types ORDER BY name";
108
+ const result = await this.db.prepare(sql).all();
109
+ return (result.results ?? []).map(rowToDocumentType);
110
+ }
111
+ async deactivate(id) {
112
+ const now = Math.floor(Date.now() / 1e3);
113
+ await this.db.prepare("UPDATE document_types SET is_active = 0, updated_at = ? WHERE id = ?").bind(now, id).run();
114
+ this.cache.delete(id);
115
+ }
116
+ clearCache() {
117
+ this.cache.clear();
118
+ }
119
+ };
120
+
121
+ // src/services/document-types-seed.ts
122
+ var anyObject = z.record(z.string(), z.unknown());
123
+ async function bootstrapDocumentTypes(db) {
124
+ const registry = new DocumentTypeRegistry(db);
125
+ await registry.register({
126
+ id: "site_settings",
127
+ name: "site_settings",
128
+ displayName: "Site Settings",
129
+ description: "Global site configuration (internal; managed via admin settings UI)",
130
+ source: "system",
131
+ schema: anyObject,
132
+ settings: {
133
+ internal: true,
134
+ maxVersionsPerRoot: 1,
135
+ baseGrants: { admin: ["read", "create", "update", "delete", "manage"] }
136
+ },
137
+ queryableFields: []
138
+ });
139
+ await registry.register({
140
+ id: "blog_post",
141
+ name: "blog_post",
142
+ displayName: "Blog Post",
143
+ description: "Blog post (document-backed; edited via the content collection UI)",
144
+ source: "system",
145
+ schema: anyObject,
146
+ settings: {
147
+ baseGrants: { public: ["read"], admin: ["read", "create", "update", "delete", "publish", "manage"], editor: ["read", "create", "update", "publish"], viewer: ["read"] },
148
+ maxVersionsPerRoot: 50
149
+ },
150
+ queryableFields: [
151
+ { name: "difficulty", kind: "scalar", type: "text", column: "q_blog_difficulty" },
152
+ { name: "author", kind: "scalar", type: "text", column: "q_blog_author" }
153
+ ]
154
+ });
155
+ await registry.register({
156
+ id: "plugin",
157
+ name: "plugin",
158
+ displayName: "Plugin",
159
+ description: "System plugin record (managed by the plugin bootstrap service)",
160
+ source: "system",
161
+ schema: anyObject,
162
+ settings: {
163
+ baseGrants: { admin: ["read", "create", "update", "delete", "publish", "manage"] },
164
+ maxVersionsPerRoot: 1,
165
+ internal: true
166
+ },
167
+ queryableFields: [
168
+ { name: "status", kind: "scalar", type: "text", column: "q_plugin_status" },
169
+ { name: "category", kind: "scalar", type: "text", column: "q_plugin_category" },
170
+ { name: "isCore", kind: "scalar", type: "integer", column: "q_plugin_is_core" }
171
+ ]
172
+ });
173
+ await registry.register({
174
+ id: "tenant",
175
+ name: "tenant",
176
+ displayName: "Tenant",
177
+ description: "Tenant record (managed by the multi-tenant plugin; slug = tenant id)",
178
+ source: "system",
179
+ schema: anyObject,
180
+ settings: {
181
+ baseGrants: { admin: ["read", "create", "update", "delete", "manage"] },
182
+ maxVersionsPerRoot: 1,
183
+ internal: true
184
+ },
185
+ queryableFields: [
186
+ { name: "status", kind: "scalar", type: "text", column: "q_tenant_status" },
187
+ { name: "domain", kind: "scalar", type: "text", column: "q_tenant_domain" }
188
+ ]
189
+ });
190
+ await registry.register({
191
+ id: "user_profile",
192
+ name: "user_profile",
193
+ displayName: "User Profile",
194
+ description: "Per-user profile record (auth-owned; one document per user, slug = userId)",
195
+ source: "system",
196
+ isAuth: true,
197
+ schema: anyObject,
198
+ settings: {
199
+ // Hidden from the content admin surfaces; a single mutable record (no version history).
200
+ internal: true,
201
+ maxVersionsPerRoot: 1,
202
+ pii: true,
203
+ baseGrants: { admin: ["read", "create", "update", "delete", "manage"] }
204
+ },
205
+ queryableFields: []
206
+ });
207
+ await registry.register({
208
+ id: "media_asset",
209
+ name: "media_asset",
210
+ displayName: "Media Asset",
211
+ description: "Uploaded media file metadata (managed via the media library; backs an R2 object)",
212
+ source: "system",
213
+ schema: anyObject,
214
+ settings: {
215
+ internal: true,
216
+ maxVersionsPerRoot: 5,
217
+ baseGrants: {
218
+ admin: ["read", "create", "update", "delete", "manage"],
219
+ editor: ["read", "create", "update"],
220
+ author: ["read", "create"],
221
+ viewer: ["read"]
222
+ }
223
+ },
224
+ queryableFields: [
225
+ { name: "mimeType", kind: "scalar", type: "text", column: "q_media_mime" },
226
+ { name: "folder", kind: "scalar", type: "text", column: "q_media_folder" },
227
+ { name: "size", kind: "scalar", type: "integer", column: "q_media_size" },
228
+ { name: "tags", kind: "facet", type: "text" }
229
+ ]
230
+ });
231
+ await registry.register({
232
+ id: "plugin_activity",
233
+ name: "plugin_activity",
234
+ displayName: "Plugin Activity",
235
+ description: "Plugin lifecycle event log (installed/activated/deactivated/settings_updated/error)",
236
+ source: "system",
237
+ schema: anyObject,
238
+ settings: {
239
+ internal: true,
240
+ maxVersionsPerRoot: 1,
241
+ baseGrants: { admin: ["read", "create", "manage"] }
242
+ },
243
+ queryableFields: [
244
+ { name: "pluginId", kind: "scalar", type: "text", column: "q_plugin_activity_plugin_id" },
245
+ { name: "action", kind: "scalar", type: "text", column: "q_plugin_activity_action" }
246
+ ]
247
+ });
248
+ await registry.register({
249
+ id: "security_event",
250
+ name: "security_event",
251
+ displayName: "Security Event",
252
+ description: "Security audit event (login attempts, lockouts, suspicious activity)",
253
+ source: "system",
254
+ schema: anyObject,
255
+ settings: {
256
+ internal: true,
257
+ maxVersionsPerRoot: 1,
258
+ baseGrants: { admin: ["read", "create", "manage"] }
259
+ },
260
+ queryableFields: [
261
+ { name: "eventType", kind: "scalar", type: "text", column: "q_sa_event_type" },
262
+ { name: "severity", kind: "scalar", type: "text", column: "q_sa_severity" },
263
+ { name: "userId", kind: "scalar", type: "text", column: "q_sa_user_id" },
264
+ { name: "email", kind: "scalar", type: "text", column: "q_sa_email" },
265
+ { name: "ipAddress", kind: "scalar", type: "text", column: "q_sa_ip_address" },
266
+ { name: "blocked", kind: "scalar", type: "integer", column: "q_sa_blocked" }
267
+ ]
268
+ });
269
+ await registry.register({
270
+ id: "analytics_event",
271
+ name: "analytics_event",
272
+ displayName: "Analytics Event",
273
+ description: "Tracked analytics event (page view, user action, custom event)",
274
+ source: "system",
275
+ schema: anyObject,
276
+ settings: {
277
+ internal: true,
278
+ maxVersionsPerRoot: 1,
279
+ baseGrants: { admin: ["read", "create", "manage"] }
280
+ },
281
+ queryableFields: [
282
+ { name: "event", kind: "scalar", type: "text", column: "q_evt_event" },
283
+ { name: "category", kind: "scalar", type: "text", column: "q_evt_category" },
284
+ { name: "userId", kind: "scalar", type: "text", column: "q_evt_user_id" },
285
+ { name: "sessionId", kind: "scalar", type: "text", column: "q_evt_session_id" },
286
+ { name: "path", kind: "scalar", type: "text", column: "q_evt_path" }
287
+ ]
288
+ });
289
+ await registry.register({
290
+ id: "media_asset",
291
+ name: "media_asset",
292
+ displayName: "Media Asset",
293
+ description: "Media file metadata (R2 object key + intrinsic properties; URL derived at read time)",
294
+ source: "system",
295
+ schema: anyObject,
296
+ settings: {
297
+ baseGrants: { public: ["read"], admin: ["read", "create", "update", "delete", "publish", "manage"], editor: ["read", "create", "update"] },
298
+ maxVersionsPerRoot: 5
299
+ },
300
+ queryableFields: [
301
+ { name: "mimeType", kind: "scalar", type: "text", column: "q_media_mime" },
302
+ { name: "folder", kind: "scalar", type: "text", column: "q_media_folder" },
303
+ { name: "size", kind: "scalar", type: "integer", column: "q_media_size" },
304
+ { name: "tags", kind: "facet", type: "text" }
305
+ ]
306
+ });
307
+ for (const [id, displayName, description] of [
308
+ ["rbac_role", "RBAC Role", "Role record with embedded grants (auth-owned)"],
309
+ ["rbac_verb", "RBAC Verb", "Permission verb (auth-owned)"],
310
+ ["rbac_user_roles", "RBAC User Roles", "Per-user role assignments (auth-owned; slug = userId)"]
311
+ ]) {
312
+ await registry.register({
313
+ id,
314
+ name: id,
315
+ displayName,
316
+ description,
317
+ source: "system",
318
+ isAuth: true,
319
+ schema: anyObject,
320
+ settings: {
321
+ internal: true,
322
+ maxVersionsPerRoot: 1,
323
+ baseGrants: { admin: ["read", "create", "update", "delete", "manage"] }
324
+ },
325
+ queryableFields: []
326
+ });
327
+ }
328
+ }
329
+ async function autoRegisterCollectionDocumentTypes(db) {
330
+ const registry = new DocumentTypeRegistry(db);
331
+ const collections = getCollectionRegistry().listActive();
332
+ const registered = [];
333
+ for (const collection of collections) {
334
+ if (collection.internal) continue;
335
+ if (collection.name === "blog_post") continue;
336
+ try {
337
+ await registry.register({
338
+ id: collection.name,
339
+ name: collection.name,
340
+ displayName: collection.displayName,
341
+ description: collection.description,
342
+ source: "system",
343
+ schema: anyObject,
344
+ settings: {
345
+ baseGrants: {
346
+ public: ["read"],
347
+ admin: ["read", "create", "update", "delete", "publish", "manage"],
348
+ editor: ["read", "create", "update", "publish"],
349
+ viewer: ["read"]
350
+ },
351
+ maxVersionsPerRoot: 50,
352
+ ...collection.versioning ? { versioning: true } : {}
353
+ },
354
+ queryableFields: []
355
+ });
356
+ registered.push(collection.name);
357
+ } catch (error) {
358
+ console.error(`[document-types-seed] Failed to register collection "${collection.name}":`, error);
359
+ }
360
+ }
361
+ return registered;
362
+ }
363
+
364
+ // src/middleware/bootstrap.ts
365
+ init_admin_layout_catalyst_template();
366
+ var bootstrapComplete = false;
367
+ function verifySecurityConfig(env) {
368
+ const warnings = [];
369
+ if (!env.JWT_SECRET) {
370
+ warnings.push(
371
+ "JWT_SECRET is not set \u2014 using hardcoded fallback. Set via `wrangler secret put JWT_SECRET`"
372
+ );
373
+ } else if (env.JWT_SECRET.includes("change-in-production")) {
374
+ warnings.push(
375
+ "JWT_SECRET contains the default value \u2014 tokens are forgeable. Generate a strong random secret"
376
+ );
377
+ }
378
+ if (!env.CORS_ORIGINS) {
379
+ warnings.push(
380
+ "CORS_ORIGINS is not set \u2014 all cross-origin API requests will be rejected"
381
+ );
382
+ }
383
+ if (!env.ENVIRONMENT) {
384
+ warnings.push(
385
+ 'ENVIRONMENT is not set \u2014 HSTS header will not be applied. Set to "production" or "development"'
386
+ );
387
+ }
388
+ if (warnings.length === 0) {
389
+ return;
390
+ }
391
+ const isProduction = env.ENVIRONMENT === "production";
392
+ for (const warning of warnings) {
393
+ console.warn(`[SonicJS Security] ${warning}`);
394
+ }
395
+ if (isProduction) {
396
+ const hasCritical = !env.JWT_SECRET || env.JWT_SECRET.includes("change-in-production");
397
+ if (hasCritical) {
398
+ throw new Error(
399
+ "[SonicJS Security] CRITICAL: Production deployment is missing a secure JWT_SECRET. Set it via `wrangler secret put JWT_SECRET` before deploying."
400
+ );
401
+ }
402
+ }
403
+ }
404
+ function bootstrapMiddleware(config = {}) {
405
+ return async (c, next) => {
406
+ if (hasHookSystem()) {
407
+ c.set("hookSystem", getHookSystem());
408
+ }
409
+ if (bootstrapComplete) {
410
+ return next();
411
+ }
412
+ const path = c.req.path;
413
+ if (path.startsWith("/images/") || path.startsWith("/assets/") || path === "/health" || path.endsWith(".js") || path.endsWith(".css") || path.endsWith(".png") || path.endsWith(".jpg") || path.endsWith(".ico")) {
414
+ return next();
415
+ }
416
+ const host = c.req.header("host") || "";
417
+ const isLocalhost = host.includes("localhost") || host.includes("127.0.0.1");
418
+ const gitBranch = c.env.GIT_BRANCH;
419
+ setBranchLabel(isLocalhost && gitBranch ? gitBranch : void 0);
420
+ try {
421
+ console.log("[Bootstrap] Starting system initialization...");
422
+ console.log("[Bootstrap] Checking schema compatibility...");
423
+ const migrationService = new MigrationService(c.env.DB);
424
+ await migrationService.ensureSchemaCompatibility();
425
+ try {
426
+ const kv = c.env.CACHE_KV;
427
+ if (kv) {
428
+ const { setGlobalKVNamespace } = await import('./cache-DDARE4QE.js');
429
+ setGlobalKVNamespace(kv);
430
+ }
431
+ } catch (error) {
432
+ console.error("[Bootstrap] Error wiring CACHE_KV namespace:", error);
433
+ }
434
+ console.log("[Bootstrap] Populating collection registry...");
435
+ try {
436
+ const configs = await loadCollectionConfigs();
437
+ getCollectionRegistry().register(configs);
438
+ console.log(`[Bootstrap] Registry populated with ${configs.length} collection(s)`);
439
+ } catch (error) {
440
+ console.error("[Bootstrap] Error populating collection registry:", error);
441
+ }
442
+ console.log("[Bootstrap] Registering document types...");
443
+ try {
444
+ await bootstrapDocumentTypes(c.env.DB);
445
+ } catch (error) {
446
+ console.error("[Bootstrap] Error registering document types:", error);
447
+ }
448
+ try {
449
+ await repairMissingCredentialAccounts(c.env.DB);
450
+ } catch (error) {
451
+ console.error("[Bootstrap] Error repairing credential accounts:", error);
452
+ }
453
+ try {
454
+ const { RbacService: RbacService2 } = await import('./rbac-O73MFKDA.js');
455
+ await new RbacService2(c.env.DB, c.env.CACHE_KV).ensureSystemRbacSeed();
456
+ } catch (error) {
457
+ console.error("[Bootstrap] Error seeding RBAC documents:", error);
458
+ }
459
+ try {
460
+ const auto = await autoRegisterCollectionDocumentTypes(c.env.DB);
461
+ if (auto.length) console.log(`[Bootstrap] Document-backed collections registered: ${auto.join(", ")}`);
462
+ } catch (error) {
463
+ console.error("[Bootstrap] Error auto-registering collection document types:", error);
464
+ }
465
+ if (!config.plugins?.disableAll) {
466
+ console.log("[Bootstrap] Bootstrapping core plugins...");
467
+ const bootstrapService = new PluginBootstrapService(c.env.DB);
468
+ const needsBootstrap = await bootstrapService.isBootstrapNeeded();
469
+ if (needsBootstrap) {
470
+ await bootstrapService.bootstrapCorePlugins();
471
+ }
472
+ } else {
473
+ console.log("[Bootstrap] Plugin bootstrap skipped (disableAll is true)");
474
+ }
475
+ bootstrapComplete = true;
476
+ console.log("[Bootstrap] System initialization completed");
477
+ try {
478
+ const registry = getCollectionRegistry();
479
+ const collections = registry.listActive();
480
+ const countResult = await c.env.DB.prepare(
481
+ `SELECT type_id, COUNT(*) AS cnt FROM documents
482
+ WHERE is_current_draft = 1 AND deleted_at IS NULL GROUP BY type_id`
483
+ ).all();
484
+ const countMap = {};
485
+ let docTotal = 0;
486
+ for (const row of countResult.results ?? []) {
487
+ countMap[row.type_id] = row.cnt;
488
+ docTotal += row.cnt;
489
+ }
490
+ const fieldTypeHistogram = {};
491
+ for (const col of collections) {
492
+ const props = col.schema?.properties ?? {};
493
+ for (const field of Object.values(props)) {
494
+ const ft = field?.type ?? "unknown";
495
+ fieldTypeHistogram[ft] = (fieldTypeHistogram[ft] ?? 0) + 1;
496
+ }
497
+ }
498
+ const activePlugins = (config.plugins?.register ?? []).map((p) => p.name ?? "unknown");
499
+ let installationId = "unknown";
500
+ try {
501
+ const kv = c.env.KV;
502
+ if (kv) {
503
+ installationId = await kv.get("_sonicjs_installation_id") ?? "";
504
+ if (!installationId) {
505
+ installationId = crypto.randomUUID();
506
+ await kv.put("_sonicjs_installation_id", installationId);
507
+ }
508
+ }
509
+ } catch {
510
+ }
511
+ const telemetry = getTelemetryService();
512
+ await telemetry.trackProjectSnapshot({
513
+ installation_id: installationId,
514
+ collection_names: collections.map((c2) => c2.name),
515
+ collection_counts: countMap,
516
+ active_plugins: activePlugins,
517
+ field_type_histogram: fieldTypeHistogram,
518
+ doc_total: docTotal,
519
+ sonicjs_version: c.env.SONICJS_VERSION ?? "unknown"
520
+ });
521
+ } catch {
522
+ }
523
+ } catch (error) {
524
+ console.error("[Bootstrap] Error during system initialization:", error);
525
+ }
526
+ verifySecurityConfig(c.env);
527
+ return next();
528
+ };
529
+ }
530
+ async function repairMissingCredentialAccounts(db) {
531
+ const { results } = await db.prepare(`
532
+ SELECT u.id, u.password_hash
533
+ FROM auth_user u
534
+ WHERE u.password_hash IS NOT NULL AND u.password_hash != ''
535
+ AND NOT EXISTS (
536
+ SELECT 1 FROM auth_account a
537
+ WHERE a.user_id = u.id AND a.provider_id = 'credential'
538
+ )
539
+ `).all();
540
+ if (!results.length) return;
541
+ console.log(`[Bootstrap] Repairing ${results.length} user(s) missing credential auth_account rows`);
542
+ const nowSec = Math.floor(Date.now() / 1e3);
543
+ for (const user of results) {
544
+ await db.prepare(`
545
+ INSERT OR IGNORE INTO auth_account (id, user_id, account_id, provider_id, password, created_at, updated_at)
546
+ VALUES (?, ?, ?, 'credential', ?, ?, ?)
547
+ `).bind(`cred-${user.id}`, user.id, user.id, user.password_hash, nowSec, nowSec).run();
548
+ }
549
+ console.log(`[Bootstrap] Credential account repair complete (${results.length} repaired)`);
550
+ }
551
+ var JWT_SECRET_FALLBACK = "your-super-secret-jwt-key-change-in-production";
552
+ var DEFAULT_JWT_EXPIRES_IN_SECONDS = 60 * 60 * 24 * 30;
553
+ function parseDuration(input) {
554
+ if (input === void 0 || input === null || input === "") return null;
555
+ if (typeof input === "number" && Number.isFinite(input) && input > 0) {
556
+ return Math.floor(input);
557
+ }
558
+ const raw = String(input).trim();
559
+ if (/^\d+$/.test(raw)) {
560
+ const n = parseInt(raw, 10);
561
+ return n > 0 ? n : null;
562
+ }
563
+ const match = raw.match(/^(\d+)\s*(s|sec|secs|seconds|m|min|mins|minutes|h|hr|hrs|hours|d|day|days)$/i);
564
+ if (!match) return null;
565
+ const value = parseInt(match[1], 10);
566
+ const unit = match[2].toLowerCase();
567
+ if (unit.startsWith("s")) return value;
568
+ if (unit.startsWith("m")) return value * 60;
569
+ if (unit.startsWith("h")) return value * 60 * 60;
570
+ if (unit.startsWith("d")) return value * 60 * 60 * 24;
571
+ return null;
572
+ }
573
+ function getJwtExpirySeconds(env) {
574
+ const configured = parseDuration(env?.JWT_EXPIRES_IN);
575
+ return configured ?? DEFAULT_JWT_EXPIRES_IN_SECONDS;
576
+ }
577
+ async function getJwtExpirySecondsFromDb(db, env) {
578
+ const envParsed = parseDuration(env?.JWT_EXPIRES_IN);
579
+ if (envParsed) return envParsed;
580
+ if (db) {
581
+ try {
582
+ const row = await db.prepare("SELECT data FROM documents WHERE type_id = 'site_settings' AND slug = 'security' AND tenant_id = 'default' AND is_current_draft = 1 AND deleted_at IS NULL").first();
583
+ if (row?.data) {
584
+ const data = JSON.parse(row.data);
585
+ const parsed = parseDuration(data.jwtExpiresIn);
586
+ if (parsed) return parsed;
587
+ }
588
+ } catch (err) {
589
+ console.warn("Failed to read jwtExpiresIn from settings, falling back to default:", err);
590
+ }
591
+ }
592
+ return DEFAULT_JWT_EXPIRES_IN_SECONDS;
593
+ }
594
+ async function getJwtRefreshGraceSecondsFromDb(db, env) {
595
+ const DEFAULT_GRACE = 60 * 60 * 24 * 7;
596
+ const envParsed = parseDuration(env?.JWT_REFRESH_GRACE_SECONDS);
597
+ if (envParsed) return envParsed;
598
+ if (db) {
599
+ try {
600
+ const row = await db.prepare("SELECT data FROM documents WHERE type_id = 'site_settings' AND slug = 'security' AND tenant_id = 'default' AND is_current_draft = 1 AND deleted_at IS NULL").first();
601
+ if (row?.data) {
602
+ const data = JSON.parse(row.data);
603
+ const parsed = parseDuration(data.jwtRefreshGraceSeconds?.toString());
604
+ if (parsed) return parsed;
605
+ }
606
+ } catch (err) {
607
+ console.warn("Failed to read jwtRefreshGraceSeconds from settings:", err);
608
+ }
609
+ }
610
+ return DEFAULT_GRACE;
611
+ }
612
+ function decodeJwtPayload(token) {
613
+ try {
614
+ const parts = token.split(".");
615
+ if (parts.length !== 3) return null;
616
+ const b64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
617
+ const padded = b64 + "=".repeat((4 - b64.length % 4) % 4);
618
+ const json = atob(padded);
619
+ const obj = JSON.parse(json);
620
+ if (!obj || typeof obj.exp !== "number") return null;
621
+ return obj;
622
+ } catch {
623
+ return null;
624
+ }
625
+ }
626
+ function base64UrlToBytes(b64url) {
627
+ const b64 = b64url.replace(/-/g, "+").replace(/_/g, "/");
628
+ const padded = b64 + "=".repeat((4 - b64.length % 4) % 4);
629
+ const bin = atob(padded);
630
+ const bytes = new Uint8Array(bin.length);
631
+ for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
632
+ return bytes;
633
+ }
634
+ async function verifyHs256Signature(token, secret) {
635
+ try {
636
+ const parts = token.split(".");
637
+ if (parts.length !== 3) return false;
638
+ const encoder = new TextEncoder();
639
+ const key = await crypto.subtle.importKey(
640
+ "raw",
641
+ encoder.encode(secret),
642
+ { name: "HMAC", hash: "SHA-256" },
643
+ false,
644
+ ["verify"]
645
+ );
646
+ const signature = base64UrlToBytes(parts[2]);
647
+ const message = encoder.encode(`${parts[0]}.${parts[1]}`);
648
+ return await crypto.subtle.verify("HMAC", key, signature, message);
649
+ } catch {
650
+ return false;
651
+ }
652
+ }
653
+ var AuthManager = class _AuthManager {
654
+ static async generateToken(userId, email, role, secret, expiresInSeconds) {
655
+ const ttl = expiresInSeconds && expiresInSeconds > 0 ? Math.floor(expiresInSeconds) : DEFAULT_JWT_EXPIRES_IN_SECONDS;
656
+ const now = Math.floor(Date.now() / 1e3);
657
+ const payload = {
658
+ userId,
659
+ email,
660
+ role,
661
+ exp: now + ttl,
662
+ iat: now
663
+ };
664
+ return await sign(payload, secret || JWT_SECRET_FALLBACK, "HS256");
665
+ }
666
+ /**
667
+ * Verify a token's signature and expiration.
668
+ *
669
+ * IMPORTANT: pass the `JWT_SECRET` binding (e.g. `c.env.JWT_SECRET`) as the
670
+ * `secret` argument. If omitted, this falls back to a development-only
671
+ * placeholder secret — tokens signed with the real `JWT_SECRET` will then
672
+ * silently fail verification. From inside a Hono handler prefer
673
+ * `AuthManager.verifyAuthRequest(c)`, which handles header/cookie extraction
674
+ * and pulls the secret from `c.env` for you.
675
+ *
676
+ * If `graceSeconds` > 0, tokens whose `exp` is within the grace window
677
+ * (i.e. expired by no more than `graceSeconds`) are still returned. This
678
+ * supports a sliding-session refresh endpoint that accepts recently-expired
679
+ * tokens. Signature failures always return null.
680
+ */
681
+ static async verifyToken(token, secret, graceSeconds = 0) {
682
+ const effectiveSecret = secret || JWT_SECRET_FALLBACK;
683
+ try {
684
+ let payload = null;
685
+ try {
686
+ payload = await verify(token, effectiveSecret, "HS256");
687
+ } catch (verifyError) {
688
+ const name = verifyError?.name || "";
689
+ const message = String(verifyError?.message || "");
690
+ const isExpired = name === "JwtTokenExpired" || message.includes("expired");
691
+ if (!isExpired || graceSeconds <= 0) {
692
+ throw verifyError;
693
+ }
694
+ const signatureValid = await verifyHs256Signature(token, effectiveSecret);
695
+ if (!signatureValid) return null;
696
+ const decoded = decodeJwtPayload(token);
697
+ if (!decoded) return null;
698
+ payload = decoded;
699
+ }
700
+ if (!payload) return null;
701
+ const now = Math.floor(Date.now() / 1e3);
702
+ if (payload.exp < now - Math.max(0, Math.floor(graceSeconds))) {
703
+ return null;
704
+ }
705
+ return payload;
706
+ } catch (error) {
707
+ console.error("Token verification failed:", error);
708
+ return null;
709
+ }
710
+ }
711
+ /**
712
+ * Verify the JWT on an incoming Hono request using the `JWT_SECRET`
713
+ * binding from `c.env`. Reads the token from the `Authorization: Bearer …`
714
+ * header first, then falls back to the `auth_token` cookie. Returns the
715
+ * decoded payload, or null when the token is missing, malformed, expired,
716
+ * or signed with a different secret.
717
+ *
718
+ * Use this from custom Hono routes mounted alongside SonicJS — it
719
+ * resolves the secret the same way `requireAuth()` does, without forcing
720
+ * the caller to plumb it through manually.
721
+ */
722
+ static async verifyAuthRequest(c) {
723
+ let token = c.req.header("Authorization")?.replace("Bearer ", "");
724
+ if (!token) {
725
+ token = getCookie(c, "auth_token");
726
+ }
727
+ if (!token) return null;
728
+ const secret = c.env?.JWT_SECRET;
729
+ return await _AuthManager.verifyToken(token, secret);
730
+ }
731
+ static async hashPassword(password) {
732
+ const iterations = 1e5;
733
+ const salt = new Uint8Array(16);
734
+ crypto.getRandomValues(salt);
735
+ const encoder = new TextEncoder();
736
+ const keyMaterial = await crypto.subtle.importKey(
737
+ "raw",
738
+ encoder.encode(password),
739
+ "PBKDF2",
740
+ false,
741
+ ["deriveBits"]
742
+ );
743
+ const hashBuffer = await crypto.subtle.deriveBits(
744
+ {
745
+ name: "PBKDF2",
746
+ salt,
747
+ iterations,
748
+ hash: "SHA-256"
749
+ },
750
+ keyMaterial,
751
+ 256
752
+ );
753
+ const saltHex = Array.from(salt).map((b) => b.toString(16).padStart(2, "0")).join("");
754
+ const hashHex = Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
755
+ return `pbkdf2:${iterations}:${saltHex}:${hashHex}`;
756
+ }
757
+ static async hashPasswordLegacy(password) {
758
+ const encoder = new TextEncoder();
759
+ const data = encoder.encode(password + "salt-change-in-production");
760
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
761
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
762
+ return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
763
+ }
764
+ static async verifyPassword(password, storedHash) {
765
+ if (storedHash.startsWith("pbkdf2:")) {
766
+ const parts = storedHash.split(":");
767
+ if (parts.length !== 4) return false;
768
+ const iterationsStr = parts[1];
769
+ const saltHex = parts[2];
770
+ const expectedHashHex = parts[3];
771
+ const iterations = parseInt(iterationsStr, 10);
772
+ const saltBytes = saltHex.match(/.{2}/g);
773
+ if (!saltBytes) return false;
774
+ const salt = new Uint8Array(saltBytes.map((byte) => parseInt(byte, 16)));
775
+ const encoder = new TextEncoder();
776
+ const keyMaterial = await crypto.subtle.importKey(
777
+ "raw",
778
+ encoder.encode(password),
779
+ "PBKDF2",
780
+ false,
781
+ ["deriveBits"]
782
+ );
783
+ const hashBuffer = await crypto.subtle.deriveBits(
784
+ {
785
+ name: "PBKDF2",
786
+ salt,
787
+ iterations,
788
+ hash: "SHA-256"
789
+ },
790
+ keyMaterial,
791
+ 256
792
+ );
793
+ const actualHashHex = Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
794
+ if (actualHashHex.length !== expectedHashHex.length) return false;
795
+ let result2 = 0;
796
+ for (let i = 0; i < actualHashHex.length; i++) {
797
+ result2 |= actualHashHex.charCodeAt(i) ^ expectedHashHex.charCodeAt(i);
798
+ }
799
+ return result2 === 0;
800
+ }
801
+ const legacyHash = await this.hashPasswordLegacy(password);
802
+ if (legacyHash.length !== storedHash.length) return false;
803
+ let result = 0;
804
+ for (let i = 0; i < legacyHash.length; i++) {
805
+ result |= legacyHash.charCodeAt(i) ^ storedHash.charCodeAt(i);
806
+ }
807
+ return result === 0;
808
+ }
809
+ static isLegacyHash(storedHash) {
810
+ return !storedHash.startsWith("pbkdf2:");
811
+ }
812
+ /**
813
+ * Set authentication cookie - useful for plugins implementing alternative auth methods
814
+ * @param c - Hono context
815
+ * @param token - JWT token to set in cookie
816
+ * @param options - Optional cookie configuration
817
+ */
818
+ static setAuthCookie(c, token, options) {
819
+ setCookie(c, "auth_token", token, {
820
+ httpOnly: options?.httpOnly ?? true,
821
+ secure: options?.secure ?? true,
822
+ sameSite: options?.sameSite ?? "Strict",
823
+ maxAge: options?.maxAge ?? getJwtExpirySeconds(c?.env)
824
+ });
825
+ }
826
+ };
827
+ var requireAuth = () => {
828
+ return async (c, next) => {
829
+ const user = c.get("user");
830
+ if (!user) {
831
+ const acceptHeader = c.req.header("Accept") || "";
832
+ if (acceptHeader.includes("text/html")) {
833
+ return c.redirect("/auth/login?error=Please login to access the admin area");
834
+ }
835
+ return c.json({ error: "Authentication required" }, 401);
836
+ }
837
+ return await next();
838
+ };
839
+ };
840
+ var requireRole = (requiredRole) => {
841
+ return async (c, next) => {
842
+ const user = c.get("user");
843
+ if (!user) {
844
+ const acceptHeader = c.req.header("Accept") || "";
845
+ if (acceptHeader.includes("text/html")) {
846
+ return c.redirect("/auth/login?error=Please login to access the admin area");
847
+ }
848
+ return c.json({ error: "Authentication required" }, 401);
849
+ }
850
+ const roles = Array.isArray(requiredRole) ? requiredRole : [requiredRole];
851
+ if (!roles.includes(user.role)) {
852
+ const acceptHeader = c.req.header("Accept") || "";
853
+ if (acceptHeader.includes("text/html")) {
854
+ return c.redirect("/auth/login?error=You do not have permission to access this area");
855
+ }
856
+ return c.json({ error: "Insufficient permissions" }, 403);
857
+ }
858
+ return await next();
859
+ };
860
+ };
861
+ var requireRbac = (resource, verb) => {
862
+ return async (c, next) => {
863
+ const user = c.get("user");
864
+ if (!user) {
865
+ const acceptHeader = c.req.header("Accept") || "";
866
+ if (acceptHeader.includes("text/html")) {
867
+ return c.redirect("/auth/login?error=Please login to access the admin area");
868
+ }
869
+ return c.json({ error: "Authentication required" }, 401);
870
+ }
871
+ const cachedPerms = c.get("rbacPerms");
872
+ let allowed;
873
+ if (cachedPerms !== void 0) {
874
+ allowed = cachedPerms.includes(`${resource}:${verb}`);
875
+ } else {
876
+ allowed = await new RbacService(c.env.DB).can(user.userId, resource, verb);
877
+ }
878
+ if (!allowed) {
879
+ const acceptHeader = c.req.header("Accept") || "";
880
+ if (acceptHeader.includes("text/html")) {
881
+ return c.redirect("/auth/login?error=You do not have permission to access this area");
882
+ }
883
+ return c.json({ error: "Insufficient permissions" }, 403);
884
+ }
885
+ return await next();
886
+ };
887
+ };
888
+ var optionalAuth = () => {
889
+ return async (_c, next) => {
890
+ return await next();
891
+ };
892
+ };
893
+
894
+ // src/middleware/metrics.ts
895
+ var metricsMiddleware = () => {
896
+ return async (c, next) => {
897
+ const path = new URL(c.req.url).pathname;
898
+ if (path !== "/admin/dashboard/api/metrics") {
899
+ metricsTracker.recordRequest();
900
+ }
901
+ await next();
902
+ };
903
+ };
904
+ var JWT_SECRET_FALLBACK2 = "your-super-secret-jwt-key-change-in-production";
905
+ function arrayBufferToBase64Url(buffer) {
906
+ const bytes = new Uint8Array(buffer);
907
+ let binary = "";
908
+ for (let i = 0; i < bytes.length; i++) {
909
+ binary += String.fromCharCode(bytes[i]);
910
+ }
911
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
912
+ }
913
+ async function getHmacKey(secret) {
914
+ const encoder = new TextEncoder();
915
+ return crypto.subtle.importKey(
916
+ "raw",
917
+ encoder.encode(secret),
918
+ { name: "HMAC", hash: "SHA-256" },
919
+ false,
920
+ ["sign", "verify"]
921
+ );
922
+ }
923
+ async function generateCsrfToken(secret) {
924
+ const nonceBytes = new Uint8Array(32);
925
+ crypto.getRandomValues(nonceBytes);
926
+ const nonce = arrayBufferToBase64Url(nonceBytes.buffer);
927
+ const key = await getHmacKey(secret);
928
+ const encoder = new TextEncoder();
929
+ const signatureBuffer = await crypto.subtle.sign("HMAC", key, encoder.encode(nonce));
930
+ const signature = arrayBufferToBase64Url(signatureBuffer);
931
+ return `${nonce}.${signature}`;
932
+ }
933
+ async function validateCsrfToken(token, secret) {
934
+ if (!token || typeof token !== "string") return false;
935
+ const dotIndex = token.indexOf(".");
936
+ if (dotIndex === -1) return false;
937
+ const nonce = token.substring(0, dotIndex);
938
+ const signature = token.substring(dotIndex + 1);
939
+ if (!nonce || !signature) return false;
940
+ try {
941
+ const key = await getHmacKey(secret);
942
+ const encoder = new TextEncoder();
943
+ const sigPadded = signature.replace(/-/g, "+").replace(/_/g, "/");
944
+ const sigBinary = atob(sigPadded);
945
+ const sigBytes = new Uint8Array(sigBinary.length);
946
+ for (let i = 0; i < sigBinary.length; i++) {
947
+ sigBytes[i] = sigBinary.charCodeAt(i);
948
+ }
949
+ return await crypto.subtle.verify("HMAC", key, sigBytes.buffer, encoder.encode(nonce));
950
+ } catch {
951
+ return false;
952
+ }
953
+ }
954
+ var DEFAULT_EXEMPT_PATHS = [
955
+ "/auth/login",
956
+ "/auth/register",
957
+ "/auth/seed-admin",
958
+ "/test-seed-defaults",
959
+ "/test-cleanup",
960
+ "/auth/accept-invitation",
961
+ "/auth/reset-password",
962
+ "/auth/request-password-reset",
963
+ "/auth/otp",
964
+ "/auth/magic-link",
965
+ "/auth/sign-out",
966
+ "/auth/sign-in",
967
+ "/auth/sign-up",
968
+ "/auth/get-session",
969
+ "/auth/verify",
970
+ "/api/stripe/webhook",
971
+ "/api/events"
972
+ ];
973
+ function isExemptPath(path, extraExemptPaths = []) {
974
+ if (path.startsWith("/forms/") || path.startsWith("/api/forms/") || path === "/forms" || path === "/api/forms") {
975
+ return true;
976
+ }
977
+ if (path.startsWith("/api/search")) {
978
+ return true;
979
+ }
980
+ const allExempt = [...DEFAULT_EXEMPT_PATHS, ...extraExemptPaths];
981
+ for (const exempt of allExempt) {
982
+ if (path === exempt || path.startsWith(exempt + "/")) {
983
+ return true;
984
+ }
985
+ }
986
+ return false;
987
+ }
988
+ function csrfProtection(options = {}) {
989
+ return async (c, next) => {
990
+ const method = c.req.method.toUpperCase();
991
+ const path = new URL(c.req.url).pathname;
992
+ const secret = c.env?.JWT_SECRET || JWT_SECRET_FALLBACK2;
993
+ if (c.env?.ENVIRONMENT === "production" && !c.env?.JWT_SECRET) {
994
+ console.warn(
995
+ "[CSRF] WARNING: JWT_SECRET is not set in production. CSRF tokens are signed with the fallback key, which is insecure."
996
+ );
997
+ }
998
+ if (method === "GET" || method === "HEAD" || method === "OPTIONS") {
999
+ await ensureCsrfCookie(c, secret);
1000
+ await next();
1001
+ return;
1002
+ }
1003
+ if (isExemptPath(path, options.exemptPaths)) {
1004
+ await next();
1005
+ return;
1006
+ }
1007
+ const authCookie = getCookie(c, "auth_token");
1008
+ if (!authCookie) {
1009
+ await next();
1010
+ return;
1011
+ }
1012
+ const authHeader = c.req.header("Authorization");
1013
+ if (authHeader) {
1014
+ await next();
1015
+ return;
1016
+ }
1017
+ const cookieToken = getCookie(c, "csrf_token");
1018
+ let headerToken = c.req.header("X-CSRF-Token");
1019
+ if (!headerToken) {
1020
+ const contentType = c.req.header("Content-Type") || "";
1021
+ if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
1022
+ try {
1023
+ const body = await c.req.parseBody();
1024
+ headerToken = body["_csrf"];
1025
+ } catch {
1026
+ }
1027
+ }
1028
+ }
1029
+ if (!cookieToken || !headerToken) {
1030
+ return csrfError(c, "CSRF token missing");
1031
+ }
1032
+ if (cookieToken !== headerToken) {
1033
+ return csrfError(c, "CSRF token mismatch");
1034
+ }
1035
+ const isValid = await validateCsrfToken(cookieToken, secret);
1036
+ if (!isValid) {
1037
+ return csrfError(c, "CSRF token invalid");
1038
+ }
1039
+ await next();
1040
+ };
1041
+ }
1042
+ async function ensureCsrfCookie(c, secret) {
1043
+ const existing = getCookie(c, "csrf_token");
1044
+ if (existing) {
1045
+ const isValid = await validateCsrfToken(existing, secret);
1046
+ if (isValid) {
1047
+ c.set("csrfToken", existing);
1048
+ return;
1049
+ }
1050
+ }
1051
+ const token = await generateCsrfToken(secret);
1052
+ c.set("csrfToken", token);
1053
+ const isDev = c.env?.ENVIRONMENT === "development" || !c.env?.ENVIRONMENT;
1054
+ setCookie(c, "csrf_token", token, {
1055
+ httpOnly: false,
1056
+ // JS must read this cookie
1057
+ secure: !isDev,
1058
+ sameSite: "Strict",
1059
+ path: "/",
1060
+ maxAge: 86400
1061
+ // 24 hours — browser-side expiry
1062
+ });
1063
+ }
1064
+ function csrfError(c, message) {
1065
+ const accept = c.req.header("Accept") || "";
1066
+ if (accept.includes("text/html")) {
1067
+ return c.html(
1068
+ `<!DOCTYPE html><html><head><title>403 Forbidden</title></head><body><h1>403 Forbidden</h1><p>${message}</p></body></html>`,
1069
+ 403
1070
+ );
1071
+ }
1072
+ return c.json({ error: message, status: 403 }, 403);
1073
+ }
1074
+
1075
+ // src/middleware/rate-limit.ts
1076
+ function rateLimit(options) {
1077
+ const { max, windowMs, keyPrefix } = options;
1078
+ return async (c, next) => {
1079
+ const kv = c.env?.CACHE_KV;
1080
+ if (!kv) {
1081
+ return await next();
1082
+ }
1083
+ const ip = c.req.header("cf-connecting-ip") || c.req.header("x-forwarded-for") || "unknown";
1084
+ const key = `ratelimit:${keyPrefix}:${ip}`;
1085
+ try {
1086
+ const now = Date.now();
1087
+ const stored = await kv.get(key, "json");
1088
+ let entry;
1089
+ if (stored && stored.resetAt > now) {
1090
+ entry = stored;
1091
+ } else {
1092
+ entry = { count: 0, resetAt: now + windowMs };
1093
+ }
1094
+ entry.count++;
1095
+ const ttlSeconds = Math.ceil((entry.resetAt - now) / 1e3);
1096
+ if (entry.count > max) {
1097
+ await kv.put(key, JSON.stringify(entry), { expirationTtl: Math.max(ttlSeconds, 60) });
1098
+ const retryAfter = Math.ceil((entry.resetAt - now) / 1e3);
1099
+ c.header("Retry-After", String(retryAfter));
1100
+ c.header("X-RateLimit-Limit", String(max));
1101
+ c.header("X-RateLimit-Remaining", "0");
1102
+ c.header("X-RateLimit-Reset", String(Math.ceil(entry.resetAt / 1e3)));
1103
+ return c.json({ error: "Too many requests. Please try again later." }, 429);
1104
+ }
1105
+ await kv.put(key, JSON.stringify(entry), { expirationTtl: Math.max(ttlSeconds, 60) });
1106
+ c.header("X-RateLimit-Limit", String(max));
1107
+ c.header("X-RateLimit-Remaining", String(max - entry.count));
1108
+ c.header("X-RateLimit-Reset", String(Math.ceil(entry.resetAt / 1e3)));
1109
+ return await next();
1110
+ } catch (error) {
1111
+ console.error("Rate limiter error (non-fatal):", error);
1112
+ return await next();
1113
+ }
1114
+ };
1115
+ }
1116
+
1117
+ // src/middleware/security-headers.ts
1118
+ var securityHeadersMiddleware = () => {
1119
+ return async (c, next) => {
1120
+ await next();
1121
+ c.header("X-Content-Type-Options", "nosniff");
1122
+ c.header("X-Frame-Options", "SAMEORIGIN");
1123
+ c.header("Referrer-Policy", "strict-origin-when-cross-origin");
1124
+ c.header("Permissions-Policy", "camera=(), microphone=(), geolocation=()");
1125
+ const environment = c.env?.ENVIRONMENT;
1126
+ if (environment !== "development") {
1127
+ c.header("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
1128
+ }
1129
+ };
1130
+ };
1131
+
1132
+ // src/middleware/plugin-middleware.ts
1133
+ async function isPluginActive(db, pluginId) {
1134
+ try {
1135
+ const docResult = await db.prepare(
1136
+ `SELECT json_extract(data, '$.status') as status FROM documents
1137
+ WHERE slug = ? AND type_id = 'plugin' AND tenant_id = 'default'
1138
+ AND is_current_draft = 1 AND deleted_at IS NULL`
1139
+ ).bind(pluginId).first();
1140
+ return docResult?.status === "active";
1141
+ } catch (error) {
1142
+ console.error(`[isPluginActive] Error checking plugin status for ${pluginId}:`, error);
1143
+ return false;
1144
+ }
1145
+ }
1146
+ async function requireActivePlugin(db, pluginId) {
1147
+ const isActive = await isPluginActive(db, pluginId);
1148
+ if (!isActive) {
1149
+ throw new Error(`Plugin '${pluginId}' is required but is not active`);
1150
+ }
1151
+ }
1152
+ async function requireActivePlugins(db, pluginIds) {
1153
+ for (const pluginId of pluginIds) {
1154
+ await requireActivePlugin(db, pluginId);
1155
+ }
1156
+ }
1157
+ async function getActivePlugins(db) {
1158
+ try {
1159
+ const { results } = await db.prepare(
1160
+ `SELECT slug as id, json_extract(data, '$.status') as status, data FROM documents
1161
+ WHERE type_id = 'plugin' AND tenant_id = 'default'
1162
+ AND q_plugin_status = 'active' AND is_current_draft = 1 AND deleted_at IS NULL`
1163
+ ).all();
1164
+ return results || [];
1165
+ } catch (error) {
1166
+ console.error("[getActivePlugins] Error fetching active plugins:", error);
1167
+ return [];
1168
+ }
1169
+ }
1170
+
1171
+ // src/middleware/index.ts
1172
+ var loggingMiddleware = () => async (_c, next) => await next();
1173
+ var detailedLoggingMiddleware = () => async (_c, next) => await next();
1174
+ var securityLoggingMiddleware = () => async (_c, next) => await next();
1175
+ var performanceLoggingMiddleware = () => async (_c, next) => await next();
1176
+ var cacheHeaders = () => async (_c, next) => await next();
1177
+ var compressionMiddleware = async (_c, next) => await next();
1178
+ var PermissionManager = {};
1179
+ var requirePermission = () => async (_c, next) => await next();
1180
+ var requireAnyPermission = () => async (_c, next) => await next();
1181
+ var logActivity = () => {
1182
+ };
1183
+
1184
+ export { AuthManager, DocumentTypeRegistry, PermissionManager, bootstrapDocumentTypes, bootstrapMiddleware, cacheHeaders, compressionMiddleware, csrfProtection, detailedLoggingMiddleware, generateCsrfToken, getActivePlugins, getJwtExpirySeconds, getJwtExpirySecondsFromDb, getJwtRefreshGraceSecondsFromDb, isPluginActive, logActivity, loggingMiddleware, metricsMiddleware, optionalAuth, performanceLoggingMiddleware, rateLimit, requireActivePlugin, requireActivePlugins, requireAnyPermission, requireAuth, requirePermission, requireRbac, requireRole, securityHeadersMiddleware, securityLoggingMiddleware, validateCsrfToken, verifySecurityConfig };
1185
+ //# sourceMappingURL=chunk-IDCZBF35.js.map
1186
+ //# sourceMappingURL=chunk-IDCZBF35.js.map