<!-- SOLID CITY · part 8 -->
ring(req.ip || '').slice(0, 64), 'x-see-pilot': b64(ctx), 'x-see-key': str(req.query.key) } }, u => {
    try { if (u.headers['x-see-blocked'] === '1') pilots.blocked(a); if (u.headers['x-see-near']) pilots.setNear(a, JSON.parse(Buffer.from(String(u.headers['x-see-near']), 'base64').toString('utf8'))); } catch (e) {}
    const h = { 'Cache-Control': 'no-store', 'Referrer-Policy': 'no-referrer' }; ['content-type', 'retry-after', 'x-content-type-options', 'x-robots-tag'].forEach(k => { if (u.headers[k]) h[k] = u.headers[k]; });
    res.status(r.err && u.statusCode === 200 ? (r.code || 400) : (u.statusCode || 502)).set(h); u.pipe(res);
  });
  up.on('timeout', () => up.destroy(new Error('timeout')));
  up.on('error', () => { if (!res.headersSent) nope(503, 'The city viewer is starting or busy; refresh in 15 s. (Your action was ' + (r.err ? 'not done: ' + r.err : 'done: ' + (r.msg || 'none')) + ')'); else res.end(); });
  up.end();
}
/* THE SEAL (the King's word, 2026-10-14): Solid City is locked down;
 * the spectator window closes with the city. The King's session alone
 * (user id 6 — the Royal Rights standard) may still look in. */
router.use(['/city/see', '/city/see.txt', '/city/see.png', '/city/see.jpg'], (req, res, next) => {
  const uid = req.session && req.session.userId;
  if (Number(uid) === 6) return next();
  const isTxt = /\.txt$/.test(req.path);
  return res.status(403).set('Cache-Control', 'no-store').type(isTxt ? 'text/plain' : 'text/html')
    .send(isTxt ? 'Solid City is sealed by the King\u2019s word.\n' : '<!doctype html><meta charset="utf-8"><title>Solid City — Sealed</title><body style="background:#0b0507;color:#f1e4c3;font:16px system-ui;padding:16px"><p>Solid City is sealed by the King\u2019s word.</p></body>');
});
router.get(['/city/see', '/city/see.txt', '/city/see.png', '/city/see.jpg'], (req, res, next) => {
  if (req.query.as != null || req.query.key != null) return pilotView(req, res).catch(e => { if (!res.headersSent) res.status(500).type('text/plain').set('Cache-Control', 'no-store').send('pilot error; refresh in a moment\n'); });
  next();
});
router.get(['/city/see', '/city/see.txt', '/city/see.png', '/city/see.jpg'], (req, res) => {
  // a whitelisted key with a value that is not a short [a-z0-9-] word (a URL, a path...) is refused, never passed on
  for (const k of KEYS) { const v = req.query[k]; if (v != null && (typeof v !== 'string' || !/^-?[a-z0-9-]{1,32}$/i.test(v))) return res.status(400).set('Cache-Control', 'no-store').type(/\.txt$/.test(req.path) ? 'text/plain' : 'text/html').send(/\.txt$/.test(req.path) ? 'Not a place in Solid City. Start here: https://epistles.xyz/city/see.txt\n' : '<!doctype html><meta charset="utf-8"><title>Solid City view</title><p>Not a place in Solid City. <a href="/city/see">Start at the Forum Plaza</a> \u00b7 <a href="/city/">the live 3D city</a></p>'); }
  const path = req.path + cleanQuery(req.query);
  const up = http.request({ host: '127.0.0.1', port: PORT, path, method: 'GET', timeout: 30000, headers: { 'x-see-ip': String(req.ip || '').slice(0, 64) } }, r => {
    const h = {}; ['content-type', 'cache-control', 'retry-after', 'x-content-type-options', 'x-robots-tag', 'x-see-stale'].forEach(k => { if (r.headers[k]) h[k] = r.headers[k]; });
    res.status(r.statusCode || 502).set(h); r.pipe(res);
  });
  up.on('timeout', () => up.destroy(new Error('timeout')));
  up.on('error', e => { if (res.headersSent) return res.end(); res.status(503).set({ 'Retry-After': '15', 'Cache-Control': 'no-store' }).type(/\.txt$/.test(req.path) ? 'text/plain' : 'text/html')
    .send(/\.txt$/.test(req.path) ? 'The city viewer is starting or busy; try again in 15 seconds.\n' : '<!doctype html><meta charset="utf-8"><title>Solid City view</title><p>The city viewer is starting or busy; try again in 15 seconds. <a href="/city/see">Retry</a> \u00b7 <a href="/city/">the live 3D city</a></p>'); });
  up.end();
});
/* /build-for-ai: the public guide 'How to let AI see and play your world' (plain HTML, no JS) + /build-for-ai.md */
const PUB = require('path').join(__dirname, '..', 'public');
router.get(['/build-for-ai', '/build-for-ai/'], (req, res) => res.set('Cache-Control', 'public, max-age=300').sendFile(require('path').join(PUB, 'build-for-ai', 'index.html')));
router.get('/build-for-ai.md', (req, res) => res.set('Cache-Control', 'public, max-age=300').type('text/markdown; charset=utf-8').sendFile(require('path').join(PUB, 'build-for-ai.md')));
module.exports = router;


===== FILE: lib/city-space.js =====
'use strict';
/* lib/city-space.js — SOLID CITY · SPACE MISSION leaderboard (2026-10-10, the King's space level at /city/space/).
   The game runs entirely in the browser; this is only the small public result board (no login, no cookies used):
     GET  /api/city/space/board   -> { ok, total, aliens, top: [{ name, timeMs, kills, at }] }   best 10: most kills, then fastest
     POST /api/city/space/score   { name, timeMs, kills, 