[object Object]

← back to Wallco Ai

wallco.ai · all target=_blank now rel=noopener noreferrer (tab-nabbing fix) + 3 external links (figma/spoonflower/dash.cloudflare) escalated to add nofollow — closes the window.opener XSS pivot vector + SEO link-juice leak. 20 internal blanks, 3 external upgraded.

d6cdb2422b68760da466af8218672d4392c737a8 · 2026-05-12 07:42:47 -0700 · SteveStudio2

Files touched

Diff

commit d6cdb2422b68760da466af8218672d4392c737a8
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Tue May 12 07:42:47 2026 -0700

    wallco.ai · all target=_blank now rel=noopener noreferrer (tab-nabbing fix) + 3 external links (figma/spoonflower/dash.cloudflare) escalated to add nofollow — closes the window.opener XSS pivot vector + SEO link-juice leak. 20 internal blanks, 3 external upgraded.
---
 server.js | 40 ++++++++++++++++++++--------------------
 1 file changed, 20 insertions(+), 20 deletions(-)

diff --git a/server.js b/server.js
index 85ed233..3ecd8c2 100644
--- a/server.js
+++ b/server.js
@@ -351,7 +351,7 @@ app.get('/admin/golive', (_req, res) => {
 <script>
 const STEPS = [
   { key:'zone',     title:'1. Cloudflare zone exists',     desc:'CF API can\'t create zones with current token scope. One-time manual click.',
-    fix:'Open <a href="https://dash.cloudflare.com" target="_blank" style="color:#d2b15c">dash.cloudflare.com</a> → Add a site → wallco.ai → Free plan. Then refresh this page.' },
+    fix:'Open <a href="https://dash.cloudflare.com" target="_blank" style="color:#d2b15c" rel="noopener noreferrer nofollow">dash.cloudflare.com</a> → Add a site → wallco.ai → Free plan. Then refresh this page.' },
   { key:'dns',      title:'2. DNS records (A, www, MX, SPF, DKIM, DMARC)', desc:'Six required records on the CF zone. Run after step 1 succeeds.',
     cmd:'bash scripts/golive.sh dns' },
   { key:'ns',       title:'3. GoDaddy NS swapped to Cloudflare', desc:'Domain still on ns41/42.domaincontrol.com — must switch to the CF nameservers.',
@@ -1041,7 +1041,7 @@ app.get('/designs', (req, res) => {
     // Admin review-mode card: sliders, K/R, chips, why-chip
     return `
     <div class="design-card review-card" data-id="${d.id}">
-      <a href="/design/${d.id}" target="_blank" class="card-img" style="background-image:url('${d.image_url}')">
+      <a href="/design/${d.id}" target="_blank" class="card-img" style="background-image:url('${d.image_url}')" rel="noopener noreferrer">
         <span class="decision-badge"></span>
       </a>
       <div class="card-meta">
@@ -1850,7 +1850,7 @@ ${_isAdmin ? `
           ? '<div class="pair-thumb" style="background-image:url('+s.thumb_url+')"></div>'
           : '<div class="pair-thumb material">◧</div>';
         return '<div class="pair-card' + pinned + '" data-pin-key="' + escapeAttr(s.pin_key) + '">' +
-               '<a class="pair-link" href="/design/' + s.design_id + '" target="_blank">' + thumb +
+               '<a class="pair-link" href="/design/' + s.design_id + '" target="_blank" rel="noopener noreferrer">' + thumb +
                '<div class="pair-body"><span class="pair-type">design · #' + s.design_id + '</span>' +
                '<div class="pair-title">' + escapeHTML(s.label) + (s.hex ? '<span class="pair-hexcode">'+s.hex+'</span>' : '') + '</div>' +
                '<div class="pair-reason">' + escapeHTML(s.reason) + '</div></div></a>' + pinBtn + '</div>';
@@ -2092,7 +2092,7 @@ ${_isAdmin ? `
       const closenessTxt = d.closeness ? ' (' + d.closeness.band + ')' : '';
       const t = escapeAttr((d.title || '#' + d.id) + ' · ΔE ' + (d.deltaE != null ? d.deltaE : '?') + closenessTxt);
       const bandClass = d.closeness ? ' iso-de-' + d.closeness.band : '';
-      return '<a href="/design/' + d.id + '" target="_blank" title="' + t + '" style="background-image:url(' + img + ')">' +
+      return '<a href="/design/' + d.id + '" target="_blank" title="' + t + '" style="background-image:url(' + img + ')" rel="noopener noreferrer">' +
              '<span class="iso-de' + bandClass + '">ΔE ' + (d.deltaE != null ? d.deltaE.toFixed(1) : '?') + '</span>' +
              '</a>';
     }).join('');
@@ -3494,7 +3494,7 @@ ${htmlHeader('/designs')}
           this.disabled = true;
           var j = await post('/api/spoonflower/upload', { design_id: this.dataset.id, dry: true });
           this.disabled = false;
-          if (j.ok) show('Dry-run OK. job=' + j.job_id + '. <a href="/api/spoonflower/status/' + j.job_id + '" target="_blank">view log</a>', false);
+          if (j.ok) show('Dry-run OK. job=' + j.job_id + '. <a href="/api/spoonflower/status/' + j.job_id + '" target="_blank" rel="noopener noreferrer">view log</a>', false);
           else show('Dry-run failed: ' + (j.error||'unknown'), true);
         });
 
@@ -3504,7 +3504,7 @@ ${htmlHeader('/designs')}
           this.disabled = true;
           var j = await post('/api/spoonflower/upload', { design_id: this.dataset.id, dry: false });
           this.disabled = false;
-          if (j.ok) show('Upload kicked off. job=' + j.job_id + '. <a href="/api/spoonflower/status/' + j.job_id + '" target="_blank">view log</a>', false);
+          if (j.ok) show('Upload kicked off. job=' + j.job_id + '. <a href="/api/spoonflower/status/' + j.job_id + '" target="_blank" rel="noopener noreferrer">view log</a>', false);
           else show('Upload failed: ' + (j.error||'unknown'), true);
         });
 
@@ -3934,7 +3934,7 @@ ${FOOTER}
           '</div>'
         : '';
       return '<figure style="margin:0;background:var(--paper,#faf8f2);border:1px solid var(--rule,#e5e5e0);position:relative;' + dim + '">' +
-        '<a href="' + r.image_path + '" target="_blank" style="display:block;aspect-ratio:1/1;overflow:hidden;background:#000;">' +
+        '<a href="' + r.image_path + '" target="_blank" style="display:block;aspect-ratio:1/1;overflow:hidden;background:#000;" rel="noopener noreferrer">' +
           '<img src="' + r.image_path + '" alt="Room with this pattern — ' + tag + '" loading="lazy" style="width:100%;height:100%;object-fit:cover;display:block;" />' +
         '</a>' +
         adminCtrls +
@@ -4276,7 +4276,7 @@ app.get('/age-themes', (req, res) => {
   <div class="credits">
     <strong style="color:#d2b15c">v${AGE_THEME_DATA.version}</strong> · validated ${AGE_THEME_DATA.validated_at} ·
     <strong style="color:#d2554a">⚠ AI-generated starting point — not user-tested, not accessibility-tooled.</strong>
-    Recommendations from the <span class="src">ui-ux-designer</span> subagent (font/hex/size swaps with research citations) and reviewed by the <span class="src">debate-team-fast</span> 3-LLM panel (Claude, Kimi K2.5, Qwen3:14b — Codex quota-blocked). Companion <span class="src">Figma file</span> at <a href="https://www.figma.com/design/WmwSirHywv530eACPtbqyA" target="_blank" style="color:#d2b15c">figma.com/design/WmwSirHywv530eACPtbqyA</a>. Canonical band data: <code style="color:#d2b15c">~/Projects/wallco-ai/src/bands.json</code> (skill mirror: <code style="color:#666">~/.claude/skills/age-adaptive-theme/bands.json</code>) · APCA module: <code style="color:#d2b15c">~/Projects/wallco-ai/src/apca.js</code>.
+    Recommendations from the <span class="src">ui-ux-designer</span> subagent (font/hex/size swaps with research citations) and reviewed by the <span class="src">debate-team-fast</span> 3-LLM panel (Claude, Kimi K2.5, Qwen3:14b — Codex quota-blocked). Companion <span class="src">Figma file</span> at <a href="https://www.figma.com/design/WmwSirHywv530eACPtbqyA" target="_blank" style="color:#d2b15c" rel="noopener noreferrer nofollow">figma.com/design/WmwSirHywv530eACPtbqyA</a>. Canonical band data: <code style="color:#d2b15c">~/Projects/wallco-ai/src/bands.json</code> (skill mirror: <code style="color:#666">~/.claude/skills/age-adaptive-theme/bands.json</code>) · APCA module: <code style="color:#d2b15c">~/Projects/wallco-ai/src/apca.js</code>.
     <br><span style="color:#888">Debate-team consensus: Atkinson Hyperlegible / tween indigo / Nunito picks are defensible. The fg #222→#1a1a1a (toddler) and -apple-system→Inter (adult) swaps were LLM "hallucinated precision" — dropped. APCA Lc values shown are calculated by src/apca.js (Myndex 0.1.9), NOT lab-verified. v9 fixes from validation-loop: teen body 16px, line-height 1.5 baseline, btn-min-height 44/48/56px per band.</span>
   </div>
 </header>
@@ -4329,7 +4329,7 @@ app.get('/moodboard', (req, res) => {
     return `
       <section class="mb-design-block">
         <header class="mb-design-header">
-          <a class="mb-design-thumb" href="/design/${design.id}" target="_blank" style="background-image:url('${design.image_url || ''}')"></a>
+          <a class="mb-design-thumb" href="/design/${design.id}" target="_blank" style="background-image:url('${design.image_url || ''}')" rel="noopener noreferrer"></a>
           <div class="mb-design-info">
             <h2 class="mb-design-title">${escHTML(design.title || design.prompt || '#'+design.id)}</h2>
             <p class="mb-design-meta">${escHTML(design.category||'')} · <span class="mb-design-dot" style="background:${design.dominant_hex||'#888'}"></span> ${escHTML(design.dominant_hex||'')}${review.decision?' · <strong style="color:'+(review.decision==='keep'?'#5fbf6e':'#d2554a')+'">'+review.decision.toUpperCase()+'</strong>':''}</p>
@@ -5024,7 +5024,7 @@ ${htmlHeader('')}
           <div style="padding:10px 12px;font-size:12px">
             <div style="font-weight:500">${(p.title||'(no title)').slice(0,48)}</div>
             <div style="color:#888;margin-top:2px">SF#${p.sf_design_id}${p.file_bytes ? ' · '+Math.round(p.file_bytes/1024)+' KB' : ''}</div>
-            <a href="${p.design_url}" target="_blank" style="color:#888;font-size:11px">view on spoonflower →</a>
+            <a href="${p.design_url}" target="_blank" style="color:#888;font-size:11px" rel="noopener noreferrer nofollow">view on spoonflower →</a>
           </div>
         </div>`).join('')}
       </div>`}
@@ -5043,7 +5043,7 @@ ${htmlHeader('')}
           <td><span style="padding:2px 6px;border-radius:3px;background:${j.status==='done'?'#d4edda':j.status==='error'?'#f8d7da':'#fff3cd'};color:${j.status==='done'?'#155724':j.status==='error'?'#721c24':'#856404'}">${j.status}</span></td>
           <td>${j.dry_run ? 'yes' : '—'}</td>
           <td style="color:#888">${j.created_at ? new Date(j.created_at).toLocaleString() : '—'}</td>
-          <td><a href="/api/spoonflower/status/${j.id}" target="_blank" style="color:#888">log →</a></td>
+          <td><a href="/api/spoonflower/status/${j.id}" target="_blank" style="color:#888" rel="noopener noreferrer">log →</a></td>
         </tr>`).join('')}
         </tbody></table>`}
 </main>
@@ -5086,7 +5086,7 @@ document.getElementById('btn-pull-test').addEventListener('click', async functio
   document.getElementById('pull-status').textContent = 'Starting limited pull (3 designs)…';
   var j = await post('/api/spoonflower/pull-all', { limit: 3, account: 'dw' });
   document.getElementById('pull-status').innerHTML = j.ok
-    ? 'Job '+j.job_id+' running — <a href="/api/spoonflower/status/'+j.job_id+'" target="_blank">view log</a>. Reload in ~60s.'
+    ? 'Job '+j.job_id+' running — <a href="/api/spoonflower/status/'+j.job_id+'" target="_blank" rel="noopener noreferrer">view log</a>. Reload in ~60s.'
     : 'Failed: ' + (j.error||'unknown');
   this.disabled=false;
 });
@@ -5096,7 +5096,7 @@ document.getElementById('btn-pull-all').addEventListener('click', async function
   document.getElementById('pull-status').textContent = 'Starting full pull…';
   var j = await post('/api/spoonflower/pull-all', { limit: 0, account: 'dw' });
   document.getElementById('pull-status').innerHTML = j.ok
-    ? 'Job '+j.job_id+' running — <a href="/api/spoonflower/status/'+j.job_id+'" target="_blank">view log</a>. Reload page to refresh table.'
+    ? 'Job '+j.job_id+' running — <a href="/api/spoonflower/status/'+j.job_id+'" target="_blank" rel="noopener noreferrer">view log</a>. Reload page to refresh table.'
     : 'Failed: ' + (j.error||'unknown');
   this.disabled=false;
 });
@@ -5611,7 +5611,7 @@ document.getElementById('btn-batch-go').addEventListener('click', async function
       }).then(r=>r.json());
       if (j.ok && j.design_id) {
         var imgUrl = '/designs/img/'+(j.filename || '');
-        var html = '<a href="/design/'+j.design_id+'" target="_blank" style="text-decoration:none;color:inherit">'+
+        var html = '<a href="/design/'+j.design_id+'" target="_blank" style="text-decoration:none;color:inherit" rel="noopener noreferrer">'+
           '<div style="border:1px solid #444;border-radius:4px;overflow:hidden;background:#0f0e0c">'+
             (j.filename ? '<img src="'+imgUrl+'" style="width:100%;aspect-ratio:1;object-fit:cover">' : '<div style="aspect-ratio:1;background:'+(j.dominant_hex||'#222')+'"></div>')+
             '<div style="padding:4px 6px;font-size:10px;color:#eee">#'+j.design_id+' '+(j.dominant_hex||'')+'</div>'+
@@ -6563,7 +6563,7 @@ document.getElementById('btn-compose').addEventListener('click', async function(
         '<div style="padding:10px 14px;background:#0f0e0c;color:#d2b15c;font-size:11px;letter-spacing:.08em;text-transform:uppercase">Final pattern · #'+j.design_id+'</div>'+
         '<div style="display:grid;grid-template-columns:minmax(0,1fr) 360px;gap:0;align-items:stretch">'+
           // Big pattern preview (left)
-          '<a href="/design/'+j.design_id+'" target="_blank" style="display:block;aspect-ratio:1;background:url(\\''+imgUrl+'\\') center/cover #f2efe8;text-decoration:none;cursor:zoom-in" title="Open detail page"></a>'+
+          '<a href="/design/'+j.design_id+'" target="_blank" style="display:block;aspect-ratio:1;background:url(\\''+imgUrl+'\\') center/cover #f2efe8;text-decoration:none;cursor:zoom-in" title="Open detail page" rel="noopener noreferrer"></a>'+
           // Sidebar (right) — prompt + actions
           '<div style="padding:18px;background:#fafafa;border-left:1px solid #eee;display:flex;flex-direction:column;gap:12px">'+
             '<div>'+
@@ -6572,7 +6572,7 @@ document.getElementById('btn-compose').addEventListener('click', async function(
             '</div>'+
             '<div id="studio-room-frame" style="border:1px solid #eee;border-radius:6px;aspect-ratio:4/3;background:#fafafa center/cover no-repeat;position:relative;display:flex;align-items:center;justify-content:center;color:#999;font-size:11px;overflow:hidden">Room setting loading…</div>'+
             '<div style="display:flex;flex-direction:column;gap:8px;margin-top:auto">'+
-              '<a href="/design/'+j.design_id+'" target="_blank" class="btn-primary" style="text-align:center">View design →</a>'+
+              '<a href="/design/'+j.design_id+'" target="_blank" class="btn-primary" style="text-align:center" rel="noopener noreferrer">View design →</a>'+
               '<a href="'+imgUrl+'" download class="btn-outline" style="text-align:center">Download pattern</a>'+
               '<button id="studio-delete-btn" data-design-id="'+j.design_id+'" class="btn-outline" style="color:#a44;border-color:#d99">Delete this render</button>'+
             '</div>'+
@@ -6728,7 +6728,7 @@ async function loadInsights(){
   var j = await get('/api/studio/category-insights?cat='+encodeURIComponent(cat));
   var div = document.getElementById('insights-body');
   var trendsHtml = '<div style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px">' + (j.trends||[]).map(function(t){
-    return '<a href="'+t.link+'" target="_blank" style="padding:4px 10px;background:#eef6ff;color:#1a5298;border-radius:11px;text-decoration:none;font-size:11px">📈 '+t.term+'</a>';
+    return '<a href="'+t.link+'" target="_blank" style="padding:4px 10px;background:#eef6ff;color:#1a5298;border-radius:11px;text-decoration:none;font-size:11px" rel="noopener noreferrer">📈 '+t.term+'</a>';
   }).join('') + '</div>';
   var bs = j.best_sellers || [];
   var bsHtml = bs.length ? '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(110px,1fr));gap:6px">' + bs.map(function(p){
@@ -6802,7 +6802,7 @@ async function loadMyRenders() {
       ? '<button onclick="restoreRender(' + r.design_id + ')" style="font-size:10px;padding:4px 8px;background:transparent;color:#5a8;border:1px solid #5a8;border-radius:4px;cursor:pointer">Restore</button>'
       : '<button onclick="deleteRender(' + r.design_id + ')" style="font-size:10px;padding:4px 8px;background:transparent;color:#a44;border:1px solid #d99;border-radius:4px;cursor:pointer">Delete</button>';
     return '<div style="border:1px solid #eee;border-radius:6px;overflow:hidden;background:#fff;position:relative;' + dim + '">' +
-      '<a href="/design/' + r.design_id + '" target="_blank" style="display:block;aspect-ratio:1;background:url(\\'' + img + '\\') center/cover #f2efe8;text-decoration:none;position:relative">' + roomBadge + '</a>' +
+      '<a href="/design/' + r.design_id + '" target="_blank" style="display:block;aspect-ratio:1;background:url(\\'' + img + '\\') center/cover #f2efe8;text-decoration:none;position:relative" rel="noopener noreferrer">' + roomBadge + '</a>' +
       '<div style="padding:6px 8px;font-size:10px;color:#666;display:flex;justify-content:space-between;align-items:center;gap:6px">' +
         '<span>#' + r.design_id + (r.deleted_at ? ' · deleted' : '') + '</span>' + delBtn +
       '</div>' +
@@ -7360,7 +7360,7 @@ ${htmlHeader('')}
         <div id="hon-title" style="font-family:'Cormorant Garamond',serif;font-weight:300;font-size:22px"></div>
         <div id="hon-sub"   style="font-size:11px;color:#888;text-transform:uppercase;letter-spacing:.06em;margin-top:3px"></div>
       </div>
-      <a id="hon-view" target="_blank" style="font-size:11px;color:#d2b15c;text-decoration:underline">view full &rarr;</a>
+      <a id="hon-view" target="_blank" style="font-size:11px;color:#d2b15c;text-decoration:underline" rel="noopener noreferrer">view full &rarr;</a>
     </div>
   </div>
   <div id="hon-buttons" style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;margin-top:18px">
@@ -7522,7 +7522,7 @@ function renderStack(){
     card.style.transform = 'translateY(' + (i * 6) + 'px) scale(' + (1 - i*0.03) + ')';
     card.innerHTML = '<img src="'+d.image_url+'" alt="'+d.title+'">' +
       '<div class="meta"><div><div class="title">'+d.title+'</div><div class="sub">'+(d.category||'')+'</div></div>' +
-      '<a href="/design/'+d.id+'" target="_blank" style="font-size:10px;color:#d2b15c;text-decoration:underline">view</a></div>' +
+      '<a href="/design/'+d.id+'" target="_blank" style="font-size:10px;color:#d2b15c;text-decoration:underline" rel="noopener noreferrer">view</a></div>' +
       '<div class="stamp stamp-love">LOVE</div><div class="stamp stamp-skip">SKIP</div><div class="stamp stamp-super">&#9733; SUPER</div>';
     s.appendChild(card);
     if (i === 0) attachDrag(card);

← 8d04faa wallco.ai · Last-Modified + 304 on /sitemap.xml + /robots.tx  ·  back to Wallco Ai  ·  wallco.ai · /api/designs/search?q=&limit= — lightweight in-m 7d5cb78 →