profile.md05-notifications.md07-trust-safety.mdJourney flow#
Generated from the authored steps below — click a node to jump to that section.
The BFF accepts authenticated Telegram webhook updates and produces honest,
audited BotResponse envelopes for commands, text, voice, inline queries, and
crisis interrupts. Grounded answers include disclosure, provenance, and source
buttons; save/quiet/stop/voice/unlink commands now perform real state changes.
The final channel-delivery join is still absent: the route returns response
instructions in JSON, and no code posts them to api.telegram.org.
Personas#
- Linked Telegram customer — starts linking from
/profile/telegramand asks a source-grounded question in the bot chat. - Voice-note customer — expects real transcription or an explicit unavailable reply, never a fabricated transcript.
- Command user — invokes read-only commands or durable
/save//quiet//stop//voice//unlinkeffects. - Customer in acute distress — needs a plain-language crisis interrupt before grounding or persona rendering.
- Operator/auditor — inspects
telegram.sendandtelegram.crisis-suppressionevents without stored response body text.
Pre-conditions#
- Telegram is configured with
OSHUN_TELEGRAM_WEBHOOK_SECRET; the route fails every webhook closed when the header does not exactly match. - The composition root injects
createBffSophiaRetriever(app). The fixture grounder is only the fallback when callers register the route without a retriever, such as focused local tests. - Real voice transcription additionally needs the bot token and STT API key.
Missing configuration installs a provider that throws
stt_not_configured. /profile/telegramcan mint a 15-minute nonce, and/start link-<nonce>can bind it. Login Widget verification separately binds the deterministiciris:<telegramUserId>identity.- Admin durable snapshots, when configured, retain Telegram captures, link state, voice preference, and stop flag; quiet hours live in the shared notification-preference store.
Steps#
1. Link the chat identity#
An authenticated profile read creates or reuses a one-time deep link. A valid
/start link-<nonce> webhook completes that specific profile link, while the
Login Widget path HMAC-verifies Telegram data and creates an
iris:<telegramUserId> binding. Expired or already-used nonces do not complete.
The ordinary webhook processing path does not look up that stored link before
answering. It derives the bot userId directly from Telegram from.id; the
effects adapter independently maps that id to iris:<id>.
2. Admit the inbound update#
POST /telegram/webhook validates x-telegram-bot-api-secret-token, reads the
optional x-tenant-id header (or uses oshun-default), and classifies message,
edited message, callback, inline query, or membership update. A process-local
limiter consumes one token for userId:chatId:update-kind; the thirteenth event
in a 12-per-60-second bucket receives retry-after copy without running Sophia.
Tenant context is not derived from the completed account binding. The webhook accepts the supplied tenant header/default and the injected retriever must enforce its source partition.
3. Execute command and callback branches#
Read-only /start, /menu, /today, /sources, and /help return fixed
copy. Mutating /save, /quiet, /stop, /voice, and /unlink go through
createTelegramEffectsPort:
- save writes an idempotent capture with provenance;
- quiet enables shared quiet hours;
- stop stores a durable delivery-stopped flag;
- voice toggles a durable preference;
- unlink revokes the stored Telegram link.
save: and quiet: callbacks use the same effects seam. These writes are real,
but there is no outbound sender in this route to consume the stop/voice
preferences when transmitting a reply.
4. Interrupt crisis language#
Typed text and successful voice transcripts are checked against the configured
phrase list before grounding. A match changes the audit persona to
plain-operator, returns emergency-resource copy and two trusted links, and
skips Sophia. This is a per-update interrupt; it does not set a durable
chat-level crisis frame or suppress later responses.
5. Transcribe or abstain on voice#
For a voice update, the provider resolves the Telegram file, downloads it, and
calls an OpenAI-compatible transcription endpoint. Provider failure, missing
credentials, or an empty transcript produces the exact “could not transcribe”
reply. Only a non-empty transcript reaches crisis detection and grounding.
Although sendVoice and synthesizeTelegramVoice exist in the type contract,
no code emits a TTS response.
6. Ground and render the answer#
The production grounder calls the injected Sophia retriever with text, raw
Telegram user id, tenant id, and current time. It requires at least one
citation, caps inline source buttons at six, and presents the result as
grounded. Retrieval error, zero citations, or renderer rejection becomes the
fixed abstention response with no fabricated source buttons.
renderGroundedTelegramAnswer accepts only grounded results with a source. It
appends the constant AI disclosure and model/confidence/source-count/time/
provenance footer. This is a Telegram-specific strictness boundary, not the web
Sophia UI's partial/ungrounded display behavior.
7. Persist the audit projection#
Each returned envelope is recorded as telegram.send, or
telegram.crisis-suppression for crisis copy. The payload includes tenant,
update/message/chat identifiers, response method, and the channel audit
envelope. It intentionally excludes the response body and raw source text. A
write failure is logged but does not fail the webhook response.
8. Return the hand-off envelope#
The HTTP response is { ok: true, responses, link? }. Each response may name
sendMessage, sendVoice, answerInlineQuery, or answerCallbackQuery, but
the BFF does not execute that method against Telegram. Dispatch, Telegram
receipt, retry, and externally observed delivery therefore remain unproven.
Post-conditions#
- A valid update yields zero or more source-grounded, command, abstention, or crisis response envelopes.
- Grounded envelopes carry verified disclosure/provenance and at most six citation buttons.
- Save/quiet/stop/voice/unlink effects mutate their real stores when requested.
- Audit rows are durable when the admin audit store is wired, without response text leakage.
- No proof exists that the envelope was sent, received, displayed, or retried by Telegram.
- The grounder receives a raw Telegram id and caller/default tenant rather than an account-binding-derived authorization context.
Failure modes#
- Invalid secret — returns
401 invalid-webhook-secret; no response or audit envelope is built. - Unresolved account/tenant — a sender can reach grounding without the
webhook verifying the stored profile link, and missing tenant headers become
oshun-default. - Rate-limit restart — buckets are process-local and reset on deploy; they are not a distributed abuse-control boundary.
- STT unavailable — voice returns honest text fallback and never grounds a fake transcript.
- Ungrounded retrieval — renderer/retriever failures abstain with HTTP 200, no citations, and no leaked candidate answer.
- Crisis phrase limits — substring matching covers only four configured English phrases and sets no continuing suppression state.
- Preference without sender —
/stopand/voicepersist, but no outbound Telegram transport is present to enforce or express them. - No external delivery — returning a
sendMessageinstruction can be mistaken for a delivered message unless operators inspect the boundary. - Audit sink failure — the route logs and still returns success, so a response envelope can exist without its audit projection.
E2E coverage#
apps/oshun/web/e2e/profile-telegram-bridge.spec.tsproves profile nonce mint/refresh/expiry, webhook link completion, and linked read-back.apps/oshun/web/e2e/telegram-binding-bff.spec.tscovers Login Widget HMAC binding and step-up classification.apps/oshun/web/e2e/telegram-webhook-delivery.spec.tsdrives real-BFF secret, limiter, commands, durable effects, crisis, text, fail-closed voice, abstention, inline/callback, and Postgres audit branches.- Coverage depth: partial. Webhook composition and state effects are deep. External Telegram download+STT needs credentials, and outbound Telegram delivery/TTS/receipt behavior has no implementation to automate.
Per-view files touched#
customer/09-account/profile.md— profile linking entry.shell/05-notifications.md— shared quiet hours and channel policy.shell/07-trust-safety.md— crisis interrupt and audit boundary.- This webhook has no Telegram DOM view in the web app; the final customer surface belongs to Telegram and is not rendered by Docs Center fixtures.
Cross-references#
messages-quiet-hours-and-channel-binding.md— broader channel binding and delivery boundaries.sophia-grounded-answer.md— web answer states and source UI.crisis-aware-tone-policy.md— broader crisis-frame behavior not joined here.- Sources:
apps/oshun/bff/src/routes/telegram.ts,apps/oshun/bff/src/telegram/webhook.ts,apps/oshun/bff/src/telegram/effects-adapter.ts, andlibs/oshun/messaging-channels/src/telegram/.
Open questions#
- Which delivery adapter executes each
BotResponse.method, honors stop/voice preferences, retries Telegram failures, and records provider receipts? - Must webhook grounding reject an unlinked sender, and how should the stored binding determine user, tenant, entitlement, memory, and source-set scope?
- Should the rate limiter and crisis frame move to shared durable state across BFF instances?
- Where should TTS synthesis and
sendVoiceselection run after/voiceis enabled? - How can operators distinguish “response composed” from “Telegram delivered” in telemetry and audit copy?