| .. | ||
| src | ||
| .gitignore | ||
| index.html | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
| vite.config.ts | ||
@vojo/widget-telegram
Vojo Telegram bridge management widget — mounts inside /bots/telegram
in the Vojo client.
This is not a Telegram client. It's a control panel for the
mautrix-telegram bridge that talks to the bridge's provisioning HTTP
API (bridgev2 /_matrix/provision/v3/*, exposed by Caddy at
https://vojo.chat/_provision/telegram). It signs the user in
(phone+code+2FA or QR), shows the linked account, lists Telegram
contacts, resolves @usernames / +phones, and creates DM portals on
demand.
Auth: the widget requests MSC1960 OpenID credentials from the host
(get_openid over the widget API; granted by BotWidgetDriver.askOpenID
when config.json opts the bot into the vojo.openid capability) and
sends them to the bridge as Authorization: Bearer openid:<token>. The
OpenID token only proves identity — it is not a Matrix access token.
There is no bot text-command transport and no reply parsing: the legacy
!tg-command dialect (bridge-protocol/) was deleted when the bridge
API became reachable. The bot control DM still exists (BotShell needs a
room and the «Show chat» fallback), the widget just doesn't read or
write it — it requests zero MSC2762 capabilities.
Layout
src/
├── bootstrap.ts Parse URL params the host appends (matches BotWidgetEmbed.ts)
├── widget-api.ts Inline matrix-widget-api postMessage transport: handshake,
│ theme, MSC1960 get_openid, io.vojo.bot-widget verbs
├── provisioning.ts Typed client for the bridgev2 provisioning API + identifier
│ helpers (wire contract documented in the file header)
├── errors.ts Bridge/Telegram error → localized copy mapping
├── login.tsx Login flow over the v3 step machine + forms (phone/code/
│ password/QR with long-poll rotation)
├── contacts.tsx Contacts list, search-as-filter, resolve-probe, create DM
├── App.tsx Shell: boot/disconnected/connected phases, tabs, account
├── ui.tsx Icons, initials avatar, command cards, notices
├── main.tsx Entry: init bootstrap, render App or diagnostic
└── styles.css Theme-aware CSS (Dawn palette, light remap via data-theme)
Local development
Don't touch the committed config.json. Create config.local.json at
the project root once — gitignored, never deployed. The host's Vite dev
server overlays it on top of /config.json responses (see
serveLocalConfigOverlay in vite.config.js); prod builds ignore the
overlay entirely.
Note the overlay merges bot entries shallowly — your local
experience object replaces the base one wholesale, so it must carry
provisioningUrl and capabilities too:
# one-time: install widget deps
cd apps/widget-telegram && npm install
# one-time: create config.local.json (gitignored) at the project root
cat > /home/ubuntu/projects/vojo/cinny/config.local.json <<'JSON'
{
"bots": [
{
"id": "telegram",
"experience": {
"type": "matrix-widget",
"url": "http://localhost:8081/",
"provisioningUrl": "https://vojo.chat/_provision/telegram",
"capabilities": ["vojo.openid"]
}
}
]
}
JSON
Run both servers:
# terminal 1 — widget on :8081 with HMR
cd apps/widget-telegram && npm run dev
# terminal 2 — host SPA on :8080
cd /home/ubuntu/projects/vojo/cinny && npm start
Open http://localhost:8080/bots/telegram. Iframe loads cross-origin
from the widget dev server, HMR works, no proxy. The provisioning calls
go straight to the prod bridge API (CORS * + per-request bearer auth),
acting on whatever account you're signed in with — same trust model as
the dev client talking to the prod homeserver.
http://localhost:* URLs are accepted by the host's URL validator only
in dev builds (import.meta.env.DEV branch in
src/app/features/bots/catalog.ts); production builds drop the branch
via Vite's dead-code elimination, AND production-only enforces an origin
allowlist (PROD_WIDGET_ORIGINS) so prod can never embed localhost even
if config.json is poisoned.
Standalone preview of the widget bundle (no host, useful for visual iteration):
cd apps/widget-telegram
npm run dev # vite dev server on :8081 — shows missing-params banner
# without host, expected.
npm run preview # serves the production build from dist/
Build
npm run build
Outputs to apps/widget-telegram/dist/. Deploy by rsyncing dist/*
into ~/vojo/widgets/telegram/ on the production host (Caddy serves
this via the widgets.vojo.chat block).
Server-side requirements
-
The widget static files at
widgets.vojo.chat/telegram/(Caddyhandle_path /telegram/*block — see git history of this README for the full runbook). -
The bridge provisioning API exposed at the URL configured in config.json
experience.provisioningUrl. Caddy block inside thevojo.chatsite:handle /_provision/telegram/* { uri replace /_provision/telegram /_matrix/provision reverse_proxy telegram-bridge:29317 # host:port from appservice.address }Path-scoped on purpose: the same bridge listener serves the appservice transaction endpoints (
/_matrix/app/*), which must stay internal. -
provisioning.shared_secretin the bridge config must NOT bedisable(a ≥16-char secret enables the API; the widget never sees the secret — it authenticates with per-user OpenID tokens).
Updating the production /config.json
"experience": {
"type": "matrix-widget",
"url": "https://widgets.vojo.chat/telegram/index.html",
"provisioningUrl": "https://vojo.chat/_provision/telegram",
"capabilities": ["vojo.openid"]
}
Capacitor (Android)
capacitor.config.ts already has a placeholder. Uncomment and set:
server: { allowNavigation: ['widgets.vojo.chat'] }
Without this, Android's WebView hijacks the cross-origin iframe URL into
Intent.ACTION_VIEW and the iframe stays blank. Rebuild the APK after.
Capability contract
The widget requests no MSC2762 capabilities — the handshake replies with an empty list. The only privileged surfaces are:
- MSC1960
get_openid— granted byBotWidgetDriver.askOpenIDiff config.json declares"capabilities": ["vojo.openid"]for this bot; io.vojo.bot-widgetside-channel verbsopen-external-urlandopen-matrix-to(origin-pinned and validated host-side inBotWidgetEmbed.onWidgetMessage).