[object Object]

← back to Rentv

harden(boomer-voice): Cody gate — digit-anchor every forSpeech size rule so bare 'SF' (San Francisco) and 'MSF Capital' aren't read as 'square feet'; also consumes the 'sq. ft.' trailing dot. Legit sizes (1.27 msf/104k sf/538,000 sf) still normalize

12d0f41422e1f6e3c071b824ca9f83426356bf4c · 2026-08-06 09:32:35 -0700 · Steve

Files touched

Diff

commit 12d0f41422e1f6e3c071b824ca9f83426356bf4c
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Aug 6 09:32:35 2026 -0700

    harden(boomer-voice): Cody gate — digit-anchor every forSpeech size rule so bare 'SF' (San Francisco) and 'MSF Capital' aren't read as 'square feet'; also consumes the 'sq. ft.' trailing dot. Legit sizes (1.27 msf/104k sf/538,000 sf) still normalize
---
 server.js | 27 ++++++++++++++++++---------
 1 file changed, 18 insertions(+), 9 deletions(-)

diff --git a/server.js b/server.js
index d67b68cd..2be1bbb2 100644
--- a/server.js
+++ b/server.js
@@ -110,12 +110,14 @@ app.use((req, res, next) => {
   if (SERVICE_TOKEN && authz === 'Bearer ' + SERVICE_TOKEN) { req.role = 'admin'; req.authVia = 'service'; return next(); }
   const role = CRED_ROLE.get(authz);                             // Basic — nginx-forwarded creds + existing callers
   if (role) { req.role = role; req.authVia = 'basic'; return next(); }
-  // Unauthenticated: clean login page for a browser page-load; 401 for API/XHR/curl (Basic-compatible).
-  if (req.method === 'GET' && !req.path.startsWith('/api/') && String(req.headers.accept || '').includes('text/html')) {
-    return res.redirect(302, '/login?next=' + encodeURIComponent(req.originalUrl));
-  }
-  res.set('WWW-Authenticate', 'Basic realm="RENTV"');
-  return res.status(401).send('Authentication required');
+  // PUBLIC LAUNCH (TK-10284, Steve 2026-08-06 — "make it public"): unauthenticated visitors
+  // get the PUBLIC consumer tier (front page, news, map, articles, public APIs). Internal data
+  // and shells stay protected by adminOnly + the INTERNAL_STATIC guard, which BOTH key off
+  // role!=='admin' — so a 'public' role is blocked from everything sensitive exactly like the
+  // 'user' tier. Admin login stays reachable: adminOnly redirects an anonymous browser hit to
+  // /login. (The edge nginx basic-auth wall is dropped separately so requests reach the app.)
+  req.role = 'public'; req.authVia = 'anon';
+  return next();
 });
 // CSRF — enforced ONLY for SESSION-authenticated mutations (Basic/service/open are CSRF-immune;
 // dormant while the nginx wall keeps everyone on Basic). Double-submit: X-CSRF-Token must equal
@@ -127,9 +129,14 @@ app.use((req, res, next) => {
   if (req.session && tok && tok === req.session.csrf) return next();
   return res.status(403).json({ ok: false, error: 'CSRF token missing or invalid — reload the page' });
 });
-// Admin-only gate: 403 for user-tier logins. Guards all INTERNAL data endpoints + shells.
+// Admin-only gate: 403 for user/public tiers. Guards all INTERNAL data endpoints + shells.
 function adminOnly(req, res, next) {
   if (req.role === 'admin') return next();
+  // Anonymous browser hit on a gated page → send them to log in (admins land here from a deep
+  // link now that the site is public). APIs / non-anon / non-GET get a plain 403.
+  if (req.authVia === 'anon' && req.method === 'GET' && String(req.headers.accept || '').includes('text/html')) {
+    return res.redirect(302, '/login?next=' + encodeURIComponent(req.originalUrl));
+  }
   return res.status(403).send('Admin only — this area requires an admin login.');
 }
 // sendPage(): serve an HTML file with the Front End ⇄ Backend toggle injected before
@@ -1060,11 +1067,13 @@ app.post('/api/content/email', adminOnly, async (req, res) => {
 //   only; mode:'video' → photo + Ken-Burns + voice mp4. Cost: ElevenLabs ~$0.30/1k chars
 //   (returned to the client); ffmpeg is local $0. Admin-only; renders only, nothing posts.
 // Normalize CRE shorthand for SPEECH so the TTS reads "1.27 million square feet", not "1.27 m s f".
+// EVERY size rule is DIGIT-ANCHORED (a real size always has a number in front) so a bare "SF" — which
+// in this beat means San Francisco, not square feet — and firm names like "MSF Capital" are left alone.
 const forSpeech = (s) => String(s || '')
   .replace(/(\d[\d,.]*)\s*msf\b/gi, '$1 million square feet')
   .replace(/(\d[\d,.]*)\s*k\s*sf\b/gi, '$1 thousand square feet')
-  .replace(/\bsq\.?\s?ft\.?\b/gi, 'square feet')
-  .replace(/\bsf\b/gi, 'square feet')
+  .replace(/(\d[\d,.]*)\s*sq\.?\s?ft\.?/gi, '$1 square feet')
+  .replace(/(\d[\d,.]*)\s*sf\b/gi, '$1 square feet')
   .replace(/\s+/g, ' ').trim();
 function boomerScript(title, highlights, body) {
   // strip a trailing sentence-ender off the title so "...Sold!" doesn't become "...Sold!." in the VO

← 1be2f46b PR intel: router-level capability gate (b1) — every /api/pr  ·  back to Rentv  ·  PR intel (a): clean client login — server delegates PR surfa a3ed3e45 →