@sonicjs-cms/core 3.0.0-beta.23 → 3.0.0-beta.25

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 (139) hide show
  1. package/bin/db-reset.js +39 -22
  2. package/dist/admin-documents-form.template-JWV5GITD.js +6 -0
  3. package/dist/{admin-documents-form.template-55V2ZGWT.js.map → admin-documents-form.template-JWV5GITD.js.map} +1 -1
  4. package/dist/admin-documents-form.template-UWKGVAMW.cjs +19 -0
  5. package/dist/{admin-layout-catalyst.template-6UQGLCKI.js.map → admin-documents-form.template-UWKGVAMW.cjs.map} +1 -1
  6. package/dist/admin-layout-catalyst.template-MOQ3BBXA.cjs +21 -0
  7. package/dist/{admin-layout-catalyst.template-3XZXDGF7.cjs.map → admin-layout-catalyst.template-MOQ3BBXA.cjs.map} +1 -1
  8. package/dist/admin-layout-catalyst.template-WCYMQVTZ.js +7 -0
  9. package/dist/{admin-documents-form.template-YNAVMJCV.cjs.map → admin-layout-catalyst.template-WCYMQVTZ.js.map} +1 -1
  10. package/dist/{chunk-KS24NPPL.js → chunk-2D3A4X52.js} +21 -2
  11. package/dist/chunk-2D3A4X52.js.map +1 -0
  12. package/dist/{chunk-DIBCKCUE.js → chunk-2OXFMMA3.js} +14 -29
  13. package/dist/chunk-2OXFMMA3.js.map +1 -0
  14. package/dist/{chunk-SOZUG52O.js → chunk-3KGY4IBW.js} +3 -3
  15. package/dist/{chunk-SOZUG52O.js.map → chunk-3KGY4IBW.js.map} +1 -1
  16. package/dist/{chunk-CTYLCWMO.cjs → chunk-6OA77UM7.cjs} +802 -481
  17. package/dist/chunk-6OA77UM7.cjs.map +1 -0
  18. package/dist/{chunk-URIBTX27.js → chunk-73RTFFMX.js} +3 -3
  19. package/dist/{chunk-URIBTX27.js.map → chunk-73RTFFMX.js.map} +1 -1
  20. package/dist/{chunk-SK5CHFJP.js → chunk-7P5OCUPS.js} +467 -149
  21. package/dist/chunk-7P5OCUPS.js.map +1 -0
  22. package/dist/{chunk-3TMTYJAJ.js → chunk-BDQ55MWV.js} +18 -2
  23. package/dist/chunk-BDQ55MWV.js.map +1 -0
  24. package/dist/{chunk-C3ZQP5NW.cjs → chunk-BJ6NJL2E.cjs} +7 -12
  25. package/dist/chunk-BJ6NJL2E.cjs.map +1 -0
  26. package/dist/{chunk-2MIYYAO4.js → chunk-C54MH5UI.js} +5 -4
  27. package/dist/chunk-C54MH5UI.js.map +1 -0
  28. package/dist/{chunk-BBEKHLTW.cjs → chunk-CN3T3TDI.cjs} +9 -9
  29. package/dist/{chunk-BBEKHLTW.cjs.map → chunk-CN3T3TDI.cjs.map} +1 -1
  30. package/dist/{chunk-DPLJYTHE.js → chunk-CWIGQYRR.js} +7 -12
  31. package/dist/chunk-CWIGQYRR.js.map +1 -0
  32. package/dist/{chunk-4WMZGHIC.cjs → chunk-D6FWEYQE.cjs} +6 -5
  33. package/dist/chunk-D6FWEYQE.cjs.map +1 -0
  34. package/dist/{chunk-A46IRC5A.js → chunk-D7RLBBHE.js} +2 -2
  35. package/dist/{chunk-A46IRC5A.js.map → chunk-D7RLBBHE.js.map} +1 -1
  36. package/dist/{chunk-IXUHXTHW.cjs → chunk-DLTT7QZU.cjs} +17 -6
  37. package/dist/chunk-DLTT7QZU.cjs.map +1 -0
  38. package/dist/{chunk-BHND5PEV.js → chunk-E3TQG7AA.js} +3 -3
  39. package/dist/{chunk-BHND5PEV.js.map → chunk-E3TQG7AA.js.map} +1 -1
  40. package/dist/{chunk-IS54FPB7.cjs → chunk-E7E3NL3Y.cjs} +27 -42
  41. package/dist/chunk-E7E3NL3Y.cjs.map +1 -0
  42. package/dist/{chunk-XXRR54AB.js → chunk-F26HP3B6.js} +36 -5
  43. package/dist/chunk-F26HP3B6.js.map +1 -0
  44. package/dist/{chunk-QZGABF2M.js → chunk-FVAK4C6L.js} +17 -6
  45. package/dist/chunk-FVAK4C6L.js.map +1 -0
  46. package/dist/{chunk-BJROXF4N.js → chunk-HSDJDWLV.js} +19 -4
  47. package/dist/chunk-HSDJDWLV.js.map +1 -0
  48. package/dist/{chunk-RFBW5RRH.cjs → chunk-I7MIRRFJ.cjs} +43 -12
  49. package/dist/chunk-I7MIRRFJ.cjs.map +1 -0
  50. package/dist/{chunk-LTZ5WYOK.cjs → chunk-JCBDXEY2.cjs} +18 -2
  51. package/dist/chunk-JCBDXEY2.cjs.map +1 -0
  52. package/dist/{chunk-UYJQDDEE.cjs → chunk-MI2YXCK4.cjs} +14 -8
  53. package/dist/chunk-MI2YXCK4.cjs.map +1 -0
  54. package/dist/{chunk-VGHEKIJL.cjs → chunk-MOGK3U4A.cjs} +2 -2
  55. package/dist/{chunk-VGHEKIJL.cjs.map → chunk-MOGK3U4A.cjs.map} +1 -1
  56. package/dist/{chunk-E66UVONV.cjs → chunk-OUBUS74I.cjs} +24 -9
  57. package/dist/chunk-OUBUS74I.cjs.map +1 -0
  58. package/dist/{chunk-GMMCCIQO.cjs → chunk-RSBZITK3.cjs} +3 -3
  59. package/dist/{chunk-GMMCCIQO.cjs.map → chunk-RSBZITK3.cjs.map} +1 -1
  60. package/dist/{chunk-THFZSDIU.js → chunk-SXC4HPK6.js} +14 -8
  61. package/dist/chunk-SXC4HPK6.js.map +1 -0
  62. package/dist/{chunk-JMJM7DMM.cjs → chunk-TT3TQSCZ.cjs} +5 -5
  63. package/dist/{chunk-JMJM7DMM.cjs.map → chunk-TT3TQSCZ.cjs.map} +1 -1
  64. package/dist/{chunk-AT4ZQGEZ.cjs → chunk-YAURVGUK.cjs} +21 -2
  65. package/dist/chunk-YAURVGUK.cjs.map +1 -0
  66. package/dist/config-4TIHSNW6.js +6 -0
  67. package/dist/{config-6FB2BXMP.js.map → config-4TIHSNW6.js.map} +1 -1
  68. package/dist/{config-OCH7S4WK.cjs → config-XN2ETAJ3.cjs} +5 -5
  69. package/dist/{config-OCH7S4WK.cjs.map → config-XN2ETAJ3.cjs.map} +1 -1
  70. package/dist/{define-plugin-D_6D9vVZ.d.ts → define-plugin-DDe8a_bh.d.ts} +3 -0
  71. package/dist/{define-plugin-DMcvEy_C.d.cts → define-plugin-rjXmGOgu.d.cts} +3 -0
  72. package/dist/index.cjs +1220 -353
  73. package/dist/index.cjs.map +1 -1
  74. package/dist/index.d.cts +38 -3
  75. package/dist/index.d.ts +38 -3
  76. package/dist/index.js +895 -30
  77. package/dist/index.js.map +1 -1
  78. package/dist/middleware.cjs +35 -35
  79. package/dist/middleware.d.cts +4 -1
  80. package/dist/middleware.d.ts +4 -1
  81. package/dist/middleware.js +6 -6
  82. package/dist/migrations-JAHMI5MK.js +4 -0
  83. package/dist/{migrations-XQ6EX6HX.js.map → migrations-JAHMI5MK.js.map} +1 -1
  84. package/dist/migrations-KU4V6T73.cjs +13 -0
  85. package/dist/{migrations-ZD7Y5NUC.cjs.map → migrations-KU4V6T73.cjs.map} +1 -1
  86. package/dist/plugins.cjs +40 -40
  87. package/dist/plugins.d.cts +1 -1
  88. package/dist/plugins.d.ts +1 -1
  89. package/dist/plugins.js +3 -3
  90. package/dist/rbac-AORZ6O3N.cjs +14 -0
  91. package/dist/{rbac-7QZ4JZ2E.cjs.map → rbac-AORZ6O3N.cjs.map} +1 -1
  92. package/dist/rbac-P44772FP.js +5 -0
  93. package/dist/{rbac-Y43IEQOP.js.map → rbac-P44772FP.js.map} +1 -1
  94. package/dist/routes.cjs +31 -31
  95. package/dist/routes.js +11 -11
  96. package/dist/services.cjs +18 -18
  97. package/dist/services.d.cts +1 -1
  98. package/dist/services.d.ts +1 -1
  99. package/dist/services.js +2 -2
  100. package/dist/{telemetry-Cku1ax74.d.cts → telemetry-QFlTv9f8.d.cts} +1 -1
  101. package/dist/{telemetry-Cku1ax74.d.ts → telemetry-QFlTv9f8.d.ts} +1 -1
  102. package/dist/templates.cjs +17 -17
  103. package/dist/templates.js +3 -3
  104. package/dist/types.d.cts +1 -1
  105. package/dist/types.d.ts +1 -1
  106. package/dist/utils.cjs +12 -12
  107. package/dist/utils.d.cts +1 -1
  108. package/dist/utils.d.ts +1 -1
  109. package/dist/utils.js +2 -2
  110. package/package.json +5 -10
  111. package/dist/admin-documents-form.template-55V2ZGWT.js +0 -6
  112. package/dist/admin-documents-form.template-YNAVMJCV.cjs +0 -19
  113. package/dist/admin-layout-catalyst.template-3XZXDGF7.cjs +0 -21
  114. package/dist/admin-layout-catalyst.template-6UQGLCKI.js +0 -7
  115. package/dist/chunk-2MIYYAO4.js.map +0 -1
  116. package/dist/chunk-3TMTYJAJ.js.map +0 -1
  117. package/dist/chunk-4WMZGHIC.cjs.map +0 -1
  118. package/dist/chunk-AT4ZQGEZ.cjs.map +0 -1
  119. package/dist/chunk-BJROXF4N.js.map +0 -1
  120. package/dist/chunk-C3ZQP5NW.cjs.map +0 -1
  121. package/dist/chunk-CTYLCWMO.cjs.map +0 -1
  122. package/dist/chunk-DIBCKCUE.js.map +0 -1
  123. package/dist/chunk-DPLJYTHE.js.map +0 -1
  124. package/dist/chunk-E66UVONV.cjs.map +0 -1
  125. package/dist/chunk-IS54FPB7.cjs.map +0 -1
  126. package/dist/chunk-IXUHXTHW.cjs.map +0 -1
  127. package/dist/chunk-KS24NPPL.js.map +0 -1
  128. package/dist/chunk-LTZ5WYOK.cjs.map +0 -1
  129. package/dist/chunk-QZGABF2M.js.map +0 -1
  130. package/dist/chunk-RFBW5RRH.cjs.map +0 -1
  131. package/dist/chunk-SK5CHFJP.js.map +0 -1
  132. package/dist/chunk-THFZSDIU.js.map +0 -1
  133. package/dist/chunk-UYJQDDEE.cjs.map +0 -1
  134. package/dist/chunk-XXRR54AB.js.map +0 -1
  135. package/dist/config-6FB2BXMP.js +0 -6
  136. package/dist/migrations-XQ6EX6HX.js +0 -4
  137. package/dist/migrations-ZD7Y5NUC.cjs +0 -13
  138. package/dist/rbac-7QZ4JZ2E.cjs +0 -14
  139. package/dist/rbac-Y43IEQOP.js +0 -5
@@ -1,18 +1,18 @@
1
1
  import { isFirstUserRegistration, isRegistrationEnabled, authValidationService } from './chunk-VVYVFCML.js';
2
- import { renderDocumentFormPage, renderAlert } from './chunk-BHND5PEV.js';
2
+ import { renderDocumentFormPage, renderAlert } from './chunk-E3TQG7AA.js';
3
3
  import { getCacheService, CACHE_CONFIGS, SettingsService, getLogger, getAppInstance, buildRouteList, CATEGORY_INFO } from './chunk-2XAQTPAE.js';
4
- import { DocumentTypeRegistry, requireAuth, requireRole, optionalAuth, isPluginActive, rateLimit, AuthManager, getJwtExpirySecondsFromDb, getJwtRefreshGraceSecondsFromDb, logActivity, generateCsrfToken } from './chunk-DIBCKCUE.js';
5
- import { DocumentsService, documentSecondsToMs, RbacService } from './chunk-3TMTYJAJ.js';
4
+ import { DocumentTypeRegistry, requireAuth, requireRole, optionalAuth, isPluginActive, rateLimit, AuthManager, getJwtExpirySecondsFromDb, getJwtRefreshGraceSecondsFromDb, logActivity, generateCsrfToken } from './chunk-2OXFMMA3.js';
5
+ import { DocumentsService, documentSecondsToMs, RbacService } from './chunk-BDQ55MWV.js';
6
6
  import { nanoid } from './chunk-UWUP2VMT.js';
7
- import { getCollectionRegistry, collectionRecordToRow, loadCollectionConfigs, getVisibleCollections, PluginService, PLUGIN_REGISTRY, findPluginByCodeName, isCodeCollectionInternal, isDbDocTypeInternal, invalidateTenantCache } from './chunk-KS24NPPL.js';
8
- import { MigrationService } from './chunk-A46IRC5A.js';
9
- import { renderCheckboxPage, renderAlert as renderAlert$1, renderTable, renderPagination, renderConfirmationDialog, getConfirmationDialogScript, renderAdminLayout, adminLayoutV2, renderForm } from './chunk-BJROXF4N.js';
10
- import { init_admin_layout_catalyst_template, renderAdminLayoutCatalyst } from './chunk-THFZSDIU.js';
11
- import { createServiceSingleton, definePlugin, getPluginDefinition, applySchemaDefaults, renderSchemaFields, parseFormDataToSettings, getPluginMenu } from './chunk-2MIYYAO4.js';
7
+ import { getCollectionRegistry, collectionRecordToRow, loadCollectionConfigs, getVisibleCollections, PluginService, PLUGIN_REGISTRY, findPluginByCodeName, isCodeCollectionInternal, isDbDocTypeInternal, invalidateTenantCache } from './chunk-2D3A4X52.js';
8
+ import { MigrationService } from './chunk-D7RLBBHE.js';
9
+ import { renderCheckboxPage, renderAlert as renderAlert$1, renderTable, renderPagination, renderConfirmationDialog, getConfirmationDialogScript, renderAdminLayout, adminLayoutV2, renderForm } from './chunk-HSDJDWLV.js';
10
+ import { init_admin_layout_catalyst_template, renderAdminLayoutCatalyst } from './chunk-SXC4HPK6.js';
11
+ import { createServiceSingleton, definePlugin, getPluginDefinition, applySchemaDefaults, renderSchemaFields, parseFormDataToSettings, getPluginMenu } from './chunk-C54MH5UI.js';
12
12
  import { hasHookSystem, getTypedHooks } from './chunk-RNZFGN4R.js';
13
- import { QueryFilterBuilder, getBlocksFieldConfig, parseBlocksValue } from './chunk-QZGABF2M.js';
13
+ import { QueryFilterBuilder, getBlocksFieldConfig, parseBlocksValue } from './chunk-FVAK4C6L.js';
14
14
  import { metricsTracker } from './chunk-FICTAGD4.js';
15
- import { getCoreVersion } from './chunk-DPLJYTHE.js';
15
+ import { getCoreVersion } from './chunk-CWIGQYRR.js';
16
16
  import { escapeHtml, sanitizeRichText, sanitizeInput } from './chunk-TQABQWOP.js';
17
17
  import { Hono } from 'hono';
18
18
  import { z } from 'zod';
@@ -45,6 +45,7 @@ var DocumentPermissionsService = class {
45
45
  const publicGrants = baseGrants["public"] ?? [];
46
46
  if (publicGrants.includes(permission)) return true;
47
47
  } else if (principal.type === "role") {
48
+ if (principal.id === "public") continue;
48
49
  const roleGrants = baseGrants[principal.id] ?? [];
49
50
  if (roleGrants.includes(permission)) return true;
50
51
  }
@@ -149,6 +150,18 @@ var DocumentRepository = class {
149
150
  const row = await this.db.prepare("SELECT * FROM documents WHERE root_id = ? AND tenant_id = ? AND is_published = 1").bind(rootId, this.tenantId).first();
150
151
  return row ? rowToDocument(row) : null;
151
152
  }
153
+ // Slug lookup for a single doc. Prefer the published row; fall back to the current
154
+ // draft so a never-published doc is still reachable by slug (matches getById, which
155
+ // resolves drafts too). ORDER BY makes the preference deterministic when both exist.
156
+ async getBySlug(typeId, slug) {
157
+ const row = await this.db.prepare(
158
+ `SELECT * FROM documents
159
+ WHERE tenant_id = ? AND type_id = ? AND slug = ? AND deleted_at IS NULL
160
+ AND (is_published = 1 OR is_current_draft = 1)
161
+ ORDER BY is_published DESC LIMIT 1`
162
+ ).bind(this.tenantId, typeId, slug).first();
163
+ return row ? rowToDocument(row) : null;
164
+ }
152
165
  // Unified, tenant-scoped list with optional generated-column / facet filters and sort. This is the
153
166
  // single place document list SQL is built — route handlers must call this, never inline SQL (R4).
154
167
  async list(opts = {}) {
@@ -982,6 +995,32 @@ apiContentCrudRoutes.get("/:id", optionalAuth(), async (c) => {
982
995
  { collection: docRow.type_id, id: docRow.root_id, data: transformedContent.data },
983
996
  "fire-and-forget"
984
997
  );
998
+ const fieldsParam = c.req.query("fields");
999
+ if (fieldsParam) {
1000
+ const projFields = fieldsParam.split(",").map((s) => s.trim()).filter(Boolean);
1001
+ if (projFields.length > 0) {
1002
+ const result = {};
1003
+ const dataSubFields = [];
1004
+ for (const f of projFields) {
1005
+ if (f === "data") {
1006
+ result.data = transformedContent.data;
1007
+ } else if (f.startsWith("data.")) {
1008
+ dataSubFields.push(f.slice(5));
1009
+ } else if (Object.prototype.hasOwnProperty.call(transformedContent, f)) {
1010
+ result[f] = transformedContent[f];
1011
+ }
1012
+ }
1013
+ if (dataSubFields.length > 0 && !result.data) {
1014
+ result.data = {};
1015
+ for (const sub of dataSubFields) {
1016
+ if (transformedContent.data && Object.prototype.hasOwnProperty.call(transformedContent.data, sub)) {
1017
+ result.data[sub] = transformedContent.data[sub];
1018
+ }
1019
+ }
1020
+ }
1021
+ return c.json({ data: result });
1022
+ }
1023
+ }
985
1024
  return c.json({ data: transformedContent });
986
1025
  } catch (error) {
987
1026
  console.error("Error fetching content:", error);
@@ -1045,8 +1084,8 @@ apiContentCrudRoutes.post("/", requireAuth(), requireRole(["admin", "editor", "a
1045
1084
  user?.userId
1046
1085
  );
1047
1086
  const cache = getCacheService(CACHE_CONFIGS.api);
1048
- await cache.invalidate("content-filtered:*");
1049
- await cache.invalidate("collection-content-filtered:*");
1087
+ await cache.invalidate("api:content-filtered:*");
1088
+ await cache.invalidate(`api:collection-content-filtered:${backing.coll.name}:*`);
1050
1089
  dispatchHookEvent(
1051
1090
  c,
1052
1091
  "content:after:create",
@@ -1109,8 +1148,8 @@ apiContentCrudRoutes.put("/:id", requireAuth(), requireRole(["admin", "editor",
1109
1148
  await svc.unpublish(pub.id);
1110
1149
  }
1111
1150
  const cache = getCacheService(CACHE_CONFIGS.api);
1112
- await cache.invalidate("content-filtered:*");
1113
- await cache.invalidate("collection-content-filtered:*");
1151
+ await cache.invalidate("api:content-filtered:*");
1152
+ await cache.invalidate(`api:collection-content-filtered:${docRow.type_id}:*`);
1114
1153
  const coll = getCollectionRegistry().getByName(docRow.type_id);
1115
1154
  const saved = await db.prepare("SELECT * FROM documents WHERE id = ?").bind(newDraft.id).first();
1116
1155
  const savedData = saved?.data ? JSON.parse(saved.data) : {};
@@ -1149,8 +1188,8 @@ apiContentCrudRoutes.delete("/:id", requireAuth(), requireRole(["admin", "editor
1149
1188
  const now = Math.floor(Date.now() / 1e3);
1150
1189
  await db.prepare("UPDATE documents SET deleted_at = ?, updated_at = ? WHERE root_id = ? AND tenant_id = ?").bind(now, now, id, tenantId).run();
1151
1190
  const cache = getCacheService(CACHE_CONFIGS.api);
1152
- await cache.invalidate("content-filtered:*");
1153
- await cache.invalidate("collection-content-filtered:*");
1191
+ await cache.invalidate("api:content-filtered:*");
1192
+ await cache.invalidate(`api:collection-content-filtered:${docRow.type_id}:*`);
1154
1193
  dispatchHookEvent(c, "content:after:delete", { collection: docRow.type_id, id, data: {}, user: actor }, "fire-and-forget");
1155
1194
  return c.json({ success: true });
1156
1195
  }
@@ -1166,6 +1205,22 @@ apiContentCrudRoutes.delete("/:id", requireAuth(), requireRole(["admin", "editor
1166
1205
  var api_content_crud_default = apiContentCrudRoutes;
1167
1206
 
1168
1207
  // src/routes/api.ts
1208
+ function isAnonPrincipal(principalSet) {
1209
+ return principalSet.length === 1 && principalSet[0]?.type === "public";
1210
+ }
1211
+ async function typeReadAllowed(db, principalSet, docType, typeName) {
1212
+ const perms = new DocumentPermissionsService(db);
1213
+ if (perms.isAllowedSync(principalSet, [], "read", docType?.settings ?? {})) return true;
1214
+ const rbac = new RbacService(db);
1215
+ let anyRbacRoleFound = false;
1216
+ for (const p of principalSet) {
1217
+ if (p.type !== "role") continue;
1218
+ if (!await rbac.hasRbacRole(p.id)) continue;
1219
+ anyRbacRoleFound = true;
1220
+ if (await rbac.isGrantedForRole(p.id, `document_type:${typeName}`, "read")) return true;
1221
+ }
1222
+ return !anyRbacRoleFound;
1223
+ }
1169
1224
  var DOC_SORTABLE_COLUMNS = /* @__PURE__ */ new Set([
1170
1225
  "created_at",
1171
1226
  "updated_at",
@@ -1249,6 +1304,33 @@ function mapDocRowToContent(row, collectionId) {
1249
1304
  updated_at: documentSecondsToMs(row.updated_at)
1250
1305
  };
1251
1306
  }
1307
+ function projectFields(item, fields) {
1308
+ if (fields.length === 0) return item;
1309
+ const result = {};
1310
+ const dataSubFields = [];
1311
+ for (const f of fields) {
1312
+ if (f === "data") {
1313
+ result.data = item.data;
1314
+ } else if (f.startsWith("data.")) {
1315
+ dataSubFields.push(f.slice(5));
1316
+ } else if (Object.prototype.hasOwnProperty.call(item, f)) {
1317
+ result[f] = item[f];
1318
+ }
1319
+ }
1320
+ if (dataSubFields.length > 0 && !result.data) {
1321
+ result.data = {};
1322
+ for (const sub of dataSubFields) {
1323
+ if (item.data && Object.prototype.hasOwnProperty.call(item.data, sub)) {
1324
+ result.data[sub] = item.data[sub];
1325
+ }
1326
+ }
1327
+ }
1328
+ return result;
1329
+ }
1330
+ function parseFieldsParam(raw2) {
1331
+ if (!raw2) return [];
1332
+ return raw2.split(",").map((s) => s.trim()).filter(Boolean);
1333
+ }
1252
1334
  var apiRoutes = new Hono();
1253
1335
  apiRoutes.use("*", async (c, next) => {
1254
1336
  const startTime = Date.now();
@@ -1738,7 +1820,12 @@ apiRoutes.get("/content", optionalAuth(), async (c) => {
1738
1820
  try {
1739
1821
  const db = c.env.DB;
1740
1822
  const queryParams = c.req.query();
1823
+ const projFields = parseFieldsParam(queryParams.fields);
1824
+ if (queryParams.fields) delete queryParams.fields;
1741
1825
  const role = c.get("user")?.role;
1826
+ const { tenantId: cTenantId, principalSet: cPrincipalSet } = getDocumentRequestContext(c);
1827
+ const cAnon = isAnonPrincipal(cPrincipalSet);
1828
+ const cNeedsAcl = !cAnon && !canReadNonPublicContent(role);
1742
1829
  const collIdByName = /* @__PURE__ */ new Map();
1743
1830
  let typeId;
1744
1831
  let typeIds;
@@ -1784,7 +1871,7 @@ apiRoutes.get("/content", optionalAuth(), async (c) => {
1784
1871
  details: queryResult.errors
1785
1872
  }, 400);
1786
1873
  }
1787
- const cacheEnabled = c.get("cacheEnabled");
1874
+ const cacheEnabled = c.get("cacheEnabled") && cAnon;
1788
1875
  const cache = getCacheService(CACHE_CONFIGS.api);
1789
1876
  const cacheKey = cache.generateKey("content-filtered", JSON.stringify({ filter: normalizedFilter, query: queryResult.sql }));
1790
1877
  if (cacheEnabled) {
@@ -1814,11 +1901,41 @@ apiRoutes.get("/content", optionalAuth(), async (c) => {
1814
1901
  const stmt = db.prepare(queryResult.sql);
1815
1902
  const boundStmt = queryResult.params.length > 0 ? stmt.bind(...queryResult.params) : stmt;
1816
1903
  const { results } = await boundStmt.all();
1817
- const transformedResults = results.map((row) => mapDocRowToContent(row, collIdByName.get(row.type_id) ?? null));
1904
+ let aclResults = results;
1905
+ if (cNeedsAcl) {
1906
+ const typeReg = new DocumentTypeRegistry(db);
1907
+ const repo = new DocumentRepository(db, cTenantId);
1908
+ const uniqueTypeIds = [...new Set(results.map((r) => r.type_id))];
1909
+ const typeSettingsMap = /* @__PURE__ */ new Map();
1910
+ const typeRbacOnlyMap = /* @__PURE__ */ new Map();
1911
+ const typeDeniedMap = /* @__PURE__ */ new Map();
1912
+ await Promise.all(uniqueTypeIds.map(async (tid) => {
1913
+ const dt = await typeReg.findById(tid);
1914
+ const settings = dt?.settings ?? {};
1915
+ typeSettingsMap.set(tid, settings);
1916
+ const allowed2 = await typeReadAllowed(db, cPrincipalSet, dt, tid);
1917
+ if (!allowed2) {
1918
+ typeDeniedMap.set(tid, true);
1919
+ return;
1920
+ }
1921
+ const perms = new DocumentPermissionsService(db);
1922
+ typeRbacOnlyMap.set(tid, !perms.isAllowedSync(cPrincipalSet, [], "read", settings));
1923
+ }));
1924
+ const allowed = await Promise.all(
1925
+ results.map((row) => {
1926
+ const tid = row.type_id;
1927
+ if (typeDeniedMap.get(tid)) return Promise.resolve(false);
1928
+ if (typeRbacOnlyMap.get(tid)) return Promise.resolve(true);
1929
+ return repo.isAllowed(cPrincipalSet, row.root_id, "read", typeSettingsMap.get(tid) ?? {});
1930
+ })
1931
+ );
1932
+ aclResults = results.filter((_, i) => allowed[i]);
1933
+ }
1934
+ const transformedResults = aclResults.map((row) => mapDocRowToContent(row, collIdByName.get(row.type_id) ?? null));
1818
1935
  const responseData = {
1819
- data: transformedResults,
1936
+ data: projFields.length ? transformedResults.map((item) => projectFields(item, projFields)) : transformedResults,
1820
1937
  meta: addTimingMeta(c, {
1821
- count: results.length,
1938
+ count: aclResults.length,
1822
1939
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1823
1940
  // D44: echo the caller's filter with the access policy applied (status=published forced for
1824
1941
  // anonymous callers — the visible enforcement proof), NOT the internal doc-augmented where-tree.
@@ -1829,7 +1946,7 @@ apiRoutes.get("/content", optionalAuth(), async (c) => {
1829
1946
  }
1830
1947
  }, executionStart)
1831
1948
  };
1832
- if (cacheEnabled) {
1949
+ if (cacheEnabled && projFields.length === 0) {
1833
1950
  await cache.set(cacheKey, responseData);
1834
1951
  }
1835
1952
  return c.json(responseData);
@@ -1854,6 +1971,16 @@ apiRoutes.get("/collections/:collection/content", optionalAuth(), async (c) => {
1854
1971
  const collIdByName = /* @__PURE__ */ new Map();
1855
1972
  collIdByName.set(collection, record.id);
1856
1973
  const role = c.get("user")?.role;
1974
+ const { tenantId: ccTenantId, principalSet: ccPrincipalSet } = getDocumentRequestContext(c);
1975
+ const ccAnon = isAnonPrincipal(ccPrincipalSet);
1976
+ const ccNeedsAcl = !ccAnon && !canReadNonPublicContent(role);
1977
+ let ccDocType;
1978
+ if (ccNeedsAcl) {
1979
+ ccDocType = await new DocumentTypeRegistry(db).findById(record.name);
1980
+ if (!await typeReadAllowed(db, ccPrincipalSet, ccDocType, record.name)) {
1981
+ return c.json({ error: "Forbidden" }, 403);
1982
+ }
1983
+ }
1857
1984
  const filter = QueryFilterBuilder.parseFromQuery(queryParams);
1858
1985
  const normalizedFilter = augmentFilterForDocuments(filter, { typeId: collection, role });
1859
1986
  if (!normalizedFilter.limit) {
@@ -1868,7 +1995,7 @@ apiRoutes.get("/collections/:collection/content", optionalAuth(), async (c) => {
1868
1995
  details: queryResult.errors
1869
1996
  }, 400);
1870
1997
  }
1871
- const cacheEnabled = c.get("cacheEnabled");
1998
+ const cacheEnabled = c.get("cacheEnabled") && ccAnon;
1872
1999
  const cache = getCacheService(CACHE_CONFIGS.api);
1873
2000
  const includeCollection = queryParams.include?.split(",").map((s) => s.trim()).includes("collection");
1874
2001
  const cacheKey = cache.generateKey("collection-content-filtered", `${collection}:${JSON.stringify({ filter: normalizedFilter, query: queryResult.sql, includeCollection })}`);
@@ -1899,12 +2026,24 @@ apiRoutes.get("/collections/:collection/content", optionalAuth(), async (c) => {
1899
2026
  const stmt = db.prepare(queryResult.sql);
1900
2027
  const boundStmt = queryResult.params.length > 0 ? stmt.bind(...queryResult.params) : stmt;
1901
2028
  const { results } = await boundStmt.all();
1902
- const transformedResults = results.map((row) => mapDocRowToContent(row, collIdByName.get(row.type_id) ?? null));
2029
+ let ccAclResults = results;
2030
+ if (ccNeedsAcl) {
2031
+ const ccPerms = new DocumentPermissionsService(db);
2032
+ const ccBaseGrantsPass = ccPerms.isAllowedSync(ccPrincipalSet, [], "read", ccDocType?.settings ?? {});
2033
+ if (ccBaseGrantsPass) {
2034
+ const repo = new DocumentRepository(db, ccTenantId);
2035
+ const allowed = await Promise.all(
2036
+ results.map((row) => repo.isAllowed(ccPrincipalSet, row.root_id, "read", ccDocType?.settings ?? {}))
2037
+ );
2038
+ ccAclResults = results.filter((_, i) => allowed[i]);
2039
+ }
2040
+ }
2041
+ const transformedResults = ccAclResults.map((row) => mapDocRowToContent(row, collIdByName.get(row.type_id) ?? null));
1903
2042
  const responseData = {
1904
2043
  data: transformedResults,
1905
2044
  meta: addTimingMeta(c, {
1906
2045
  ...includeCollection ? { collection: collectionRecordToRow(record) } : {},
1907
- count: results.length,
2046
+ count: ccAclResults.length,
1908
2047
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1909
2048
  // D44: echo the caller's filter with the access policy applied (status=published forced for
1910
2049
  // anonymous callers — the visible enforcement proof), NOT the internal doc-augmented where-tree.
@@ -1934,6 +2073,8 @@ apiRoutes.get("/:collection", optionalAuth(), async (c) => {
1934
2073
  const collection = c.req.param("collection");
1935
2074
  const db = c.env.DB;
1936
2075
  const queryParams = c.req.query();
2076
+ const projFields = parseFieldsParam(queryParams.fields);
2077
+ if (queryParams.fields) delete queryParams.fields;
1937
2078
  const record = getCollectionRegistry().getBySlugOrName(collection);
1938
2079
  if (!record || record.isActive === false) {
1939
2080
  return c.json({ error: "Collection not found" }, 404);
@@ -1941,6 +2082,16 @@ apiRoutes.get("/:collection", optionalAuth(), async (c) => {
1941
2082
  const collIdByName = /* @__PURE__ */ new Map();
1942
2083
  collIdByName.set(record.name, record.id);
1943
2084
  const role = c.get("user")?.role;
2085
+ const { tenantId, principalSet } = getDocumentRequestContext(c);
2086
+ const anon = isAnonPrincipal(principalSet);
2087
+ const needsAcl = !anon && !canReadNonPublicContent(role);
2088
+ let docTypeForCheck;
2089
+ if (needsAcl) {
2090
+ docTypeForCheck = await new DocumentTypeRegistry(db).findById(record.name);
2091
+ if (!await typeReadAllowed(db, principalSet, docTypeForCheck, record.name)) {
2092
+ return c.json({ error: "Forbidden" }, 403);
2093
+ }
2094
+ }
1944
2095
  const filter = QueryFilterBuilder.parseFromQuery(queryParams);
1945
2096
  const normalizedFilter = augmentFilterForDocuments(filter, { typeId: record.name, role });
1946
2097
  if (!normalizedFilter.limit) normalizedFilter.limit = 50;
@@ -1952,7 +2103,7 @@ apiRoutes.get("/:collection", optionalAuth(), async (c) => {
1952
2103
  }
1953
2104
  const collectionCache = record.cache;
1954
2105
  const collectionCacheDisabled = collectionCache?.enabled === false;
1955
- const cacheEnabled = c.get("cacheEnabled") && !collectionCacheDisabled;
2106
+ const cacheEnabled = c.get("cacheEnabled") && !collectionCacheDisabled && projFields.length === 0 && anon;
1956
2107
  const cache = getCacheService(CACHE_CONFIGS.api);
1957
2108
  const includeCollection = queryParams.include?.split(",").map((s) => s.trim()).includes("collection");
1958
2109
  const cacheKey = cache.generateKey("collection-content-filtered", `${collection}:${JSON.stringify({ filter: normalizedFilter, query: queryResult.sql, includeCollection })}`);
@@ -1970,12 +2121,25 @@ apiRoutes.get("/:collection", optionalAuth(), async (c) => {
1970
2121
  const stmt = db.prepare(queryResult.sql);
1971
2122
  const boundStmt = queryResult.params.length > 0 ? stmt.bind(...queryResult.params) : stmt;
1972
2123
  const { results } = await boundStmt.all();
1973
- const transformedResults = results.map((row) => mapDocRowToContent(row, collIdByName.get(row.type_id) ?? null));
2124
+ let aclResults = results;
2125
+ if (needsAcl) {
2126
+ const perms = new DocumentPermissionsService(db);
2127
+ const baseGrantsPass = perms.isAllowedSync(principalSet, [], "read", docTypeForCheck?.settings ?? {});
2128
+ if (!baseGrantsPass) {
2129
+ } else {
2130
+ const repo = new DocumentRepository(db, tenantId);
2131
+ const allowed = await Promise.all(
2132
+ results.map((row) => repo.isAllowed(principalSet, row.root_id, "read", docTypeForCheck?.settings ?? {}))
2133
+ );
2134
+ aclResults = results.filter((_, i) => allowed[i]);
2135
+ }
2136
+ }
2137
+ const transformedResults = aclResults.map((row) => mapDocRowToContent(row, collIdByName.get(row.type_id) ?? null));
1974
2138
  const responseData = {
1975
- data: transformedResults,
2139
+ data: projFields.length ? transformedResults.map((item) => projectFields(item, projFields)) : transformedResults,
1976
2140
  meta: addTimingMeta(c, {
1977
2141
  ...includeCollection ? { collection: collectionRecordToRow(record) } : {},
1978
- count: results.length,
2142
+ count: aclResults.length,
1979
2143
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1980
2144
  filter: normalizePublicContentFilter(filter, role),
1981
2145
  cache: { hit: false, source: "database" }
@@ -2006,6 +2170,23 @@ apiRoutes.get("/:collection/:id", optionalAuth(), async (c) => {
2006
2170
  privileged ? "SELECT * FROM documents WHERE root_id = ? AND type_id = ? AND tenant_id = 'default' AND is_current_draft = 1 AND deleted_at IS NULL" : "SELECT * FROM documents WHERE root_id = ? AND type_id = ? AND tenant_id = 'default' AND is_published = 1 AND deleted_at IS NULL"
2007
2171
  ).bind(id, record.name).first();
2008
2172
  if (!docRow) return c.json({ error: "Content not found" }, 404);
2173
+ const colRole = c.get("user")?.role;
2174
+ const { tenantId: colTenantId, principalSet: colPrincipalSet } = getDocumentRequestContext(c);
2175
+ const colNeedsAcl = !isAnonPrincipal(colPrincipalSet) && !canReadNonPublicContent(colRole);
2176
+ if (colNeedsAcl) {
2177
+ const docType = await new DocumentTypeRegistry(db).findById(record.name);
2178
+ if (!await typeReadAllowed(db, colPrincipalSet, docType, record.name)) {
2179
+ return c.json({ error: "Content not found" }, 404);
2180
+ }
2181
+ const perms = new DocumentPermissionsService(db);
2182
+ if (perms.isAllowedSync(colPrincipalSet, [], "read", docType?.settings ?? {})) {
2183
+ const repo = new DocumentRepository(db, colTenantId);
2184
+ if (!await repo.isAllowed(colPrincipalSet, docRow.root_id, "read", docType?.settings ?? {})) {
2185
+ return c.json({ error: "Content not found" }, 404);
2186
+ }
2187
+ }
2188
+ }
2189
+ const projFields = parseFieldsParam(c.req.query("fields"));
2009
2190
  const coll = getCollectionRegistry().getByName(docRow.type_id);
2010
2191
  const transformedContent = {
2011
2192
  id: docRow.root_id,
@@ -2018,7 +2199,7 @@ apiRoutes.get("/:collection/:id", optionalAuth(), async (c) => {
2018
2199
  updated_at: documentSecondsToMs(docRow.updated_at)
2019
2200
  };
2020
2201
  dispatchHookEvent(c, "content:read", { collection: docRow.type_id, id: docRow.root_id, data: transformedContent.data }, "fire-and-forget");
2021
- return c.json({ data: transformedContent });
2202
+ return c.json({ data: projFields.length ? projectFields(transformedContent, projFields) : transformedContent });
2022
2203
  } catch (error) {
2023
2204
  console.error("Error fetching content:", error);
2024
2205
  return c.json({ error: "Failed to fetch content", details: error instanceof Error ? error.message : String(error) }, 500);
@@ -2048,8 +2229,8 @@ apiRoutes.post("/:collection", requireAuth(), requireRole(["admin", "editor", "a
2048
2229
  const svc = new DocumentsService(db, { queryableFields: backing.docType.queryableFields ?? [], typeSchemaVersion: backing.docType.schemaVersion ?? 1, maxVersionsPerRoot: backing.docType.settings?.maxVersionsPerRoot ?? 50, tenantId: "default", versioning: backing.docType.settings?.versioning ?? false });
2049
2230
  const doc = await svc.create(createDocumentSchema.parse({ typeId: backing.coll.name, tenantId: "default", locale: "default", title, slug: finalSlug, data: hookData, publishOnCreate: (status || "draft") === "published" }), user?.userId);
2050
2231
  const cache = getCacheService(CACHE_CONFIGS.api);
2051
- await cache.invalidate("content-filtered:*");
2052
- await cache.invalidate("collection-content-filtered:*");
2232
+ await cache.invalidate("api:content-filtered:*");
2233
+ await cache.invalidate(`api:collection-content-filtered:${backing.coll.name}:*`);
2053
2234
  dispatchHookEvent(c, "content:after:create", { collection: backing.coll.name, id: doc.rootId, data: doc.data ?? {}, user: actor }, "fire-and-forget");
2054
2235
  return c.json({ data: { id: doc.rootId, title: doc.title, slug: doc.slug, status: doc.status, collectionId: backing.coll.id, data: doc.data, created_at: documentSecondsToMs(doc.createdAt), updated_at: documentSecondsToMs(doc.updatedAt) } }, 201);
2055
2236
  } catch (error) {
@@ -2091,8 +2272,8 @@ apiRoutes.put("/:collection/:id", requireAuth(), requireRole(["admin", "editor",
2091
2272
  await svc.unpublish(pub.id);
2092
2273
  }
2093
2274
  const cache = getCacheService(CACHE_CONFIGS.api);
2094
- await cache.invalidate("content-filtered:*");
2095
- await cache.invalidate("collection-content-filtered:*");
2275
+ await cache.invalidate("api:content-filtered:*");
2276
+ await cache.invalidate(`api:collection-content-filtered:${docRow.type_id}:*`);
2096
2277
  const coll = getCollectionRegistry().getByName(docRow.type_id);
2097
2278
  const saved = await db.prepare("SELECT * FROM documents WHERE id = ?").bind(newDraft.id).first();
2098
2279
  const savedData = saved?.data ? JSON.parse(saved.data) : {};
@@ -2124,8 +2305,8 @@ apiRoutes.delete("/:collection/:id", requireAuth(), requireRole(["admin", "edito
2124
2305
  const now = Math.floor(Date.now() / 1e3);
2125
2306
  await db.prepare("UPDATE documents SET deleted_at = ?, updated_at = ? WHERE root_id = ? AND tenant_id = 'default'").bind(now, now, id).run();
2126
2307
  const cache = getCacheService(CACHE_CONFIGS.api);
2127
- await cache.invalidate("content-filtered:*");
2128
- await cache.invalidate("collection-content-filtered:*");
2308
+ await cache.invalidate("api:content-filtered:*");
2309
+ await cache.invalidate(`api:collection-content-filtered:${docRow.type_id}:*`);
2129
2310
  dispatchHookEvent(c, "content:after:delete", { collection: docRow.type_id, id, data: {}, user: actor }, "fire-and-forget");
2130
2311
  return c.json({ success: true });
2131
2312
  } catch (error) {
@@ -2687,9 +2868,13 @@ apiMediaRoutes.post("/create-folder", async (c) => {
2687
2868
  error: "Folder name can only contain lowercase letters, numbers, hyphens, and underscores"
2688
2869
  }, 400);
2689
2870
  }
2690
- const checkStmt = c.env.DB.prepare("SELECT COUNT(*) as count FROM media WHERE folder = ? AND deleted_at IS NULL");
2691
- const existingFolder = await checkStmt.bind(folderName).first();
2692
- if (existingFolder && existingFolder.count > 0) {
2871
+ let folderExists = false;
2872
+ try {
2873
+ const existing = await c.env.DB.prepare("SELECT COUNT(*) as count FROM documents WHERE type_id = 'media_asset' AND tenant_id = 'default' AND json_extract(data, '$.folder') = ? AND deleted_at IS NULL AND is_current_draft = 1").bind(folderName).first();
2874
+ folderExists = (existing?.count ?? 0) > 0;
2875
+ } catch {
2876
+ }
2877
+ if (folderExists) {
2693
2878
  return c.json({
2694
2879
  success: false,
2695
2880
  error: `Folder "${folderName}" already exists`
@@ -3399,7 +3584,7 @@ adminApiRoutes.patch("/collections/:id", collectionsReadOnly);
3399
3584
  adminApiRoutes.delete("/collections/:id", collectionsReadOnly);
3400
3585
  adminApiRoutes.get("/migrations/status", async (c) => {
3401
3586
  try {
3402
- const { MigrationService: MigrationService2 } = await import('./migrations-XQ6EX6HX.js');
3587
+ const { MigrationService: MigrationService2 } = await import('./migrations-JAHMI5MK.js');
3403
3588
  const db = c.env.DB;
3404
3589
  const migrationService = new MigrationService2(db);
3405
3590
  const status = await migrationService.getMigrationStatus();
@@ -3442,7 +3627,7 @@ adminApiRoutes.post("/migrations/run", async (c) => {
3442
3627
  });
3443
3628
  adminApiRoutes.get("/migrations/validate", async (c) => {
3444
3629
  try {
3445
- const { MigrationService: MigrationService2 } = await import('./migrations-XQ6EX6HX.js');
3630
+ const { MigrationService: MigrationService2 } = await import('./migrations-JAHMI5MK.js');
3446
3631
  const db = c.env.DB;
3447
3632
  const migrationService = new MigrationService2(db);
3448
3633
  const validation = await migrationService.validateSchema();
@@ -3601,6 +3786,8 @@ function renderLoginPage(data, demoLoginActive = false) {
3601
3786
  <!-- Form -->
3602
3787
  <form
3603
3788
  id="login-form"
3789
+ action="/auth/login/form${data.redirect ? `?redirect=${encodeURIComponent(data.redirect)}` : ""}"
3790
+ method="post"
3604
3791
  hx-post="/auth/login/form${data.redirect ? `?redirect=${encodeURIComponent(data.redirect)}` : ""}"
3605
3792
  hx-target="#form-response"
3606
3793
  hx-swap="innerHTML"
@@ -5116,9 +5303,11 @@ function renderDynamicField(field, options = {}) {
5116
5303
  min="${opts.min || ""}"
5117
5304
  max="${opts.max || ""}"
5118
5305
  class="${baseClasses} ${errorClasses} !w-auto dark:[color-scheme:dark]"
5306
+ placeholder="YYYY-MM-DD"
5119
5307
  ${required}
5120
5308
  ${disabled ? "disabled" : ""}
5121
5309
  >
5310
+ <p class="mt-1 text-xs text-zinc-500 dark:text-zinc-400">Format: YYYY-MM-DD</p>
5122
5311
  `;
5123
5312
  break;
5124
5313
  case "datetime":
@@ -5134,6 +5323,7 @@ function renderDynamicField(field, options = {}) {
5134
5323
  ${required}
5135
5324
  ${disabled ? "disabled" : ""}
5136
5325
  >
5326
+ <p class="mt-1 text-xs text-zinc-500 dark:text-zinc-400">Format: YYYY-MM-DD HH:MM</p>
5137
5327
  `;
5138
5328
  break;
5139
5329
  case "slug":
@@ -7090,6 +7280,21 @@ function createUserProfilesPlugin() {
7090
7280
 
7091
7281
  // src/routes/auth.ts
7092
7282
  var JWT_SECRET_FALLBACK = "your-super-secret-jwt-key-change-in-production";
7283
+ var DEMO_EMAIL = "admin@sonicjs.com";
7284
+ var STATS_EVENTS_ENDPOINT = "https://stats.sonicjs.com/v1/events";
7285
+ async function trackDemoLogin(kv) {
7286
+ try {
7287
+ const installationId = await kv?.get("_sonicjs_installation_id") ?? "demo-unknown";
7288
+ await fetch(STATS_EVENTS_ENDPOINT, {
7289
+ method: "POST",
7290
+ headers: { "Content-Type": "application/json" },
7291
+ body: JSON.stringify({
7292
+ data: { installation_id: installationId, event_type: "demo_login", properties: {}, timestamp: (/* @__PURE__ */ new Date()).toISOString() }
7293
+ })
7294
+ });
7295
+ } catch {
7296
+ }
7297
+ }
7093
7298
  function renderPasswordResetEmail(resetLink, firstName) {
7094
7299
  const greeting = firstName ? `Hi ${firstName},` : "Hello,";
7095
7300
  return `<!DOCTYPE html>
@@ -7281,8 +7486,8 @@ authRoutes.post(
7281
7486
  }
7282
7487
  const { email, password } = validation.data;
7283
7488
  const normalizedEmail = email.toLowerCase();
7284
- const { createAuth } = await import('./config-6FB2BXMP.js');
7285
- const auth = createAuth(c.env);
7489
+ const { createAuth } = await import('./config-4TIHSNW6.js');
7490
+ const auth = createAuth(c.env, void 0, new URL(c.req.url).origin);
7286
7491
  const baReq = new Request(new URL("/auth/sign-in/email", c.req.url).href, {
7287
7492
  method: "POST",
7288
7493
  headers: { "Content-Type": "application/json", "Origin": new URL(c.req.url).origin },
@@ -7292,12 +7497,14 @@ authRoutes.post(
7292
7497
  if (!baRes.ok) {
7293
7498
  return c.json({ error: "Invalid email or password" }, 401);
7294
7499
  }
7295
- const rawSetCookie = baRes.headers.get("set-cookie");
7296
- if (rawSetCookie) {
7297
- c.res.headers.append("Set-Cookie", rawSetCookie);
7298
- } else if (baRes.headers.getSetCookie) {
7500
+ if (baRes.headers.getSetCookie) {
7299
7501
  for (const sc of baRes.headers.getSetCookie()) {
7300
- c.res.headers.append("Set-Cookie", sc);
7502
+ c.header("Set-Cookie", sc, { append: true });
7503
+ }
7504
+ } else {
7505
+ const rawSetCookie = baRes.headers.get("set-cookie");
7506
+ if (rawSetCookie) {
7507
+ c.header("Set-Cookie", rawSetCookie, { append: true });
7301
7508
  }
7302
7509
  }
7303
7510
  await setCsrfCookie(c);
@@ -7317,6 +7524,9 @@ authRoutes.post(
7317
7524
  sameSite: "Strict",
7318
7525
  maxAge: tokenTtl
7319
7526
  });
7527
+ if (normalizedEmail === DEMO_EMAIL) {
7528
+ c.executionCtx?.waitUntil(trackDemoLogin(c.env.CACHE_KV));
7529
+ }
7320
7530
  return c.json({
7321
7531
  user: {
7322
7532
  id: user.id,
@@ -7335,8 +7545,8 @@ authRoutes.post(
7335
7545
  );
7336
7546
  authRoutes.post("/logout", async (c) => {
7337
7547
  try {
7338
- const { createAuth } = await import('./config-6FB2BXMP.js');
7339
- const auth = createAuth(c.env);
7548
+ const { createAuth } = await import('./config-4TIHSNW6.js');
7549
+ const auth = createAuth(c.env, void 0, new URL(c.req.url).origin);
7340
7550
  const baReq = new Request(new URL("/auth/sign-out", c.req.url).href, {
7341
7551
  method: "POST",
7342
7552
  headers: { "Content-Type": "application/json", "Origin": new URL(c.req.url).origin, "Cookie": c.req.header("Cookie") || "" },
@@ -7345,14 +7555,15 @@ authRoutes.post("/logout", async (c) => {
7345
7555
  await auth.handler(baReq);
7346
7556
  } catch {
7347
7557
  }
7348
- setCookie(c, "better-auth.session_token", "", { httpOnly: true, sameSite: "Lax", path: "/", maxAge: 0 });
7558
+ const isSecure = new URL(c.req.url).protocol === "https:";
7559
+ setCookie(c, "better-auth.session_token", "", { httpOnly: true, sameSite: "Lax", path: "/", maxAge: 0, secure: isSecure });
7349
7560
  clearCsrfCookie(c);
7350
7561
  return c.json({ message: "Logged out successfully" });
7351
7562
  });
7352
7563
  authRoutes.get("/logout", async (c) => {
7353
7564
  try {
7354
- const { createAuth } = await import('./config-6FB2BXMP.js');
7355
- const auth = createAuth(c.env);
7565
+ const { createAuth } = await import('./config-4TIHSNW6.js');
7566
+ const auth = createAuth(c.env, void 0, new URL(c.req.url).origin);
7356
7567
  const baReq = new Request(new URL("/auth/sign-out", c.req.url).href, {
7357
7568
  method: "POST",
7358
7569
  headers: { "Content-Type": "application/json", "Origin": new URL(c.req.url).origin, "Cookie": c.req.header("Cookie") || "" },
@@ -7361,7 +7572,8 @@ authRoutes.get("/logout", async (c) => {
7361
7572
  await auth.handler(baReq);
7362
7573
  } catch {
7363
7574
  }
7364
- setCookie(c, "better-auth.session_token", "", { httpOnly: true, sameSite: "Lax", path: "/", maxAge: 0 });
7575
+ const isSecure = new URL(c.req.url).protocol === "https:";
7576
+ setCookie(c, "better-auth.session_token", "", { httpOnly: true, sameSite: "Lax", path: "/", maxAge: 0, secure: isSecure });
7365
7577
  clearCsrfCookie(c);
7366
7578
  return c.redirect("/auth/login?message=You have been logged out successfully");
7367
7579
  });
@@ -7558,8 +7770,8 @@ authRoutes.post(
7558
7770
  </div>
7559
7771
  `);
7560
7772
  }
7561
- const { createAuth } = await import('./config-6FB2BXMP.js');
7562
- const auth = createAuth(c.env);
7773
+ const { createAuth } = await import('./config-4TIHSNW6.js');
7774
+ const auth = createAuth(c.env, void 0, new URL(c.req.url).origin);
7563
7775
  const baReq = new Request(new URL("/auth/sign-in/email", c.req.url).href, {
7564
7776
  method: "POST",
7565
7777
  headers: { "Content-Type": "application/json", "Origin": new URL(c.req.url).origin },
@@ -7573,17 +7785,27 @@ authRoutes.post(
7573
7785
  </div>
7574
7786
  `);
7575
7787
  }
7576
- const rawSetCookie = baRes.headers.get("set-cookie");
7577
- if (rawSetCookie) {
7578
- c.res.headers.append("Set-Cookie", rawSetCookie);
7579
- } else if (baRes.headers.getSetCookie) {
7788
+ if (baRes.headers.getSetCookie) {
7580
7789
  for (const sc of baRes.headers.getSetCookie()) {
7581
- c.res.headers.append("Set-Cookie", sc);
7790
+ c.header("Set-Cookie", sc, { append: true });
7791
+ }
7792
+ } else {
7793
+ const rawSetCookie = baRes.headers.get("set-cookie");
7794
+ if (rawSetCookie) {
7795
+ c.header("Set-Cookie", rawSetCookie, { append: true });
7582
7796
  }
7583
7797
  }
7584
7798
  await setCsrfCookie(c);
7799
+ if (email === DEMO_EMAIL) {
7800
+ c.executionCtx?.waitUntil(trackDemoLogin(c.env.CACHE_KV));
7801
+ }
7585
7802
  const rawRedirect = c.req.query("redirect");
7586
7803
  const redirectUrl = rawRedirect && rawRedirect.startsWith("/") ? rawRedirect : "/admin/content";
7804
+ const isHtmx = c.req.header("HX-Request") === "true";
7805
+ if (isHtmx) {
7806
+ c.header("HX-Redirect", redirectUrl);
7807
+ return c.html(html`<div id="form-response"><p class="text-sm text-green-600">Login successful! Redirecting...</p></div>`);
7808
+ }
7587
7809
  return c.html(html`
7588
7810
  <div id="form-response">
7589
7811
  <div class="rounded-lg bg-green-100 dark:bg-lime-500/10 p-4 ring-1 ring-green-400 dark:ring-lime-500/20">
@@ -9008,20 +9230,20 @@ function renderContentFormPage(data, opts) {
9008
9230
  <dl class="space-y-3 text-sm">
9009
9231
  <div>
9010
9232
  <dt class="text-zinc-500 dark:text-zinc-400">Created</dt>
9011
- <dd class="mt-1 text-zinc-950 dark:text-white">${data.created_at ? new Date(data.created_at).toLocaleDateString() : "Unknown"}</dd>
9233
+ <dd class="mt-1 text-zinc-950 dark:text-white">${data.created_at ? new Date(data.created_at * 1e3).toLocaleDateString() : "Unknown"}</dd>
9012
9234
  </div>
9013
9235
  <div>
9014
9236
  <dt class="text-zinc-500 dark:text-zinc-400">Last Modified</dt>
9015
- <dd class="mt-1 text-zinc-950 dark:text-white">${data.updated_at ? new Date(data.updated_at).toLocaleDateString() : "Unknown"}</dd>
9237
+ <dd class="mt-1 text-zinc-950 dark:text-white">${data.updated_at ? new Date(data.updated_at * 1e3).toLocaleDateString() : "Unknown"}</dd>
9016
9238
  </div>
9017
9239
  <div>
9018
9240
  <dt class="text-zinc-500 dark:text-zinc-400">Author</dt>
9019
- <dd class="mt-1 text-zinc-950 dark:text-white">${data.data?.author || "Unknown"}</dd>
9241
+ <dd class="mt-1 text-zinc-950 dark:text-white">${escapeHtml(String(data.data?.author || data.author_name || "Unknown"))}</dd>
9020
9242
  </div>
9021
9243
  ${data.published_at ? `
9022
9244
  <div>
9023
9245
  <dt class="text-zinc-500 dark:text-zinc-400">Published</dt>
9024
- <dd class="mt-1 text-zinc-950 dark:text-white">${new Date(data.published_at).toLocaleDateString()}</dd>
9246
+ <dd class="mt-1 text-zinc-950 dark:text-white">${new Date(data.published_at * 1e3).toLocaleDateString()}</dd>
9025
9247
  </div>
9026
9248
  ` : ""}
9027
9249
  </dl>
@@ -9095,33 +9317,44 @@ function renderContentFormPage(data, opts) {
9095
9317
  </a>
9096
9318
 
9097
9319
  <div class="flex items-center gap-x-3">
9098
- ${data.versioningEnabled ? `
9099
- <button
9100
- type="submit"
9101
- form="content-form"
9102
- name="action"
9103
- value="save"
9104
- class="inline-flex items-center justify-center gap-x-1.5 rounded-lg bg-zinc-950 dark:bg-white px-3.5 py-2.5 text-sm font-semibold text-white dark:text-zinc-950 hover:bg-zinc-800 dark:hover:bg-zinc-100 transition-colors shadow-sm"
9105
- >
9106
- <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
9107
- <path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/>
9108
- </svg>
9109
- ${isEdit ? "Update" : "Save"}
9110
- </button>
9111
- ` : ""}
9320
+ <button
9321
+ type="submit"
9322
+ form="content-form"
9323
+ name="action"
9324
+ value="save"
9325
+ class="inline-flex items-center justify-center gap-x-1.5 rounded-lg bg-zinc-950 dark:bg-white px-3.5 py-2.5 text-sm font-semibold text-white dark:text-zinc-950 hover:bg-zinc-800 dark:hover:bg-zinc-100 transition-colors shadow-sm"
9326
+ >
9327
+ <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
9328
+ <path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/>
9329
+ </svg>
9330
+ Save
9331
+ </button>
9332
+
9333
+ <button
9334
+ type="submit"
9335
+ form="content-form"
9336
+ name="action"
9337
+ value="save_and_close"
9338
+ class="inline-flex items-center justify-center gap-x-1.5 rounded-lg bg-white dark:bg-zinc-800 px-3.5 py-2.5 text-sm font-semibold text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 hover:bg-zinc-50 dark:hover:bg-zinc-700 transition-colors shadow-sm"
9339
+ >
9340
+ <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
9341
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
9342
+ </svg>
9343
+ Save & close
9344
+ </button>
9112
9345
 
9113
- ${data.user?.role !== "viewer" ? `
9346
+ ${data.user?.role !== "viewer" && data.versioningEnabled ? `
9114
9347
  <button
9115
9348
  type="submit"
9116
9349
  form="content-form"
9117
9350
  name="action"
9118
9351
  value="save_and_publish"
9119
- class="inline-flex items-center justify-center gap-x-1.5 rounded-lg ${data.versioningEnabled ? "bg-lime-600 dark:bg-lime-500 hover:bg-lime-700 dark:hover:bg-lime-600" : "bg-zinc-950 dark:bg-white hover:bg-zinc-800 dark:hover:bg-zinc-100 text-white dark:text-zinc-950"} px-3.5 py-2.5 text-sm font-semibold text-white transition-colors shadow-sm"
9352
+ class="inline-flex items-center justify-center gap-x-1.5 rounded-lg bg-lime-600 dark:bg-lime-500 hover:bg-lime-700 dark:hover:bg-lime-600 px-3.5 py-2.5 text-sm font-semibold text-white transition-colors shadow-sm"
9120
9353
  >
9121
9354
  <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
9122
9355
  <path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
9123
9356
  </svg>
9124
- ${data.versioningEnabled ? `${isEdit ? "Update" : "Save"} & Publish` : isEdit ? "Update" : "Save"}
9357
+ Publish
9125
9358
  </button>
9126
9359
  ` : ""}
9127
9360
  </div>
@@ -11623,6 +11856,14 @@ adminContentRoutes.get("/", async (c) => {
11623
11856
  docSql += " ORDER BY updated_at DESC LIMIT ? OFFSET ?";
11624
11857
  docParams.push(limit, offset);
11625
11858
  const { results: docRows } = await db.prepare(docSql).bind(...docParams).all();
11859
+ const docUserIds = [...new Set((docRows ?? []).map((r) => r.created_by).filter(Boolean))];
11860
+ const docAuthorMap = {};
11861
+ for (let i = 0; i < docUserIds.length; i += 99) {
11862
+ const chunk = docUserIds.slice(i, i + 99);
11863
+ const ph = chunk.map(() => "?").join(",");
11864
+ const { results: uRows } = await db.prepare(`SELECT id, email FROM auth_user WHERE id IN (${ph})`).bind(...chunk).all();
11865
+ for (const u of uRows ?? []) docAuthorMap[u.id] = u.email;
11866
+ }
11626
11867
  const statusBadgeCss = {
11627
11868
  published: "bg-green-50 dark:bg-green-500/10 text-green-700 dark:text-green-400 ring-1 ring-inset ring-green-600/20 dark:ring-green-500/20",
11628
11869
  draft: "bg-zinc-50 dark:bg-zinc-500/10 text-zinc-700 dark:text-zinc-400 ring-1 ring-inset ring-zinc-600/20 dark:ring-zinc-500/20"
@@ -11639,7 +11880,7 @@ adminContentRoutes.get("/", async (c) => {
11639
11880
  slug: row.slug || "",
11640
11881
  modelName: docType?.displayName ?? typeId,
11641
11882
  statusBadge: `<span class="inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ${css}">${label}</span>`,
11642
- authorName: row.created_by || "System",
11883
+ authorName: (row.created_by ? docAuthorMap[row.created_by] ?? row.created_by : null) || "System",
11643
11884
  // Document timestamps are stored in SECONDS (documents.ts), unlike legacy content rows which
11644
11885
  // store MILLISECONDS — hence the *1000 here (D23).
11645
11886
  formattedDate: new Date((row.updated_at ?? 0) * 1e3).toLocaleDateString(),
@@ -11687,8 +11928,8 @@ adminContentRoutes.get("/", async (c) => {
11687
11928
  SELECT d.root_id AS id, d.title AS title, d.slug AS slug,
11688
11929
  CASE WHEN d.is_published = 1 THEN 'published' ELSE 'draft' END AS status,
11689
11930
  d.updated_at * 1000 AS updated_at,
11690
- dt.display_name AS cdisplay, COALESCE(d.created_by, 'System') AS author_label
11691
- FROM documents d JOIN document_types dt ON dt.id = d.type_id
11931
+ dt.display_name AS cdisplay, COALESCE(u.email, d.created_by, 'System') AS author_label
11932
+ FROM documents d JOIN document_types dt ON dt.id = d.type_id LEFT JOIN auth_user u ON u.id = d.created_by
11692
11933
  WHERE ${docConds.join(" AND ")}
11693
11934
  ORDER BY updated_at DESC LIMIT ? OFFSET ?`;
11694
11935
  const { results: unionRows } = await db.prepare(unionSql).bind(...docParams, limit, offset).all();
@@ -12029,6 +12270,13 @@ adminContentRoutes.get("/:id/edit", async (c) => {
12029
12270
  if (docType && dcoll) {
12030
12271
  const fields2 = await getCollectionFields(db, dcoll.id);
12031
12272
  const flags = await loadContentEditorFlags(db);
12273
+ let authorName;
12274
+ if (docRow.created_by) {
12275
+ const authorRow = await db.prepare("SELECT name, first_name, last_name, email FROM auth_user WHERE id = ?").bind(docRow.created_by).first();
12276
+ if (authorRow) {
12277
+ authorName = authorRow.name || [authorRow.first_name, authorRow.last_name].filter(Boolean).join(" ") || authorRow.email;
12278
+ }
12279
+ }
12032
12280
  const formData2 = {
12033
12281
  id: docRow.root_id,
12034
12282
  title: docRow.title,
@@ -12043,6 +12291,7 @@ adminContentRoutes.get("/:id/edit", async (c) => {
12043
12291
  isEdit: true,
12044
12292
  referrerParams,
12045
12293
  versioningEnabled: docType?.settings?.versioning === true,
12294
+ author_name: authorName,
12046
12295
  user: user ? { name: user.email, email: user.email, role: user.role } : void 0,
12047
12296
  version: c.get("appVersion"),
12048
12297
  ...flags
@@ -12202,6 +12451,7 @@ adminContentRoutes.post("/", async (c) => {
12202
12451
  const fields = await getCollectionFields(db, collectionId);
12203
12452
  const { data, errors } = extractFieldData(fields, formData);
12204
12453
  if (Object.keys(errors).length > 0) {
12454
+ const flags = await loadContentEditorFlags(db);
12205
12455
  const formDataWithErrors = {
12206
12456
  collection,
12207
12457
  fields,
@@ -12212,7 +12462,8 @@ adminContentRoutes.post("/", async (c) => {
12212
12462
  name: user.email,
12213
12463
  email: user.email,
12214
12464
  role: user.role
12215
- } : void 0
12465
+ } : void 0,
12466
+ ...flags
12216
12467
  };
12217
12468
  if (c.req.header("HX-Request") === "true") {
12218
12469
  c.header("HX-Retarget", "#content-form-page");
@@ -12246,6 +12497,9 @@ adminContentRoutes.post("/", async (c) => {
12246
12497
  }), user?.userId);
12247
12498
  const cache2 = getCacheService(CACHE_CONFIGS.content);
12248
12499
  await cache2.invalidate(`content:list:${collectionId}:*`);
12500
+ const apiCacheCreate = getCacheService(CACHE_CONFIGS.api);
12501
+ await apiCacheCreate.invalidate("api:content-filtered:*");
12502
+ await apiCacheCreate.invalidate(`api:collection-content-filtered:${createDocType.id}:*`);
12249
12503
  const redirectUrl2 = `/admin/content/${doc.rootId}/edit?success=Content created successfully!`;
12250
12504
  return c.req.header("HX-Request") === "true" ? c.text("", 200, { "HX-Redirect": redirectUrl2 }) : c.redirect(redirectUrl2);
12251
12505
  }
@@ -12364,8 +12618,28 @@ adminContentRoutes.put("/:id", async (c) => {
12364
12618
  if (status2 === "published") await svc.publish(newDraft.id, user?.userId);
12365
12619
  else if (pub) await svc.unpublish(pub.id);
12366
12620
  await getCacheService(CACHE_CONFIGS.content).invalidate(`content:list:*`);
12621
+ const apiCacheEdit = getCacheService(CACHE_CONFIGS.api);
12622
+ await apiCacheEdit.invalidate("api:content-filtered:*");
12623
+ await apiCacheEdit.invalidate(`api:collection-content-filtered:${docRowU.type_id}:*`);
12624
+ const isHTMX2 = c.req.header("HX-Request") === "true";
12625
+ const referrerParams = formData.get("referrer_params");
12626
+ const listUrl = referrerParams ? `/admin/content?${referrerParams}` : `/admin/content?collection=${dcoll.id}`;
12627
+ if (action === "save_and_close") {
12628
+ return isHTMX2 ? c.text("", 200, { "HX-Redirect": listUrl }) : c.redirect(listUrl);
12629
+ }
12630
+ if (isHTMX2) {
12631
+ const label = status2 === "published" ? "Content published successfully!" : "Content saved successfully!";
12632
+ return c.html(html`<div id="form-messages">
12633
+ <div class="rounded-lg bg-green-50 dark:bg-green-900/20 p-4 mb-4 flex items-center gap-3 border border-green-200 dark:border-green-800">
12634
+ <svg class="h-5 w-5 text-green-500 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
12635
+ <path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
12636
+ </svg>
12637
+ <span class="text-sm font-medium text-green-800 dark:text-green-200">${label}</span>
12638
+ </div>
12639
+ </div>`);
12640
+ }
12367
12641
  const redirectUrl2 = `/admin/content/${id}/edit?success=Content updated successfully!`;
12368
- return c.req.header("HX-Request") === "true" ? c.text("", 200, { "HX-Redirect": redirectUrl2 }) : c.redirect(redirectUrl2);
12642
+ return c.redirect(redirectUrl2);
12369
12643
  }
12370
12644
  }
12371
12645
  return c.html(html`
@@ -12393,6 +12667,7 @@ adminContentRoutes.put("/:id", async (c) => {
12393
12667
  const fields = await getCollectionFields(db, existingContent.collection_id);
12394
12668
  const { data, errors } = extractFieldData(fields, formData);
12395
12669
  if (Object.keys(errors).length > 0) {
12670
+ const flags = await loadContentEditorFlags(db);
12396
12671
  const formDataWithErrors = {
12397
12672
  id,
12398
12673
  collection,
@@ -12405,7 +12680,8 @@ adminContentRoutes.put("/:id", async (c) => {
12405
12680
  name: user.email,
12406
12681
  email: user.email,
12407
12682
  role: user.role
12408
- } : void 0
12683
+ } : void 0,
12684
+ ...flags
12409
12685
  };
12410
12686
  if (c.req.header("HX-Request") === "true") {
12411
12687
  c.header("HX-Retarget", "#content-form-page");
@@ -12757,8 +13033,8 @@ adminContentRoutes.post("/bulk-action", async (c) => {
12757
13033
  }
12758
13034
  await cache.invalidate("content:list:*");
12759
13035
  const apiCache = getCacheService(CACHE_CONFIGS.api);
12760
- await apiCache.invalidate("content-filtered:*");
12761
- await apiCache.invalidate("collection-content-filtered:*");
13036
+ await apiCache.invalidate("api:content-filtered:*");
13037
+ await apiCache.invalidate("api:collection-content-filtered:*");
12762
13038
  return c.json({ success: true, count: ids.length });
12763
13039
  } catch (error) {
12764
13040
  console.error("Bulk action error:", error);
@@ -12776,6 +13052,9 @@ adminContentRoutes.delete("/:id", async (c) => {
12776
13052
  const now2 = Math.floor(Date.now() / 1e3);
12777
13053
  await db.prepare("UPDATE documents SET deleted_at = ?, updated_at = ? WHERE root_id = ? AND tenant_id = ?").bind(now2, now2, id, tenantId).run();
12778
13054
  await getCacheService(CACHE_CONFIGS.content).invalidate("content:list:*");
13055
+ const apiCacheDel = getCacheService(CACHE_CONFIGS.api);
13056
+ await apiCacheDel.invalidate("api:content-filtered:*");
13057
+ await apiCacheDel.invalidate(`api:collection-content-filtered:${docDel.type_id}:*`);
12779
13058
  return c.html(`
12780
13059
  <div id="content-list" hx-get="/admin/content?model=${docDel.type_id}" hx-trigger="load" hx-swap="outerHTML">
12781
13060
  <div class="flex items-center justify-center p-8"><span class="text-zinc-500">Deleting\u2026</span></div>
@@ -13071,12 +13350,12 @@ adminContentRoutes.post("/documents/:typeId/:documentId/delete", async (c) => {
13071
13350
  adminContentRoutes.get("/documents/:typeId/:rootId/versions", async (c) => {
13072
13351
  const { typeId, rootId } = c.req.param();
13073
13352
  const db = c.env.DB;
13074
- const { renderVersionHistoryFragment } = await import('./admin-documents-form.template-55V2ZGWT.js');
13353
+ const { renderVersionHistoryFragment } = await import('./admin-documents-form.template-JWV5GITD.js');
13075
13354
  const registry = new DocumentTypeRegistry(db);
13076
13355
  const docType = await registry.findById(typeId);
13077
13356
  if (!docType) return c.html("<div>Unknown type.</div>", 404);
13078
13357
  const result = await db.prepare(
13079
- "SELECT id, version_number, is_current_draft, is_published, status, updated_at, created_by FROM documents WHERE root_id = ? AND tenant_id = ? ORDER BY version_number DESC LIMIT 50"
13358
+ "SELECT d.id, d.version_number, d.is_current_draft, d.is_published, d.status, d.updated_at, COALESCE(u.email, d.created_by) AS created_by FROM documents d LEFT JOIN auth_user u ON u.id = d.created_by WHERE d.root_id = ? AND d.tenant_id = ? ORDER BY d.version_number DESC LIMIT 50"
13080
13359
  ).bind(rootId, reqTenant(c)).all();
13081
13360
  const versions = (result.results ?? []).map((r) => ({
13082
13361
  id: r.id,
@@ -15326,12 +15605,10 @@ var LANGUAGES = [
15326
15605
  { value: "ko", label: "Korean" },
15327
15606
  { value: "zh", label: "Chinese" }
15328
15607
  ];
15329
- var ROLES = [
15330
- { value: "admin", label: "Administrator" },
15331
- { value: "editor", label: "Editor" },
15332
- { value: "author", label: "Author" },
15333
- { value: "viewer", label: "Viewer" }
15334
- ];
15608
+ async function fetchRoleOptions(db) {
15609
+ const roles = await new RbacService(db).getRoles();
15610
+ return roles.map((r) => ({ value: r.id, label: r.display_name }));
15611
+ }
15335
15612
  userRoutes.get("/profile", async (c) => {
15336
15613
  const user = c.get("user");
15337
15614
  const db = c.env.DB;
@@ -15780,8 +16057,9 @@ userRoutes.get("/users/new", async (c) => {
15780
16057
  const regFieldNames = profileConfig?.registrationFields ?? [];
15781
16058
  const regFields = profileConfig && upRow ? regFieldNames.length > 0 ? profileConfig.fields.filter((f) => regFieldNames.includes(f.name)) : profileConfig.fields : [];
15782
16059
  const registrationFieldsHtml = regFields.length > 0 ? renderCustomProfileSection({ fields: regFields }, {}) : void 0;
16060
+ const roles = await fetchRoleOptions(db);
15783
16061
  const pageData = {
15784
- roles: ROLES,
16062
+ roles,
15785
16063
  registrationFieldsHtml,
15786
16064
  user: {
15787
16065
  name: user.email.split("@")[0] || user.email,
@@ -15809,9 +16087,9 @@ userRoutes.post("/users/new", async (c) => {
15809
16087
  const email = formData.get("email")?.toString()?.trim().toLowerCase() || "";
15810
16088
  const phone = sanitizeInput(formData.get("phone")?.toString()) || null;
15811
16089
  const bio = sanitizeInput(formData.get("bio")?.toString()) || null;
15812
- const roleInput = formData.get("role")?.toString() || "viewer";
15813
- const validRoles = ["admin", "editor", "author", "viewer"];
15814
- const role = validRoles.includes(roleInput) ? roleInput : "viewer";
16090
+ const roleInput = formData.get("role")?.toString() || "";
16091
+ const validRoles = (await new RbacService(db).getRoles()).map((r) => r.id);
16092
+ const role = validRoles.includes(roleInput) ? roleInput : validRoles[0] ?? "admin";
15815
16093
  const password = formData.get("password")?.toString() || "";
15816
16094
  const confirmPassword = formData.get("confirm_password")?.toString() || "";
15817
16095
  const isActive = formData.get("is_active") === "1";
@@ -16033,9 +16311,10 @@ userRoutes.get("/users/:id/edit", async (c) => {
16033
16311
  }
16034
16312
  const queryMessage = c.req.query("message");
16035
16313
  const queryType = c.req.query("type") === "error" ? "error" : "success";
16314
+ const roles = await fetchRoleOptions(db);
16036
16315
  const pageData = {
16037
16316
  userToEdit: editData,
16038
- roles: ROLES,
16317
+ roles,
16039
16318
  hasProfilePlugin: profileConfig !== null && upRow !== null,
16040
16319
  customProfileFieldsHtml,
16041
16320
  tenantMemberships,
@@ -16067,9 +16346,9 @@ userRoutes.put("/users/:id", async (c) => {
16067
16346
  const lastName = sanitizeInput(formData.get("last_name")?.toString());
16068
16347
  const email = formData.get("email")?.toString()?.trim().toLowerCase() || "";
16069
16348
  const phone = sanitizeInput(formData.get("phone")?.toString()) || null;
16070
- const roleInput = formData.get("role")?.toString() || "viewer";
16071
- const validRoles = ["admin", "editor", "author", "viewer"];
16072
- const role = validRoles.includes(roleInput) ? roleInput : "viewer";
16349
+ const roleInput = formData.get("role")?.toString() || "";
16350
+ const validRoles = (await new RbacService(db).getRoles()).map((r) => r.id);
16351
+ const role = validRoles.includes(roleInput) ? roleInput : validRoles[0] ?? "admin";
16073
16352
  const isActive = formData.get("is_active") === "1";
16074
16353
  const emailVerified = formData.get("email_verified") === "1";
16075
16354
  const newPassword = formData.get("new_password")?.toString() || "";
@@ -18656,15 +18935,6 @@ function renderPluginsListPage(data) {
18656
18935
  </div>
18657
18936
 
18658
18937
  <div class="flex items-center gap-3 w-full sm:w-auto">
18659
- <select id="sort-filter" onchange="filterAndSortPlugins()" class="block w-full sm:w-auto h-9 rounded-md border-0 py-1.5 pl-3 pr-8 text-zinc-900 ring-1 ring-inset ring-zinc-300 focus:ring-2 focus:ring-inset focus:ring-zinc-600 dark:bg-zinc-900 dark:text-white dark:ring-zinc-700 dark:focus:ring-zinc-500 sm:text-sm sm:leading-6">
18660
- <option value="name-asc">Name (A-Z)</option>
18661
- <option value="name-desc">Name (Z-A)</option>
18662
- <option value="newest">Newest Installed</option>
18663
- <option value="updated">Recently Updated</option>
18664
- <option value="popular">Popularity</option>
18665
- <option value="rating">Highest Rated</option>
18666
- </select>
18667
-
18668
18938
  <button
18669
18939
  onclick="location.reload()"
18670
18940
  class="inline-flex items-center gap-x-1.5 rounded-md bg-white dark:bg-zinc-900 px-3 py-1.5 h-9 text-sm font-semibold text-zinc-900 dark:text-white shadow-sm ring-1 ring-inset ring-zinc-300 dark:ring-zinc-700 hover:bg-zinc-50 dark:hover:bg-zinc-800"
@@ -18828,8 +19098,6 @@ function renderPluginsListPage(data) {
18828
19098
  .forEach(cb => params.append('status', cb.value));
18829
19099
  const search = document.getElementById('search-input').value;
18830
19100
  if (search) params.set('search', search);
18831
- const sort = document.getElementById('sort-filter').value;
18832
- if (sort && sort !== 'name-asc') params.set('sort', sort);
18833
19101
  const url = new URL(window.location.href);
18834
19102
  url.search = params.toString();
18835
19103
  history.replaceState(null, '', url.toString());
@@ -18847,8 +19115,6 @@ function renderPluginsListPage(data) {
18847
19115
  });
18848
19116
  const search = params.get('search');
18849
19117
  if (search) document.getElementById('search-input').value = search;
18850
- const sort = params.get('sort');
18851
- if (sort) document.getElementById('sort-filter').value = sort;
18852
19118
  if (params.toString()) filterAndSortPlugins();
18853
19119
  }
18854
19120
 
@@ -18864,7 +19130,6 @@ function renderPluginsListPage(data) {
18864
19130
  .map(cb => cb.value.toLowerCase());
18865
19131
 
18866
19132
  const searchInput = document.getElementById('search-input').value.toLowerCase();
18867
- const sortValue = document.getElementById('sort-filter').value;
18868
19133
 
18869
19134
  const pluginsGrid = document.getElementById('plugins-grid');
18870
19135
  const pluginCards = Array.from(pluginsGrid.querySelectorAll('.plugin-card'));
@@ -18888,30 +19153,6 @@ function renderPluginsListPage(data) {
18888
19153
  return true;
18889
19154
  });
18890
19155
 
18891
- // Sort
18892
- visibleCards.sort((a, b) => {
18893
- const aName = a.getAttribute('data-name') || '';
18894
- const bName = b.getAttribute('data-name') || '';
18895
- const aInstalled = parseInt(a.getAttribute('data-installed') || '0');
18896
- const bInstalled = parseInt(b.getAttribute('data-installed') || '0');
18897
- const aUpdated = parseInt(a.getAttribute('data-updated') || '0');
18898
- const bUpdated = parseInt(b.getAttribute('data-updated') || '0');
18899
- const aDownloads = parseInt(a.getAttribute('data-downloads') || '0');
18900
- const bDownloads = parseInt(b.getAttribute('data-downloads') || '0');
18901
- const aRating = parseFloat(a.getAttribute('data-rating') || '0');
18902
- const bRating = parseFloat(b.getAttribute('data-rating') || '0');
18903
-
18904
- switch (sortValue) {
18905
- case 'name-desc': return bName.localeCompare(aName);
18906
- case 'newest': return bInstalled - aInstalled;
18907
- case 'updated': return bUpdated - aUpdated;
18908
- case 'popular': return bDownloads - aDownloads;
18909
- case 'rating': return bRating - aRating;
18910
- case 'name-asc':
18911
- default: return aName.localeCompare(bName);
18912
- }
18913
- });
18914
-
18915
19156
  // Re-append
18916
19157
  pluginCards.forEach(card => card.style.display = 'none'); // Hide all first
18917
19158
 
@@ -19346,7 +19587,8 @@ function escapeHtmlAttr(value) {
19346
19587
  function renderPluginSettingsPage(data) {
19347
19588
  const { plugin, activity = [], user, settingsTabData } = data;
19348
19589
  const pluginDef = getPluginDefinition(plugin.id || plugin.name);
19349
- const hasUserSettings = pluginDef?.settingsTabContent != null || Object.keys(plugin.settings || {}).some((k) => !k.startsWith("_"));
19590
+ const pluginId = plugin.id || plugin.name;
19591
+ const hasUserSettings = pluginDef?.settingsTabContent != null || Object.keys(plugin.settings || {}).some((k) => !k.startsWith("_")) || hasCustomSettingsComponent(pluginId);
19350
19592
  const defaultTab = hasUserSettings ? "settings" : "info";
19351
19593
  const pageContent = `
19352
19594
  <div class="w-full px-4 sm:px-6 lg:px-8 py-6">
@@ -19511,9 +19753,28 @@ function renderPluginSettingsPage(data) {
19511
19753
  const form = document.getElementById('settings-form');
19512
19754
  const formData = new FormData(form);
19513
19755
  const isAuthPlugin = '${plugin.id}' === 'core-auth';
19756
+ const isOAuthPlugin = '${plugin.id}' === 'oauth-providers';
19514
19757
  let settings = {};
19515
19758
 
19516
- if (isAuthPlugin) {
19759
+ if (isOAuthPlugin) {
19760
+ // Build nested provider settings structure
19761
+ const getVal = id => (document.getElementById(id) || {}).value || '';
19762
+ const getChecked = id => !!(document.getElementById(id) || {}).checked;
19763
+ settings = {
19764
+ providers: {
19765
+ github: {
19766
+ clientId: getVal('oauth_github_clientId'),
19767
+ clientSecret: getVal('oauth_github_clientSecret'),
19768
+ enabled: getChecked('oauth_github_enabled'),
19769
+ },
19770
+ google: {
19771
+ clientId: getVal('oauth_google_clientId'),
19772
+ clientSecret: getVal('oauth_google_clientSecret'),
19773
+ enabled: getChecked('oauth_google_enabled'),
19774
+ },
19775
+ }
19776
+ };
19777
+ } else if (isAuthPlugin) {
19517
19778
  // Handle nested auth settings structure
19518
19779
  settings = {
19519
19780
  requiredFields: {},
@@ -20001,7 +20262,8 @@ function formatTimestamp(timestamp) {
20001
20262
  }
20002
20263
  var pluginSettingsComponents = {
20003
20264
  "otp-login": renderOTPLoginSettingsContent,
20004
- "email": renderEmailSettingsContent
20265
+ "email": renderEmailSettingsContent,
20266
+ "oauth-providers": renderOAuthProvidersSettingsContent
20005
20267
  };
20006
20268
  function renderOTPLoginSettingsContent(plugin, settings) {
20007
20269
  const siteName = settings.siteName || "SonicJS";
@@ -20724,6 +20986,62 @@ defineUserProfile({
20724
20986
  </div>
20725
20987
  `;
20726
20988
  }
20989
+ function renderOAuthProvidersSettingsContent(plugin, settings) {
20990
+ const providers = settings.providers || {};
20991
+ const gh = providers.github || {};
20992
+ const gg = providers.google || {};
20993
+ const inputClass = "backdrop-blur-sm bg-white/10 border border-white/20 rounded-lg px-3 py-2 text-white placeholder-gray-300 focus:border-blue-400 focus:outline-none transition-colors w-full";
20994
+ return `
20995
+ <div class="space-y-6">
20996
+ <form id="settings-form">
20997
+ <div class="backdrop-blur-md bg-white/5 rounded-xl border border-white/10 p-6 space-y-6">
20998
+ <h3 class="text-lg font-semibold text-white">GitHub OAuth</h3>
20999
+ <div>
21000
+ <label for="oauth_github_clientId" class="block text-sm font-medium text-gray-300 mb-2">Client ID</label>
21001
+ <input type="text" id="oauth_github_clientId" name="oauth_github_clientId"
21002
+ value="${escapeHtmlAttr(gh.clientId || "")}"
21003
+ placeholder="GitHub OAuth App Client ID" class="${inputClass}" />
21004
+ </div>
21005
+ <div>
21006
+ <label for="oauth_github_clientSecret" class="block text-sm font-medium text-gray-300 mb-2">Client Secret</label>
21007
+ <input type="password" id="oauth_github_clientSecret" name="oauth_github_clientSecret"
21008
+ value="${escapeHtmlAttr(gh.clientSecret || "")}"
21009
+ placeholder="GitHub OAuth App Client Secret" class="${inputClass}" />
21010
+ </div>
21011
+ <div class="flex items-center gap-3">
21012
+ <input type="checkbox" id="oauth_github_enabled" name="oauth_github_enabled"
21013
+ ${gh.enabled ? "checked" : ""} class="w-4 h-4 rounded border-white/10" />
21014
+ <label for="oauth_github_enabled" class="text-sm text-gray-300">Enable GitHub login</label>
21015
+ </div>
21016
+ </div>
21017
+
21018
+ <div class="backdrop-blur-md bg-white/5 rounded-xl border border-white/10 p-6 space-y-6 mt-6">
21019
+ <h3 class="text-lg font-semibold text-white">Google OAuth</h3>
21020
+ <div>
21021
+ <label for="oauth_google_clientId" class="block text-sm font-medium text-gray-300 mb-2">Client ID</label>
21022
+ <input type="text" id="oauth_google_clientId" name="oauth_google_clientId"
21023
+ value="${escapeHtmlAttr(gg.clientId || "")}"
21024
+ placeholder="Google OAuth Client ID" class="${inputClass}" />
21025
+ </div>
21026
+ <div>
21027
+ <label for="oauth_google_clientSecret" class="block text-sm font-medium text-gray-300 mb-2">Client Secret</label>
21028
+ <input type="password" id="oauth_google_clientSecret" name="oauth_google_clientSecret"
21029
+ value="${escapeHtmlAttr(gg.clientSecret || "")}"
21030
+ placeholder="Google OAuth Client Secret" class="${inputClass}" />
21031
+ </div>
21032
+ <div class="flex items-center gap-3">
21033
+ <input type="checkbox" id="oauth_google_enabled" name="oauth_google_enabled"
21034
+ ${gg.enabled ? "checked" : ""} class="w-4 h-4 rounded border-white/10" />
21035
+ <label for="oauth_google_enabled" class="text-sm text-gray-300">Enable Google login</label>
21036
+ </div>
21037
+ </div>
21038
+ </form>
21039
+ </div>
21040
+ `;
21041
+ }
21042
+ function hasCustomSettingsComponent(pluginId) {
21043
+ return pluginId in pluginSettingsComponents;
21044
+ }
20727
21045
 
20728
21046
  // src/plugins/core-plugins/menu-plugin/services/menu-reconcile.ts
20729
21047
  async function upsertPluginRow(db, slug, entry, visible) {
@@ -27084,6 +27402,6 @@ var ROUTES_INFO = {
27084
27402
  reference: "https://github.com/sonicjs/sonicjs"
27085
27403
  };
27086
27404
 
27087
- export { DocumentRepository, ROUTES_INFO, TenantService, VALID_MEMBER_ROLES, adminCheckboxRoutes, adminCollectionsRoutes, adminDocumentsRoutes, adminLogsRoutes, adminMediaRoutes, adminPluginRoutes, adminSettingsRoutes, admin_api_default, admin_content_default, apiDocumentsRoutes, api_content_crud_default, api_default, api_media_default, api_system_default, auth_default, createDocumentSchema, createUserProfilesPlugin, defineUserProfile, denyIfNotAllowed, dispatchHookEvent, getConfirmationDialogScript2 as getConfirmationDialogScript, getCustomData, getEmailService, getUserProfileConfig, getVariablesCached, hasEmailService, invalidateVariablesCache, isValidMemberRole, lexicalEditorPlugin, reconcileMenuFromPlugins, renderConfirmationDialog2 as renderConfirmationDialog, renderTable2 as renderTable, resolveDocScope, resolveVariablesInObject, router, router2, setEmailService, setVariablesCache, test_cleanup_default, userProfilesPlugin, userRoutes };
27088
- //# sourceMappingURL=chunk-SK5CHFJP.js.map
27089
- //# sourceMappingURL=chunk-SK5CHFJP.js.map
27405
+ export { DocumentRepository, ROUTES_INFO, TenantService, VALID_MEMBER_ROLES, adminCheckboxRoutes, adminCollectionsRoutes, adminDocumentsRoutes, adminLogsRoutes, adminMediaRoutes, adminPluginRoutes, adminSettingsRoutes, admin_api_default, admin_content_default, apiDocumentsRoutes, api_content_crud_default, api_default, api_media_default, api_system_default, auth_default, createDocumentSchema, createUserProfilesPlugin, defineUserProfile, denyIfNotAllowed, dispatchHookEvent, effectiveTenantForType, getConfirmationDialogScript2 as getConfirmationDialogScript, getCustomData, getDocumentRequestContext, getEmailService, getUserProfileConfig, getVariablesCached, hasEmailService, invalidateVariablesCache, isValidMemberRole, lexicalEditorPlugin, reconcileMenuFromPlugins, renderConfirmationDialog2 as renderConfirmationDialog, renderTable2 as renderTable, resolveDocScope, resolveVariablesInObject, router, router2, setEmailService, setVariablesCache, test_cleanup_default, updateDocumentSchema, userProfilesPlugin, userRoutes };
27406
+ //# sourceMappingURL=chunk-7P5OCUPS.js.map
27407
+ //# sourceMappingURL=chunk-7P5OCUPS.js.map