@sonicjs-cms/core 3.0.0-beta.24 → 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 (137) 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-RJ2O5IIE.js → chunk-2D3A4X52.js} +2 -2
  11. package/dist/{chunk-RJ2O5IIE.js.map → chunk-2D3A4X52.js.map} +1 -1
  12. package/dist/{chunk-FHPEM77Y.js → chunk-2OXFMMA3.js} +11 -10
  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-I767LVN7.cjs → chunk-6OA77UM7.cjs} +757 -471
  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-VC5XVPLM.js → chunk-7P5OCUPS.js} +424 -138
  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-KKNEYIZJ.cjs → chunk-BJ6NJL2E.cjs} +7 -12
  25. package/dist/chunk-BJ6NJL2E.cjs.map +1 -0
  26. package/dist/{chunk-VGEZNFWH.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-LNI3FYSI.js → chunk-CWIGQYRR.js} +7 -12
  31. package/dist/chunk-CWIGQYRR.js.map +1 -0
  32. package/dist/{chunk-BP52BFIV.cjs → chunk-D6FWEYQE.cjs} +6 -5
  33. package/dist/chunk-D6FWEYQE.cjs.map +1 -0
  34. package/dist/{chunk-UIHIXTIS.js → chunk-D7RLBBHE.js} +2 -2
  35. package/dist/{chunk-UIHIXTIS.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-37V3RET3.cjs → chunk-E7E3NL3Y.cjs} +24 -23
  41. package/dist/chunk-E7E3NL3Y.cjs.map +1 -0
  42. package/dist/{chunk-VYQVDL2M.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-QJUJNTIU.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-A2SN3RVL.cjs → chunk-MOGK3U4A.cjs} +2 -2
  55. package/dist/{chunk-A2SN3RVL.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-QCJHCSOK.cjs → chunk-YAURVGUK.cjs} +2 -2
  65. package/dist/{chunk-QCJHCSOK.cjs.map → chunk-YAURVGUK.cjs.map} +1 -1
  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 +408 -370
  73. package/dist/index.cjs.map +1 -1
  74. package/dist/index.d.cts +10 -2
  75. package/dist/index.d.ts +10 -2
  76. package/dist/index.js +67 -29
  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-Z6D27NAW.js.map → migrations-JAHMI5MK.js.map} +1 -1
  84. package/dist/migrations-KU4V6T73.cjs +13 -0
  85. package/dist/{migrations-FGXZPKDC.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-37V3RET3.cjs.map +0 -1
  116. package/dist/chunk-3TMTYJAJ.js.map +0 -1
  117. package/dist/chunk-BJROXF4N.js.map +0 -1
  118. package/dist/chunk-BP52BFIV.cjs.map +0 -1
  119. package/dist/chunk-E66UVONV.cjs.map +0 -1
  120. package/dist/chunk-FHPEM77Y.js.map +0 -1
  121. package/dist/chunk-I767LVN7.cjs.map +0 -1
  122. package/dist/chunk-IXUHXTHW.cjs.map +0 -1
  123. package/dist/chunk-KKNEYIZJ.cjs.map +0 -1
  124. package/dist/chunk-LNI3FYSI.js.map +0 -1
  125. package/dist/chunk-LTZ5WYOK.cjs.map +0 -1
  126. package/dist/chunk-QJUJNTIU.cjs.map +0 -1
  127. package/dist/chunk-QZGABF2M.js.map +0 -1
  128. package/dist/chunk-THFZSDIU.js.map +0 -1
  129. package/dist/chunk-UYJQDDEE.cjs.map +0 -1
  130. package/dist/chunk-VC5XVPLM.js.map +0 -1
  131. package/dist/chunk-VGEZNFWH.js.map +0 -1
  132. package/dist/chunk-VYQVDL2M.js.map +0 -1
  133. package/dist/config-6FB2BXMP.js +0 -6
  134. package/dist/migrations-FGXZPKDC.cjs +0 -13
  135. package/dist/migrations-Z6D27NAW.js +0 -4
  136. package/dist/rbac-7QZ4JZ2E.cjs +0 -14
  137. 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-FHPEM77Y.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-RJ2O5IIE.js';
8
- import { MigrationService } from './chunk-UIHIXTIS.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-VGEZNFWH.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-LNI3FYSI.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
  }
@@ -994,6 +995,32 @@ apiContentCrudRoutes.get("/:id", optionalAuth(), async (c) => {
994
995
  { collection: docRow.type_id, id: docRow.root_id, data: transformedContent.data },
995
996
  "fire-and-forget"
996
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
+ }
997
1024
  return c.json({ data: transformedContent });
998
1025
  } catch (error) {
999
1026
  console.error("Error fetching content:", error);
@@ -1057,8 +1084,8 @@ apiContentCrudRoutes.post("/", requireAuth(), requireRole(["admin", "editor", "a
1057
1084
  user?.userId
1058
1085
  );
1059
1086
  const cache = getCacheService(CACHE_CONFIGS.api);
1060
- await cache.invalidate("content-filtered:*");
1061
- await cache.invalidate("collection-content-filtered:*");
1087
+ await cache.invalidate("api:content-filtered:*");
1088
+ await cache.invalidate(`api:collection-content-filtered:${backing.coll.name}:*`);
1062
1089
  dispatchHookEvent(
1063
1090
  c,
1064
1091
  "content:after:create",
@@ -1121,8 +1148,8 @@ apiContentCrudRoutes.put("/:id", requireAuth(), requireRole(["admin", "editor",
1121
1148
  await svc.unpublish(pub.id);
1122
1149
  }
1123
1150
  const cache = getCacheService(CACHE_CONFIGS.api);
1124
- await cache.invalidate("content-filtered:*");
1125
- await cache.invalidate("collection-content-filtered:*");
1151
+ await cache.invalidate("api:content-filtered:*");
1152
+ await cache.invalidate(`api:collection-content-filtered:${docRow.type_id}:*`);
1126
1153
  const coll = getCollectionRegistry().getByName(docRow.type_id);
1127
1154
  const saved = await db.prepare("SELECT * FROM documents WHERE id = ?").bind(newDraft.id).first();
1128
1155
  const savedData = saved?.data ? JSON.parse(saved.data) : {};
@@ -1161,8 +1188,8 @@ apiContentCrudRoutes.delete("/:id", requireAuth(), requireRole(["admin", "editor
1161
1188
  const now = Math.floor(Date.now() / 1e3);
1162
1189
  await db.prepare("UPDATE documents SET deleted_at = ?, updated_at = ? WHERE root_id = ? AND tenant_id = ?").bind(now, now, id, tenantId).run();
1163
1190
  const cache = getCacheService(CACHE_CONFIGS.api);
1164
- await cache.invalidate("content-filtered:*");
1165
- await cache.invalidate("collection-content-filtered:*");
1191
+ await cache.invalidate("api:content-filtered:*");
1192
+ await cache.invalidate(`api:collection-content-filtered:${docRow.type_id}:*`);
1166
1193
  dispatchHookEvent(c, "content:after:delete", { collection: docRow.type_id, id, data: {}, user: actor }, "fire-and-forget");
1167
1194
  return c.json({ success: true });
1168
1195
  }
@@ -1178,6 +1205,22 @@ apiContentCrudRoutes.delete("/:id", requireAuth(), requireRole(["admin", "editor
1178
1205
  var api_content_crud_default = apiContentCrudRoutes;
1179
1206
 
1180
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
+ }
1181
1224
  var DOC_SORTABLE_COLUMNS = /* @__PURE__ */ new Set([
1182
1225
  "created_at",
1183
1226
  "updated_at",
@@ -1261,6 +1304,33 @@ function mapDocRowToContent(row, collectionId) {
1261
1304
  updated_at: documentSecondsToMs(row.updated_at)
1262
1305
  };
1263
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
+ }
1264
1334
  var apiRoutes = new Hono();
1265
1335
  apiRoutes.use("*", async (c, next) => {
1266
1336
  const startTime = Date.now();
@@ -1750,7 +1820,12 @@ apiRoutes.get("/content", optionalAuth(), async (c) => {
1750
1820
  try {
1751
1821
  const db = c.env.DB;
1752
1822
  const queryParams = c.req.query();
1823
+ const projFields = parseFieldsParam(queryParams.fields);
1824
+ if (queryParams.fields) delete queryParams.fields;
1753
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);
1754
1829
  const collIdByName = /* @__PURE__ */ new Map();
1755
1830
  let typeId;
1756
1831
  let typeIds;
@@ -1796,7 +1871,7 @@ apiRoutes.get("/content", optionalAuth(), async (c) => {
1796
1871
  details: queryResult.errors
1797
1872
  }, 400);
1798
1873
  }
1799
- const cacheEnabled = c.get("cacheEnabled");
1874
+ const cacheEnabled = c.get("cacheEnabled") && cAnon;
1800
1875
  const cache = getCacheService(CACHE_CONFIGS.api);
1801
1876
  const cacheKey = cache.generateKey("content-filtered", JSON.stringify({ filter: normalizedFilter, query: queryResult.sql }));
1802
1877
  if (cacheEnabled) {
@@ -1826,11 +1901,41 @@ apiRoutes.get("/content", optionalAuth(), async (c) => {
1826
1901
  const stmt = db.prepare(queryResult.sql);
1827
1902
  const boundStmt = queryResult.params.length > 0 ? stmt.bind(...queryResult.params) : stmt;
1828
1903
  const { results } = await boundStmt.all();
1829
- 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));
1830
1935
  const responseData = {
1831
- data: transformedResults,
1936
+ data: projFields.length ? transformedResults.map((item) => projectFields(item, projFields)) : transformedResults,
1832
1937
  meta: addTimingMeta(c, {
1833
- count: results.length,
1938
+ count: aclResults.length,
1834
1939
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1835
1940
  // D44: echo the caller's filter with the access policy applied (status=published forced for
1836
1941
  // anonymous callers — the visible enforcement proof), NOT the internal doc-augmented where-tree.
@@ -1841,7 +1946,7 @@ apiRoutes.get("/content", optionalAuth(), async (c) => {
1841
1946
  }
1842
1947
  }, executionStart)
1843
1948
  };
1844
- if (cacheEnabled) {
1949
+ if (cacheEnabled && projFields.length === 0) {
1845
1950
  await cache.set(cacheKey, responseData);
1846
1951
  }
1847
1952
  return c.json(responseData);
@@ -1866,6 +1971,16 @@ apiRoutes.get("/collections/:collection/content", optionalAuth(), async (c) => {
1866
1971
  const collIdByName = /* @__PURE__ */ new Map();
1867
1972
  collIdByName.set(collection, record.id);
1868
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
+ }
1869
1984
  const filter = QueryFilterBuilder.parseFromQuery(queryParams);
1870
1985
  const normalizedFilter = augmentFilterForDocuments(filter, { typeId: collection, role });
1871
1986
  if (!normalizedFilter.limit) {
@@ -1880,7 +1995,7 @@ apiRoutes.get("/collections/:collection/content", optionalAuth(), async (c) => {
1880
1995
  details: queryResult.errors
1881
1996
  }, 400);
1882
1997
  }
1883
- const cacheEnabled = c.get("cacheEnabled");
1998
+ const cacheEnabled = c.get("cacheEnabled") && ccAnon;
1884
1999
  const cache = getCacheService(CACHE_CONFIGS.api);
1885
2000
  const includeCollection = queryParams.include?.split(",").map((s) => s.trim()).includes("collection");
1886
2001
  const cacheKey = cache.generateKey("collection-content-filtered", `${collection}:${JSON.stringify({ filter: normalizedFilter, query: queryResult.sql, includeCollection })}`);
@@ -1911,12 +2026,24 @@ apiRoutes.get("/collections/:collection/content", optionalAuth(), async (c) => {
1911
2026
  const stmt = db.prepare(queryResult.sql);
1912
2027
  const boundStmt = queryResult.params.length > 0 ? stmt.bind(...queryResult.params) : stmt;
1913
2028
  const { results } = await boundStmt.all();
1914
- 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));
1915
2042
  const responseData = {
1916
2043
  data: transformedResults,
1917
2044
  meta: addTimingMeta(c, {
1918
2045
  ...includeCollection ? { collection: collectionRecordToRow(record) } : {},
1919
- count: results.length,
2046
+ count: ccAclResults.length,
1920
2047
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1921
2048
  // D44: echo the caller's filter with the access policy applied (status=published forced for
1922
2049
  // anonymous callers — the visible enforcement proof), NOT the internal doc-augmented where-tree.
@@ -1946,6 +2073,8 @@ apiRoutes.get("/:collection", optionalAuth(), async (c) => {
1946
2073
  const collection = c.req.param("collection");
1947
2074
  const db = c.env.DB;
1948
2075
  const queryParams = c.req.query();
2076
+ const projFields = parseFieldsParam(queryParams.fields);
2077
+ if (queryParams.fields) delete queryParams.fields;
1949
2078
  const record = getCollectionRegistry().getBySlugOrName(collection);
1950
2079
  if (!record || record.isActive === false) {
1951
2080
  return c.json({ error: "Collection not found" }, 404);
@@ -1953,6 +2082,16 @@ apiRoutes.get("/:collection", optionalAuth(), async (c) => {
1953
2082
  const collIdByName = /* @__PURE__ */ new Map();
1954
2083
  collIdByName.set(record.name, record.id);
1955
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
+ }
1956
2095
  const filter = QueryFilterBuilder.parseFromQuery(queryParams);
1957
2096
  const normalizedFilter = augmentFilterForDocuments(filter, { typeId: record.name, role });
1958
2097
  if (!normalizedFilter.limit) normalizedFilter.limit = 50;
@@ -1964,7 +2103,7 @@ apiRoutes.get("/:collection", optionalAuth(), async (c) => {
1964
2103
  }
1965
2104
  const collectionCache = record.cache;
1966
2105
  const collectionCacheDisabled = collectionCache?.enabled === false;
1967
- const cacheEnabled = c.get("cacheEnabled") && !collectionCacheDisabled;
2106
+ const cacheEnabled = c.get("cacheEnabled") && !collectionCacheDisabled && projFields.length === 0 && anon;
1968
2107
  const cache = getCacheService(CACHE_CONFIGS.api);
1969
2108
  const includeCollection = queryParams.include?.split(",").map((s) => s.trim()).includes("collection");
1970
2109
  const cacheKey = cache.generateKey("collection-content-filtered", `${collection}:${JSON.stringify({ filter: normalizedFilter, query: queryResult.sql, includeCollection })}`);
@@ -1982,12 +2121,25 @@ apiRoutes.get("/:collection", optionalAuth(), async (c) => {
1982
2121
  const stmt = db.prepare(queryResult.sql);
1983
2122
  const boundStmt = queryResult.params.length > 0 ? stmt.bind(...queryResult.params) : stmt;
1984
2123
  const { results } = await boundStmt.all();
1985
- 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));
1986
2138
  const responseData = {
1987
- data: transformedResults,
2139
+ data: projFields.length ? transformedResults.map((item) => projectFields(item, projFields)) : transformedResults,
1988
2140
  meta: addTimingMeta(c, {
1989
2141
  ...includeCollection ? { collection: collectionRecordToRow(record) } : {},
1990
- count: results.length,
2142
+ count: aclResults.length,
1991
2143
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1992
2144
  filter: normalizePublicContentFilter(filter, role),
1993
2145
  cache: { hit: false, source: "database" }
@@ -2018,6 +2170,23 @@ apiRoutes.get("/:collection/:id", optionalAuth(), async (c) => {
2018
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"
2019
2171
  ).bind(id, record.name).first();
2020
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"));
2021
2190
  const coll = getCollectionRegistry().getByName(docRow.type_id);
2022
2191
  const transformedContent = {
2023
2192
  id: docRow.root_id,
@@ -2030,7 +2199,7 @@ apiRoutes.get("/:collection/:id", optionalAuth(), async (c) => {
2030
2199
  updated_at: documentSecondsToMs(docRow.updated_at)
2031
2200
  };
2032
2201
  dispatchHookEvent(c, "content:read", { collection: docRow.type_id, id: docRow.root_id, data: transformedContent.data }, "fire-and-forget");
2033
- return c.json({ data: transformedContent });
2202
+ return c.json({ data: projFields.length ? projectFields(transformedContent, projFields) : transformedContent });
2034
2203
  } catch (error) {
2035
2204
  console.error("Error fetching content:", error);
2036
2205
  return c.json({ error: "Failed to fetch content", details: error instanceof Error ? error.message : String(error) }, 500);
@@ -2060,8 +2229,8 @@ apiRoutes.post("/:collection", requireAuth(), requireRole(["admin", "editor", "a
2060
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 });
2061
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);
2062
2231
  const cache = getCacheService(CACHE_CONFIGS.api);
2063
- await cache.invalidate("content-filtered:*");
2064
- await cache.invalidate("collection-content-filtered:*");
2232
+ await cache.invalidate("api:content-filtered:*");
2233
+ await cache.invalidate(`api:collection-content-filtered:${backing.coll.name}:*`);
2065
2234
  dispatchHookEvent(c, "content:after:create", { collection: backing.coll.name, id: doc.rootId, data: doc.data ?? {}, user: actor }, "fire-and-forget");
2066
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);
2067
2236
  } catch (error) {
@@ -2103,8 +2272,8 @@ apiRoutes.put("/:collection/:id", requireAuth(), requireRole(["admin", "editor",
2103
2272
  await svc.unpublish(pub.id);
2104
2273
  }
2105
2274
  const cache = getCacheService(CACHE_CONFIGS.api);
2106
- await cache.invalidate("content-filtered:*");
2107
- await cache.invalidate("collection-content-filtered:*");
2275
+ await cache.invalidate("api:content-filtered:*");
2276
+ await cache.invalidate(`api:collection-content-filtered:${docRow.type_id}:*`);
2108
2277
  const coll = getCollectionRegistry().getByName(docRow.type_id);
2109
2278
  const saved = await db.prepare("SELECT * FROM documents WHERE id = ?").bind(newDraft.id).first();
2110
2279
  const savedData = saved?.data ? JSON.parse(saved.data) : {};
@@ -2136,8 +2305,8 @@ apiRoutes.delete("/:collection/:id", requireAuth(), requireRole(["admin", "edito
2136
2305
  const now = Math.floor(Date.now() / 1e3);
2137
2306
  await db.prepare("UPDATE documents SET deleted_at = ?, updated_at = ? WHERE root_id = ? AND tenant_id = 'default'").bind(now, now, id).run();
2138
2307
  const cache = getCacheService(CACHE_CONFIGS.api);
2139
- await cache.invalidate("content-filtered:*");
2140
- await cache.invalidate("collection-content-filtered:*");
2308
+ await cache.invalidate("api:content-filtered:*");
2309
+ await cache.invalidate(`api:collection-content-filtered:${docRow.type_id}:*`);
2141
2310
  dispatchHookEvent(c, "content:after:delete", { collection: docRow.type_id, id, data: {}, user: actor }, "fire-and-forget");
2142
2311
  return c.json({ success: true });
2143
2312
  } catch (error) {
@@ -2699,9 +2868,13 @@ apiMediaRoutes.post("/create-folder", async (c) => {
2699
2868
  error: "Folder name can only contain lowercase letters, numbers, hyphens, and underscores"
2700
2869
  }, 400);
2701
2870
  }
2702
- const checkStmt = c.env.DB.prepare("SELECT COUNT(*) as count FROM media WHERE folder = ? AND deleted_at IS NULL");
2703
- const existingFolder = await checkStmt.bind(folderName).first();
2704
- 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) {
2705
2878
  return c.json({
2706
2879
  success: false,
2707
2880
  error: `Folder "${folderName}" already exists`
@@ -3411,7 +3584,7 @@ adminApiRoutes.patch("/collections/:id", collectionsReadOnly);
3411
3584
  adminApiRoutes.delete("/collections/:id", collectionsReadOnly);
3412
3585
  adminApiRoutes.get("/migrations/status", async (c) => {
3413
3586
  try {
3414
- const { MigrationService: MigrationService2 } = await import('./migrations-Z6D27NAW.js');
3587
+ const { MigrationService: MigrationService2 } = await import('./migrations-JAHMI5MK.js');
3415
3588
  const db = c.env.DB;
3416
3589
  const migrationService = new MigrationService2(db);
3417
3590
  const status = await migrationService.getMigrationStatus();
@@ -3454,7 +3627,7 @@ adminApiRoutes.post("/migrations/run", async (c) => {
3454
3627
  });
3455
3628
  adminApiRoutes.get("/migrations/validate", async (c) => {
3456
3629
  try {
3457
- const { MigrationService: MigrationService2 } = await import('./migrations-Z6D27NAW.js');
3630
+ const { MigrationService: MigrationService2 } = await import('./migrations-JAHMI5MK.js');
3458
3631
  const db = c.env.DB;
3459
3632
  const migrationService = new MigrationService2(db);
3460
3633
  const validation = await migrationService.validateSchema();
@@ -3613,6 +3786,8 @@ function renderLoginPage(data, demoLoginActive = false) {
3613
3786
  <!-- Form -->
3614
3787
  <form
3615
3788
  id="login-form"
3789
+ action="/auth/login/form${data.redirect ? `?redirect=${encodeURIComponent(data.redirect)}` : ""}"
3790
+ method="post"
3616
3791
  hx-post="/auth/login/form${data.redirect ? `?redirect=${encodeURIComponent(data.redirect)}` : ""}"
3617
3792
  hx-target="#form-response"
3618
3793
  hx-swap="innerHTML"
@@ -5128,9 +5303,11 @@ function renderDynamicField(field, options = {}) {
5128
5303
  min="${opts.min || ""}"
5129
5304
  max="${opts.max || ""}"
5130
5305
  class="${baseClasses} ${errorClasses} !w-auto dark:[color-scheme:dark]"
5306
+ placeholder="YYYY-MM-DD"
5131
5307
  ${required}
5132
5308
  ${disabled ? "disabled" : ""}
5133
5309
  >
5310
+ <p class="mt-1 text-xs text-zinc-500 dark:text-zinc-400">Format: YYYY-MM-DD</p>
5134
5311
  `;
5135
5312
  break;
5136
5313
  case "datetime":
@@ -5146,6 +5323,7 @@ function renderDynamicField(field, options = {}) {
5146
5323
  ${required}
5147
5324
  ${disabled ? "disabled" : ""}
5148
5325
  >
5326
+ <p class="mt-1 text-xs text-zinc-500 dark:text-zinc-400">Format: YYYY-MM-DD HH:MM</p>
5149
5327
  `;
5150
5328
  break;
5151
5329
  case "slug":
@@ -7102,6 +7280,21 @@ function createUserProfilesPlugin() {
7102
7280
 
7103
7281
  // src/routes/auth.ts
7104
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
+ }
7105
7298
  function renderPasswordResetEmail(resetLink, firstName) {
7106
7299
  const greeting = firstName ? `Hi ${firstName},` : "Hello,";
7107
7300
  return `<!DOCTYPE html>
@@ -7293,8 +7486,8 @@ authRoutes.post(
7293
7486
  }
7294
7487
  const { email, password } = validation.data;
7295
7488
  const normalizedEmail = email.toLowerCase();
7296
- const { createAuth } = await import('./config-6FB2BXMP.js');
7297
- 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);
7298
7491
  const baReq = new Request(new URL("/auth/sign-in/email", c.req.url).href, {
7299
7492
  method: "POST",
7300
7493
  headers: { "Content-Type": "application/json", "Origin": new URL(c.req.url).origin },
@@ -7304,12 +7497,14 @@ authRoutes.post(
7304
7497
  if (!baRes.ok) {
7305
7498
  return c.json({ error: "Invalid email or password" }, 401);
7306
7499
  }
7307
- const rawSetCookie = baRes.headers.get("set-cookie");
7308
- if (rawSetCookie) {
7309
- c.res.headers.append("Set-Cookie", rawSetCookie);
7310
- } else if (baRes.headers.getSetCookie) {
7500
+ if (baRes.headers.getSetCookie) {
7311
7501
  for (const sc of baRes.headers.getSetCookie()) {
7312
- 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 });
7313
7508
  }
7314
7509
  }
7315
7510
  await setCsrfCookie(c);
@@ -7329,6 +7524,9 @@ authRoutes.post(
7329
7524
  sameSite: "Strict",
7330
7525
  maxAge: tokenTtl
7331
7526
  });
7527
+ if (normalizedEmail === DEMO_EMAIL) {
7528
+ c.executionCtx?.waitUntil(trackDemoLogin(c.env.CACHE_KV));
7529
+ }
7332
7530
  return c.json({
7333
7531
  user: {
7334
7532
  id: user.id,
@@ -7347,8 +7545,8 @@ authRoutes.post(
7347
7545
  );
7348
7546
  authRoutes.post("/logout", async (c) => {
7349
7547
  try {
7350
- const { createAuth } = await import('./config-6FB2BXMP.js');
7351
- 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);
7352
7550
  const baReq = new Request(new URL("/auth/sign-out", c.req.url).href, {
7353
7551
  method: "POST",
7354
7552
  headers: { "Content-Type": "application/json", "Origin": new URL(c.req.url).origin, "Cookie": c.req.header("Cookie") || "" },
@@ -7357,14 +7555,15 @@ authRoutes.post("/logout", async (c) => {
7357
7555
  await auth.handler(baReq);
7358
7556
  } catch {
7359
7557
  }
7360
- 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 });
7361
7560
  clearCsrfCookie(c);
7362
7561
  return c.json({ message: "Logged out successfully" });
7363
7562
  });
7364
7563
  authRoutes.get("/logout", async (c) => {
7365
7564
  try {
7366
- const { createAuth } = await import('./config-6FB2BXMP.js');
7367
- 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);
7368
7567
  const baReq = new Request(new URL("/auth/sign-out", c.req.url).href, {
7369
7568
  method: "POST",
7370
7569
  headers: { "Content-Type": "application/json", "Origin": new URL(c.req.url).origin, "Cookie": c.req.header("Cookie") || "" },
@@ -7373,7 +7572,8 @@ authRoutes.get("/logout", async (c) => {
7373
7572
  await auth.handler(baReq);
7374
7573
  } catch {
7375
7574
  }
7376
- 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 });
7377
7577
  clearCsrfCookie(c);
7378
7578
  return c.redirect("/auth/login?message=You have been logged out successfully");
7379
7579
  });
@@ -7570,8 +7770,8 @@ authRoutes.post(
7570
7770
  </div>
7571
7771
  `);
7572
7772
  }
7573
- const { createAuth } = await import('./config-6FB2BXMP.js');
7574
- 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);
7575
7775
  const baReq = new Request(new URL("/auth/sign-in/email", c.req.url).href, {
7576
7776
  method: "POST",
7577
7777
  headers: { "Content-Type": "application/json", "Origin": new URL(c.req.url).origin },
@@ -7585,17 +7785,27 @@ authRoutes.post(
7585
7785
  </div>
7586
7786
  `);
7587
7787
  }
7588
- const rawSetCookie = baRes.headers.get("set-cookie");
7589
- if (rawSetCookie) {
7590
- c.res.headers.append("Set-Cookie", rawSetCookie);
7591
- } else if (baRes.headers.getSetCookie) {
7788
+ if (baRes.headers.getSetCookie) {
7592
7789
  for (const sc of baRes.headers.getSetCookie()) {
7593
- 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 });
7594
7796
  }
7595
7797
  }
7596
7798
  await setCsrfCookie(c);
7799
+ if (email === DEMO_EMAIL) {
7800
+ c.executionCtx?.waitUntil(trackDemoLogin(c.env.CACHE_KV));
7801
+ }
7597
7802
  const rawRedirect = c.req.query("redirect");
7598
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
+ }
7599
7809
  return c.html(html`
7600
7810
  <div id="form-response">
7601
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">
@@ -9107,33 +9317,44 @@ function renderContentFormPage(data, opts) {
9107
9317
  </a>
9108
9318
 
9109
9319
  <div class="flex items-center gap-x-3">
9110
- ${data.versioningEnabled ? `
9111
- <button
9112
- type="submit"
9113
- form="content-form"
9114
- name="action"
9115
- value="save"
9116
- 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"
9117
- >
9118
- <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
9119
- <path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/>
9120
- </svg>
9121
- ${isEdit ? "Update" : "Save"}
9122
- </button>
9123
- ` : ""}
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>
9124
9332
 
9125
- ${data.user?.role !== "viewer" ? `
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>
9345
+
9346
+ ${data.user?.role !== "viewer" && data.versioningEnabled ? `
9126
9347
  <button
9127
9348
  type="submit"
9128
9349
  form="content-form"
9129
9350
  name="action"
9130
9351
  value="save_and_publish"
9131
- 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"
9132
9353
  >
9133
9354
  <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
9134
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"/>
9135
9356
  </svg>
9136
- ${data.versioningEnabled ? `${isEdit ? "Update" : "Save"} & Publish` : isEdit ? "Update" : "Save"}
9357
+ Publish
9137
9358
  </button>
9138
9359
  ` : ""}
9139
9360
  </div>
@@ -12276,6 +12497,9 @@ adminContentRoutes.post("/", async (c) => {
12276
12497
  }), user?.userId);
12277
12498
  const cache2 = getCacheService(CACHE_CONFIGS.content);
12278
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}:*`);
12279
12503
  const redirectUrl2 = `/admin/content/${doc.rootId}/edit?success=Content created successfully!`;
12280
12504
  return c.req.header("HX-Request") === "true" ? c.text("", 200, { "HX-Redirect": redirectUrl2 }) : c.redirect(redirectUrl2);
12281
12505
  }
@@ -12394,8 +12618,28 @@ adminContentRoutes.put("/:id", async (c) => {
12394
12618
  if (status2 === "published") await svc.publish(newDraft.id, user?.userId);
12395
12619
  else if (pub) await svc.unpublish(pub.id);
12396
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
+ }
12397
12641
  const redirectUrl2 = `/admin/content/${id}/edit?success=Content updated successfully!`;
12398
- return c.req.header("HX-Request") === "true" ? c.text("", 200, { "HX-Redirect": redirectUrl2 }) : c.redirect(redirectUrl2);
12642
+ return c.redirect(redirectUrl2);
12399
12643
  }
12400
12644
  }
12401
12645
  return c.html(html`
@@ -12789,8 +13033,8 @@ adminContentRoutes.post("/bulk-action", async (c) => {
12789
13033
  }
12790
13034
  await cache.invalidate("content:list:*");
12791
13035
  const apiCache = getCacheService(CACHE_CONFIGS.api);
12792
- await apiCache.invalidate("content-filtered:*");
12793
- await apiCache.invalidate("collection-content-filtered:*");
13036
+ await apiCache.invalidate("api:content-filtered:*");
13037
+ await apiCache.invalidate("api:collection-content-filtered:*");
12794
13038
  return c.json({ success: true, count: ids.length });
12795
13039
  } catch (error) {
12796
13040
  console.error("Bulk action error:", error);
@@ -12808,6 +13052,9 @@ adminContentRoutes.delete("/:id", async (c) => {
12808
13052
  const now2 = Math.floor(Date.now() / 1e3);
12809
13053
  await db.prepare("UPDATE documents SET deleted_at = ?, updated_at = ? WHERE root_id = ? AND tenant_id = ?").bind(now2, now2, id, tenantId).run();
12810
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}:*`);
12811
13058
  return c.html(`
12812
13059
  <div id="content-list" hx-get="/admin/content?model=${docDel.type_id}" hx-trigger="load" hx-swap="outerHTML">
12813
13060
  <div class="flex items-center justify-center p-8"><span class="text-zinc-500">Deleting\u2026</span></div>
@@ -13103,7 +13350,7 @@ adminContentRoutes.post("/documents/:typeId/:documentId/delete", async (c) => {
13103
13350
  adminContentRoutes.get("/documents/:typeId/:rootId/versions", async (c) => {
13104
13351
  const { typeId, rootId } = c.req.param();
13105
13352
  const db = c.env.DB;
13106
- const { renderVersionHistoryFragment } = await import('./admin-documents-form.template-55V2ZGWT.js');
13353
+ const { renderVersionHistoryFragment } = await import('./admin-documents-form.template-JWV5GITD.js');
13107
13354
  const registry = new DocumentTypeRegistry(db);
13108
13355
  const docType = await registry.findById(typeId);
13109
13356
  if (!docType) return c.html("<div>Unknown type.</div>", 404);
@@ -15358,12 +15605,10 @@ var LANGUAGES = [
15358
15605
  { value: "ko", label: "Korean" },
15359
15606
  { value: "zh", label: "Chinese" }
15360
15607
  ];
15361
- var ROLES = [
15362
- { value: "admin", label: "Administrator" },
15363
- { value: "editor", label: "Editor" },
15364
- { value: "author", label: "Author" },
15365
- { value: "viewer", label: "Viewer" }
15366
- ];
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
+ }
15367
15612
  userRoutes.get("/profile", async (c) => {
15368
15613
  const user = c.get("user");
15369
15614
  const db = c.env.DB;
@@ -15812,8 +16057,9 @@ userRoutes.get("/users/new", async (c) => {
15812
16057
  const regFieldNames = profileConfig?.registrationFields ?? [];
15813
16058
  const regFields = profileConfig && upRow ? regFieldNames.length > 0 ? profileConfig.fields.filter((f) => regFieldNames.includes(f.name)) : profileConfig.fields : [];
15814
16059
  const registrationFieldsHtml = regFields.length > 0 ? renderCustomProfileSection({ fields: regFields }, {}) : void 0;
16060
+ const roles = await fetchRoleOptions(db);
15815
16061
  const pageData = {
15816
- roles: ROLES,
16062
+ roles,
15817
16063
  registrationFieldsHtml,
15818
16064
  user: {
15819
16065
  name: user.email.split("@")[0] || user.email,
@@ -15841,9 +16087,9 @@ userRoutes.post("/users/new", async (c) => {
15841
16087
  const email = formData.get("email")?.toString()?.trim().toLowerCase() || "";
15842
16088
  const phone = sanitizeInput(formData.get("phone")?.toString()) || null;
15843
16089
  const bio = sanitizeInput(formData.get("bio")?.toString()) || null;
15844
- const roleInput = formData.get("role")?.toString() || "viewer";
15845
- const validRoles = ["admin", "editor", "author", "viewer"];
15846
- 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";
15847
16093
  const password = formData.get("password")?.toString() || "";
15848
16094
  const confirmPassword = formData.get("confirm_password")?.toString() || "";
15849
16095
  const isActive = formData.get("is_active") === "1";
@@ -16065,9 +16311,10 @@ userRoutes.get("/users/:id/edit", async (c) => {
16065
16311
  }
16066
16312
  const queryMessage = c.req.query("message");
16067
16313
  const queryType = c.req.query("type") === "error" ? "error" : "success";
16314
+ const roles = await fetchRoleOptions(db);
16068
16315
  const pageData = {
16069
16316
  userToEdit: editData,
16070
- roles: ROLES,
16317
+ roles,
16071
16318
  hasProfilePlugin: profileConfig !== null && upRow !== null,
16072
16319
  customProfileFieldsHtml,
16073
16320
  tenantMemberships,
@@ -16099,9 +16346,9 @@ userRoutes.put("/users/:id", async (c) => {
16099
16346
  const lastName = sanitizeInput(formData.get("last_name")?.toString());
16100
16347
  const email = formData.get("email")?.toString()?.trim().toLowerCase() || "";
16101
16348
  const phone = sanitizeInput(formData.get("phone")?.toString()) || null;
16102
- const roleInput = formData.get("role")?.toString() || "viewer";
16103
- const validRoles = ["admin", "editor", "author", "viewer"];
16104
- 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";
16105
16352
  const isActive = formData.get("is_active") === "1";
16106
16353
  const emailVerified = formData.get("email_verified") === "1";
16107
16354
  const newPassword = formData.get("new_password")?.toString() || "";
@@ -18688,15 +18935,6 @@ function renderPluginsListPage(data) {
18688
18935
  </div>
18689
18936
 
18690
18937
  <div class="flex items-center gap-3 w-full sm:w-auto">
18691
- <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">
18692
- <option value="name-asc">Name (A-Z)</option>
18693
- <option value="name-desc">Name (Z-A)</option>
18694
- <option value="newest">Newest Installed</option>
18695
- <option value="updated">Recently Updated</option>
18696
- <option value="popular">Popularity</option>
18697
- <option value="rating">Highest Rated</option>
18698
- </select>
18699
-
18700
18938
  <button
18701
18939
  onclick="location.reload()"
18702
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"
@@ -18860,8 +19098,6 @@ function renderPluginsListPage(data) {
18860
19098
  .forEach(cb => params.append('status', cb.value));
18861
19099
  const search = document.getElementById('search-input').value;
18862
19100
  if (search) params.set('search', search);
18863
- const sort = document.getElementById('sort-filter').value;
18864
- if (sort && sort !== 'name-asc') params.set('sort', sort);
18865
19101
  const url = new URL(window.location.href);
18866
19102
  url.search = params.toString();
18867
19103
  history.replaceState(null, '', url.toString());
@@ -18879,8 +19115,6 @@ function renderPluginsListPage(data) {
18879
19115
  });
18880
19116
  const search = params.get('search');
18881
19117
  if (search) document.getElementById('search-input').value = search;
18882
- const sort = params.get('sort');
18883
- if (sort) document.getElementById('sort-filter').value = sort;
18884
19118
  if (params.toString()) filterAndSortPlugins();
18885
19119
  }
18886
19120
 
@@ -18896,7 +19130,6 @@ function renderPluginsListPage(data) {
18896
19130
  .map(cb => cb.value.toLowerCase());
18897
19131
 
18898
19132
  const searchInput = document.getElementById('search-input').value.toLowerCase();
18899
- const sortValue = document.getElementById('sort-filter').value;
18900
19133
 
18901
19134
  const pluginsGrid = document.getElementById('plugins-grid');
18902
19135
  const pluginCards = Array.from(pluginsGrid.querySelectorAll('.plugin-card'));
@@ -18920,30 +19153,6 @@ function renderPluginsListPage(data) {
18920
19153
  return true;
18921
19154
  });
18922
19155
 
18923
- // Sort
18924
- visibleCards.sort((a, b) => {
18925
- const aName = a.getAttribute('data-name') || '';
18926
- const bName = b.getAttribute('data-name') || '';
18927
- const aInstalled = parseInt(a.getAttribute('data-installed') || '0');
18928
- const bInstalled = parseInt(b.getAttribute('data-installed') || '0');
18929
- const aUpdated = parseInt(a.getAttribute('data-updated') || '0');
18930
- const bUpdated = parseInt(b.getAttribute('data-updated') || '0');
18931
- const aDownloads = parseInt(a.getAttribute('data-downloads') || '0');
18932
- const bDownloads = parseInt(b.getAttribute('data-downloads') || '0');
18933
- const aRating = parseFloat(a.getAttribute('data-rating') || '0');
18934
- const bRating = parseFloat(b.getAttribute('data-rating') || '0');
18935
-
18936
- switch (sortValue) {
18937
- case 'name-desc': return bName.localeCompare(aName);
18938
- case 'newest': return bInstalled - aInstalled;
18939
- case 'updated': return bUpdated - aUpdated;
18940
- case 'popular': return bDownloads - aDownloads;
18941
- case 'rating': return bRating - aRating;
18942
- case 'name-asc':
18943
- default: return aName.localeCompare(bName);
18944
- }
18945
- });
18946
-
18947
19156
  // Re-append
18948
19157
  pluginCards.forEach(card => card.style.display = 'none'); // Hide all first
18949
19158
 
@@ -19378,7 +19587,8 @@ function escapeHtmlAttr(value) {
19378
19587
  function renderPluginSettingsPage(data) {
19379
19588
  const { plugin, activity = [], user, settingsTabData } = data;
19380
19589
  const pluginDef = getPluginDefinition(plugin.id || plugin.name);
19381
- 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);
19382
19592
  const defaultTab = hasUserSettings ? "settings" : "info";
19383
19593
  const pageContent = `
19384
19594
  <div class="w-full px-4 sm:px-6 lg:px-8 py-6">
@@ -19543,9 +19753,28 @@ function renderPluginSettingsPage(data) {
19543
19753
  const form = document.getElementById('settings-form');
19544
19754
  const formData = new FormData(form);
19545
19755
  const isAuthPlugin = '${plugin.id}' === 'core-auth';
19756
+ const isOAuthPlugin = '${plugin.id}' === 'oauth-providers';
19546
19757
  let settings = {};
19547
19758
 
19548
- 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) {
19549
19778
  // Handle nested auth settings structure
19550
19779
  settings = {
19551
19780
  requiredFields: {},
@@ -20033,7 +20262,8 @@ function formatTimestamp(timestamp) {
20033
20262
  }
20034
20263
  var pluginSettingsComponents = {
20035
20264
  "otp-login": renderOTPLoginSettingsContent,
20036
- "email": renderEmailSettingsContent
20265
+ "email": renderEmailSettingsContent,
20266
+ "oauth-providers": renderOAuthProvidersSettingsContent
20037
20267
  };
20038
20268
  function renderOTPLoginSettingsContent(plugin, settings) {
20039
20269
  const siteName = settings.siteName || "SonicJS";
@@ -20756,6 +20986,62 @@ defineUserProfile({
20756
20986
  </div>
20757
20987
  `;
20758
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
+ }
20759
21045
 
20760
21046
  // src/plugins/core-plugins/menu-plugin/services/menu-reconcile.ts
20761
21047
  async function upsertPluginRow(db, slug, entry, visible) {
@@ -27117,5 +27403,5 @@ var ROUTES_INFO = {
27117
27403
  };
27118
27404
 
27119
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 };
27120
- //# sourceMappingURL=chunk-VC5XVPLM.js.map
27121
- //# sourceMappingURL=chunk-VC5XVPLM.js.map
27406
+ //# sourceMappingURL=chunk-7P5OCUPS.js.map
27407
+ //# sourceMappingURL=chunk-7P5OCUPS.js.map