domma-cms 0.109.3 → 0.109.5

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.
@@ -1,4 +1,4 @@
1
- import{api as h}from"../api.js";import{isPathAllowed as Y}from"./admin-scope.js";import{colourToCss as U,applyFloat as Q,applyHighlight as Z,itemHighlightAttrs as ee}from"/public/js/menu-decor.mjs?v=20260918-item-highlight";import{placeTools as te,ensureLockedTools as ne,managePluginsItem as se,stripItemByUrl as ie,insertFoldersBeforeSystem as ae,pruneEmptyToolsFolder as oe,sidebarStateKey as re,canAny as B,filterByPermission as le,MANAGE_PLUGINS_URL as z}from"./sidebar-grouping.js";import{arrangeButtonMarkup as ce,canArrange as de,startArrange as ue}from"./sidebar-arrange.js";import{collapseButtonMarkup as me,wireSidebarSize as fe,COLLAPSED_WIDTH as ge}from"./sidebar-size.js";function p(e){return String(e??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}const pe=[{text:"Dashboard",url:"#/",icon:"home"},{text:"Menus",url:"#/menus",icon:"menu",permission:"menus"}];async function he(e){if(!B(e,"projects"))return[];let s=[];try{s=await h.projects.listWithCounts()}catch{return[]}if(!Array.isArray(s))return[];const i=[{key:"pages",text:"Pages",icon:"file-text",path:"/pages"},{key:"collections",text:"Collections",icon:"database",path:"/collections"},{key:"forms",text:"Forms",icon:"layout",path:"/forms"},{key:"actions",text:"Actions",icon:"zap",path:"/actions"},{key:"menus",text:"Menus",icon:"menu",path:"/menus"},{key:"blocks",text:"Blocks",icon:"box",path:"/blocks"},{key:"components",text:"Components",icon:"component",path:"/components"},{key:"views",text:"Views",icon:"eye",path:"/views"},{key:"roles",text:"Roles",icon:"shield",path:"/roles"},{key:"users",text:"Users",icon:"users",path:"/users"},{key:"apis",text:"APIs",icon:"code",path:"/apis"}];return s.map(t=>{const d="#/projects/"+encodeURIComponent(t.slug),c=t.counts||{},o=[{text:"Overview",url:d,icon:t.icon||"folder"}];for(const r of i){const l=Number(c[r.key])||0;l<=0||o.push({text:`${r.text} (${l})`,url:d+r.path,icon:r.icon})}o.push({text:"Settings",url:d+"/settings",icon:"settings"});const m=t.enabled===!1;return{text:t.name||t.slug,key:`project.${t.slug}`,icon:t.icon||"folder",items:o,...m&&{colour:"neutral",badge:{text:"off"}}}})}function be(e,s){for(const i of e)if(!i.hidden&&(i.key==="projects"||!i.key&&(i.text||"").toLowerCase()==="projects")){i.items=Array.isArray(i.items)?i.items.slice():[],i.items.push(...s);return}}function O(e,s){const i=D.includes(s.tone)?s.tone:"info",t=s.label||String(s.count);s.details?E.set(e,s.details):E.delete(e);const n=!s.inventory&&w.some(d=>d.url===e&&!d.inventory)?" data-live":"";return`<span class="sidebar-badge sidebar-badge--${i}" data-count-for="${p(e)}"${n} data-badge-label="${p(t)}" aria-label="${p(t)}">${p(String(s.count))}</span>`}function W(e){for(const s of e.querySelectorAll("details")){const i=s.querySelector(":scope > summary");if(!i)continue;let t=0,n=!1;const d=[];for(const l of s.querySelectorAll(":scope > .sidebar-children .sidebar-badge[data-live]")){const u=Number(l.textContent)||0;u&&(t+=u,n||(n=l.classList.contains("sidebar-badge--danger")),d.push({text:l.closest("a")?.querySelector(".sidebar-text")?.textContent.trim()??"",meta:l.dataset.badgeLabel,tone:l.classList.contains("sidebar-badge--danger")?"danger":void 0}))}const c=`folder:${s.dataset.stateKey||i.textContent.trim()}`;let o=i.querySelector(":scope > .sidebar-badge--sum");if(!t){o?.remove(),E.delete(c);continue}o||(o=document.createElement("span"),i.querySelector(".sidebar-text")?.after(o));const m=i.querySelector(".sidebar-text")?.textContent.trim()||"This folder",r=d.map(l=>`${l.text}: ${l.meta}`).join(" \xB7 ");o.className=`sidebar-badge sidebar-badge--sum sidebar-badge--${n?"danger":"info"}`,o.dataset.countFor=c,o.dataset.badgeLabel=r,o.setAttribute("aria-label",r),o.textContent=String(t),E.set(c,{title:m,items:d})}}const E=new Map;function ye(e,s){if(!e.url||!s)return"";const i=s[e.url];return!i||!(i.count>0)?"":O(e.url,i)}const D=["info","attention","danger","live"],ve={"#/pages":["page","pages"],"#/media":["file in the media library","files in the media library"],"#/menus":["menu","menus"],"#/context-menus":["context menu","context menus"],"#/theme":["element override in the theme","element overrides in the theme"],"#/collections":["collection","collections"],"#/forms":["form","forms"],"#/views":["view","views"],"#/actions":["action","actions"],"#/blocks":["block","blocks"],"#/components":["component","components"],"#/users":["user","users"],"#/plugins":["plugin installed","plugins installed"],"#/system/notifications":["unread notification","unread notifications"]};function _(e,{fallbackLabel:s="",tone:i="info"}={}){const t=Number(typeof e=="object"&&e!==null?e.count:e);if(!Number.isFinite(t)||t<0)return null;const n=typeof e?.label=="string"&&e.label||(s?`${t} - ${s}`:String(t)),d={count:Math.round(t),label:n,tone:D.includes(e?.tone)?e.tone:i};return e?.details&&typeof e.details=="object"&&(d.details=e.details),e?.inventory===!0&&(d.inventory=!0),d}function G(e,s,i=null){if(e&&e.type==="separator")return'<hr class="sidebar-divider">';if(e&&e.type==="spacer"){const b=["xs","sm","md","lg","xl","flex"].includes(e.size)?e.size:"md";return`<div class="sidebar-spacer sidebar-spacer--${p(b)}" aria-hidden="true"></div>`}const t=String(e.text||"?").trim().charAt(0).toUpperCase()||"?",n=e.icon?`<span data-icon="${p(e.icon)}"></span>`:`<span class="sidebar-initial" aria-hidden="true">${p(t)}</span>`,d=p(e.text||""),c=e.url?p(e.url):"",o=Array.isArray(e.items)&&e.items.length>0,m=e.colour?U(e.colour):"",r=ee(e),l=[r.hover&&` data-menu-item-hover="${p(r.hover)}"`,r.active&&` data-menu-item-active="${p(r.active)}"`].filter(Boolean).join(""),u=[m&&`color:${p(m)}`,r.accent&&`--dm-menu-highlight:${p(r.accent)}`].filter(Boolean).join(";"),y=u?` style="${u}"`:"";let v="";if(e.badge&&e.badge.text!=null&&e.badge.text!==""){const b=e.badge.variant?U(e.badge.variant):"";v=`<span class="dm-menu-badge"${b?` style="background:${p(b)};color:var(--dm-text-inverse,#fff)"`:""}>${p(String(e.badge.text))}</span>`}if(o){const b=re(e,i),C=S.get(b)!==!1,L=e.items.map(j=>G(j,s,b)).join("");return`<details data-state-key="${p(b)}"${C?" open":""}>
1
+ import{api as h}from"../api.js";import{isPathAllowed as J}from"./admin-scope.js";import{colourToCss as U,applyFloat as Y,applyHighlight as Q,itemHighlightAttrs as ee}from"/public/js/menu-decor.mjs?v=20260918-item-highlight";import{placeTools as te,ensureLockedTools as ne,managePluginsItem as se,stripItemByUrl as ie,insertFoldersBeforeSystem as ae,pruneEmptyToolsFolder as oe,sidebarStateKey as re,canAny as B,filterByPermission as le,MANAGE_PLUGINS_URL as z}from"./sidebar-grouping.js";import{arrangeButtonMarkup as ce,canArrange as de,startArrange as ue}from"./sidebar-arrange.js";import{collapseButtonMarkup as me,wireSidebarSize as fe,COLLAPSED_WIDTH as ge}from"./sidebar-size.js";function p(e){return String(e??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}const pe=[{text:"Dashboard",url:"#/",icon:"home"},{text:"Menus",url:"#/menus",icon:"menu",permission:"menus"}];async function he(e){if(!B(e,"projects"))return[];let s=[];try{s=await h.projects.listWithCounts()}catch{return[]}if(!Array.isArray(s))return[];const i=[{key:"pages",text:"Pages",icon:"file-text",path:"/pages"},{key:"collections",text:"Collections",icon:"database",path:"/collections"},{key:"forms",text:"Forms",icon:"layout",path:"/forms"},{key:"actions",text:"Actions",icon:"zap",path:"/actions"},{key:"menus",text:"Menus",icon:"menu",path:"/menus"},{key:"blocks",text:"Blocks",icon:"box",path:"/blocks"},{key:"components",text:"Components",icon:"component",path:"/components"},{key:"views",text:"Views",icon:"eye",path:"/views"},{key:"roles",text:"Roles",icon:"shield",path:"/roles"},{key:"users",text:"Users",icon:"users",path:"/users"},{key:"apis",text:"APIs",icon:"code",path:"/apis"}];return s.map(t=>{const d="#/projects/"+encodeURIComponent(t.slug),c=t.counts||{},o=[{text:"Overview",url:d,icon:t.icon||"folder"}];for(const r of i){const l=Number(c[r.key])||0;l<=0||o.push({text:`${r.text} (${l})`,url:d+r.path,icon:r.icon})}o.push({text:"Settings",url:d+"/settings",icon:"settings"});const m=t.enabled===!1;return{text:t.name||t.slug,key:`project.${t.slug}`,icon:t.icon||"folder",items:o,...m&&{colour:"neutral",badge:{text:"off"}}}})}function be(e,s){for(const i of e)if(!i.hidden&&(i.key==="projects"||!i.key&&(i.text||"").toLowerCase()==="projects")){i.items=Array.isArray(i.items)?i.items.slice():[],i.items.push(...s);return}}function O(e,s){const i=D.includes(s.tone)?s.tone:"info",t=s.label||String(s.count);s.details?E.set(e,s.details):E.delete(e);const n=!s.inventory&&A.some(d=>d.url===e&&!d.inventory)?" data-live":"";return`<span class="sidebar-badge sidebar-badge--${i}" data-count-for="${p(e)}"${n} data-badge-label="${p(t)}" aria-label="${p(t)}">${p(String(s.count))}</span>`}function W(e){for(const s of e.querySelectorAll("details")){const i=s.querySelector(":scope > summary");if(!i)continue;let t=0,n=!1;const d=[];for(const l of s.querySelectorAll(":scope > .sidebar-children .sidebar-badge[data-live]")){const u=Number(l.textContent)||0;u&&(t+=u,n||(n=l.classList.contains("sidebar-badge--danger")),d.push({text:l.closest("a")?.querySelector(".sidebar-text")?.textContent.trim()??"",meta:l.dataset.badgeLabel,tone:l.classList.contains("sidebar-badge--danger")?"danger":void 0}))}const c=`folder:${s.dataset.stateKey||i.textContent.trim()}`;let o=i.querySelector(":scope > .sidebar-badge--sum");if(!t){o?.remove(),E.delete(c);continue}o||(o=document.createElement("span"),i.querySelector(".sidebar-text")?.after(o));const m=i.querySelector(".sidebar-text")?.textContent.trim()||"This folder",r=d.map(l=>`${l.text}: ${l.meta}`).join(" \xB7 ");o.className=`sidebar-badge sidebar-badge--sum sidebar-badge--${n?"danger":"info"}`,o.dataset.countFor=c,o.dataset.badgeLabel=r,o.setAttribute("aria-label",r),o.textContent=String(t),E.set(c,{title:m,items:d})}}const E=new Map;function ye(e,s){if(!e.url||!s)return"";const i=s[e.url];return!i||!(i.count>0)&&!i.showZero?"":O(e.url,i)}const D=["info","attention","danger","live"],ve={"#/pages":["page","pages"],"#/media":["file in the media library","files in the media library"],"#/menus":["menu","menus"],"#/context-menus":["context menu","context menus"],"#/theme":["element override in the theme","element overrides in the theme"],"#/collections":["collection","collections"],"#/forms":["form","forms"],"#/views":["view","views"],"#/actions":["action","actions"],"#/blocks":["block","blocks"],"#/components":["component","components"],"#/users":["user","users"],"#/plugins":["plugin installed","plugins installed"],"#/system/notifications":["unread notification","unread notifications"]};function _(e,{fallbackLabel:s="",tone:i="info"}={}){const t=Number(typeof e=="object"&&e!==null?e.count:e);if(!Number.isFinite(t)||t<0)return null;const n=typeof e?.label=="string"&&e.label||(s?`${t} - ${s}`:String(t)),d={count:Math.round(t),label:n,tone:D.includes(e?.tone)?e.tone:i};return e?.details&&typeof e.details=="object"&&(d.details=e.details),e?.inventory===!0&&(d.inventory=!0),e?.showZero===!0&&(d.showZero=!0),d}function G(e,s,i=null){if(e&&e.type==="separator")return'<hr class="sidebar-divider">';if(e&&e.type==="spacer"){const b=["xs","sm","md","lg","xl","flex"].includes(e.size)?e.size:"md";return`<div class="sidebar-spacer sidebar-spacer--${p(b)}" aria-hidden="true"></div>`}const t=String(e.text||"?").trim().charAt(0).toUpperCase()||"?",n=e.icon?`<span data-icon="${p(e.icon)}"></span>`:`<span class="sidebar-initial" aria-hidden="true">${p(t)}</span>`,d=p(e.text||""),c=e.url?p(e.url):"",o=Array.isArray(e.items)&&e.items.length>0,m=e.colour?U(e.colour):"",r=ee(e),l=[r.hover&&` data-menu-item-hover="${p(r.hover)}"`,r.active&&` data-menu-item-active="${p(r.active)}"`].filter(Boolean).join(""),u=[m&&`color:${p(m)}`,r.accent&&`--dm-menu-highlight:${p(r.accent)}`].filter(Boolean).join(";"),y=u?` style="${u}"`:"";let v="";if(e.badge&&e.badge.text!=null&&e.badge.text!==""){const b=e.badge.variant?U(e.badge.variant):"";v=`<span class="dm-menu-badge"${b?` style="background:${p(b)};color:var(--dm-text-inverse,#fff)"`:""}>${p(String(e.badge.text))}</span>`}if(o){const b=re(e,i),C=S.get(b)!==!1,L=e.items.map(j=>G(j,s,b)).join("");return`<details data-state-key="${p(b)}"${C?" open":""}>
2
2
  <summary${y}>${n} <span class="sidebar-text">${d}</span>${v}</summary>
3
3
  <div class="sidebar-children">${L}</div>
4
- </details>`}return`<a href="${c}" class="sidebar-link" data-url="${c}"${y}${l}>${n} <span class="sidebar-text">${d}</span>${ye(e,s)}${v}</a>`}async function xe(e=[],s=[],i=null){const t=g=>g.then(a=>Array.isArray(a)?a.length:Array.isArray(a?.entries)?a.entries.length:0).catch(()=>0);w=Ce(e,i);const n=new Set(w.map(g=>g.url)),d={pages:"#/pages",media:"#/media",menus:"#/menus","context-menus":"#/context-menus",theme:"#/theme",collections:"#/collections",forms:"#/forms",views:"#/views",actions:"#/actions",blocks:"#/blocks",components:"#/components",users:"#/users",plugins:"#/plugins",notifications:"#/system/notifications"},c=(g,a)=>B(s,g)&&!n.has(d[g])?a():Promise.resolve(0),[o,m,r,l,u,y,v,b,C,L,j,F,N,P]=await Promise.all([c("pages",()=>t(h.pages.list())),c("media",()=>t(h.media.list())),c("menus",()=>t(h.menus.list())),c("context-menus",()=>t(h.contextMenus.list())),c("theme",()=>h.theme.get().then(g=>(g?.overrides||[]).length).catch(()=>0)),c("collections",()=>t(h.collections.list())),c("forms",()=>t(h.forms.list())),c("views",()=>t(h.views.list())),c("actions",()=>t(h.actions.list())),c("blocks",()=>t(h.blocks.list())),c("components",()=>t(h.components.list())),c("users",()=>t(h.users.list())),c("plugins",()=>t(h.plugins.list())),c("notifications",()=>h.system?.notifications?.unreadCount?.().then(g=>g?.count??0).catch(()=>0)??Promise.resolve(0))]),q={};await Promise.all(w.map(async g=>{const a=await K(g);a&&(q[g.url]=a)}));const T={"#/pages":o,"#/media":m,"#/menus":r,"#/context-menus":l,"#/theme":u,"#/collections":y,"#/forms":v,"#/views":b,"#/actions":C,"#/blocks":L,"#/components":j,"#/users":F,"#/plugins":N,"#/system/notifications":P},A={};for(const[g,a]of Object.entries(T)){if(n.has(g))continue;const[x,f]=ve[g]??["item","items"],k=_(a);k&&(A[g]={...k,label:`${k.count} ${k.count===1?x:f}`})}return{...A,...q}}let w=[];const $e="dm:sidebar-count",Se=6e4;function Ce(e=[],s=null){const i=[],t=n=>{if(n&&!(s&&typeof n.url=="string"&&n.url.startsWith("#/")&&!Y(s,n.url.slice(1)))){n.url&&typeof n.countUrl=="string"&&n.countUrl&&i.push({url:n.url,countUrl:n.countUrl,text:n.text||"",countLabel:n.countLabel||"",inventory:n.inventory===!0});for(const d of[...n.children||[],...n.items||[]])t(d)}};for(const n of e??[])t(n?.item??n);return i}async function K(e){try{const s=new Date,i=`${s.getFullYear()}-${String(s.getMonth()+1).padStart(2,"0")}-${String(s.getDate()).padStart(2,"0")}`,t=e.countUrl+(e.countUrl.includes("?")?"&":"?")+`today=${i}`;return _(await H.get(t),{fallbackLabel:e.countLabel||e.text})}catch{return null}}function V(e,s,i){const t=[...e.querySelectorAll("a.sidebar-link")].find(d=>d.dataset.url===s);if(!t)return;const n=t.querySelector(".sidebar-badge[data-count-for]");if(!i||!(i.count>0))n?.remove();else{const d=document.createRange();d.selectNodeContents(t);const c=d.createContextualFragment(O(s,i)).firstElementChild;n?n.replaceWith(c):t.querySelector(".sidebar-text")?.after(c)}R.set(s,i),W(e)}const R=new Map;async function I(e,s=null){const i=s?w.filter(t=>t.url===s):w;await Promise.all(i.map(async t=>{const n=await K(t),d=R.get(t.url);JSON.stringify(d??null)!==JSON.stringify(n)&&V(e,t.url,n)}))}function ke(e){e.dataset.liveCounts||(e.dataset.liveCounts="1",window.addEventListener($e,s=>{const i=s.detail||{};if(i.url)if(i.count!==void 0){const t=w.find(n=>n.url===i.url);V(e,i.url,_(i,{fallbackLabel:t?.countLabel||t?.text||""}))}else I(e,i.url)}),setInterval(()=>{!document.hidden&&document.body.contains(e)&&I(e)},Se),document.addEventListener("visibilitychange",()=>{document.hidden||I(e)}),Ae(e))}function X(e,s,i){const t=(c,o,m)=>{const r=document.createElement(c);return o&&(r.className=o),m!=null&&(r.textContent=String(m)),r};if(!i){e.textContent=s;return}e.appendChild(t("strong","dm-badge-pop-title",i.title||s)),(i.rows?.length||!i.items?.length)&&e.appendChild(t("p","dm-badge-pop-sub",s));const n=Array.isArray(i.rows)?i.rows.slice(0,6):[];if(n.length){const c=t("dl","dm-badge-pop-rows");for(const o of n)c.appendChild(t("dt",null,o.label)),c.appendChild(t("dd",o.tone==="danger"?"is-danger":null,o.value));e.appendChild(c)}const d=Array.isArray(i.items)?i.items.slice(0,6):[];if(d.length){const c=t("ul","dm-badge-pop-items");for(const o of d){const m=t("li",o.tone==="danger"?"is-danger":null);m.appendChild(t("span","dm-badge-pop-text",o.text)),o.meta&&m.appendChild(t("span","dm-badge-pop-meta",o.meta)),c.appendChild(m)}e.appendChild(c)}else i.empty&&e.appendChild(t("p","dm-badge-pop-empty",i.empty))}function Ae(e){let s=null,i=null;const t=()=>e.classList.contains("is-collapsed"),n=()=>{clearTimeout(i),s?.remove(),s=null},d=(r,l)=>{n(),i=setTimeout(()=>{s=document.createElement("div"),s.className="dm-help-pop dm-badge-pop",s.setAttribute("role","tooltip"),l(s),document.body.appendChild(s);const u=r.getBoundingClientRect(),y=s.getBoundingClientRect(),v=Math.min(u.right+10,innerWidth-y.width-8);s.style.left=`${Math.max(8,v)}px`,s.style.top=`${Math.max(8,Math.min(u.top+u.height/2-y.height/2,innerHeight-y.height-8))}px`},150)},c=r=>{const l=r?.dataset.badgeLabel;if(!l)return n();d(r,u=>X(u,l,E.get(r.dataset.countFor)))},o=r=>{const l=r.querySelector(":scope > .sidebar-text")?.textContent.trim()||"",u=r.querySelector(":scope > .sidebar-badge[data-badge-label]:not([hidden])");d(r,y=>{const v=u&&E.get(u.dataset.countFor);if(v)return X(y,u.dataset.badgeLabel,{...v,title:v.title||l});if(!u){y.textContent=l;return}const b=document.createElement("strong");b.className="dm-badge-pop-title",b.textContent=l;const C=document.createElement("p");C.className="dm-badge-pop-sub",C.textContent=u.dataset.badgeLabel,y.append(b,C)})},m=r=>{const l=r.closest?.("a.sidebar-link, summary");return l&&e.contains(l)?l:null};e.addEventListener("mouseover",r=>{if(t()){const u=m(r.target);u&&!u.contains(r.relatedTarget)&&o(u);return}const l=r.target.closest?.(".sidebar-badge[data-badge-label]");l&&c(l)}),e.addEventListener("mouseout",r=>{if(t()){const l=m(r.target);l&&!l.contains(r.relatedTarget)&&n();return}r.target.closest?.(".sidebar-badge[data-badge-label]")&&n()}),e.addEventListener("focusin",r=>{const l=r.target.closest?.("a, summary");if(t()&&l&&e.contains(l)&&l.querySelector(":scope > .sidebar-text"))return o(l);const u=l?.querySelector(":scope > .sidebar-badge[data-badge-label]:not([hidden])");u?c(u):n()}),e.addEventListener("focusout",n),e.addEventListener("click",n),window.addEventListener("scroll",n,!0),window.addEventListener("hashchange",n)}async function we(e=[]){if(!B(e,"settings"))return"Admin";try{const s=await h.settings.get();return s?.adminBrand?.title||s?.title||"Admin"}catch{return"Admin"}}export async function renderAdminSidebar({mount:e,permissions:s,adminScope:i=null}){const t=$(e).get(0);if(!t)return;let n,d=null,c=null,o=null,m=null,r=null;try{const a=await h.get("/sidebar/menu");n=Array.isArray(a?.items)?a.items:null,d=a?.variant||null,c=a?.position||null,o=a?.style||null,m=a?.float||null,r=a?.highlight||null}catch{n=null}(!n||!n.some(a=>a&&(!a.hidden||a.restricted)))&&(console.warn("[admin-sidebar] No admin-sidebar menu found; using fallback tree"),n=pe.slice());let l=[];try{l=await H.get("/api/sidebar/registered-items")||[]}catch{}n=ie(n,z),n=te(n,l.filter(a=>(a?.item??a)?.url!==z)),n=ne(n),n=ae(n,[se()]);const u=await he(s);u.length&&be(n,u),n=le(n,s,i),n=oe(n);const[y,v]=await Promise.all([xe(l.filter(a=>B(s,a?.item?.permission)),s,i).catch(()=>({})),we(s)]),b=d?` dm-admin-sidebar--${p(d)}`:"";function C(a,x="px"){if(a==null)return a;const f=String(a).trim();return/^\d+(\.\d+)?$/.test(f)?`${f}${x}`:f}let L="";if(o){const a=[];o.fontFamily&&a.push(`font-family: ${o.fontFamily}, sans-serif`),o.fontSize&&a.push(`font-size: ${C(o.fontSize)}`),o.fontWeight&&a.push(`font-weight: ${o.fontWeight}`),o.letterSpacing&&a.push(`letter-spacing: ${C(o.letterSpacing,"em")}`);const x=[];if(a.length&&x.push(`#admin-sidebar .dm-admin-sidebar, #admin-sidebar .dm-admin-sidebar .sidebar-link, #admin-sidebar .dm-admin-sidebar summary, #admin-sidebar .dm-admin-sidebar .sidebar-text { ${a.join("; ")} }`),o.iconSize){const f=C(o.iconSize);x.push(`#admin-sidebar .dm-admin-sidebar [data-icon], #admin-sidebar .dm-admin-sidebar [data-icon] svg { width: ${f} !important; height: ${f} !important; }`)}x.length&&(L=`<style data-admin-sidebar-style>${x.join(" ")}</style>`)}const j=de(s)?ce():"",F=`<div class="dm-admin-sidebar-header"><span data-icon="layout"></span><span class="dm-admin-sidebar-title">${p(v)}</span>${j}${me()}</div>`,N=n.some(a=>a&&a.type==="spacer"&&a.size==="flex")?" dm-admin-sidebar--has-flex-spacer":"",P=`${L}<nav class="dm-admin-sidebar${b}${N}">${F}${n.map(a=>G(a,y)).join("")}</nav>`,q=document.createRange();q.selectNodeContents(t);const T=q.createContextualFragment(P);t.replaceChildren(T),Domma.icons.scan(t),t.querySelector(".dm-admin-sidebar-arrange")?.addEventListener("click",()=>ue(t));for(const a of w)R.set(a.url,y[a.url]??null);W(t),ke(t),t.removeAttribute("data-menu-hover"),t.removeAttribute("data-menu-active"),t.style.removeProperty("--dm-menu-highlight"),Z(t,{highlight:r});const A=document.querySelector(".dashboard-wrapper");["position","top","bottom","left","right","zIndex","maxHeight","transform"].forEach(a=>{t.style[a]=""}),t.classList.remove("dm-menu-floating"),A&&(A.style.paddingLeft="",A.style.paddingRight=""),t.__dmGutter=()=>{if(c!=="floating"||!A)return;const a=m&&m.anchor||"TL",x=Number.isFinite(Number(m?.offsetX))?Number(m.offsetX):16,f=t.classList.contains("is-collapsed")?ge:parseInt(t.style.width,10)||t.getBoundingClientRect().width||260,k=Math.round(f+x*2)+"px";a==="R"||a==="TR"||a==="BR"?A.style.paddingRight=k:A.style.paddingLeft=k},c==="floating"&&Q(t,{position:"floating",float:m||{}}),t.querySelectorAll("details").forEach(a=>{a.addEventListener("toggle",function(){const x=this.getAttribute("data-state-key");x&&S.set(x,this.open);const f=this.querySelector(":scope > .sidebar-children");if(f)if(this.open){const k=f.scrollHeight;f.style.maxHeight="0",requestAnimationFrame(()=>{f.style.maxHeight=k+"px",f.addEventListener("transitionend",function J(){f.style.maxHeight="none",f.removeEventListener("transitionend",J)})})}else{const k=f.scrollHeight;f.style.maxHeight=k+"px",requestAnimationFrame(()=>{f.style.maxHeight="0"})}})}),fe(t),t.dataset.routeWired||(t.dataset.routeWired="1",M.subscribe("router:afterChange",()=>t.__dmSetActive?.()));function g(){const a=location.hash||"#/";$(t).find(".sidebar-link").removeClass("active"),$(t).find(`.sidebar-link[data-url="${a}"]`).addClass("active")}g(),t.__dmSetActive=g}
4
+ </details>`}return`<a href="${c}" class="sidebar-link" data-url="${c}"${y}${l}>${n} <span class="sidebar-text">${d}</span>${ye(e,s)}${v}</a>`}async function xe(e=[],s=[],i=null){const t=g=>g.then(a=>Array.isArray(a)?a.length:Array.isArray(a?.entries)?a.entries.length:0).catch(()=>0);A=Ce(e,i);const n=new Set(A.map(g=>g.url)),d={pages:"#/pages",media:"#/media",menus:"#/menus","context-menus":"#/context-menus",theme:"#/theme",collections:"#/collections",forms:"#/forms",views:"#/views",actions:"#/actions",blocks:"#/blocks",components:"#/components",users:"#/users",plugins:"#/plugins",notifications:"#/system/notifications"},c=(g,a)=>B(s,g)&&!n.has(d[g])?a():Promise.resolve(0),[o,m,r,l,u,y,v,b,C,L,j,F,N,P]=await Promise.all([c("pages",()=>t(h.pages.list())),c("media",()=>t(h.media.list())),c("menus",()=>t(h.menus.list())),c("context-menus",()=>t(h.contextMenus.list())),c("theme",()=>h.theme.get().then(g=>(g?.overrides||[]).length).catch(()=>0)),c("collections",()=>t(h.collections.list())),c("forms",()=>t(h.forms.list())),c("views",()=>t(h.views.list())),c("actions",()=>t(h.actions.list())),c("blocks",()=>t(h.blocks.list())),c("components",()=>t(h.components.list())),c("users",()=>t(h.users.list())),c("plugins",()=>t(h.plugins.list())),c("notifications",()=>h.system?.notifications?.unreadCount?.().then(g=>g?.count??0).catch(()=>0)??Promise.resolve(0))]),q={};await Promise.all(A.map(async g=>{const a=await K(g);a&&(q[g.url]=a)}));const T={"#/pages":o,"#/media":m,"#/menus":r,"#/context-menus":l,"#/theme":u,"#/collections":y,"#/forms":v,"#/views":b,"#/actions":C,"#/blocks":L,"#/components":j,"#/users":F,"#/plugins":N,"#/system/notifications":P},k={};for(const[g,a]of Object.entries(T)){if(n.has(g))continue;const[x,f]=ve[g]??["item","items"],w=_(a);w&&(k[g]={...w,label:`${w.count} ${w.count===1?x:f}`})}return{...k,...q}}let A=[];const $e="dm:sidebar-count",Se=6e4;function Ce(e=[],s=null){const i=[],t=n=>{if(n&&!(s&&typeof n.url=="string"&&n.url.startsWith("#/")&&!J(s,n.url.slice(1)))){n.url&&typeof n.countUrl=="string"&&n.countUrl&&i.push({url:n.url,countUrl:n.countUrl,text:n.text||"",countLabel:n.countLabel||"",inventory:n.inventory===!0});for(const d of[...n.children||[],...n.items||[]])t(d)}};for(const n of e??[])t(n?.item??n);return i}async function K(e){try{const s=new Date,i=`${s.getFullYear()}-${String(s.getMonth()+1).padStart(2,"0")}-${String(s.getDate()).padStart(2,"0")}`,t=e.countUrl+(e.countUrl.includes("?")?"&":"?")+`today=${i}`;return _(await H.get(t),{fallbackLabel:e.countLabel||e.text})}catch{return null}}function V(e,s,i){const t=[...e.querySelectorAll("a.sidebar-link")].find(d=>d.dataset.url===s);if(!t)return;const n=t.querySelector(".sidebar-badge[data-count-for]");if(!i||!(i.count>0)&&!i.showZero)n?.remove();else{const d=document.createRange();d.selectNodeContents(t);const c=d.createContextualFragment(O(s,i)).firstElementChild;n?n.replaceWith(c):t.querySelector(".sidebar-text")?.after(c)}R.set(s,i),W(e)}const R=new Map;async function I(e,s=null){const i=s?A.filter(t=>t.url===s):A;await Promise.all(i.map(async t=>{const n=await K(t),d=R.get(t.url);JSON.stringify(d??null)!==JSON.stringify(n)&&V(e,t.url,n)}))}function we(e){e.dataset.liveCounts||(e.dataset.liveCounts="1",window.addEventListener($e,s=>{const i=s.detail||{};if(i.url)if(i.count!==void 0){const t=A.find(n=>n.url===i.url);V(e,i.url,_(i,{fallbackLabel:t?.countLabel||t?.text||""}))}else I(e,i.url)}),setInterval(()=>{!document.hidden&&document.body.contains(e)&&I(e)},Se),document.addEventListener("visibilitychange",()=>{document.hidden||I(e)}),ke(e))}function Z(e,s,i){const t=(c,o,m)=>{const r=document.createElement(c);return o&&(r.className=o),m!=null&&(r.textContent=String(m)),r};if(!i){e.textContent=s;return}e.appendChild(t("strong","dm-badge-pop-title",i.title||s)),(i.rows?.length||!i.items?.length)&&e.appendChild(t("p","dm-badge-pop-sub",s));const n=Array.isArray(i.rows)?i.rows.slice(0,6):[];if(n.length){const c=t("dl","dm-badge-pop-rows");for(const o of n)c.appendChild(t("dt",null,o.label)),c.appendChild(t("dd",o.tone==="danger"?"is-danger":null,o.value));e.appendChild(c)}const d=Array.isArray(i.items)?i.items.slice(0,6):[];if(d.length){const c=t("ul","dm-badge-pop-items");for(const o of d){const m=t("li",o.tone==="danger"?"is-danger":null);m.appendChild(t("span","dm-badge-pop-text",o.text)),o.meta&&m.appendChild(t("span","dm-badge-pop-meta",o.meta)),c.appendChild(m)}e.appendChild(c)}else i.empty&&e.appendChild(t("p","dm-badge-pop-empty",i.empty))}function ke(e){let s=null,i=null;const t=()=>e.classList.contains("is-collapsed"),n=()=>{clearTimeout(i),s?.remove(),s=null},d=(r,l)=>{n(),i=setTimeout(()=>{s=document.createElement("div"),s.className="dm-help-pop dm-badge-pop",s.setAttribute("role","tooltip"),l(s),document.body.appendChild(s);const u=r.getBoundingClientRect(),y=s.getBoundingClientRect(),v=Math.min(u.right+10,innerWidth-y.width-8);s.style.left=`${Math.max(8,v)}px`,s.style.top=`${Math.max(8,Math.min(u.top+u.height/2-y.height/2,innerHeight-y.height-8))}px`},150)},c=r=>{const l=r?.dataset.badgeLabel;if(!l)return n();d(r,u=>Z(u,l,E.get(r.dataset.countFor)))},o=r=>{const l=r.querySelector(":scope > .sidebar-text")?.textContent.trim()||"",u=r.querySelector(":scope > .sidebar-badge[data-badge-label]:not([hidden])");d(r,y=>{const v=u&&E.get(u.dataset.countFor);if(v)return Z(y,u.dataset.badgeLabel,{...v,title:v.title||l});if(!u){y.textContent=l;return}const b=document.createElement("strong");b.className="dm-badge-pop-title",b.textContent=l;const C=document.createElement("p");C.className="dm-badge-pop-sub",C.textContent=u.dataset.badgeLabel,y.append(b,C)})},m=r=>{const l=r.closest?.("a.sidebar-link, summary");return l&&e.contains(l)?l:null};e.addEventListener("mouseover",r=>{if(t()){const u=m(r.target);u&&!u.contains(r.relatedTarget)&&o(u);return}const l=r.target.closest?.(".sidebar-badge[data-badge-label]");l&&c(l)}),e.addEventListener("mouseout",r=>{if(t()){const l=m(r.target);l&&!l.contains(r.relatedTarget)&&n();return}r.target.closest?.(".sidebar-badge[data-badge-label]")&&n()}),e.addEventListener("focusin",r=>{const l=r.target.closest?.("a, summary");if(t()&&l&&e.contains(l)&&l.querySelector(":scope > .sidebar-text"))return o(l);const u=l?.querySelector(":scope > .sidebar-badge[data-badge-label]:not([hidden])");u?c(u):n()}),e.addEventListener("focusout",n),e.addEventListener("click",n),window.addEventListener("scroll",n,!0),window.addEventListener("hashchange",n)}async function Ae(e=[]){if(!B(e,"settings"))return"Admin";try{const s=await h.settings.get();return s?.adminBrand?.title||s?.title||"Admin"}catch{return"Admin"}}export async function renderAdminSidebar({mount:e,permissions:s,adminScope:i=null}){const t=$(e).get(0);if(!t)return;let n,d=null,c=null,o=null,m=null,r=null;try{const a=await h.get("/sidebar/menu");n=Array.isArray(a?.items)?a.items:null,d=a?.variant||null,c=a?.position||null,o=a?.style||null,m=a?.float||null,r=a?.highlight||null}catch{n=null}(!n||!n.some(a=>a&&(!a.hidden||a.restricted)))&&(console.warn("[admin-sidebar] No admin-sidebar menu found; using fallback tree"),n=pe.slice());let l=[];try{l=await H.get("/api/sidebar/registered-items")||[]}catch{}n=ie(n,z),n=te(n,l.filter(a=>(a?.item??a)?.url!==z)),n=ne(n),n=ae(n,[se()]);const u=await he(s);u.length&&be(n,u),n=le(n,s,i),n=oe(n);const[y,v]=await Promise.all([xe(l.filter(a=>B(s,a?.item?.permission)),s,i).catch(()=>({})),Ae(s)]),b=d?` dm-admin-sidebar--${p(d)}`:"";function C(a,x="px"){if(a==null)return a;const f=String(a).trim();return/^\d+(\.\d+)?$/.test(f)?`${f}${x}`:f}let L="";if(o){const a=[];o.fontFamily&&a.push(`font-family: ${o.fontFamily}, sans-serif`),o.fontSize&&a.push(`font-size: ${C(o.fontSize)}`),o.fontWeight&&a.push(`font-weight: ${o.fontWeight}`),o.letterSpacing&&a.push(`letter-spacing: ${C(o.letterSpacing,"em")}`);const x=[];if(a.length&&x.push(`#admin-sidebar .dm-admin-sidebar, #admin-sidebar .dm-admin-sidebar .sidebar-link, #admin-sidebar .dm-admin-sidebar summary, #admin-sidebar .dm-admin-sidebar .sidebar-text { ${a.join("; ")} }`),o.iconSize){const f=C(o.iconSize);x.push(`#admin-sidebar .dm-admin-sidebar [data-icon], #admin-sidebar .dm-admin-sidebar [data-icon] svg { width: ${f} !important; height: ${f} !important; }`)}x.length&&(L=`<style data-admin-sidebar-style>${x.join(" ")}</style>`)}const j=de(s)?ce():"",F=`<div class="dm-admin-sidebar-header"><span data-icon="layout"></span><span class="dm-admin-sidebar-title">${p(v)}</span>${j}${me()}</div>`,N=n.some(a=>a&&a.type==="spacer"&&a.size==="flex")?" dm-admin-sidebar--has-flex-spacer":"",P=`${L}<nav class="dm-admin-sidebar${b}${N}">${F}${n.map(a=>G(a,y)).join("")}</nav>`,q=document.createRange();q.selectNodeContents(t);const T=q.createContextualFragment(P);t.replaceChildren(T),Domma.icons.scan(t),t.querySelector(".dm-admin-sidebar-arrange")?.addEventListener("click",()=>ue(t));for(const a of A)R.set(a.url,y[a.url]??null);W(t),we(t),t.removeAttribute("data-menu-hover"),t.removeAttribute("data-menu-active"),t.style.removeProperty("--dm-menu-highlight"),Q(t,{highlight:r});const k=document.querySelector(".dashboard-wrapper");["position","top","bottom","left","right","zIndex","maxHeight","transform"].forEach(a=>{t.style[a]=""}),t.classList.remove("dm-menu-floating"),k&&(k.style.paddingLeft="",k.style.paddingRight=""),t.__dmGutter=()=>{if(c!=="floating"||!k)return;const a=m&&m.anchor||"TL",x=Number.isFinite(Number(m?.offsetX))?Number(m.offsetX):16,f=t.classList.contains("is-collapsed")?ge:parseInt(t.style.width,10)||t.getBoundingClientRect().width||260,w=Math.round(f+x*2)+"px";a==="R"||a==="TR"||a==="BR"?k.style.paddingRight=w:k.style.paddingLeft=w},c==="floating"&&Y(t,{position:"floating",float:m||{}}),t.querySelectorAll("details").forEach(a=>{a.addEventListener("toggle",function(){const x=this.getAttribute("data-state-key");x&&S.set(x,this.open);const f=this.querySelector(":scope > .sidebar-children");if(f)if(this.open){const w=f.scrollHeight;f.style.maxHeight="0",requestAnimationFrame(()=>{f.style.maxHeight=w+"px",f.addEventListener("transitionend",function X(){f.style.maxHeight="none",f.removeEventListener("transitionend",X)})})}else{const w=f.scrollHeight;f.style.maxHeight=w+"px",requestAnimationFrame(()=>{f.style.maxHeight="0"})}})}),fe(t),t.dataset.routeWired||(t.dataset.routeWired="1",M.subscribe("router:afterChange",()=>t.__dmSetActive?.()));function g(){const a=location.hash||"#/";$(t).find(".sidebar-link").removeClass("active"),$(t).find(`.sidebar-link[data-url="${a}"]`).addClass("active")}g(),t.__dmSetActive=g}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "0.109.3",
3
+ "version": "0.109.5",
4
4
  "description": "File-based CMS powered by Domma and Fastify. Run npx domma-cms my-site to create a new project.",
5
5
  "type": "module",
6
6
  "main": "server/server.js",
@@ -117,7 +117,7 @@ export function classifyEntitlement(slug, entitlement = readEntitlement(slug), n
117
117
  return {state: 'unlicensed', reason: `entitlement is for "${entitlement.slug}"`, entitlement};
118
118
  }
119
119
 
120
- const pem = trustedKey(entitlement.keyId);
120
+ const pem = trustedKey(entitlement.keyId, 'entitlement');
121
121
  if (!pem) {
122
122
  return {state: 'unlicensed', reason: `unknown signing key "${entitlement.keyId}"`, entitlement};
123
123
  }
@@ -42,14 +42,32 @@ const AUDIT_LOG = path.join(ROOT, 'content', 'instances_log.json');
42
42
  // every CMS accepts as ours, permanently. Nothing had ever been signed with it,
43
43
  // so there was no artifact to stay compatible with.
44
44
  //
45
- // Private counterpart: generated 2026-09-22, held off-repo. See the marketplace
46
- // design's Signing keys section for custody.
45
+ // Each key says what it may sign: `bundle` (plugin code - every CMS installs
46
+ // what it signs) and/or `entitlement` (a licence for a plugin a site already
47
+ // has). A key a manager must hold online, to mint licences as it grants them,
48
+ // is trusted for entitlements ONLY: if it leaks, the damage is forged
49
+ // licences, never code every CMS would run.
50
+ //
51
+ // marketplace-1: generated 2026-09-22, held off-repo; signs the catalogue and
52
+ // bundles, and svr-1's licences. See the marketplace design's Signing keys
53
+ // section for custody.
54
+ // svr-2-1: generated 2026-10-01 on svr-2 (aurelius), where it stays; svr-2's
55
+ // licences only.
47
56
  // ---------------------------------------------------------------------------
48
57
 
49
58
  const TRUSTED_KEYS = {
50
- 'marketplace-1': `-----BEGIN PUBLIC KEY-----
59
+ 'marketplace-1': {
60
+ uses: ['bundle', 'entitlement'],
61
+ pem: `-----BEGIN PUBLIC KEY-----
51
62
  MCowBQYDK2VwAyEAkgYWv0IckLy2lCeNoaNot++KMJLpJa6FTd6RW0sH2zg=
52
63
  -----END PUBLIC KEY-----`
64
+ },
65
+ 'svr-2-1': {
66
+ uses: ['entitlement'],
67
+ pem: `-----BEGIN PUBLIC KEY-----
68
+ MCowBQYDK2VwAyEAqAlx1lfHeCaNiwJmoXZMDY56yafU2GlpNG3VInw9Q14=
69
+ -----END PUBLIC KEY-----`
70
+ }
53
71
  };
54
72
 
55
73
  /**
@@ -71,16 +89,20 @@ function requireSignedPlugins() {
71
89
  }
72
90
 
73
91
  /**
74
- * One trusted public key by id, or null.
92
+ * One trusted public key by id, for one use, or null.
75
93
  *
76
- * Entitlement verification needs the same keys bundle installation does, and a
77
- * second copy of them would be a second thing to rotate.
94
+ * Bundles and entitlements share this one list, so there is one place to
95
+ * rotate - but not every key may sign both. A key not trusted for `use` is
96
+ * answered exactly like an unknown one. The default is `bundle`, the stricter
97
+ * of the two, so a caller that forgets to say gets the safe answer.
78
98
  *
79
99
  * @param {string} keyId
100
+ * @param {'bundle'|'entitlement'} [use]
80
101
  * @returns {string|null} PEM
81
102
  */
82
- export function trustedKey(keyId) {
83
- return TRUSTED_KEYS[keyId] ?? null;
103
+ export function trustedKey(keyId, use = 'bundle') {
104
+ const key = Object.hasOwn(TRUSTED_KEYS, keyId) ? TRUSTED_KEYS[keyId] : null;
105
+ return key && key.uses.includes(use) ? key.pem : null;
84
106
  }
85
107
 
86
108
  /**
@@ -90,10 +112,11 @@ export function trustedKey(keyId) {
90
112
  * trusting a key it should not, or having lost the one it should, is the kind of
91
113
  * mistake that is invisible until a bundle is refused in the field.
92
114
  *
115
+ * @param {'bundle'|'entitlement'} [use] - only the keys trusted for this; all when omitted
93
116
  * @returns {string[]}
94
117
  */
95
- export function trustedKeyIds() {
96
- return Object.keys(TRUSTED_KEYS);
118
+ export function trustedKeyIds(use) {
119
+ return Object.keys(TRUSTED_KEYS).filter((id) => !use || TRUSTED_KEYS[id].uses.includes(use));
97
120
  }
98
121
 
99
122
  // ---------------------------------------------------------------------------
@@ -333,7 +356,7 @@ export async function installPlugin(slug, { tarball, signature, publicKeyId, all
333
356
  }
334
357
  }
335
358
 
336
- const pubKeyPem = unsigned ? null : TRUSTED_KEYS[publicKeyId];
359
+ const pubKeyPem = unsigned ? null : trustedKey(publicKeyId, 'bundle');
337
360
  if (!unsigned && !pubKeyPem) {
338
361
  throw new Error(`Unknown public key: ${publicKeyId}`);
339
362
  }