// Parse the URL params the bot widget host appends when loading // experience.url. Source of truth on the host side: // src/app/features/bots/BotWidgetEmbed.ts (getBotWidgetUrl). // Keep this in sync if the host adds params. export type WidgetBootstrap = { widgetId: string; parentUrl: string; parentOrigin: string; roomId: string; userId: string; botId: string; botMxid: string; /** Base URL of the bridge provisioning HTTP API (bridgev2 * `/_matrix/provision` mount behind the reverse proxy), e.g. * `https://vojo.chat/_provision/telegram`. Empty string when the host * config hasn't exposed it — the App renders a config-required notice * instead of booting the transport. */ provisioningUrl: string; theme: 'light' | 'dark'; clientLanguage: string; }; export type BootstrapResult = | { ok: true; bootstrap: WidgetBootstrap } | { ok: false; missing: string[] }; const REQUIRED = ['widgetId', 'parentUrl', 'roomId', 'userId', 'botMxid'] as const; export const readBootstrap = (search: string): BootstrapResult => { const params = new URLSearchParams(search); const get = (k: string) => params.get(k) ?? ''; const missing = REQUIRED.filter((k) => !params.get(k)); if (missing.length > 0) return { ok: false, missing: [...missing] }; // Origin is what the widget validates against on incoming postMessage — // see widget-api.ts. Falling back to '*' would defeat the security // boundary, so a malformed parentUrl bails out as a missing-param error. let parentOrigin: string; try { parentOrigin = new URL(get('parentUrl')).origin; } catch { return { ok: false, missing: ['parentUrl'] }; } // The host validator (catalog.ts normalizeProvisioningUrl) already // enforces https + no embedded credentials; re-parse defensively anyway // because this is the widget's fetch target. Malformed → '' → the App // shows the config-required notice rather than fetching a garbage URL. let provisioningUrl = ''; const rawProvisioning = get('provisioningUrl').trim(); if (rawProvisioning) { try { const parsed = new URL(rawProvisioning); if ( !parsed.username && !parsed.password && (parsed.protocol === 'https:' || (import.meta.env.DEV && parsed.protocol === 'http:')) ) { provisioningUrl = parsed.toString().replace(/\/+$/, ''); } } catch { /* keep '' */ } } const themeRaw = get('theme'); const theme: 'light' | 'dark' = themeRaw === 'dark' ? 'dark' : 'light'; return { ok: true, bootstrap: { widgetId: get('widgetId'), parentUrl: get('parentUrl'), parentOrigin, roomId: get('roomId'), userId: get('userId'), botId: get('botId'), botMxid: get('botMxid'), provisioningUrl, theme, clientLanguage: get('clientLanguage'), }, }; };