fix(routing): prevent domain stacking and remove Try Demo button

- Fix server-side redirect to always use canonical rspace.online as base
  domain instead of deriving from hostClean (prevents demo.rspace.rspace.online)
- Fix bare-domain check to exact match instead of .includes() (prevents
  stacked subdomains from triggering bare-domain routing)
- Fix client-side rspaceNavUrl to guard against reserved subdomain names
  being used as space subdomains (e.g. rspace.rspace.online)
- Fix identity component _navUrl with same canonical domain guards
- Remove "Try Demo" header button from all shell rendering functions
- Remove demo-btn CSS styles
- Fix pre-existing SchemaType error in Gemini tool declarations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-25 11:16:46 -07:00
parent dd46905e12
commit 22b5c00a13
5 changed files with 36 additions and 84 deletions

View File

@ -2029,7 +2029,7 @@ app.post("/api/trips/ai-prompt", async (c) => {
const geminiModel = genAI.getGenerativeModel({ const geminiModel = genAI.getGenerativeModel({
model: GEMINI_MODELS[model], model: GEMINI_MODELS[model],
tools: [{ functionDeclarations: allDeclarations }], tools: [{ functionDeclarations: allDeclarations as any }],
systemInstruction: systemPrompt || "You are a travel planning assistant.", systemInstruction: systemPrompt || "You are a travel planning assistant.",
}); });
@ -3284,7 +3284,8 @@ const server = Bun.serve<WSData>({
} }
// ── Bare-domain routing: rspace.online/{...} ── // ── Bare-domain routing: rspace.online/{...} ──
if (!subdomain && hostClean.includes("rspace.online")) { // Only match canonical bare domain, not stacked subdomains like rspace.rspace.online
if (!subdomain && (hostClean === "rspace.online" || hostClean === "www.rspace.online")) {
// Top-level routes that must bypass module rewriting // Top-level routes that must bypass module rewriting
if (url.pathname.startsWith("/rtasks/check/")) { if (url.pathname.startsWith("/rtasks/check/")) {
return app.fetch(req); return app.fetch(req);
@ -3349,9 +3350,8 @@ const server = Bun.serve<WSData>({
// Page navigation: redirect to canonical subdomain URL // Page navigation: redirect to canonical subdomain URL
const space = firstSegment; const space = firstSegment;
const rest = "/" + pathSegments.slice(1).join("/"); const rest = "/" + pathSegments.slice(1).join("/");
const baseDomain = hostClean.replace(/^www\./, "");
return Response.redirect( return Response.redirect(
`${proto}//${space}.${baseDomain}${rest}${url.search}`, 301 `${proto}//${space}.rspace.online${rest}${url.search}`, 301
); );
} }
} }

View File

@ -173,7 +173,6 @@ export function renderShell(opts: ShellOptions): string {
...m, ...m,
enabled: !enabledModules || enabledModules.includes(m.id) || m.id === "rspace", enabled: !enabledModules || enabledModules.includes(m.id) || m.id === "rspace",
}))); })));
const shellDemoUrl = `https://demo.rspace.online/${escapeAttr(moduleId)}`;
return versionAssetUrls(`<!DOCTYPE html> return versionAssetUrls(`<!DOCTYPE html>
<html lang="en"> <html lang="en">
@ -241,7 +240,6 @@ export function renderShell(opts: ShellOptions): string {
<rstack-mi></rstack-mi> <rstack-mi></rstack-mi>
</div> </div>
<div class="rstack-header__right"> <div class="rstack-header__right">
<a class="rstack-header__demo-btn" ${spaceSlug === "demo" ? 'data-hide' : ''} href="${shellDemoUrl}">Try Demo</a>
<rstack-offline-indicator></rstack-offline-indicator> <rstack-offline-indicator></rstack-offline-indicator>
<rstack-comment-bell></rstack-comment-bell> <rstack-comment-bell></rstack-comment-bell>
<rstack-notification-bell></rstack-notification-bell> <rstack-notification-bell></rstack-notification-bell>
@ -439,23 +437,6 @@ export function renderShell(opts: ShellOptions): string {
_switcher?.setModules(window.__rspaceModuleList); _switcher?.setModules(window.__rspaceModuleList);
_switcher?.setAllModules(window.__rspaceAllModules); _switcher?.setAllModules(window.__rspaceAllModules);
// ── "Try Demo" button visibility ──
// Hidden when logged in. When logged out, shown everywhere except demo.rspace.online
// (bare rspace.online rewrites to demo internally but still shows the button).
(function() {
var btn = document.querySelector('.rstack-header__demo-btn');
if (!btn) return;
function update() {
var loggedIn = false;
try { loggedIn = !!localStorage.getItem('encryptid_session'); } catch(e) {}
if (loggedIn) { btn.setAttribute('data-hide', ''); return; }
var host = window.location.host.split(':')[0];
if (host === 'demo.rspace.online') { btn.setAttribute('data-hide', ''); }
else { btn.removeAttribute('data-hide'); }
}
update();
document.addEventListener('auth-change', update);
})();
// ── Welcome tour (guided feature walkthrough for first-time visitors) ── // ── Welcome tour (guided feature walkthrough for first-time visitors) ──
(function() { (function() {
@ -2065,7 +2046,6 @@ export function renderModuleLanding(opts: ModuleLandingOptions): string {
<a href="/" style="display:flex;align-items:center;margin-right:4px"><img src="/favicon.png" alt="rSpace" class="rstack-header__logo"></a> <a href="/" style="display:flex;align-items:center;margin-right:4px"><img src="/favicon.png" alt="rSpace" class="rstack-header__logo"></a>
<rstack-app-switcher current="${escapeAttr(mod.id)}"></rstack-app-switcher> <rstack-app-switcher current="${escapeAttr(mod.id)}"></rstack-app-switcher>
<rstack-space-switcher current="" name="Spaces"></rstack-space-switcher> <rstack-space-switcher current="" name="Spaces"></rstack-space-switcher>
<a class="rstack-header__demo-btn" href="${demoUrl}">Try Demo</a>
</div> </div>
<div class="rstack-header__center"> <div class="rstack-header__center">
<rstack-mi></rstack-mi> <rstack-mi></rstack-mi>
@ -2082,17 +2062,6 @@ export function renderModuleLanding(opts: ModuleLandingOptions): string {
navigator.serviceWorker.register("/sw.js").catch(() => {}); navigator.serviceWorker.register("/sw.js").catch(() => {});
} }
document.querySelector('rstack-app-switcher')?.setModules(${moduleListJSON}); document.querySelector('rstack-app-switcher')?.setModules(${moduleListJSON});
function _updateDemoBtn() {
var btn = document.querySelector('.rstack-header__demo-btn');
if (!btn) return;
try {
var raw = localStorage.getItem('encryptid_session');
if (raw && JSON.parse(raw)?.accessToken) { btn.setAttribute('data-hide', ''); }
else { btn.removeAttribute('data-hide'); }
} catch(e) {}
}
_updateDemoBtn();
document.addEventListener('auth-change', _updateDemoBtn);
try { try {
var raw = localStorage.getItem('encryptid_session'); var raw = localStorage.getItem('encryptid_session');
if (raw) { if (raw) {
@ -2412,7 +2381,6 @@ export function renderSubPageInfo(opts: SubPageInfoOptions): string {
<a href="/" style="display:flex;align-items:center;margin-right:4px"><img src="/favicon.png" alt="rSpace" class="rstack-header__logo"></a> <a href="/" style="display:flex;align-items:center;margin-right:4px"><img src="/favicon.png" alt="rSpace" class="rstack-header__logo"></a>
<rstack-app-switcher current="${escapeAttr(mod.id)}"></rstack-app-switcher> <rstack-app-switcher current="${escapeAttr(mod.id)}"></rstack-app-switcher>
<rstack-space-switcher current="" name="Spaces"></rstack-space-switcher> <rstack-space-switcher current="" name="Spaces"></rstack-space-switcher>
<a class="rstack-header__demo-btn" href="${demoUrl}">Try Demo</a>
</div> </div>
<div class="rstack-header__center"> <div class="rstack-header__center">
<rstack-mi></rstack-mi> <rstack-mi></rstack-mi>
@ -2429,17 +2397,6 @@ export function renderSubPageInfo(opts: SubPageInfoOptions): string {
navigator.serviceWorker.register("/sw.js").catch(() => {}); navigator.serviceWorker.register("/sw.js").catch(() => {});
} }
document.querySelector('rstack-app-switcher')?.setModules(${moduleListJSON}); document.querySelector('rstack-app-switcher')?.setModules(${moduleListJSON});
function _updateDemoBtn() {
var btn = document.querySelector('.rstack-header__demo-btn');
if (!btn) return;
try {
var raw = localStorage.getItem('encryptid_session');
if (raw && JSON.parse(raw)?.accessToken) { btn.setAttribute('data-hide', ''); }
else { btn.removeAttribute('data-hide'); }
} catch(e) {}
}
_updateDemoBtn();
document.addEventListener('auth-change', _updateDemoBtn);
try { try {
var raw = localStorage.getItem('encryptid_session'); var raw = localStorage.getItem('encryptid_session');
if (raw) { if (raw) {

View File

@ -333,13 +333,18 @@ function _getCurrentModule(): string {
} }
function _navUrl(space: string, moduleId: string): string { function _navUrl(space: string, moduleId: string): string {
const h = window.location.host.split(":")[0].split("."); const h = window.location.host.split(":")[0].split(".");
const onSub = h.length >= 3 && h.slice(-2).join(".") === "rspace.online" && !_RESERVED.includes(h[0]); const proto = window.location.protocol;
const BASE = "rspace.online";
const onSub = h.length >= 3 && h.slice(-2).join(".") === BASE && !_RESERVED.includes(h[0]);
if (onSub) { if (onSub) {
if (h[0] === space) return "/" + moduleId; if (h[0] === space) return "/" + moduleId;
return window.location.protocol + "//" + space + "." + h.slice(-2).join(".") + "/" + moduleId; if (_RESERVED.includes(space)) return proto + "//" + BASE + "/" + moduleId;
return proto + "//" + space + "." + BASE + "/" + moduleId;
} }
if (window.location.host.includes("rspace.online") && !window.location.host.startsWith("www")) { const host = window.location.host.split(":")[0];
return window.location.protocol + "//" + space + ".rspace.online/" + moduleId; if (host === BASE || host === "www." + BASE || host.endsWith("." + BASE)) {
if (space === "demo" || _RESERVED.includes(space)) return "/" + moduleId;
return proto + "//" + space + "." + BASE + "/" + moduleId;
} }
return "/" + space + "/" + moduleId; return "/" + space + "/" + moduleId;
} }

View File

@ -68,18 +68,24 @@ export function getCurrentModule(): string {
* On localhost: uses /{space}/{moduleId}. * On localhost: uses /{space}/{moduleId}.
*/ */
export function rspaceNavUrl(space: string, moduleId: string): string { export function rspaceNavUrl(space: string, moduleId: string): string {
const hostParts = window.location.host.split(":")[0].split("."); const host = window.location.host.split(":")[0];
const hostParts = host.split(".");
const proto = window.location.protocol;
// Always use canonical base domain to prevent stacking (e.g. rspace.rspace.online)
const BASE = "rspace.online";
const onSubdomain = const onSubdomain =
hostParts.length >= 3 && hostParts.length >= 3 &&
hostParts.slice(-2).join(".") === "rspace.online" && hostParts.slice(-2).join(".") === BASE &&
!RESERVED_SUBDOMAINS.includes(hostParts[0]); !RESERVED_SUBDOMAINS.includes(hostParts[0]);
// Standalone r*.online domains → redirect to rspace.online for navigation // Standalone r*.online domains → redirect to rspace.online for navigation
if (isStandaloneDomain()) { if (isStandaloneDomain()) {
if (space === "demo") { if (space === "demo") {
return `${window.location.protocol}//demo.rspace.online/${moduleId}`; return `${proto}//demo.${BASE}/${moduleId}`;
} }
return `${window.location.protocol}//${space}.rspace.online/${moduleId}`; return `${proto}//${space}.${BASE}/${moduleId}`;
} }
if (onSubdomain) { if (onSubdomain) {
@ -87,19 +93,26 @@ export function rspaceNavUrl(space: string, moduleId: string): string {
if (hostParts[0] === space) { if (hostParts[0] === space) {
return `/${moduleId}`; return `/${moduleId}`;
} }
// Different space → switch subdomain // Guard: reserved words can't be subdomains — treat as demo
const baseDomain = hostParts.slice(-2).join("."); if (RESERVED_SUBDOMAINS.includes(space)) {
return `${window.location.protocol}//${space}.${baseDomain}/${moduleId}`; return `${proto}//${BASE}/${moduleId}`;
}
// Different space → switch subdomain (always use canonical base)
return `${proto}//${space}.${BASE}/${moduleId}`;
} }
// Bare domain (rspace.online) // Bare domain (rspace.online) or any non-standard rspace.online host
if (isBareDomain()) { if (isBareDomain() || host.endsWith(`.${BASE}`)) {
// Default space → stay on bare domain: /{moduleId} // Default space → stay on bare domain: /{moduleId}
if (space === "demo") { if (space === "demo") {
return `/${moduleId}`; return `/${moduleId}`;
} }
// Guard: reserved words can't be subdomains
if (RESERVED_SUBDOMAINS.includes(space)) {
return `/${moduleId}`;
}
// Explicit space → switch to subdomain // Explicit space → switch to subdomain
return `${window.location.protocol}//${space}.rspace.online/${moduleId}`; return `${proto}//${space}.${BASE}/${moduleId}`;
} }
// Localhost/dev // Localhost/dev

View File

@ -62,25 +62,6 @@ body {
z-index: 2; z-index: 2;
} }
.rstack-header__demo-btn {
display: inline-flex;
align-items: center;
padding: 5px 14px;
border-radius: 6px;
font-size: 0.78rem;
font-weight: 600;
text-decoration: none;
white-space: nowrap;
transition: background 0.15s, opacity 0.15s;
background: var(--rs-gradient-cta);
color: #fff;
box-shadow: 0 1px 4px rgba(20, 184, 166, 0.25);
}
.rstack-header__demo-btn:hover {
opacity: 0.88;
}
/* Hide the demo button when already on demo space */
.rstack-header__demo-btn[data-hide] { display: none; }
.rstack-header__logo { .rstack-header__logo {
width: 28px; width: 28px;
@ -403,10 +384,6 @@ body.rstack-sidebar-open #toolbar {
gap: 6px; gap: 6px;
margin-left: auto; margin-left: auto;
} }
.rstack-header__demo-btn {
padding: 4px 10px;
font-size: 0.72rem;
}
.rstack-header__brand { .rstack-header__brand {
font-size: 1rem; font-size: 1rem;
gap: 6px; gap: 6px;