vojo/apps/widget-telegram/src/bootstrap.ts

86 lines
2.8 KiB
TypeScript

// 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'),
},
};
};