33< head >
44< meta charset ="UTF-8 "/>
55< meta name ="viewport " content ="width=device-width,initial-scale=1.0 "/>
6+ < meta http-equiv ="Content-Security-Policy " content ="default-src 'none'; script-src 'nonce-__ATTR_CSP_NONCE__'; style-src 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none' "/>
67< title > Table Permissions Plan — __SITE_NAME__</ title >
78< style >
89: root {
@@ -216,7 +217,7 @@ <h3>Legend</h3>
216217 </ div >
217218
218219 < div style ="display:flex;justify-content:flex-end;align-items:center;margin-bottom:16px; ">
219- < button id ="expandAllBtn " onclick =" toggleExpandAll() " style ="padding:5px 12px;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--surface);color:var(--text-dim);font-size:12px;font-weight:600;cursor:pointer;font-family:var(--sans);transition:all 0.15s; "> Expand All</ button >
220+ < button id ="expandAllBtn " style ="padding:5px 12px;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--surface);color:var(--text-dim);font-size:12px;font-weight:600;cursor:pointer;font-family:var(--sans);transition:all 0.15s; "> Expand All</ button >
220221 </ div >
221222 < div id ="permsContainer "> </ div >
222223 </ div >
@@ -225,21 +226,41 @@ <h3>Legend</h3>
225226 </ div >
226227</ div >
227228
228- < script >
229+ < script nonce =" __ATTR_CSP_NONCE__ " >
229230// Data is populated by the table-permissions-architect agent
230- const SITE_NAME = "__SITE_NAME__" ;
231-
232- const ROLES = __ROLES_DATA__ ;
231+ const ROLES = __JSON_ROLES_DATA__ ;
233232// Each role: { id, name, desc, builtin, isNew, color }
234233// builtin: true only for "Authenticated Users" and "Anonymous Users"
235234// isNew: true if proposed by this plan, false if already exists in .powerpages-site/web-roles/
236235
237- const PERMS = __PERMISSIONS_DATA__ ;
236+ const PERMS = __JSON_PERMISSIONS_DATA__ ;
238237// Each perm: { id, name, displayName, table, scope, read, create, write, delete, append, appendto, roles (array of role ids), parent (perm id or null), parentRelationship, rationale: { scope, read, create, write, delete, append, appendto }, isNew }
239238
240- const RATIONALE = __RATIONALE_DATA__ ;
239+ const RATIONALE = __JSON_RATIONALE_DATA__ ;
241240// Array of { icon, title, desc }
242241
242+ function esc ( value ) {
243+ return String ( value ?? '' )
244+ . replace ( / & / g, '&' )
245+ . replace ( / < / g, '<' )
246+ . replace ( / > / g, '>' )
247+ . replace ( / " / g, '"' )
248+ . replace ( / ' / g, ''' ) ;
249+ }
250+
251+ function safeIcon ( value ) {
252+ const icon = String ( value ?? '' ) ;
253+ return / ^ (?: & # (?: x [ 0 - 9 a - f ] + | \d + ) ; ) + $ / i. test ( icon ) ? icon : esc ( icon ) ;
254+ }
255+
256+ function safeScope ( value ) {
257+ return [ 'Global' , 'Contact' , 'Account' , 'Parent' , 'Self' ] . includes ( value ) ? value : 'Self' ;
258+ }
259+
260+ function safeColor ( value ) {
261+ return / ^ # [ 0 - 9 a - f ] { 6 } $ / i. test ( String ( value ?? '' ) ) ? value : '#8890a4' ;
262+ }
263+
243264// Tab navigation
244265document . querySelectorAll ( '.nav-btn' ) . forEach ( btn => {
245266 btn . addEventListener ( 'click' , ( ) => {
@@ -251,7 +272,7 @@ <h3>Legend</h3>
251272} ) ;
252273
253274function getRoleName ( id ) { return ROLES . find ( r => r . id === id ) ?. name || id ; }
254- function getRoleColor ( id ) { return ROLES . find ( r => r . id === id ) ?. color || "#8890a4" ; }
275+ function getRoleColor ( id ) { return safeColor ( ROLES . find ( r => r . id === id ) ?. color ) ; }
255276
256277const scopeColors = {
257278 Global : 'var(--critical)' , Contact : 'var(--pass)' , Account : 'var(--accent)' ,
@@ -292,10 +313,10 @@ <h3>Legend</h3>
292313 let html = '' ;
293314 RATIONALE . forEach ( r => {
294315 html += `<div class="principle">
295- <div class="principle-icon">${ r . icon } </div>
316+ <div class="principle-icon">${ safeIcon ( r . icon ) } </div>
296317 <div>
297- <div class="principle-title">${ r . title } </div>
298- <div class="principle-desc">${ r . desc } </div>
318+ <div class="principle-title">${ esc ( r . title ) } </div>
319+ <div class="principle-desc">${ esc ( r . desc ) } </div>
299320 </div>
300321 </div>` ;
301322 } ) ;
@@ -309,19 +330,23 @@ <h3>Legend</h3>
309330 const sortedRoles = [ ...ROLES ] . sort ( ( a , b ) => ( b . isNew ? 1 : 0 ) - ( a . isNew ? 1 : 0 ) ) ;
310331 sortedRoles . forEach ( r => {
311332 const perms = PERMS . filter ( p => p . roles . includes ( r . id ) ) ;
333+ const roleColor = safeColor ( r . color ) ;
312334 html += `<div class="card" style="cursor:default;${ r . isNew ? 'background:var(--accent-bg);border-color:var(--accent-border);' : '' } ">
313335 <div style="display:flex;justify-content:space-between;align-items:flex-start;">
314336 <div style="display:flex;align-items:center;gap:10px;">
315- <div style="width:10px;height:10px;border-radius:50%;background:${ r . color } ;flex-shrink:0;"></div>
337+ <div style="width:10px;height:10px;border-radius:50%;background:${ roleColor } ;flex-shrink:0;"></div>
316338 <div>
317- <span style="font-size:14px;font-weight:700;color:var(--text-bright)">${ r . name } </span>
339+ <span style="font-size:14px;font-weight:700;color:var(--text-bright)">${ esc ( r . name ) } </span>
318340 ${ r . builtin ? '<span class="builtin-badge">BUILT-IN</span>' : r . isNew ? '<span class="new-badge">PROPOSED</span>' : '<span class="existing-badge">EXISTING</span>' }
319- <div style="font-size:12px;color:var(--text-dim);margin-top:2px;">${ r . desc } </div>
341+ <div style="font-size:12px;color:var(--text-dim);margin-top:2px;">${ esc ( r . desc ) } </div>
320342 </div>
321343 </div>
322344 </div>
323345 <div style="margin-top:10px;padding-top:10px;border-top:1px solid var(--border);display:flex;flex-wrap:wrap;gap:5px;">
324- ${ perms . length > 0 ? perms . map ( p => `<span style="font-size:10px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;padding:3px 8px;color:var(--text);"><span class="scope-tag scope-${ p . scope } " style="font-size:9px;padding:1px 5px;min-width:auto;margin-right:4px;">${ p . scope } </span>${ p . name } </span>` ) . join ( '' ) : '<span style="font-size:11px;color:var(--text-dim);font-style:italic;">No direct table permissions</span>' }
346+ ${ perms . length > 0 ? perms . map ( p => {
347+ const scope = safeScope ( p . scope ) ;
348+ return `<span style="font-size:10px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;padding:3px 8px;color:var(--text);"><span class="scope-tag scope-${ scope } " style="font-size:9px;padding:1px 5px;min-width:auto;margin-right:4px;">${ esc ( scope ) } </span>${ esc ( p . name ) } </span>` ;
349+ } ) . join ( '' ) : '<span style="font-size:11px;color:var(--text-dim);font-style:italic;">No direct table permissions</span>' }
325350 </div>
326351 </div>` ;
327352 } ) ;
@@ -345,34 +370,34 @@ <h3>Legend</h3>
345370
346371 function renderPermCard ( p , depth ) {
347372 const parentName = p . parent ? PERMS . find ( x => x . id === p . parent ) ?. name : null ;
373+ const scope = safeScope ( p . scope ) ;
348374 const privs = [ 'Read' , 'Create' , 'Write' , 'Delete' , 'Append' , 'AppendTo' ] ;
349375 const privFlags = { Read : p . read , Create : p . create , Write : p . write , Delete : p . delete , Append : p . append , AppendTo : p . appendto } ;
350- const indent = depth * 32 ;
351376 const children = childrenOf [ p . id ] || [ ] ;
352377
353378 let html = `<div style="position:relative;">` ;
354379
355- html += `<div class="card" style="border-left:3px solid ${ scopeColors [ p . scope ] } ;padding:0;${ p . isNew ? 'background:var(--accent-bg);border-color:var(--accent-border);' : '' } ">
356- <div class="expandable-header" onclick="this.parentElement.classList.toggle('expanded')" >
380+ html += `<div class="card" style="border-left:3px solid ${ scopeColors [ scope ] } ;padding:0;${ p . isNew ? 'background:var(--accent-bg);border-color:var(--accent-border);' : '' } ">
381+ <div class="expandable-header">
357382 ${ depth > 0 ? '<span style="font-size:15px;color:var(--purple);font-weight:700;margin-right:2px;">↳</span>' : '' }
358- <span class="scope-tag scope-${ p . scope } ">${ p . scope } </span>
359- <span style="font-size:13px;font-weight:600;color:var(--text-bright);flex:1;">${ p . name } </span>
383+ <span class="scope-tag scope-${ scope } ">${ esc ( scope ) } </span>
384+ <span style="font-size:13px;font-weight:600;color:var(--text-bright);flex:1;">${ esc ( p . name ) } </span>
360385 ${ p . isNew ? '<span class="new-badge">PROPOSED</span>' : '<span class="existing-badge">EXISTING</span>' }
361- <code style="font-size:11px;color:var(--accent);background:var(--accent-bg);padding:1px 6px;border-radius:3px;border:1px solid var(--accent-border);">${ p . table } </code>
386+ <code style="font-size:11px;color:var(--accent);background:var(--accent-bg);padding:1px 6px;border-radius:3px;border:1px solid var(--accent-border);">${ esc ( p . table ) } </code>
362387 <span style="display:flex;gap:3px;">${ privs . map ( pr => `<span class="priv ${ privFlags [ pr ] ? 'priv-on' : 'priv-off' } ">${ pr [ 0 ] } ${ pr === 'AppendTo' ? 'T' : '' } </span>` ) . join ( '' ) } </span>
363388 ${ children . length > 0 ? `<span style="font-size:10px;color:var(--purple);font-weight:700;font-family:var(--mono);background:var(--purple-bg);border:1px solid var(--purple-border);padding:1px 6px;border-radius:3px;">${ children . length } child${ children . length > 1 ? 'ren' : '' } </span>` : '' }
364389 <span class="expand-chevron">▶</span>
365390 </div>
366391 <div class="expandable-body">
367392 <div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:12px;font-size:12px;">
368- <div><div class="field-label">Scope</div><span class="scope-tag scope-${ p . scope } " style="font-size:10px;padding:1px 6px;min-width:auto;">${ p . scope } ${ p . scope === 'Parent' && parentName ? ' \u2192 ' + parentName : '' } </span></div>
369- <div><div class="field-label">Display Name</div><span style="color:var(--text-bright);font-weight:600;">${ p . displayName || p . name } </span></div>
370- <div><div class="field-label">Table Logical Name</div><span style="color:var(--text);font-family:var(--mono);font-size:11px;">${ p . table } </span></div>
371- <div><div class="field-label">Roles</div>${ p . roles . map ( rid => `<span style="color:${ getRoleColor ( rid ) } ;font-weight:600;margin-right:6px;">${ getRoleName ( rid ) } </span>` ) . join ( '' ) } </div>
393+ <div><div class="field-label">Scope</div><span class="scope-tag scope-${ scope } " style="font-size:10px;padding:1px 6px;min-width:auto;">${ esc ( scope ) } ${ scope === 'Parent' && parentName ? ' \u2192 ' + esc ( parentName ) : '' } </span></div>
394+ <div><div class="field-label">Display Name</div><span style="color:var(--text-bright);font-weight:600;">${ esc ( p . displayName || p . name ) } </span></div>
395+ <div><div class="field-label">Table Logical Name</div><span style="color:var(--text);font-family:var(--mono);font-size:11px;">${ esc ( p . table ) } </span></div>
396+ <div><div class="field-label">Roles</div>${ p . roles . map ( rid => `<span style="color:${ getRoleColor ( rid ) } ;font-weight:600;margin-right:6px;">${ esc ( getRoleName ( rid ) ) } </span>` ) . join ( '' ) } </div>
372397 <div style="grid-column:span 2"><div class="field-label">Privileges</div>${ privs . map ( pr => `<span class="priv ${ privFlags [ pr ] ? 'priv-on' : 'priv-off' } " style="margin-right:4px;">${ pr } : ${ privFlags [ pr ] ? '\u2713' : '\u2717' } </span>` ) . join ( '' ) } </div>
373- ${ p . parentRelationship ? `<div style="grid-column:span 2"><div class="field-label">Parent Relationship</div><span style="color:var(--text);font-family:var(--mono);font-size:11px;">${ p . parentRelationship } </span> <span style="font-size:11px;color:var(--text-dim);">\u2192 ${ parentName } </span></div>` : '' }
398+ ${ p . parentRelationship ? `<div style="grid-column:span 2"><div class="field-label">Parent Relationship</div><span style="color:var(--text);font-family:var(--mono);font-size:11px;">${ esc ( p . parentRelationship ) } </span> <span style="font-size:11px;color:var(--text-dim);">\u2192 ${ esc ( parentName ) } </span></div>` : '' }
374399 </div>
375- ${ p . rationale ? ( ( ) => { const labels = { read :'Read' , create :'Create' , write :'Write' , delete :'Delete' , append :'Append' , appendto :'AppendTo' } ; return `<div style="margin-top:12px;"><div class="field-label" style="margin-bottom:6px;">Reasoning</div><div style="font-size:12px;color:var(--text);background:var(--surface2);padding:10px 14px;border-radius:var(--radius-sm);border-left:2px solid var(--accent);line-height:1.8;"><ul class="reasoning-list">${ p . rationale . scope ? `<li><strong>Scope:</strong> ${ p . rationale . scope } </li>` : '' } ${ Object . entries ( labels ) . map ( ( [ k , l ] ) => p . rationale [ k ] ? `<li><strong>${ l } :</strong> ${ p . rationale [ k ] } </li>` : '' ) . join ( '' ) } </ul></div></div>` ; } ) ( ) : '' }
400+ ${ p . rationale ? ( ( ) => { const labels = { read :'Read' , create :'Create' , write :'Write' , delete :'Delete' , append :'Append' , appendto :'AppendTo' } ; return `<div style="margin-top:12px;"><div class="field-label" style="margin-bottom:6px;">Reasoning</div><div style="font-size:12px;color:var(--text);background:var(--surface2);padding:10px 14px;border-radius:var(--radius-sm);border-left:2px solid var(--accent);line-height:1.8;"><ul class="reasoning-list">${ p . rationale . scope ? `<li><strong>Scope:</strong> ${ esc ( p . rationale . scope ) } </li>` : '' } ${ Object . entries ( labels ) . map ( ( [ k , l ] ) => p . rationale [ k ] ? `<li><strong>${ l } :</strong> ${ esc ( p . rationale [ k ] ) } </li>` : '' ) . join ( '' ) } </ul></div></div>` ; } ) ( ) : '' }
376401 </div>
377402 </div>` ;
378403
@@ -393,6 +418,9 @@ <h3>Legend</h3>
393418 roots . forEach ( p => { html += renderPermCard ( p , 0 ) ; } ) ;
394419
395420 c . innerHTML = html ;
421+ c . querySelectorAll ( '.expandable-header' ) . forEach ( header => {
422+ header . addEventListener ( 'click' , ( ) => header . parentElement . classList . toggle ( 'expanded' ) ) ;
423+ } ) ;
396424}
397425
398426// Expand All toggle
@@ -408,6 +436,7 @@ <h3>Legend</h3>
408436}
409437
410438// Init
439+ document . getElementById ( 'expandAllBtn' ) . addEventListener ( 'click' , toggleExpandAll ) ;
411440renderRationale ( ) ;
412441renderRoles ( ) ;
413442renderPerms ( ) ;
0 commit comments