@sonicjs-cms/core 3.0.0-beta.24 → 3.0.0-beta.26

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 (163) 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/{app-01M0dIwt.d.ts → app-BP5IlK94.d.ts} +7 -7
  11. package/dist/{app-aj39TGHB.d.cts → app-fdq4K3vw.d.cts} +7 -7
  12. package/dist/catalog-56DR6OXI.cjs +41 -0
  13. package/dist/catalog-56DR6OXI.cjs.map +1 -0
  14. package/dist/catalog-6MB37KY7.js +4 -0
  15. package/dist/catalog-6MB37KY7.js.map +1 -0
  16. package/dist/{chunk-QCJHCSOK.cjs → chunk-4DHPR7KR.cjs} +77 -17
  17. package/dist/chunk-4DHPR7KR.cjs.map +1 -0
  18. package/dist/{chunk-UIHIXTIS.js → chunk-5YSFQO7X.js} +40 -13
  19. package/dist/chunk-5YSFQO7X.js.map +1 -0
  20. package/dist/{chunk-GMMCCIQO.cjs → chunk-6UAI4SGN.cjs} +3 -3
  21. package/dist/{chunk-GMMCCIQO.cjs.map → chunk-6UAI4SGN.cjs.map} +1 -1
  22. package/dist/{chunk-2XAQTPAE.js → chunk-6X22VFEV.js} +8 -2
  23. package/dist/chunk-6X22VFEV.js.map +1 -0
  24. package/dist/{chunk-URIBTX27.js → chunk-73RTFFMX.js} +3 -3
  25. package/dist/{chunk-URIBTX27.js.map → chunk-73RTFFMX.js.map} +1 -1
  26. package/dist/{chunk-QJUJNTIU.cjs → chunk-A22RBHO7.cjs} +43 -12
  27. package/dist/chunk-A22RBHO7.cjs.map +1 -0
  28. package/dist/{chunk-QZGABF2M.js → chunk-AMP4BSJE.js} +18 -7
  29. package/dist/chunk-AMP4BSJE.js.map +1 -0
  30. package/dist/{chunk-37V3RET3.cjs → chunk-BRKXMRMB.cjs} +99 -98
  31. package/dist/chunk-BRKXMRMB.cjs.map +1 -0
  32. package/dist/{chunk-36Z3AKX3.cjs → chunk-C5LDQ3SH.cjs} +8 -2
  33. package/dist/chunk-C5LDQ3SH.cjs.map +1 -0
  34. package/dist/{chunk-BBEKHLTW.cjs → chunk-CN3T3TDI.cjs} +9 -9
  35. package/dist/{chunk-BBEKHLTW.cjs.map → chunk-CN3T3TDI.cjs.map} +1 -1
  36. package/dist/{chunk-BHND5PEV.js → chunk-E3TQG7AA.js} +3 -3
  37. package/dist/{chunk-BHND5PEV.js.map → chunk-E3TQG7AA.js.map} +1 -1
  38. package/dist/{chunk-FHPEM77Y.js → chunk-EEA476CJ.js} +88 -83
  39. package/dist/chunk-EEA476CJ.js.map +1 -0
  40. package/dist/chunk-F6BTBE4S.js +119 -0
  41. package/dist/chunk-F6BTBE4S.js.map +1 -0
  42. package/dist/chunk-FZGIU5AZ.cjs +128 -0
  43. package/dist/chunk-FZGIU5AZ.cjs.map +1 -0
  44. package/dist/{chunk-BJROXF4N.js → chunk-HSDJDWLV.js} +19 -4
  45. package/dist/chunk-HSDJDWLV.js.map +1 -0
  46. package/dist/{chunk-VGEZNFWH.js → chunk-JD7BZU3A.js} +5 -4
  47. package/dist/chunk-JD7BZU3A.js.map +1 -0
  48. package/dist/{chunk-SOZUG52O.js → chunk-JG5J434C.js} +3 -3
  49. package/dist/{chunk-SOZUG52O.js.map → chunk-JG5J434C.js.map} +1 -1
  50. package/dist/{chunk-BP52BFIV.cjs → chunk-KWQNB6UM.cjs} +6 -5
  51. package/dist/chunk-KWQNB6UM.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-VC5XVPLM.js → chunk-OJFIQGZB.js} +512 -148
  55. package/dist/chunk-OJFIQGZB.js.map +1 -0
  56. package/dist/{chunk-LTZ5WYOK.cjs → chunk-OLEOKHDX.cjs} +25 -2
  57. package/dist/chunk-OLEOKHDX.cjs.map +1 -0
  58. package/dist/{chunk-E66UVONV.cjs → chunk-OUBUS74I.cjs} +24 -9
  59. package/dist/chunk-OUBUS74I.cjs.map +1 -0
  60. package/dist/{chunk-A2SN3RVL.cjs → chunk-PAS7GZBS.cjs} +40 -13
  61. package/dist/chunk-PAS7GZBS.cjs.map +1 -0
  62. package/dist/{chunk-RJ2O5IIE.js → chunk-RVOX2WH2.js} +74 -18
  63. package/dist/chunk-RVOX2WH2.js.map +1 -0
  64. package/dist/{chunk-3TMTYJAJ.js → chunk-RXLI3ADU.js} +25 -2
  65. package/dist/chunk-RXLI3ADU.js.map +1 -0
  66. package/dist/{chunk-I767LVN7.cjs → chunk-S2QN4LFD.cjs} +881 -515
  67. package/dist/chunk-S2QN4LFD.cjs.map +1 -0
  68. package/dist/{chunk-IXUHXTHW.cjs → chunk-SFGYZUWU.cjs} +18 -7
  69. package/dist/chunk-SFGYZUWU.cjs.map +1 -0
  70. package/dist/{chunk-THFZSDIU.js → chunk-SXC4HPK6.js} +14 -8
  71. package/dist/chunk-SXC4HPK6.js.map +1 -0
  72. package/dist/{chunk-JMJM7DMM.cjs → chunk-TT3TQSCZ.cjs} +5 -5
  73. package/dist/{chunk-JMJM7DMM.cjs.map → chunk-TT3TQSCZ.cjs.map} +1 -1
  74. package/dist/{chunk-LNI3FYSI.js → chunk-UB5QSRO7.js} +9 -14
  75. package/dist/chunk-UB5QSRO7.js.map +1 -0
  76. package/dist/{chunk-VYQVDL2M.js → chunk-UKIDWRA3.js} +36 -5
  77. package/dist/chunk-UKIDWRA3.js.map +1 -0
  78. package/dist/{chunk-KKNEYIZJ.cjs → chunk-XZUEGFXE.cjs} +9 -14
  79. package/dist/chunk-XZUEGFXE.cjs.map +1 -0
  80. package/dist/{collection-config-BTzAvKt2.d.cts → collection-config-U2dydD_O.d.cts} +14 -0
  81. package/dist/{collection-config-BTzAvKt2.d.ts → collection-config-U2dydD_O.d.ts} +14 -0
  82. package/dist/config-DOAWXZBG.js +6 -0
  83. package/dist/{config-6FB2BXMP.js.map → config-DOAWXZBG.js.map} +1 -1
  84. package/dist/{config-OCH7S4WK.cjs → config-VLU73ZW7.cjs} +5 -5
  85. package/dist/{config-OCH7S4WK.cjs.map → config-VLU73ZW7.cjs.map} +1 -1
  86. package/dist/{define-plugin-D_6D9vVZ.d.ts → define-plugin-DDe8a_bh.d.ts} +3 -0
  87. package/dist/{define-plugin-DMcvEy_C.d.cts → define-plugin-rjXmGOgu.d.cts} +3 -0
  88. package/dist/index.cjs +698 -392
  89. package/dist/index.cjs.map +1 -1
  90. package/dist/index.d.cts +27 -12
  91. package/dist/index.d.ts +27 -12
  92. package/dist/index.js +340 -35
  93. package/dist/index.js.map +1 -1
  94. package/dist/middleware.cjs +47 -47
  95. package/dist/middleware.d.cts +10 -10
  96. package/dist/middleware.d.ts +10 -10
  97. package/dist/middleware.js +6 -6
  98. package/dist/migrations-6M6772UC.cjs +13 -0
  99. package/dist/{migrations-FGXZPKDC.cjs.map → migrations-6M6772UC.cjs.map} +1 -1
  100. package/dist/migrations-GZ2HIB4I.js +4 -0
  101. package/dist/{migrations-Z6D27NAW.js.map → migrations-GZ2HIB4I.js.map} +1 -1
  102. package/dist/{plugin-bootstrap-BHWQ2df8.d.cts → plugin-bootstrap-CfVILQeP.d.ts} +1 -1
  103. package/dist/{plugin-bootstrap-Bj4fxX1q.d.ts → plugin-bootstrap-RY5pkoOT.d.cts} +1 -1
  104. package/dist/plugins.cjs +40 -40
  105. package/dist/plugins.d.cts +1 -1
  106. package/dist/plugins.d.ts +1 -1
  107. package/dist/plugins.js +3 -3
  108. package/dist/rbac-TFUOYIVN.cjs +14 -0
  109. package/dist/{rbac-7QZ4JZ2E.cjs.map → rbac-TFUOYIVN.cjs.map} +1 -1
  110. package/dist/rbac-XSNP76CV.js +5 -0
  111. package/dist/{rbac-Y43IEQOP.js.map → rbac-XSNP76CV.js.map} +1 -1
  112. package/dist/routes.cjs +33 -32
  113. package/dist/routes.d.cts +3 -5
  114. package/dist/routes.d.ts +3 -5
  115. package/dist/routes.js +13 -12
  116. package/dist/services.cjs +30 -30
  117. package/dist/services.d.cts +10 -3
  118. package/dist/services.d.ts +10 -3
  119. package/dist/services.js +3 -3
  120. package/dist/{telemetry-Cku1ax74.d.cts → telemetry-QFlTv9f8.d.cts} +1 -1
  121. package/dist/{telemetry-Cku1ax74.d.ts → telemetry-QFlTv9f8.d.ts} +1 -1
  122. package/dist/templates.cjs +17 -17
  123. package/dist/templates.js +3 -3
  124. package/dist/types.d.cts +2 -2
  125. package/dist/types.d.ts +2 -2
  126. package/dist/utils.cjs +12 -12
  127. package/dist/utils.d.cts +2 -2
  128. package/dist/utils.d.ts +2 -2
  129. package/dist/utils.js +2 -2
  130. package/package.json +7 -12
  131. package/dist/admin-documents-form.template-55V2ZGWT.js +0 -6
  132. package/dist/admin-documents-form.template-YNAVMJCV.cjs +0 -19
  133. package/dist/admin-layout-catalyst.template-3XZXDGF7.cjs +0 -21
  134. package/dist/admin-layout-catalyst.template-6UQGLCKI.js +0 -7
  135. package/dist/chunk-2XAQTPAE.js.map +0 -1
  136. package/dist/chunk-36Z3AKX3.cjs.map +0 -1
  137. package/dist/chunk-37V3RET3.cjs.map +0 -1
  138. package/dist/chunk-3TMTYJAJ.js.map +0 -1
  139. package/dist/chunk-A2SN3RVL.cjs.map +0 -1
  140. package/dist/chunk-BJROXF4N.js.map +0 -1
  141. package/dist/chunk-BP52BFIV.cjs.map +0 -1
  142. package/dist/chunk-E66UVONV.cjs.map +0 -1
  143. package/dist/chunk-FHPEM77Y.js.map +0 -1
  144. package/dist/chunk-I767LVN7.cjs.map +0 -1
  145. package/dist/chunk-IXUHXTHW.cjs.map +0 -1
  146. package/dist/chunk-KKNEYIZJ.cjs.map +0 -1
  147. package/dist/chunk-LNI3FYSI.js.map +0 -1
  148. package/dist/chunk-LTZ5WYOK.cjs.map +0 -1
  149. package/dist/chunk-QCJHCSOK.cjs.map +0 -1
  150. package/dist/chunk-QJUJNTIU.cjs.map +0 -1
  151. package/dist/chunk-QZGABF2M.js.map +0 -1
  152. package/dist/chunk-RJ2O5IIE.js.map +0 -1
  153. package/dist/chunk-THFZSDIU.js.map +0 -1
  154. package/dist/chunk-UIHIXTIS.js.map +0 -1
  155. package/dist/chunk-UYJQDDEE.cjs.map +0 -1
  156. package/dist/chunk-VC5XVPLM.js.map +0 -1
  157. package/dist/chunk-VGEZNFWH.js.map +0 -1
  158. package/dist/chunk-VYQVDL2M.js.map +0 -1
  159. package/dist/config-6FB2BXMP.js +0 -6
  160. package/dist/migrations-FGXZPKDC.cjs +0 -13
  161. package/dist/migrations-Z6D27NAW.js +0 -4
  162. package/dist/rbac-7QZ4JZ2E.cjs +0 -14
  163. package/dist/rbac-Y43IEQOP.js +0 -5
@@ -1,18 +1,19 @@
1
+ import { renderDocumentFormPage, renderAlert } from './chunk-E3TQG7AA.js';
2
+ import { recordCatalogRequest, scheduleKvWrite } from './chunk-F6BTBE4S.js';
1
3
  import { isFirstUserRegistration, isRegistrationEnabled, authValidationService } from './chunk-VVYVFCML.js';
2
- import { renderDocumentFormPage, renderAlert } from './chunk-BHND5PEV.js';
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 { getCacheService, CACHE_CONFIGS, SettingsService, getLogger, getAppInstance, buildRouteList, CATEGORY_INFO } from './chunk-6X22VFEV.js';
5
+ import { DocumentTypeRegistry, requireAuth, requireRole, optionalAuth, rateLimit, AuthManager, getJwtExpirySecondsFromDb, getJwtRefreshGraceSecondsFromDb, logActivity, generateCsrfToken } from './chunk-EEA476CJ.js';
6
+ import { DocumentsService, documentSecondsToMs, RbacService } from './chunk-RXLI3ADU.js';
6
7
  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';
8
+ import { getCollectionRegistry, isPluginActive, collectionRecordToRow, loadCollectionConfigs, getVisibleCollections, PluginService, PLUGIN_REGISTRY, findPluginByCodeName, isCodeCollectionInternal, isDbDocTypeInternal, invalidateTenantCache } from './chunk-RVOX2WH2.js';
9
+ import { MigrationService } from './chunk-5YSFQO7X.js';
10
+ import { renderCheckboxPage, renderAlert as renderAlert$1, renderTable, renderPagination, renderConfirmationDialog, getConfirmationDialogScript, renderAdminLayout, adminLayoutV2, renderForm } from './chunk-HSDJDWLV.js';
11
+ import { init_admin_layout_catalyst_template, renderAdminLayoutCatalyst } from './chunk-SXC4HPK6.js';
12
+ import { createServiceSingleton, definePlugin, getPluginDefinition, applySchemaDefaults, renderSchemaFields, parseFormDataToSettings, getPluginMenu } from './chunk-JD7BZU3A.js';
12
13
  import { hasHookSystem, getTypedHooks } from './chunk-RNZFGN4R.js';
13
- import { QueryFilterBuilder, getBlocksFieldConfig, parseBlocksValue } from './chunk-QZGABF2M.js';
14
+ import { QueryFilterBuilder, getBlocksFieldConfig, parseBlocksValue } from './chunk-AMP4BSJE.js';
14
15
  import { metricsTracker } from './chunk-FICTAGD4.js';
15
- import { getCoreVersion } from './chunk-LNI3FYSI.js';
16
+ import { getCoreVersion } from './chunk-UB5QSRO7.js';
16
17
  import { escapeHtml, sanitizeRichText, sanitizeInput } from './chunk-TQABQWOP.js';
17
18
  import { Hono } from 'hono';
18
19
  import { z } from 'zod';
@@ -45,6 +46,7 @@ var DocumentPermissionsService = class {
45
46
  const publicGrants = baseGrants["public"] ?? [];
46
47
  if (publicGrants.includes(permission)) return true;
47
48
  } else if (principal.type === "role") {
49
+ if (principal.id === "public") continue;
48
50
  const roleGrants = baseGrants[principal.id] ?? [];
49
51
  if (roleGrants.includes(permission)) return true;
50
52
  }
@@ -842,6 +844,34 @@ async function dispatchHookEvent(c, event, payload, mode = "fire-and-forget") {
842
844
  return hooks.dispatch(event, payload);
843
845
  }
844
846
 
847
+ // src/plugins/cache/services/swr.ts
848
+ var STALE_TTL_MS = 3e4;
849
+ var staleMap = /* @__PURE__ */ new Map();
850
+ function markStale(cacheKey, data, collection) {
851
+ staleMap.set(cacheKey, {
852
+ data,
853
+ collection,
854
+ expiresAt: Date.now() + STALE_TTL_MS
855
+ });
856
+ }
857
+ function getAndConsumeStale(cacheKey) {
858
+ const entry = staleMap.get(cacheKey);
859
+ if (!entry) return null;
860
+ if (Date.now() > entry.expiresAt) {
861
+ staleMap.delete(cacheKey);
862
+ return null;
863
+ }
864
+ staleMap.delete(cacheKey);
865
+ return entry.data;
866
+ }
867
+ function swrStats() {
868
+ const now = Date.now();
869
+ for (const [key, entry] of staleMap.entries()) {
870
+ if (now > entry.expiresAt) staleMap.delete(key);
871
+ }
872
+ return { count: staleMap.size };
873
+ }
874
+
845
875
  // src/plugins/core-plugins/global-variables-plugin/variable-resolver.ts
846
876
  var TOKEN_PATTERN = /\{([a-z0-9_]+)\}/g;
847
877
  function resolveVariables(text, variables) {
@@ -994,6 +1024,32 @@ apiContentCrudRoutes.get("/:id", optionalAuth(), async (c) => {
994
1024
  { collection: docRow.type_id, id: docRow.root_id, data: transformedContent.data },
995
1025
  "fire-and-forget"
996
1026
  );
1027
+ const fieldsParam = c.req.query("fields");
1028
+ if (fieldsParam) {
1029
+ const projFields = fieldsParam.split(",").map((s) => s.trim()).filter(Boolean);
1030
+ if (projFields.length > 0) {
1031
+ const result = {};
1032
+ const dataSubFields = [];
1033
+ for (const f of projFields) {
1034
+ if (f === "data") {
1035
+ result.data = transformedContent.data;
1036
+ } else if (f.startsWith("data.")) {
1037
+ dataSubFields.push(f.slice(5));
1038
+ } else if (Object.prototype.hasOwnProperty.call(transformedContent, f)) {
1039
+ result[f] = transformedContent[f];
1040
+ }
1041
+ }
1042
+ if (dataSubFields.length > 0 && !result.data) {
1043
+ result.data = {};
1044
+ for (const sub of dataSubFields) {
1045
+ if (transformedContent.data && Object.prototype.hasOwnProperty.call(transformedContent.data, sub)) {
1046
+ result.data[sub] = transformedContent.data[sub];
1047
+ }
1048
+ }
1049
+ }
1050
+ return c.json({ data: result });
1051
+ }
1052
+ }
997
1053
  return c.json({ data: transformedContent });
998
1054
  } catch (error) {
999
1055
  console.error("Error fetching content:", error);
@@ -1057,8 +1113,8 @@ apiContentCrudRoutes.post("/", requireAuth(), requireRole(["admin", "editor", "a
1057
1113
  user?.userId
1058
1114
  );
1059
1115
  const cache = getCacheService(CACHE_CONFIGS.api);
1060
- await cache.invalidate("content-filtered:*");
1061
- await cache.invalidate("collection-content-filtered:*");
1116
+ await cache.invalidate("api:content-filtered:*");
1117
+ await cache.invalidate(`api:collection-content-filtered:${backing.coll.name}:*`);
1062
1118
  dispatchHookEvent(
1063
1119
  c,
1064
1120
  "content:after:create",
@@ -1121,8 +1177,8 @@ apiContentCrudRoutes.put("/:id", requireAuth(), requireRole(["admin", "editor",
1121
1177
  await svc.unpublish(pub.id);
1122
1178
  }
1123
1179
  const cache = getCacheService(CACHE_CONFIGS.api);
1124
- await cache.invalidate("content-filtered:*");
1125
- await cache.invalidate("collection-content-filtered:*");
1180
+ await cache.invalidate("api:content-filtered:*");
1181
+ await cache.invalidate(`api:collection-content-filtered:${docRow.type_id}:*`);
1126
1182
  const coll = getCollectionRegistry().getByName(docRow.type_id);
1127
1183
  const saved = await db.prepare("SELECT * FROM documents WHERE id = ?").bind(newDraft.id).first();
1128
1184
  const savedData = saved?.data ? JSON.parse(saved.data) : {};
@@ -1161,8 +1217,8 @@ apiContentCrudRoutes.delete("/:id", requireAuth(), requireRole(["admin", "editor
1161
1217
  const now = Math.floor(Date.now() / 1e3);
1162
1218
  await db.prepare("UPDATE documents SET deleted_at = ?, updated_at = ? WHERE root_id = ? AND tenant_id = ?").bind(now, now, id, tenantId).run();
1163
1219
  const cache = getCacheService(CACHE_CONFIGS.api);
1164
- await cache.invalidate("content-filtered:*");
1165
- await cache.invalidate("collection-content-filtered:*");
1220
+ await cache.invalidate("api:content-filtered:*");
1221
+ await cache.invalidate(`api:collection-content-filtered:${docRow.type_id}:*`);
1166
1222
  dispatchHookEvent(c, "content:after:delete", { collection: docRow.type_id, id, data: {}, user: actor }, "fire-and-forget");
1167
1223
  return c.json({ success: true });
1168
1224
  }
@@ -1178,6 +1234,22 @@ apiContentCrudRoutes.delete("/:id", requireAuth(), requireRole(["admin", "editor
1178
1234
  var api_content_crud_default = apiContentCrudRoutes;
1179
1235
 
1180
1236
  // src/routes/api.ts
1237
+ function isAnonPrincipal(principalSet) {
1238
+ return principalSet.length === 1 && principalSet[0]?.type === "public";
1239
+ }
1240
+ async function typeReadAllowed(db, principalSet, docType, typeName) {
1241
+ const perms = new DocumentPermissionsService(db);
1242
+ if (perms.isAllowedSync(principalSet, [], "read", docType?.settings ?? {})) return true;
1243
+ const rbac = new RbacService(db);
1244
+ let anyRbacRoleFound = false;
1245
+ for (const p of principalSet) {
1246
+ if (p.type !== "role") continue;
1247
+ if (!await rbac.hasRbacRole(p.id)) continue;
1248
+ anyRbacRoleFound = true;
1249
+ if (await rbac.isGrantedForRole(p.id, `document_type:${typeName}`, "read")) return true;
1250
+ }
1251
+ return !anyRbacRoleFound;
1252
+ }
1181
1253
  var DOC_SORTABLE_COLUMNS = /* @__PURE__ */ new Set([
1182
1254
  "created_at",
1183
1255
  "updated_at",
@@ -1261,6 +1333,33 @@ function mapDocRowToContent(row, collectionId) {
1261
1333
  updated_at: documentSecondsToMs(row.updated_at)
1262
1334
  };
1263
1335
  }
1336
+ function projectFields(item, fields) {
1337
+ if (fields.length === 0) return item;
1338
+ const result = {};
1339
+ const dataSubFields = [];
1340
+ for (const f of fields) {
1341
+ if (f === "data") {
1342
+ result.data = item.data;
1343
+ } else if (f.startsWith("data.")) {
1344
+ dataSubFields.push(f.slice(5));
1345
+ } else if (Object.prototype.hasOwnProperty.call(item, f)) {
1346
+ result[f] = item[f];
1347
+ }
1348
+ }
1349
+ if (dataSubFields.length > 0 && !result.data) {
1350
+ result.data = {};
1351
+ for (const sub of dataSubFields) {
1352
+ if (item.data && Object.prototype.hasOwnProperty.call(item.data, sub)) {
1353
+ result.data[sub] = item.data[sub];
1354
+ }
1355
+ }
1356
+ }
1357
+ return result;
1358
+ }
1359
+ function parseFieldsParam(raw2) {
1360
+ if (!raw2) return [];
1361
+ return raw2.split(",").map((s) => s.trim()).filter(Boolean);
1362
+ }
1264
1363
  var apiRoutes = new Hono();
1265
1364
  apiRoutes.use("*", async (c, next) => {
1266
1365
  const startTime = Date.now();
@@ -1270,7 +1369,8 @@ apiRoutes.use("*", async (c, next) => {
1270
1369
  c.header("X-Response-Time", `${totalTime}ms`);
1271
1370
  });
1272
1371
  apiRoutes.use("*", async (c, next) => {
1273
- const cacheEnabled = await isPluginActive(c.env.DB, "core-cache");
1372
+ const bypass = c.req.header("Cache-Control") === "no-cache";
1373
+ const cacheEnabled = !bypass && await isPluginActive(c.env.DB, "core-cache");
1274
1374
  c.set("cacheEnabled", cacheEnabled);
1275
1375
  await next();
1276
1376
  });
@@ -1750,7 +1850,12 @@ apiRoutes.get("/content", optionalAuth(), async (c) => {
1750
1850
  try {
1751
1851
  const db = c.env.DB;
1752
1852
  const queryParams = c.req.query();
1853
+ const projFields = parseFieldsParam(queryParams.fields);
1854
+ if (queryParams.fields) delete queryParams.fields;
1753
1855
  const role = c.get("user")?.role;
1856
+ const { tenantId: cTenantId, principalSet: cPrincipalSet } = getDocumentRequestContext(c);
1857
+ const cAnon = isAnonPrincipal(cPrincipalSet);
1858
+ const cNeedsAcl = !cAnon && !canReadNonPublicContent(role);
1754
1859
  const collIdByName = /* @__PURE__ */ new Map();
1755
1860
  let typeId;
1756
1861
  let typeIds;
@@ -1796,7 +1901,7 @@ apiRoutes.get("/content", optionalAuth(), async (c) => {
1796
1901
  details: queryResult.errors
1797
1902
  }, 400);
1798
1903
  }
1799
- const cacheEnabled = c.get("cacheEnabled");
1904
+ const cacheEnabled = c.get("cacheEnabled") && !cNeedsAcl;
1800
1905
  const cache = getCacheService(CACHE_CONFIGS.api);
1801
1906
  const cacheKey = cache.generateKey("content-filtered", JSON.stringify({ filter: normalizedFilter, query: queryResult.sql }));
1802
1907
  if (cacheEnabled) {
@@ -1818,19 +1923,61 @@ apiRoutes.get("/content", optionalAuth(), async (c) => {
1818
1923
  }
1819
1924
  }, executionStart)
1820
1925
  };
1926
+ recordCatalogRequest({ cacheKey, collection: typeId ?? null, path: c.req.path, queryString: c.req.raw.url.split("?")[1] ?? "", source: cacheResult.source });
1927
+ scheduleKvWrite(cacheKey, c.executionCtx);
1821
1928
  return c.json(dataWithMeta);
1822
1929
  }
1930
+ const swrData = getAndConsumeStale(cacheKey);
1931
+ if (swrData) {
1932
+ c.header("X-Cache-Status", "STALE");
1933
+ c.header("X-Cache-Source", "swr");
1934
+ recordCatalogRequest({ cacheKey, collection: typeId ?? null, path: c.req.path, queryString: c.req.raw.url.split("?")[1] ?? "", source: "swr" });
1935
+ scheduleKvWrite(cacheKey, c.executionCtx);
1936
+ return c.json({ ...swrData, meta: addTimingMeta(c, { ...swrData.meta, cache: { hit: true, source: "swr", stale: true } }, executionStart) });
1937
+ }
1938
+ recordCatalogRequest({ cacheKey, collection: typeId ?? null, path: c.req.path, queryString: c.req.raw.url.split("?")[1] ?? "", source: "miss" });
1939
+ scheduleKvWrite(cacheKey, c.executionCtx);
1823
1940
  }
1824
1941
  c.header("X-Cache-Status", "MISS");
1825
1942
  c.header("X-Cache-Source", "database");
1826
1943
  const stmt = db.prepare(queryResult.sql);
1827
1944
  const boundStmt = queryResult.params.length > 0 ? stmt.bind(...queryResult.params) : stmt;
1828
1945
  const { results } = await boundStmt.all();
1829
- const transformedResults = results.map((row) => mapDocRowToContent(row, collIdByName.get(row.type_id) ?? null));
1946
+ let aclResults = results;
1947
+ if (cNeedsAcl) {
1948
+ const typeReg = new DocumentTypeRegistry(db);
1949
+ const repo = new DocumentRepository(db, cTenantId);
1950
+ const uniqueTypeIds = [...new Set(results.map((r) => r.type_id))];
1951
+ const typeSettingsMap = /* @__PURE__ */ new Map();
1952
+ const typeRbacOnlyMap = /* @__PURE__ */ new Map();
1953
+ const typeDeniedMap = /* @__PURE__ */ new Map();
1954
+ await Promise.all(uniqueTypeIds.map(async (tid) => {
1955
+ const dt = await typeReg.findById(tid);
1956
+ const settings = dt?.settings ?? {};
1957
+ typeSettingsMap.set(tid, settings);
1958
+ const allowed2 = await typeReadAllowed(db, cPrincipalSet, dt, tid);
1959
+ if (!allowed2) {
1960
+ typeDeniedMap.set(tid, true);
1961
+ return;
1962
+ }
1963
+ const perms = new DocumentPermissionsService(db);
1964
+ typeRbacOnlyMap.set(tid, !perms.isAllowedSync(cPrincipalSet, [], "read", settings));
1965
+ }));
1966
+ const allowed = await Promise.all(
1967
+ results.map((row) => {
1968
+ const tid = row.type_id;
1969
+ if (typeDeniedMap.get(tid)) return Promise.resolve(false);
1970
+ if (typeRbacOnlyMap.get(tid)) return Promise.resolve(true);
1971
+ return repo.isAllowed(cPrincipalSet, row.root_id, "read", typeSettingsMap.get(tid) ?? {});
1972
+ })
1973
+ );
1974
+ aclResults = results.filter((_, i) => allowed[i]);
1975
+ }
1976
+ const transformedResults = aclResults.map((row) => mapDocRowToContent(row, collIdByName.get(row.type_id) ?? null));
1830
1977
  const responseData = {
1831
- data: transformedResults,
1978
+ data: projFields.length ? transformedResults.map((item) => projectFields(item, projFields)) : transformedResults,
1832
1979
  meta: addTimingMeta(c, {
1833
- count: results.length,
1980
+ count: aclResults.length,
1834
1981
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1835
1982
  // D44: echo the caller's filter with the access policy applied (status=published forced for
1836
1983
  // anonymous callers — the visible enforcement proof), NOT the internal doc-augmented where-tree.
@@ -1841,7 +1988,7 @@ apiRoutes.get("/content", optionalAuth(), async (c) => {
1841
1988
  }
1842
1989
  }, executionStart)
1843
1990
  };
1844
- if (cacheEnabled) {
1991
+ if (cacheEnabled && projFields.length === 0) {
1845
1992
  await cache.set(cacheKey, responseData);
1846
1993
  }
1847
1994
  return c.json(responseData);
@@ -1866,6 +2013,16 @@ apiRoutes.get("/collections/:collection/content", optionalAuth(), async (c) => {
1866
2013
  const collIdByName = /* @__PURE__ */ new Map();
1867
2014
  collIdByName.set(collection, record.id);
1868
2015
  const role = c.get("user")?.role;
2016
+ const { tenantId: ccTenantId, principalSet: ccPrincipalSet } = getDocumentRequestContext(c);
2017
+ const ccAnon = isAnonPrincipal(ccPrincipalSet);
2018
+ const ccNeedsAcl = !ccAnon && !canReadNonPublicContent(role);
2019
+ let ccDocType;
2020
+ if (ccNeedsAcl) {
2021
+ ccDocType = await new DocumentTypeRegistry(db).findById(record.name);
2022
+ if (!await typeReadAllowed(db, ccPrincipalSet, ccDocType, record.name)) {
2023
+ return c.json({ error: "Forbidden" }, 403);
2024
+ }
2025
+ }
1869
2026
  const filter = QueryFilterBuilder.parseFromQuery(queryParams);
1870
2027
  const normalizedFilter = augmentFilterForDocuments(filter, { typeId: collection, role });
1871
2028
  if (!normalizedFilter.limit) {
@@ -1880,7 +2037,7 @@ apiRoutes.get("/collections/:collection/content", optionalAuth(), async (c) => {
1880
2037
  details: queryResult.errors
1881
2038
  }, 400);
1882
2039
  }
1883
- const cacheEnabled = c.get("cacheEnabled");
2040
+ const cacheEnabled = c.get("cacheEnabled") && !ccNeedsAcl;
1884
2041
  const cache = getCacheService(CACHE_CONFIGS.api);
1885
2042
  const includeCollection = queryParams.include?.split(",").map((s) => s.trim()).includes("collection");
1886
2043
  const cacheKey = cache.generateKey("collection-content-filtered", `${collection}:${JSON.stringify({ filter: normalizedFilter, query: queryResult.sql, includeCollection })}`);
@@ -1903,20 +2060,44 @@ apiRoutes.get("/collections/:collection/content", optionalAuth(), async (c) => {
1903
2060
  }
1904
2061
  }, executionStart)
1905
2062
  };
2063
+ recordCatalogRequest({ cacheKey, collection: collection ?? null, path: c.req.path, queryString: c.req.raw.url.split("?")[1] ?? "", source: cacheResult.source });
2064
+ scheduleKvWrite(cacheKey, c.executionCtx);
1906
2065
  return c.json(dataWithMeta);
1907
2066
  }
2067
+ const swrData = getAndConsumeStale(cacheKey);
2068
+ if (swrData) {
2069
+ c.header("X-Cache-Status", "STALE");
2070
+ c.header("X-Cache-Source", "swr");
2071
+ recordCatalogRequest({ cacheKey, collection: collection ?? null, path: c.req.path, queryString: c.req.raw.url.split("?")[1] ?? "", source: "swr" });
2072
+ scheduleKvWrite(cacheKey, c.executionCtx);
2073
+ return c.json({ ...swrData, meta: addTimingMeta(c, { ...swrData.meta, cache: { hit: true, source: "swr", stale: true } }, executionStart) });
2074
+ }
2075
+ recordCatalogRequest({ cacheKey, collection: collection ?? null, path: c.req.path, queryString: c.req.raw.url.split("?")[1] ?? "", source: "miss" });
2076
+ scheduleKvWrite(cacheKey, c.executionCtx);
1908
2077
  }
1909
2078
  c.header("X-Cache-Status", "MISS");
1910
2079
  c.header("X-Cache-Source", "database");
1911
2080
  const stmt = db.prepare(queryResult.sql);
1912
2081
  const boundStmt = queryResult.params.length > 0 ? stmt.bind(...queryResult.params) : stmt;
1913
2082
  const { results } = await boundStmt.all();
1914
- const transformedResults = results.map((row) => mapDocRowToContent(row, collIdByName.get(row.type_id) ?? null));
2083
+ let ccAclResults = results;
2084
+ if (ccNeedsAcl) {
2085
+ const ccPerms = new DocumentPermissionsService(db);
2086
+ const ccBaseGrantsPass = ccPerms.isAllowedSync(ccPrincipalSet, [], "read", ccDocType?.settings ?? {});
2087
+ if (ccBaseGrantsPass) {
2088
+ const repo = new DocumentRepository(db, ccTenantId);
2089
+ const allowed = await Promise.all(
2090
+ results.map((row) => repo.isAllowed(ccPrincipalSet, row.root_id, "read", ccDocType?.settings ?? {}))
2091
+ );
2092
+ ccAclResults = results.filter((_, i) => allowed[i]);
2093
+ }
2094
+ }
2095
+ const transformedResults = ccAclResults.map((row) => mapDocRowToContent(row, collIdByName.get(row.type_id) ?? null));
1915
2096
  const responseData = {
1916
2097
  data: transformedResults,
1917
2098
  meta: addTimingMeta(c, {
1918
2099
  ...includeCollection ? { collection: collectionRecordToRow(record) } : {},
1919
- count: results.length,
2100
+ count: ccAclResults.length,
1920
2101
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1921
2102
  // D44: echo the caller's filter with the access policy applied (status=published forced for
1922
2103
  // anonymous callers — the visible enforcement proof), NOT the internal doc-augmented where-tree.
@@ -1946,6 +2127,8 @@ apiRoutes.get("/:collection", optionalAuth(), async (c) => {
1946
2127
  const collection = c.req.param("collection");
1947
2128
  const db = c.env.DB;
1948
2129
  const queryParams = c.req.query();
2130
+ const projFields = parseFieldsParam(queryParams.fields);
2131
+ if (queryParams.fields) delete queryParams.fields;
1949
2132
  const record = getCollectionRegistry().getBySlugOrName(collection);
1950
2133
  if (!record || record.isActive === false) {
1951
2134
  return c.json({ error: "Collection not found" }, 404);
@@ -1953,6 +2136,16 @@ apiRoutes.get("/:collection", optionalAuth(), async (c) => {
1953
2136
  const collIdByName = /* @__PURE__ */ new Map();
1954
2137
  collIdByName.set(record.name, record.id);
1955
2138
  const role = c.get("user")?.role;
2139
+ const { tenantId, principalSet } = getDocumentRequestContext(c);
2140
+ const anon = isAnonPrincipal(principalSet);
2141
+ const needsAcl = !anon && !canReadNonPublicContent(role);
2142
+ let docTypeForCheck;
2143
+ if (needsAcl) {
2144
+ docTypeForCheck = await new DocumentTypeRegistry(db).findById(record.name);
2145
+ if (!await typeReadAllowed(db, principalSet, docTypeForCheck, record.name)) {
2146
+ return c.json({ error: "Forbidden" }, 403);
2147
+ }
2148
+ }
1956
2149
  const filter = QueryFilterBuilder.parseFromQuery(queryParams);
1957
2150
  const normalizedFilter = augmentFilterForDocuments(filter, { typeId: record.name, role });
1958
2151
  if (!normalizedFilter.limit) normalizedFilter.limit = 50;
@@ -1964,7 +2157,7 @@ apiRoutes.get("/:collection", optionalAuth(), async (c) => {
1964
2157
  }
1965
2158
  const collectionCache = record.cache;
1966
2159
  const collectionCacheDisabled = collectionCache?.enabled === false;
1967
- const cacheEnabled = c.get("cacheEnabled") && !collectionCacheDisabled;
2160
+ const cacheEnabled = c.get("cacheEnabled") && !collectionCacheDisabled && projFields.length === 0 && !needsAcl;
1968
2161
  const cache = getCacheService(CACHE_CONFIGS.api);
1969
2162
  const includeCollection = queryParams.include?.split(",").map((s) => s.trim()).includes("collection");
1970
2163
  const cacheKey = cache.generateKey("collection-content-filtered", `${collection}:${JSON.stringify({ filter: normalizedFilter, query: queryResult.sql, includeCollection })}`);
@@ -1974,20 +2167,45 @@ apiRoutes.get("/:collection", optionalAuth(), async (c) => {
1974
2167
  c.header("X-Cache-Status", "HIT");
1975
2168
  c.header("X-Cache-Source", cacheResult.source);
1976
2169
  if (cacheResult.ttl) c.header("X-Cache-TTL", Math.floor(cacheResult.ttl).toString());
2170
+ recordCatalogRequest({ cacheKey, collection: collection ?? null, path: c.req.path, queryString: c.req.raw.url.split("?")[1] ?? "", source: cacheResult.source });
2171
+ scheduleKvWrite(cacheKey, c.executionCtx);
1977
2172
  return c.json({ ...cacheResult.data, meta: addTimingMeta(c, { ...cacheResult.data.meta, cache: { hit: true, source: cacheResult.source, ttl: cacheResult.ttl ? Math.floor(cacheResult.ttl) : void 0 } }, executionStart) });
1978
2173
  }
2174
+ const swrData = getAndConsumeStale(cacheKey);
2175
+ if (swrData) {
2176
+ c.header("X-Cache-Status", "STALE");
2177
+ c.header("X-Cache-Source", "swr");
2178
+ recordCatalogRequest({ cacheKey, collection: collection ?? null, path: c.req.path, queryString: c.req.raw.url.split("?")[1] ?? "", source: "swr" });
2179
+ scheduleKvWrite(cacheKey, c.executionCtx);
2180
+ return c.json({ ...swrData, meta: addTimingMeta(c, { ...swrData.meta, cache: { hit: true, source: "swr", stale: true } }, executionStart) });
2181
+ }
2182
+ recordCatalogRequest({ cacheKey, collection: collection ?? null, path: c.req.path, queryString: c.req.raw.url.split("?")[1] ?? "", source: "miss" });
2183
+ scheduleKvWrite(cacheKey, c.executionCtx);
1979
2184
  }
1980
2185
  c.header("X-Cache-Status", "MISS");
1981
2186
  c.header("X-Cache-Source", "database");
1982
2187
  const stmt = db.prepare(queryResult.sql);
1983
2188
  const boundStmt = queryResult.params.length > 0 ? stmt.bind(...queryResult.params) : stmt;
1984
2189
  const { results } = await boundStmt.all();
1985
- const transformedResults = results.map((row) => mapDocRowToContent(row, collIdByName.get(row.type_id) ?? null));
2190
+ let aclResults = results;
2191
+ if (needsAcl) {
2192
+ const perms = new DocumentPermissionsService(db);
2193
+ const baseGrantsPass = perms.isAllowedSync(principalSet, [], "read", docTypeForCheck?.settings ?? {});
2194
+ if (!baseGrantsPass) {
2195
+ } else {
2196
+ const repo = new DocumentRepository(db, tenantId);
2197
+ const allowed = await Promise.all(
2198
+ results.map((row) => repo.isAllowed(principalSet, row.root_id, "read", docTypeForCheck?.settings ?? {}))
2199
+ );
2200
+ aclResults = results.filter((_, i) => allowed[i]);
2201
+ }
2202
+ }
2203
+ const transformedResults = aclResults.map((row) => mapDocRowToContent(row, collIdByName.get(row.type_id) ?? null));
1986
2204
  const responseData = {
1987
- data: transformedResults,
2205
+ data: projFields.length ? transformedResults.map((item) => projectFields(item, projFields)) : transformedResults,
1988
2206
  meta: addTimingMeta(c, {
1989
2207
  ...includeCollection ? { collection: collectionRecordToRow(record) } : {},
1990
- count: results.length,
2208
+ count: aclResults.length,
1991
2209
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1992
2210
  filter: normalizePublicContentFilter(filter, role),
1993
2211
  cache: { hit: false, source: "database" }
@@ -2018,6 +2236,23 @@ apiRoutes.get("/:collection/:id", optionalAuth(), async (c) => {
2018
2236
  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
2237
  ).bind(id, record.name).first();
2020
2238
  if (!docRow) return c.json({ error: "Content not found" }, 404);
2239
+ const colRole = c.get("user")?.role;
2240
+ const { tenantId: colTenantId, principalSet: colPrincipalSet } = getDocumentRequestContext(c);
2241
+ const colNeedsAcl = !isAnonPrincipal(colPrincipalSet) && !canReadNonPublicContent(colRole);
2242
+ if (colNeedsAcl) {
2243
+ const docType = await new DocumentTypeRegistry(db).findById(record.name);
2244
+ if (!await typeReadAllowed(db, colPrincipalSet, docType, record.name)) {
2245
+ return c.json({ error: "Content not found" }, 404);
2246
+ }
2247
+ const perms = new DocumentPermissionsService(db);
2248
+ if (perms.isAllowedSync(colPrincipalSet, [], "read", docType?.settings ?? {})) {
2249
+ const repo = new DocumentRepository(db, colTenantId);
2250
+ if (!await repo.isAllowed(colPrincipalSet, docRow.root_id, "read", docType?.settings ?? {})) {
2251
+ return c.json({ error: "Content not found" }, 404);
2252
+ }
2253
+ }
2254
+ }
2255
+ const projFields = parseFieldsParam(c.req.query("fields"));
2021
2256
  const coll = getCollectionRegistry().getByName(docRow.type_id);
2022
2257
  const transformedContent = {
2023
2258
  id: docRow.root_id,
@@ -2030,7 +2265,7 @@ apiRoutes.get("/:collection/:id", optionalAuth(), async (c) => {
2030
2265
  updated_at: documentSecondsToMs(docRow.updated_at)
2031
2266
  };
2032
2267
  dispatchHookEvent(c, "content:read", { collection: docRow.type_id, id: docRow.root_id, data: transformedContent.data }, "fire-and-forget");
2033
- return c.json({ data: transformedContent });
2268
+ return c.json({ data: projFields.length ? projectFields(transformedContent, projFields) : transformedContent });
2034
2269
  } catch (error) {
2035
2270
  console.error("Error fetching content:", error);
2036
2271
  return c.json({ error: "Failed to fetch content", details: error instanceof Error ? error.message : String(error) }, 500);
@@ -2060,8 +2295,11 @@ apiRoutes.post("/:collection", requireAuth(), requireRole(["admin", "editor", "a
2060
2295
  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
2296
  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
2297
  const cache = getCacheService(CACHE_CONFIGS.api);
2063
- await cache.invalidate("content-filtered:*");
2064
- await cache.invalidate("collection-content-filtered:*");
2298
+ const createAffectedKeys = (await cache.listKeys()).map((k) => k.key).filter((k) => k.startsWith("api:content-filtered:") || k.startsWith(`api:collection-content-filtered:${backing.coll.name}:`));
2299
+ const createStaleValues = await cache.getMany(createAffectedKeys);
2300
+ for (const [key, val] of createStaleValues) markStale(key, val, backing.coll.name);
2301
+ await cache.invalidate("api:content-filtered:*");
2302
+ await cache.invalidate(`api:collection-content-filtered:${backing.coll.name}:*`);
2065
2303
  dispatchHookEvent(c, "content:after:create", { collection: backing.coll.name, id: doc.rootId, data: doc.data ?? {}, user: actor }, "fire-and-forget");
2066
2304
  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
2305
  } catch (error) {
@@ -2103,8 +2341,11 @@ apiRoutes.put("/:collection/:id", requireAuth(), requireRole(["admin", "editor",
2103
2341
  await svc.unpublish(pub.id);
2104
2342
  }
2105
2343
  const cache = getCacheService(CACHE_CONFIGS.api);
2106
- await cache.invalidate("content-filtered:*");
2107
- await cache.invalidate("collection-content-filtered:*");
2344
+ const updateAffectedKeys = (await cache.listKeys()).map((k) => k.key).filter((k) => k.startsWith("api:content-filtered:") || k.startsWith(`api:collection-content-filtered:${docRow.type_id}:`));
2345
+ const updateStaleValues = await cache.getMany(updateAffectedKeys);
2346
+ for (const [key, val] of updateStaleValues) markStale(key, val, docRow.type_id);
2347
+ await cache.invalidate("api:content-filtered:*");
2348
+ await cache.invalidate(`api:collection-content-filtered:${docRow.type_id}:*`);
2108
2349
  const coll = getCollectionRegistry().getByName(docRow.type_id);
2109
2350
  const saved = await db.prepare("SELECT * FROM documents WHERE id = ?").bind(newDraft.id).first();
2110
2351
  const savedData = saved?.data ? JSON.parse(saved.data) : {};
@@ -2136,8 +2377,11 @@ apiRoutes.delete("/:collection/:id", requireAuth(), requireRole(["admin", "edito
2136
2377
  const now = Math.floor(Date.now() / 1e3);
2137
2378
  await db.prepare("UPDATE documents SET deleted_at = ?, updated_at = ? WHERE root_id = ? AND tenant_id = 'default'").bind(now, now, id).run();
2138
2379
  const cache = getCacheService(CACHE_CONFIGS.api);
2139
- await cache.invalidate("content-filtered:*");
2140
- await cache.invalidate("collection-content-filtered:*");
2380
+ const deleteAffectedKeys = (await cache.listKeys()).map((k) => k.key).filter((k) => k.startsWith("api:content-filtered:") || k.startsWith(`api:collection-content-filtered:${docRow.type_id}:`));
2381
+ const deleteStaleValues = await cache.getMany(deleteAffectedKeys);
2382
+ for (const [key, val] of deleteStaleValues) markStale(key, val, docRow.type_id);
2383
+ await cache.invalidate("api:content-filtered:*");
2384
+ await cache.invalidate(`api:collection-content-filtered:${docRow.type_id}:*`);
2141
2385
  dispatchHookEvent(c, "content:after:delete", { collection: docRow.type_id, id, data: {}, user: actor }, "fire-and-forget");
2142
2386
  return c.json({ success: true });
2143
2387
  } catch (error) {
@@ -2699,9 +2943,13 @@ apiMediaRoutes.post("/create-folder", async (c) => {
2699
2943
  error: "Folder name can only contain lowercase letters, numbers, hyphens, and underscores"
2700
2944
  }, 400);
2701
2945
  }
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) {
2946
+ let folderExists = false;
2947
+ try {
2948
+ 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();
2949
+ folderExists = (existing?.count ?? 0) > 0;
2950
+ } catch {
2951
+ }
2952
+ if (folderExists) {
2705
2953
  return c.json({
2706
2954
  success: false,
2707
2955
  error: `Folder "${folderName}" already exists`
@@ -3411,7 +3659,7 @@ adminApiRoutes.patch("/collections/:id", collectionsReadOnly);
3411
3659
  adminApiRoutes.delete("/collections/:id", collectionsReadOnly);
3412
3660
  adminApiRoutes.get("/migrations/status", async (c) => {
3413
3661
  try {
3414
- const { MigrationService: MigrationService2 } = await import('./migrations-Z6D27NAW.js');
3662
+ const { MigrationService: MigrationService2 } = await import('./migrations-GZ2HIB4I.js');
3415
3663
  const db = c.env.DB;
3416
3664
  const migrationService = new MigrationService2(db);
3417
3665
  const status = await migrationService.getMigrationStatus();
@@ -3454,7 +3702,7 @@ adminApiRoutes.post("/migrations/run", async (c) => {
3454
3702
  });
3455
3703
  adminApiRoutes.get("/migrations/validate", async (c) => {
3456
3704
  try {
3457
- const { MigrationService: MigrationService2 } = await import('./migrations-Z6D27NAW.js');
3705
+ const { MigrationService: MigrationService2 } = await import('./migrations-GZ2HIB4I.js');
3458
3706
  const db = c.env.DB;
3459
3707
  const migrationService = new MigrationService2(db);
3460
3708
  const validation = await migrationService.validateSchema();
@@ -3613,6 +3861,8 @@ function renderLoginPage(data, demoLoginActive = false) {
3613
3861
  <!-- Form -->
3614
3862
  <form
3615
3863
  id="login-form"
3864
+ action="/auth/login/form${data.redirect ? `?redirect=${encodeURIComponent(data.redirect)}` : ""}"
3865
+ method="post"
3616
3866
  hx-post="/auth/login/form${data.redirect ? `?redirect=${encodeURIComponent(data.redirect)}` : ""}"
3617
3867
  hx-target="#form-response"
3618
3868
  hx-swap="innerHTML"
@@ -5128,9 +5378,11 @@ function renderDynamicField(field, options = {}) {
5128
5378
  min="${opts.min || ""}"
5129
5379
  max="${opts.max || ""}"
5130
5380
  class="${baseClasses} ${errorClasses} !w-auto dark:[color-scheme:dark]"
5381
+ placeholder="YYYY-MM-DD"
5131
5382
  ${required}
5132
5383
  ${disabled ? "disabled" : ""}
5133
5384
  >
5385
+ <p class="mt-1 text-xs text-zinc-500 dark:text-zinc-400">Format: YYYY-MM-DD</p>
5134
5386
  `;
5135
5387
  break;
5136
5388
  case "datetime":
@@ -5146,6 +5398,7 @@ function renderDynamicField(field, options = {}) {
5146
5398
  ${required}
5147
5399
  ${disabled ? "disabled" : ""}
5148
5400
  >
5401
+ <p class="mt-1 text-xs text-zinc-500 dark:text-zinc-400">Format: YYYY-MM-DD HH:MM</p>
5149
5402
  `;
5150
5403
  break;
5151
5404
  case "slug":
@@ -7100,8 +7353,32 @@ function createUserProfilesPlugin() {
7100
7353
  return userProfilesPlugin;
7101
7354
  }
7102
7355
 
7356
+ // src/services/demo-mode.ts
7357
+ var _demoModeActive = false;
7358
+ function setDemoModeActive() {
7359
+ _demoModeActive = true;
7360
+ }
7361
+ function isDemoModeActive() {
7362
+ return _demoModeActive;
7363
+ }
7364
+
7103
7365
  // src/routes/auth.ts
7104
7366
  var JWT_SECRET_FALLBACK = "your-super-secret-jwt-key-change-in-production";
7367
+ var DEMO_EMAIL = "admin@sonicjs.com";
7368
+ var STATS_EVENTS_ENDPOINT = "https://stats.sonicjs.com/v1/events";
7369
+ async function trackDemoLogin(kv) {
7370
+ try {
7371
+ const installationId = await kv?.get("_sonicjs_installation_id") ?? "demo-unknown";
7372
+ await fetch(STATS_EVENTS_ENDPOINT, {
7373
+ method: "POST",
7374
+ headers: { "Content-Type": "application/json" },
7375
+ body: JSON.stringify({
7376
+ data: { installation_id: installationId, event_type: "demo_login", properties: {}, timestamp: (/* @__PURE__ */ new Date()).toISOString() }
7377
+ })
7378
+ });
7379
+ } catch {
7380
+ }
7381
+ }
7105
7382
  function renderPasswordResetEmail(resetLink, firstName) {
7106
7383
  const greeting = firstName ? `Hi ${firstName},` : "Hello,";
7107
7384
  return `<!DOCTYPE html>
@@ -7151,13 +7428,7 @@ authRoutes.get("/login", async (c) => {
7151
7428
  version: c.get("appVersion"),
7152
7429
  redirect: redirect && redirect.startsWith("/") ? redirect : void 0
7153
7430
  };
7154
- const db = c.env.DB;
7155
- let demoLoginActive = false;
7156
- try {
7157
- const plugin = await db.prepare("SELECT * FROM plugins WHERE id = ? AND status = ?").bind("demo-login-prefill", "active").first();
7158
- demoLoginActive = !!plugin;
7159
- } catch (error2) {
7160
- }
7431
+ const demoLoginActive = isDemoModeActive();
7161
7432
  return c.html(renderLoginPage(pageData, demoLoginActive));
7162
7433
  });
7163
7434
  authRoutes.get("/register", async (c) => {
@@ -7293,8 +7564,8 @@ authRoutes.post(
7293
7564
  }
7294
7565
  const { email, password } = validation.data;
7295
7566
  const normalizedEmail = email.toLowerCase();
7296
- const { createAuth } = await import('./config-6FB2BXMP.js');
7297
- const auth = createAuth(c.env);
7567
+ const { createAuth } = await import('./config-DOAWXZBG.js');
7568
+ const auth = createAuth(c.env, void 0, new URL(c.req.url).origin);
7298
7569
  const baReq = new Request(new URL("/auth/sign-in/email", c.req.url).href, {
7299
7570
  method: "POST",
7300
7571
  headers: { "Content-Type": "application/json", "Origin": new URL(c.req.url).origin },
@@ -7304,12 +7575,14 @@ authRoutes.post(
7304
7575
  if (!baRes.ok) {
7305
7576
  return c.json({ error: "Invalid email or password" }, 401);
7306
7577
  }
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) {
7578
+ if (baRes.headers.getSetCookie) {
7311
7579
  for (const sc of baRes.headers.getSetCookie()) {
7312
- c.res.headers.append("Set-Cookie", sc);
7580
+ c.header("Set-Cookie", sc, { append: true });
7581
+ }
7582
+ } else {
7583
+ const rawSetCookie = baRes.headers.get("set-cookie");
7584
+ if (rawSetCookie) {
7585
+ c.header("Set-Cookie", rawSetCookie, { append: true });
7313
7586
  }
7314
7587
  }
7315
7588
  await setCsrfCookie(c);
@@ -7329,6 +7602,9 @@ authRoutes.post(
7329
7602
  sameSite: "Strict",
7330
7603
  maxAge: tokenTtl
7331
7604
  });
7605
+ if (normalizedEmail === DEMO_EMAIL) {
7606
+ c.executionCtx?.waitUntil(trackDemoLogin(c.env.CACHE_KV));
7607
+ }
7332
7608
  return c.json({
7333
7609
  user: {
7334
7610
  id: user.id,
@@ -7347,8 +7623,8 @@ authRoutes.post(
7347
7623
  );
7348
7624
  authRoutes.post("/logout", async (c) => {
7349
7625
  try {
7350
- const { createAuth } = await import('./config-6FB2BXMP.js');
7351
- const auth = createAuth(c.env);
7626
+ const { createAuth } = await import('./config-DOAWXZBG.js');
7627
+ const auth = createAuth(c.env, void 0, new URL(c.req.url).origin);
7352
7628
  const baReq = new Request(new URL("/auth/sign-out", c.req.url).href, {
7353
7629
  method: "POST",
7354
7630
  headers: { "Content-Type": "application/json", "Origin": new URL(c.req.url).origin, "Cookie": c.req.header("Cookie") || "" },
@@ -7357,14 +7633,15 @@ authRoutes.post("/logout", async (c) => {
7357
7633
  await auth.handler(baReq);
7358
7634
  } catch {
7359
7635
  }
7360
- setCookie(c, "better-auth.session_token", "", { httpOnly: true, sameSite: "Lax", path: "/", maxAge: 0 });
7636
+ const isSecure = new URL(c.req.url).protocol === "https:";
7637
+ setCookie(c, "better-auth.session_token", "", { httpOnly: true, sameSite: "Lax", path: "/", maxAge: 0, secure: isSecure });
7361
7638
  clearCsrfCookie(c);
7362
7639
  return c.json({ message: "Logged out successfully" });
7363
7640
  });
7364
7641
  authRoutes.get("/logout", async (c) => {
7365
7642
  try {
7366
- const { createAuth } = await import('./config-6FB2BXMP.js');
7367
- const auth = createAuth(c.env);
7643
+ const { createAuth } = await import('./config-DOAWXZBG.js');
7644
+ const auth = createAuth(c.env, void 0, new URL(c.req.url).origin);
7368
7645
  const baReq = new Request(new URL("/auth/sign-out", c.req.url).href, {
7369
7646
  method: "POST",
7370
7647
  headers: { "Content-Type": "application/json", "Origin": new URL(c.req.url).origin, "Cookie": c.req.header("Cookie") || "" },
@@ -7373,7 +7650,8 @@ authRoutes.get("/logout", async (c) => {
7373
7650
  await auth.handler(baReq);
7374
7651
  } catch {
7375
7652
  }
7376
- setCookie(c, "better-auth.session_token", "", { httpOnly: true, sameSite: "Lax", path: "/", maxAge: 0 });
7653
+ const isSecure = new URL(c.req.url).protocol === "https:";
7654
+ setCookie(c, "better-auth.session_token", "", { httpOnly: true, sameSite: "Lax", path: "/", maxAge: 0, secure: isSecure });
7377
7655
  clearCsrfCookie(c);
7378
7656
  return c.redirect("/auth/login?message=You have been logged out successfully");
7379
7657
  });
@@ -7570,8 +7848,8 @@ authRoutes.post(
7570
7848
  </div>
7571
7849
  `);
7572
7850
  }
7573
- const { createAuth } = await import('./config-6FB2BXMP.js');
7574
- const auth = createAuth(c.env);
7851
+ const { createAuth } = await import('./config-DOAWXZBG.js');
7852
+ const auth = createAuth(c.env, void 0, new URL(c.req.url).origin);
7575
7853
  const baReq = new Request(new URL("/auth/sign-in/email", c.req.url).href, {
7576
7854
  method: "POST",
7577
7855
  headers: { "Content-Type": "application/json", "Origin": new URL(c.req.url).origin },
@@ -7585,17 +7863,27 @@ authRoutes.post(
7585
7863
  </div>
7586
7864
  `);
7587
7865
  }
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) {
7866
+ if (baRes.headers.getSetCookie) {
7592
7867
  for (const sc of baRes.headers.getSetCookie()) {
7593
- c.res.headers.append("Set-Cookie", sc);
7868
+ c.header("Set-Cookie", sc, { append: true });
7869
+ }
7870
+ } else {
7871
+ const rawSetCookie = baRes.headers.get("set-cookie");
7872
+ if (rawSetCookie) {
7873
+ c.header("Set-Cookie", rawSetCookie, { append: true });
7594
7874
  }
7595
7875
  }
7596
7876
  await setCsrfCookie(c);
7877
+ if (email === DEMO_EMAIL) {
7878
+ c.executionCtx?.waitUntil(trackDemoLogin(c.env.CACHE_KV));
7879
+ }
7597
7880
  const rawRedirect = c.req.query("redirect");
7598
7881
  const redirectUrl = rawRedirect && rawRedirect.startsWith("/") ? rawRedirect : "/admin/content";
7882
+ const isHtmx = c.req.header("HX-Request") === "true";
7883
+ if (isHtmx) {
7884
+ c.header("HX-Redirect", redirectUrl);
7885
+ return c.html(html`<div id="form-response"><p class="text-sm text-green-600">Login successful! Redirecting...</p></div>`);
7886
+ }
7599
7887
  return c.html(html`
7600
7888
  <div id="form-response">
7601
7889
  <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 +9395,44 @@ function renderContentFormPage(data, opts) {
9107
9395
  </a>
9108
9396
 
9109
9397
  <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
- ` : ""}
9398
+ <button
9399
+ type="submit"
9400
+ form="content-form"
9401
+ name="action"
9402
+ value="save"
9403
+ 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"
9404
+ >
9405
+ <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
9406
+ <path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/>
9407
+ </svg>
9408
+ Save
9409
+ </button>
9410
+
9411
+ <button
9412
+ type="submit"
9413
+ form="content-form"
9414
+ name="action"
9415
+ value="save_and_close"
9416
+ 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"
9417
+ >
9418
+ <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
9419
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
9420
+ </svg>
9421
+ Save & close
9422
+ </button>
9124
9423
 
9125
- ${data.user?.role !== "viewer" ? `
9424
+ ${data.user?.role !== "viewer" && data.versioningEnabled ? `
9126
9425
  <button
9127
9426
  type="submit"
9128
9427
  form="content-form"
9129
9428
  name="action"
9130
9429
  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"
9430
+ 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
9431
  >
9133
9432
  <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
9134
9433
  <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
9434
  </svg>
9136
- ${data.versioningEnabled ? `${isEdit ? "Update" : "Save"} & Publish` : isEdit ? "Update" : "Save"}
9435
+ Publish
9137
9436
  </button>
9138
9437
  ` : ""}
9139
9438
  </div>
@@ -12276,6 +12575,9 @@ adminContentRoutes.post("/", async (c) => {
12276
12575
  }), user?.userId);
12277
12576
  const cache2 = getCacheService(CACHE_CONFIGS.content);
12278
12577
  await cache2.invalidate(`content:list:${collectionId}:*`);
12578
+ const apiCacheCreate = getCacheService(CACHE_CONFIGS.api);
12579
+ await apiCacheCreate.invalidate("api:content-filtered:*");
12580
+ await apiCacheCreate.invalidate(`api:collection-content-filtered:${createDocType.id}:*`);
12279
12581
  const redirectUrl2 = `/admin/content/${doc.rootId}/edit?success=Content created successfully!`;
12280
12582
  return c.req.header("HX-Request") === "true" ? c.text("", 200, { "HX-Redirect": redirectUrl2 }) : c.redirect(redirectUrl2);
12281
12583
  }
@@ -12394,8 +12696,28 @@ adminContentRoutes.put("/:id", async (c) => {
12394
12696
  if (status2 === "published") await svc.publish(newDraft.id, user?.userId);
12395
12697
  else if (pub) await svc.unpublish(pub.id);
12396
12698
  await getCacheService(CACHE_CONFIGS.content).invalidate(`content:list:*`);
12699
+ const apiCacheEdit = getCacheService(CACHE_CONFIGS.api);
12700
+ await apiCacheEdit.invalidate("api:content-filtered:*");
12701
+ await apiCacheEdit.invalidate(`api:collection-content-filtered:${docRowU.type_id}:*`);
12702
+ const isHTMX2 = c.req.header("HX-Request") === "true";
12703
+ const referrerParams = formData.get("referrer_params");
12704
+ const listUrl = referrerParams ? `/admin/content?${referrerParams}` : `/admin/content?collection=${dcoll.id}`;
12705
+ if (action === "save_and_close") {
12706
+ return isHTMX2 ? c.text("", 200, { "HX-Redirect": listUrl }) : c.redirect(listUrl);
12707
+ }
12708
+ if (isHTMX2) {
12709
+ const label = status2 === "published" ? "Content published successfully!" : "Content saved successfully!";
12710
+ return c.html(html`<div id="form-messages">
12711
+ <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">
12712
+ <svg class="h-5 w-5 text-green-500 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
12713
+ <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"/>
12714
+ </svg>
12715
+ <span class="text-sm font-medium text-green-800 dark:text-green-200">${label}</span>
12716
+ </div>
12717
+ </div>`);
12718
+ }
12397
12719
  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);
12720
+ return c.redirect(redirectUrl2);
12399
12721
  }
12400
12722
  }
12401
12723
  return c.html(html`
@@ -12789,8 +13111,8 @@ adminContentRoutes.post("/bulk-action", async (c) => {
12789
13111
  }
12790
13112
  await cache.invalidate("content:list:*");
12791
13113
  const apiCache = getCacheService(CACHE_CONFIGS.api);
12792
- await apiCache.invalidate("content-filtered:*");
12793
- await apiCache.invalidate("collection-content-filtered:*");
13114
+ await apiCache.invalidate("api:content-filtered:*");
13115
+ await apiCache.invalidate("api:collection-content-filtered:*");
12794
13116
  return c.json({ success: true, count: ids.length });
12795
13117
  } catch (error) {
12796
13118
  console.error("Bulk action error:", error);
@@ -12808,6 +13130,9 @@ adminContentRoutes.delete("/:id", async (c) => {
12808
13130
  const now2 = Math.floor(Date.now() / 1e3);
12809
13131
  await db.prepare("UPDATE documents SET deleted_at = ?, updated_at = ? WHERE root_id = ? AND tenant_id = ?").bind(now2, now2, id, tenantId).run();
12810
13132
  await getCacheService(CACHE_CONFIGS.content).invalidate("content:list:*");
13133
+ const apiCacheDel = getCacheService(CACHE_CONFIGS.api);
13134
+ await apiCacheDel.invalidate("api:content-filtered:*");
13135
+ await apiCacheDel.invalidate(`api:collection-content-filtered:${docDel.type_id}:*`);
12811
13136
  return c.html(`
12812
13137
  <div id="content-list" hx-get="/admin/content?model=${docDel.type_id}" hx-trigger="load" hx-swap="outerHTML">
12813
13138
  <div class="flex items-center justify-center p-8"><span class="text-zinc-500">Deleting\u2026</span></div>
@@ -13103,7 +13428,7 @@ adminContentRoutes.post("/documents/:typeId/:documentId/delete", async (c) => {
13103
13428
  adminContentRoutes.get("/documents/:typeId/:rootId/versions", async (c) => {
13104
13429
  const { typeId, rootId } = c.req.param();
13105
13430
  const db = c.env.DB;
13106
- const { renderVersionHistoryFragment } = await import('./admin-documents-form.template-55V2ZGWT.js');
13431
+ const { renderVersionHistoryFragment } = await import('./admin-documents-form.template-JWV5GITD.js');
13107
13432
  const registry = new DocumentTypeRegistry(db);
13108
13433
  const docType = await registry.findById(typeId);
13109
13434
  if (!docType) return c.html("<div>Unknown type.</div>", 404);
@@ -15358,12 +15683,10 @@ var LANGUAGES = [
15358
15683
  { value: "ko", label: "Korean" },
15359
15684
  { value: "zh", label: "Chinese" }
15360
15685
  ];
15361
- var ROLES = [
15362
- { value: "admin", label: "Administrator" },
15363
- { value: "editor", label: "Editor" },
15364
- { value: "author", label: "Author" },
15365
- { value: "viewer", label: "Viewer" }
15366
- ];
15686
+ async function fetchRoleOptions(db) {
15687
+ const roles = await new RbacService(db).getRoles();
15688
+ return roles.map((r) => ({ value: r.id, label: r.display_name }));
15689
+ }
15367
15690
  userRoutes.get("/profile", async (c) => {
15368
15691
  const user = c.get("user");
15369
15692
  const db = c.env.DB;
@@ -15812,8 +16135,9 @@ userRoutes.get("/users/new", async (c) => {
15812
16135
  const regFieldNames = profileConfig?.registrationFields ?? [];
15813
16136
  const regFields = profileConfig && upRow ? regFieldNames.length > 0 ? profileConfig.fields.filter((f) => regFieldNames.includes(f.name)) : profileConfig.fields : [];
15814
16137
  const registrationFieldsHtml = regFields.length > 0 ? renderCustomProfileSection({ fields: regFields }, {}) : void 0;
16138
+ const roles = await fetchRoleOptions(db);
15815
16139
  const pageData = {
15816
- roles: ROLES,
16140
+ roles,
15817
16141
  registrationFieldsHtml,
15818
16142
  user: {
15819
16143
  name: user.email.split("@")[0] || user.email,
@@ -15841,9 +16165,9 @@ userRoutes.post("/users/new", async (c) => {
15841
16165
  const email = formData.get("email")?.toString()?.trim().toLowerCase() || "";
15842
16166
  const phone = sanitizeInput(formData.get("phone")?.toString()) || null;
15843
16167
  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";
16168
+ const roleInput = formData.get("role")?.toString() || "";
16169
+ const validRoles = (await new RbacService(db).getRoles()).map((r) => r.id);
16170
+ const role = validRoles.includes(roleInput) ? roleInput : validRoles[0] ?? "admin";
15847
16171
  const password = formData.get("password")?.toString() || "";
15848
16172
  const confirmPassword = formData.get("confirm_password")?.toString() || "";
15849
16173
  const isActive = formData.get("is_active") === "1";
@@ -16065,9 +16389,10 @@ userRoutes.get("/users/:id/edit", async (c) => {
16065
16389
  }
16066
16390
  const queryMessage = c.req.query("message");
16067
16391
  const queryType = c.req.query("type") === "error" ? "error" : "success";
16392
+ const roles = await fetchRoleOptions(db);
16068
16393
  const pageData = {
16069
16394
  userToEdit: editData,
16070
- roles: ROLES,
16395
+ roles,
16071
16396
  hasProfilePlugin: profileConfig !== null && upRow !== null,
16072
16397
  customProfileFieldsHtml,
16073
16398
  tenantMemberships,
@@ -16099,9 +16424,9 @@ userRoutes.put("/users/:id", async (c) => {
16099
16424
  const lastName = sanitizeInput(formData.get("last_name")?.toString());
16100
16425
  const email = formData.get("email")?.toString()?.trim().toLowerCase() || "";
16101
16426
  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";
16427
+ const roleInput = formData.get("role")?.toString() || "";
16428
+ const validRoles = (await new RbacService(db).getRoles()).map((r) => r.id);
16429
+ const role = validRoles.includes(roleInput) ? roleInput : validRoles[0] ?? "admin";
16105
16430
  const isActive = formData.get("is_active") === "1";
16106
16431
  const emailVerified = formData.get("email_verified") === "1";
16107
16432
  const newPassword = formData.get("new_password")?.toString() || "";
@@ -18688,15 +19013,6 @@ function renderPluginsListPage(data) {
18688
19013
  </div>
18689
19014
 
18690
19015
  <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
19016
  <button
18701
19017
  onclick="location.reload()"
18702
19018
  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 +19176,6 @@ function renderPluginsListPage(data) {
18860
19176
  .forEach(cb => params.append('status', cb.value));
18861
19177
  const search = document.getElementById('search-input').value;
18862
19178
  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
19179
  const url = new URL(window.location.href);
18866
19180
  url.search = params.toString();
18867
19181
  history.replaceState(null, '', url.toString());
@@ -18879,8 +19193,6 @@ function renderPluginsListPage(data) {
18879
19193
  });
18880
19194
  const search = params.get('search');
18881
19195
  if (search) document.getElementById('search-input').value = search;
18882
- const sort = params.get('sort');
18883
- if (sort) document.getElementById('sort-filter').value = sort;
18884
19196
  if (params.toString()) filterAndSortPlugins();
18885
19197
  }
18886
19198
 
@@ -18896,7 +19208,6 @@ function renderPluginsListPage(data) {
18896
19208
  .map(cb => cb.value.toLowerCase());
18897
19209
 
18898
19210
  const searchInput = document.getElementById('search-input').value.toLowerCase();
18899
- const sortValue = document.getElementById('sort-filter').value;
18900
19211
 
18901
19212
  const pluginsGrid = document.getElementById('plugins-grid');
18902
19213
  const pluginCards = Array.from(pluginsGrid.querySelectorAll('.plugin-card'));
@@ -18920,30 +19231,6 @@ function renderPluginsListPage(data) {
18920
19231
  return true;
18921
19232
  });
18922
19233
 
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
19234
  // Re-append
18948
19235
  pluginCards.forEach(card => card.style.display = 'none'); // Hide all first
18949
19236
 
@@ -19378,7 +19665,8 @@ function escapeHtmlAttr(value) {
19378
19665
  function renderPluginSettingsPage(data) {
19379
19666
  const { plugin, activity = [], user, settingsTabData } = data;
19380
19667
  const pluginDef = getPluginDefinition(plugin.id || plugin.name);
19381
- const hasUserSettings = pluginDef?.settingsTabContent != null || Object.keys(plugin.settings || {}).some((k) => !k.startsWith("_"));
19668
+ const pluginId = plugin.id || plugin.name;
19669
+ const hasUserSettings = pluginDef?.settingsTabContent != null || Object.keys(plugin.settings || {}).some((k) => !k.startsWith("_")) || hasCustomSettingsComponent(pluginId);
19382
19670
  const defaultTab = hasUserSettings ? "settings" : "info";
19383
19671
  const pageContent = `
19384
19672
  <div class="w-full px-4 sm:px-6 lg:px-8 py-6">
@@ -19543,9 +19831,28 @@ function renderPluginSettingsPage(data) {
19543
19831
  const form = document.getElementById('settings-form');
19544
19832
  const formData = new FormData(form);
19545
19833
  const isAuthPlugin = '${plugin.id}' === 'core-auth';
19834
+ const isOAuthPlugin = '${plugin.id}' === 'oauth-providers';
19546
19835
  let settings = {};
19547
19836
 
19548
- if (isAuthPlugin) {
19837
+ if (isOAuthPlugin) {
19838
+ // Build nested provider settings structure
19839
+ const getVal = id => (document.getElementById(id) || {}).value || '';
19840
+ const getChecked = id => !!(document.getElementById(id) || {}).checked;
19841
+ settings = {
19842
+ providers: {
19843
+ github: {
19844
+ clientId: getVal('oauth_github_clientId'),
19845
+ clientSecret: getVal('oauth_github_clientSecret'),
19846
+ enabled: getChecked('oauth_github_enabled'),
19847
+ },
19848
+ google: {
19849
+ clientId: getVal('oauth_google_clientId'),
19850
+ clientSecret: getVal('oauth_google_clientSecret'),
19851
+ enabled: getChecked('oauth_google_enabled'),
19852
+ },
19853
+ }
19854
+ };
19855
+ } else if (isAuthPlugin) {
19549
19856
  // Handle nested auth settings structure
19550
19857
  settings = {
19551
19858
  requiredFields: {},
@@ -20033,7 +20340,8 @@ function formatTimestamp(timestamp) {
20033
20340
  }
20034
20341
  var pluginSettingsComponents = {
20035
20342
  "otp-login": renderOTPLoginSettingsContent,
20036
- "email": renderEmailSettingsContent
20343
+ "email": renderEmailSettingsContent,
20344
+ "oauth-providers": renderOAuthProvidersSettingsContent
20037
20345
  };
20038
20346
  function renderOTPLoginSettingsContent(plugin, settings) {
20039
20347
  const siteName = settings.siteName || "SonicJS";
@@ -20756,6 +21064,62 @@ defineUserProfile({
20756
21064
  </div>
20757
21065
  `;
20758
21066
  }
21067
+ function renderOAuthProvidersSettingsContent(plugin, settings) {
21068
+ const providers = settings.providers || {};
21069
+ const gh = providers.github || {};
21070
+ const gg = providers.google || {};
21071
+ 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";
21072
+ return `
21073
+ <div class="space-y-6">
21074
+ <form id="settings-form">
21075
+ <div class="backdrop-blur-md bg-white/5 rounded-xl border border-white/10 p-6 space-y-6">
21076
+ <h3 class="text-lg font-semibold text-white">GitHub OAuth</h3>
21077
+ <div>
21078
+ <label for="oauth_github_clientId" class="block text-sm font-medium text-gray-300 mb-2">Client ID</label>
21079
+ <input type="text" id="oauth_github_clientId" name="oauth_github_clientId"
21080
+ value="${escapeHtmlAttr(gh.clientId || "")}"
21081
+ placeholder="GitHub OAuth App Client ID" class="${inputClass}" />
21082
+ </div>
21083
+ <div>
21084
+ <label for="oauth_github_clientSecret" class="block text-sm font-medium text-gray-300 mb-2">Client Secret</label>
21085
+ <input type="password" id="oauth_github_clientSecret" name="oauth_github_clientSecret"
21086
+ value="${escapeHtmlAttr(gh.clientSecret || "")}"
21087
+ placeholder="GitHub OAuth App Client Secret" class="${inputClass}" />
21088
+ </div>
21089
+ <div class="flex items-center gap-3">
21090
+ <input type="checkbox" id="oauth_github_enabled" name="oauth_github_enabled"
21091
+ ${gh.enabled ? "checked" : ""} class="w-4 h-4 rounded border-white/10" />
21092
+ <label for="oauth_github_enabled" class="text-sm text-gray-300">Enable GitHub login</label>
21093
+ </div>
21094
+ </div>
21095
+
21096
+ <div class="backdrop-blur-md bg-white/5 rounded-xl border border-white/10 p-6 space-y-6 mt-6">
21097
+ <h3 class="text-lg font-semibold text-white">Google OAuth</h3>
21098
+ <div>
21099
+ <label for="oauth_google_clientId" class="block text-sm font-medium text-gray-300 mb-2">Client ID</label>
21100
+ <input type="text" id="oauth_google_clientId" name="oauth_google_clientId"
21101
+ value="${escapeHtmlAttr(gg.clientId || "")}"
21102
+ placeholder="Google OAuth Client ID" class="${inputClass}" />
21103
+ </div>
21104
+ <div>
21105
+ <label for="oauth_google_clientSecret" class="block text-sm font-medium text-gray-300 mb-2">Client Secret</label>
21106
+ <input type="password" id="oauth_google_clientSecret" name="oauth_google_clientSecret"
21107
+ value="${escapeHtmlAttr(gg.clientSecret || "")}"
21108
+ placeholder="Google OAuth Client Secret" class="${inputClass}" />
21109
+ </div>
21110
+ <div class="flex items-center gap-3">
21111
+ <input type="checkbox" id="oauth_google_enabled" name="oauth_google_enabled"
21112
+ ${gg.enabled ? "checked" : ""} class="w-4 h-4 rounded border-white/10" />
21113
+ <label for="oauth_google_enabled" class="text-sm text-gray-300">Enable Google login</label>
21114
+ </div>
21115
+ </div>
21116
+ </form>
21117
+ </div>
21118
+ `;
21119
+ }
21120
+ function hasCustomSettingsComponent(pluginId) {
21121
+ return pluginId in pluginSettingsComponents;
21122
+ }
20759
21123
 
20760
21124
  // src/plugins/core-plugins/menu-plugin/services/menu-reconcile.ts
20761
21125
  async function upsertPluginRow(db, slug, entry, visible) {
@@ -27116,6 +27480,6 @@ var ROUTES_INFO = {
27116
27480
  reference: "https://github.com/sonicjs/sonicjs"
27117
27481
  };
27118
27482
 
27119
- 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
27483
+ 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, setDemoModeActive, setEmailService, setVariablesCache, swrStats, test_cleanup_default, updateDocumentSchema, userProfilesPlugin, userRoutes };
27484
+ //# sourceMappingURL=chunk-OJFIQGZB.js.map
27485
+ //# sourceMappingURL=chunk-OJFIQGZB.js.map