Merge pull request 'preview demo: drop a stored login for another server before the app reads it' (#80) from void/demo-homeserver-unreachable into dev
Some checks failed
Rat dev preview / Build dev preview (push) Has been cancelled

This commit is contained in:
Void 2026-11-10 18:24:00 +00:00
commit 84770a2d2c
4 changed files with 103 additions and 35 deletions

View file

@ -40,17 +40,17 @@ export function setFallbackSession(
localStorage.setItem('cinny_user_id', userId);
localStorage.setItem('cinny_hs_base_url', baseUrl);
}
export const removeFallbackSession = () => {
localStorage.removeItem('cinny_hs_base_url');
localStorage.removeItem('cinny_user_id');
localStorage.removeItem('cinny_device_id');
localStorage.removeItem('cinny_access_token');
export const removeFallbackSession = (storage: Storage = localStorage) => {
storage.removeItem('cinny_hs_base_url');
storage.removeItem('cinny_user_id');
storage.removeItem('cinny_device_id');
storage.removeItem('cinny_access_token');
};
export const getFallbackSession = (): Session | undefined => {
const baseUrl = localStorage.getItem('cinny_hs_base_url');
const userId = localStorage.getItem('cinny_user_id');
const deviceId = localStorage.getItem('cinny_device_id');
const accessToken = localStorage.getItem('cinny_access_token');
export const getFallbackSession = (storage: Storage = localStorage): Session | undefined => {
const baseUrl = storage.getItem('cinny_hs_base_url');
const userId = storage.getItem('cinny_user_id');
const deviceId = storage.getItem('cinny_device_id');
const accessToken = storage.getItem('cinny_access_token');
if (baseUrl && userId && deviceId && accessToken) {
const session: Session = {

View file

@ -30,29 +30,32 @@ import { getFallbackSession } from './app/state/sessions';
document.body.classList.add(configClass, varsClass);
// Register Service Worker
if ('serviceWorker' in navigator) {
const swUrl =
import.meta.env.MODE === 'production'
? `${trimTrailingSlash(import.meta.env.BASE_URL)}/sw.js`
: `/dev-sw.js?dev-sw`;
// Register Service Worker. After the preview demo has settled the stored session: the worker is
// handed that session, and the demo may drop one it cannot serve.
const registerServiceWorker = () => {
if ('serviceWorker' in navigator) {
const swUrl =
import.meta.env.MODE === 'production'
? `${trimTrailingSlash(import.meta.env.BASE_URL)}/sw.js`
: `/dev-sw.js?dev-sw`;
const sendSessionToSW = () => {
const session = getFallbackSession();
pushSessionToSW(session?.baseUrl, session?.accessToken);
};
const sendSessionToSW = () => {
const session = getFallbackSession();
pushSessionToSW(session?.baseUrl, session?.accessToken);
};
navigator.serviceWorker.register(swUrl).then(sendSessionToSW);
navigator.serviceWorker.ready.then(sendSessionToSW);
navigator.serviceWorker.register(swUrl).then(sendSessionToSW);
navigator.serviceWorker.ready.then(sendSessionToSW);
navigator.serviceWorker.addEventListener('message', (ev) => {
const { type } = ev.data ?? {};
navigator.serviceWorker.addEventListener('message', (ev) => {
const { type } = ev.data ?? {};
if (type === 'requestSession') {
sendSessionToSW();
}
});
}
if (type === 'requestSession') {
sendSessionToSW();
}
});
}
};
// The pretend homeserver of the dev preview: built in only with VITE_PREVIEW_DEMO=1 (the dev
// workflow) and switched on by `previewDemo: true` in config.json. Any other build drops it.
@ -76,4 +79,7 @@ const mountApp = () => {
root.render(<App />);
};
installPreviewDemo().finally(mountApp);
installPreviewDemo().finally(() => {
registerServiceWorker();
mountApp();
});

View file

@ -4,6 +4,7 @@
// only by a build with VITE_PREVIEW_DEMO=1 whose config.json says previewDemo: true (src/index.tsx).
// Own-origin requests (the app's static files) pass; the demo origin is answered in-page; any
// other origin is refused, so no real homeserver can be contacted while this is on.
import { removeFallbackSession } from '../app/state/sessions';
import { DEMO_ORIGIN } from './fixtures';
import { createDemoServer, DemoServer } from './server';
@ -27,8 +28,6 @@ const refusal = (what: string, url: string) =>
new Error(`preview: pretend server only; ${what} to ${url} refused`);
const BANNER_TEXT = 'preview: pretend server. Any password works.';
const FOREIGN_SESSION_TEXT =
'preview: pretend server. A login for another server is stored here and cannot be used.';
// In flow, above the app: body becomes a column and the app root takes the rest, so the banner
// never covers the room header or the connection strip.
@ -78,7 +77,16 @@ export const guardedFetch =
});
};
// Every preview of this site shares one origin, so one localStorage: a login another preview left
// (a real homeserver) would make the app connect to that server, which the guard refuses. The
// demo has one server, so a stored login for any other is dropped before the app reads it.
export const dropForeignSession = (storage: Storage) => {
const base = storage.getItem('cinny_hs_base_url');
if (base !== null && base !== DEMO_ORIGIN) removeFallbackSession(storage);
};
export const installDemo = (win: Window & typeof globalThis = window) => {
dropForeignSession(win.localStorage);
const server = createDemoServer();
const { origin } = win.location;
const realFetch = win.fetch.bind(win);
@ -111,7 +119,6 @@ export const installDemo = (win: Window & typeof globalThis = window) => {
routeOf(String(url), origin) === 'own' ? realBeacon(url, data) : false;
}
const stored = win.localStorage.getItem('cinny_hs_base_url');
showBanner(stored !== null && stored !== DEMO_ORIGIN ? FOREIGN_SESSION_TEXT : BANNER_TEXT);
showBanner(BANNER_TEXT);
return server;
};

View file

@ -2,9 +2,10 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { ClientEvent, MatrixClient, SyncState, createClient } from 'matrix-js-sdk';
import { DEMO_ORIGIN, ME, ROOMS, GENERAL, COMMONS } from './fixtures';
import { guardedFetch, installDemo, routeOf } from './install';
import { dropForeignSession, guardedFetch, installDemo, routeOf } from './install';
import { createDemoServer, DemoServer } from './server';
import { seedDemoSettings } from './seed';
import { getFallbackSession } from '../app/state/sessions';
import { mindOfSender, POSE_KEY, resolvePose } from '../app/components/message/pose/poses';
import { parseRoomNotice, roomTrainingLine } from '../app/utils/trainingNotice';
@ -233,6 +234,7 @@ describe('the guard', () => {
localStorage: {
getItem: (k: string) => store.get(k) ?? null,
setItem: (k: string, v: string) => void store.set(k, v),
removeItem: (k: string) => void store.delete(k),
},
} as unknown as Window & typeof globalThis;
vi.stubGlobal('document', {
@ -254,6 +256,59 @@ describe('the guard', () => {
vi.unstubAllGlobals();
});
it('drops a stored login for a real homeserver before the app reads it, keeps the demo one', () => {
const SESSION = {
cinny_access_token: 'tok',
cinny_device_id: 'DEV',
cinny_user_id: '@a:b',
cinny_hs_base_url: 'https://chat.gamecult.org',
};
const store = new Map<string, string>(Object.entries({ ...SESSION, settings: '{}' }));
const storage = {
getItem: (k: string) => store.get(k) ?? null,
setItem: (k: string, v: string) => void store.set(k, v),
removeItem: (k: string) => void store.delete(k),
} as Storage;
dropForeignSession(storage);
expect(getFallbackSession(storage)).toBeUndefined();
expect([...store.keys()]).toEqual(['settings']);
Object.entries({ ...SESSION, cinny_hs_base_url: DEMO_ORIGIN }).forEach(([k, v]) =>
store.set(k, v)
);
dropForeignSession(storage);
expect(getFallbackSession(storage)?.baseUrl).toBe(DEMO_ORIGIN);
});
it('installDemo clears a foreign login so the app starts at the login screen', () => {
const store = new Map<string, string>([
['cinny_hs_base_url', 'https://chat.gamecult.org'],
['cinny_access_token', 'tok'],
['cinny_device_id', 'DEV'],
['cinny_user_id', '@a:b'],
]);
const win = {
location: { origin: PAGE },
fetch: never,
WebSocket: class {},
XMLHttpRequest: { prototype: { open: vi.fn() } },
navigator: {},
localStorage: {
getItem: (k: string) => store.get(k) ?? null,
removeItem: (k: string) => void store.delete(k),
},
} as unknown as Window & typeof globalThis;
vi.stubGlobal('document', {
createElement: () => ({ style: {}, setAttribute: vi.fn() }),
getElementById: () => null,
body: { style: {}, insertBefore: vi.fn() },
});
const server = installDemo(win);
live.push({ server, mx: clientFor(server) });
expect(store.size).toBe(0);
vi.unstubAllGlobals();
});
it('seeds pose portraits on for a first visit and keeps what a visitor already chose', () => {
const fresh = new Map<string, string>();
seedDemoSettings({