← back to Rentv 2026
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 2026
·
PR intel (a): clean client login — server delegates PR surfa a3ed3e45 →