Make the Telegram API base URL settable, and close what that opens - #45
Merged
Conversation
The bot API host was fixed at api.telegram.org, reachable only through an unexported field the package's own tests set. Two consequences: an operator behind a proxy had no way in, and the Telegram notifier was unreachable from any test unwilling to talk to the live API. remark42 hit the second one. Its Telegram auth now points at a stub through go-pkgz/auth, which gained the same option, but the notify service still reaches the public API, and the update dispatcher takes its requester from that service, so the subscription flow cannot be covered by a browser test at all. APIURL takes the base and the "bot" segment is appended here, so a caller passes https://proxy.example.com and requests come out as https://proxy.example.com/bot<token>/<method>, matching what Telegram serves and what go-pkgz/auth's own option accepts. Moving a token-bearing URL across a boundary that used to be frozen is what the rest of this is for. The base is validated rather than trimmed. "https://api.telegram.org@evil.tld" is a valid URL whose host is evil.tld, and every request built from the base carries the bot token in its path, so it would ship the token there. Absolute http or https, host required, no userinfo, query, fragment or opaque part; a path prefix is allowed for a proxy mounted under one. No rejection echoes the value, since it is configuration that can carry credentials in its userinfo or a secret where the port belongs, and the parse error is dropped rather than wrapped because *url.Error prints the URL it was given. Errors are scrubbed of the token itself, not only of *url.Error's URL field. The upstream decides an API error's text, and parseError interpolated its description raw, so something standing in for Telegram could echo the request URI back and put the token in a caller's log. The scrub covers the raw and encoded forms and then checks a decoded copy, withholding the text when the token cannot be shown absent from it: after a decode failure nothing was established, and withholding is the only answer that cannot leak. It applies only to a value shaped like a bot token, "<id>:<secret>", since blanking a short arbitrary string out of a diagnostic corrupts more than it protects. Redirects are refused. Go copies the previous URL into Referer on any hop that is not https-to-http, so following one hands the destination the token. Telegram does not redirect; something standing in for it can. Each guard fails against the thing it names: reverting the validation fails eight of the rejection cases, dropping the redaction fails two, following redirects fails the referer case, and answering "not recoverable" on a decode error releases a double-encoded token. One thing this does not change: the HTTP client is still built here, so a proxy whose TLS material lives on a caller-supplied client cannot be used yet.
paskal
added a commit
to umputun/remark42
that referenced
this pull request
Aug 27, 2026
Telegram is the one auth provider whose flow leaves the browser: the reader messages a bot, and no page can reach that. e2e/telegramstub answers as the bot API for the four calls the flow makes and takes the reader's side of the exchange through /control/send, so the round trip becomes drivable end to end. It runs on its own instance, because adding a provider to the main one would change the auth panel every other case reads. Reaching it needs the bot API base url to be settable, which is what --telegram.api-url adds. It serves a real operator need beyond the tests, a proxy or a self-hosted Bot API server where Telegram is blocked, and both consumers honour it: the auth provider and the notification service. The value is an origin, and the token travels in the request path, so it has to be a host the operator controls. Writing the coverage found two defects in the wiring. An instance with telegram auth enabled and notifications failing registered the provider and never started anything to listen, so it accepted the configuration and stayed permanently deaf. And a nil *notify.Telegram was assigned into an interface, where it is not nil, so the guard downstream let it through. Blocked on two upstream releases, and cannot merge before both: the base url reaches the notification service through go-pkgz/notify#45, which is open, and the auth provider through a go-pkgz/auth change that is merged but unreleased. backend/go.mod carries a replace for the first, which a published module cannot ship, so it comes off when they land.
This was referenced Aug 27, 2026
Merged
paskal
added a commit
to umputun/remark42
that referenced
this pull request
Aug 27, 2026
Telegram is the one auth provider whose flow leaves the browser: the reader messages a bot, and no page can reach that. e2e/telegramstub answers as the bot API for the four calls the flow makes and takes the reader's side of the exchange through /control/send, so the round trip becomes drivable end to end. It runs on its own instance, because adding a provider to the main one would change the auth panel every other case reads. Reaching it needs the bot API base url to be settable, which is what --telegram.api-url adds. It serves a real operator need beyond the tests, a proxy or a self-hosted Bot API server where Telegram is blocked, and both consumers honour it: the auth provider and the notification service. The value is an origin, and the token travels in the request path, so it has to be a host the operator controls. Writing the coverage found two defects in the wiring. An instance with telegram auth enabled and notifications failing registered the provider and never started anything to listen, so it accepted the configuration and stayed permanently deaf. And a nil *notify.Telegram was assigned into an interface, where it is not nil, so the guard downstream let it through. Blocked on two upstream releases, and cannot merge before both: the base url reaches the notification service through go-pkgz/notify#45, which is open, and the auth provider through a go-pkgz/auth change that is merged but unreleased. backend/go.mod carries a replace for the first, which a published module cannot ship, so it comes off when they land.
paskal
added a commit
to umputun/remark42
that referenced
this pull request
Aug 27, 2026
Telegram is the one auth provider whose flow leaves the browser: the reader messages a bot, and no page can reach that. e2e/telegramstub answers as the bot API for the four calls the flow makes and takes the reader's side of the exchange through /control/send, so the round trip becomes drivable end to end. It runs on its own instance, because adding a provider to the main one would change the auth panel every other case reads. Reaching it needs the bot API base url to be settable, which is what --telegram.api-url adds. It serves a real operator need beyond the tests, a proxy or a self-hosted Bot API server where Telegram is blocked, and both consumers honour it: the auth provider and the notification service. The value is an origin, and the token travels in the request path, so it has to be a host the operator controls. Writing the coverage found two defects in the wiring. An instance with telegram auth enabled and notifications failing registered the provider and never started anything to listen, so it accepted the configuration and stayed permanently deaf. And a nil *notify.Telegram was assigned into an interface, where it is not nil, so the guard downstream let it through. Blocked on two upstream releases, and cannot merge before both: the base url reaches the notification service through go-pkgz/notify#45, which is open, and the auth provider through a go-pkgz/auth change that is merged but unreleased. backend/go.mod carries a replace for the first, which a published module cannot ship, so it comes off when they land.
paskal
added a commit
to umputun/remark42
that referenced
this pull request
Aug 27, 2026
Telegram is the one auth provider whose flow leaves the browser: the reader messages a bot, and no page can reach that. e2e/telegramstub answers as the bot API for the four calls the flow makes and takes the reader's side of the exchange through /control/send, so the round trip becomes drivable end to end. It runs on its own instance, because adding a provider to the main one would change the auth panel every other case reads. Reaching it needs the bot API base url to be settable, which is what --telegram.api-url adds. It serves a real operator need beyond the tests, a proxy or a self-hosted Bot API server where Telegram is blocked, and both consumers honour it: the auth provider and the notification service. The value is an origin, and the token travels in the request path, so it has to be a host the operator controls. Writing the coverage found two defects in the wiring. An instance with telegram auth enabled and notifications failing registered the provider and never started anything to listen, so it accepted the configuration and stayed permanently deaf. And a nil *notify.Telegram was assigned into an interface, where it is not nil, so the guard downstream let it through. Blocked on two upstream releases, and cannot merge before both: the base url reaches the notification service through go-pkgz/notify#45, which is open, and the auth provider through a go-pkgz/auth change that is merged but unreleased. backend/go.mod carries a replace for the first, which a published module cannot ship, so it comes off when they land.
paskal
added a commit
to umputun/remark42
that referenced
this pull request
Aug 27, 2026
Telegram is the one auth provider whose flow leaves the browser: the reader messages a bot, and no page can reach that. e2e/telegramstub answers as the bot API for the four calls the flow makes and takes the reader's side of the exchange through /control/send, so the round trip becomes drivable end to end. It runs on its own instance, because adding a provider to the main one would change the auth panel every other case reads. Reaching it needs the bot API base url to be settable, which is what --telegram.api-url adds. It serves a real operator need beyond the tests, a proxy or a self-hosted Bot API server where Telegram is blocked, and both consumers honour it: the auth provider and the notification service. The value is an origin, and the token travels in the request path, so it has to be a host the operator controls. Writing the coverage found two defects in the wiring. An instance with telegram auth enabled and notifications failing registered the provider and never started anything to listen, so it accepted the configuration and stayed permanently deaf. And a nil *notify.Telegram was assigned into an interface, where it is not nil, so the guard downstream let it through. Blocked on two upstream releases, and cannot merge before both: the base url reaches the notification service through go-pkgz/notify#45, which is open, and the auth provider through a go-pkgz/auth change that is merged but unreleased. backend/go.mod carries a replace for the first, which a published module cannot ship, so it comes off when they land.
paskal
added a commit
to umputun/remark42
that referenced
this pull request
Aug 27, 2026
Telegram is the one auth provider whose flow leaves the browser: the reader messages a bot, and no page can reach that. e2e/telegramstub answers as the bot API for the four calls the flow makes and takes the reader's side of the exchange through /control/send, so the round trip becomes drivable end to end. It runs on its own instance, because adding a provider to the main one would change the auth panel every other case reads. Reaching it needs the bot API base url to be settable, which is what --telegram.api-url adds. It serves a real operator need beyond the tests, a proxy or a self-hosted Bot API server where Telegram is blocked, and both consumers honour it: the auth provider and the notification service. The value is an origin, and the token travels in the request path, so it has to be a host the operator controls. Writing the coverage found two defects in the wiring. An instance with telegram auth enabled and notifications failing registered the provider and never started anything to listen, so it accepted the configuration and stayed permanently deaf. And a nil *notify.Telegram was assigned into an interface, where it is not nil, so the guard downstream let it through. Blocked on two upstream releases, and cannot merge before both: the base url reaches the notification service through go-pkgz/notify#45, which is open, and the auth provider through a go-pkgz/auth change that is merged but unreleased. backend/go.mod carries a replace for the first, which a published module cannot ship, so it comes off when they land.
paskal
added a commit
to umputun/remark42
that referenced
this pull request
Aug 27, 2026
Telegram is the one auth provider whose flow leaves the browser: the reader messages a bot, and no page can reach that. e2e/telegramstub answers as the bot API for the four calls the flow makes and takes the reader's side of the exchange through /control/send, so the round trip becomes drivable end to end. It runs on its own instance, because adding a provider to the main one would change the auth panel every other case reads. Reaching it needs the bot API base url to be settable, which is what --telegram.api-url adds. It serves a real operator need beyond the tests, a proxy or a self-hosted Bot API server where Telegram is blocked, and both consumers honour it: the auth provider and the notification service. The value is an origin, and the token travels in the request path, so it has to be a host the operator controls. Writing the coverage found two defects in the wiring. An instance with telegram auth enabled and notifications failing registered the provider and never started anything to listen, so it accepted the configuration and stayed permanently deaf. And a nil *notify.Telegram was assigned into an interface, where it is not nil, so the guard downstream let it through. Blocked on two upstream releases, and cannot merge before both: the base url reaches the notification service through go-pkgz/notify#45, which is open, and the auth provider through a go-pkgz/auth change that is merged but unreleased. backend/go.mod carries a replace for the first, which a published module cannot ship, so it comes off when they land.
paskal
added a commit
to umputun/remark42
that referenced
this pull request
Aug 27, 2026
Telegram is the one auth provider whose flow leaves the browser: the reader messages a bot, and no page can reach that. e2e/telegramstub answers as the bot API for the four calls the flow makes and takes the reader's side of the exchange through /control/send, so the round trip becomes drivable end to end. It runs on its own instance, because adding a provider to the main one would change the auth panel every other case reads. Reaching it needs the bot API base url to be settable, which is what --telegram.api-url adds. It serves a real operator need beyond the tests, a proxy or a self-hosted Bot API server where Telegram is blocked, and both consumers honour it: the auth provider and the notification service. The value is an origin, and the token travels in the request path, so it has to be a host the operator controls. Writing the coverage found two defects in the wiring. An instance with telegram auth enabled and notifications failing registered the provider and never started anything to listen, so it accepted the configuration and stayed permanently deaf. And a nil *notify.Telegram was assigned into an interface, where it is not nil, so the guard downstream let it through. Blocked on two upstream releases, and cannot merge before both: the base url reaches the notification service through go-pkgz/notify#45, which is open, and the auth provider through a go-pkgz/auth change that is merged but unreleased. backend/go.mod carries a replace for the first, which a published module cannot ship, so it comes off when they land.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bot API host is fixed at
api.telegram.org, reachable only through an unexported field the package's own tests set (apiPrefix, "changed only in tests"). Two consequences: an operator behind a proxy has no way in, and the Telegram notifier is unreachable from any test unwilling to talk to the live API.remark42 hit the second one. Its Telegram auth now points at a stub, since go-pkgz/auth gained the same option in go-pkgz/auth#316, but the notify service still reaches the public API — and remark42's update dispatcher takes its requester from that service, so the whole Telegram subscription flow cannot be covered by a browser test at all. Those cases are the last block of its jest suite with no e2e equivalent.
APIURLtakes the base and thebotsegment is appended here, so a caller passeshttps://proxy.example.comand requests come out ashttps://proxy.example.com/bot<token>/<method>— what Telegram itself serves, and what go-pkgz/auth's option accepts, so an operator configuring both writes the same value twice.Moving a token-bearing URL across a boundary that used to be frozen is what the rest of this is for. Each of these is inert while the host is
api.telegram.organd live the moment it is not, which is why they belong in this change rather than after it.The base is validated rather than trimmed.
https://api.telegram.org@evil.tldis a valid URL whose host isevil.tld, and every request built from the base carries the bot token in its path, so it would ship the token there. Absolute http or https, host required, no userinfo, query, fragment or opaque part; a path prefix is allowed. No rejection echoes the value — it is configuration that can carry credentials in its userinfo or a secret where the port belongs — and the parse error is dropped rather than wrapped, because*url.Errorprints the URL it was given and its inner error quotes the input back too.Errors are scrubbed of the token, not only of
*url.Error's URL field. The upstream decides an API error's text, andparseErrorinterpolated itsdescriptionraw, so a proxy answering for the API could echo the request URI back and put the token into a caller's log. The scrub covers the raw and encoded forms, then checks a decoded copy and withholds the text when the token cannot be shown absent from it: after a decode failure nothing was established, and withholding is the only answer that cannot leak. It applies only to a value shaped like a bot token,<id>:<secret>— blanking a short arbitrary string out of a diagnostic corrupts more than it protects, and the package's own tests use a token of"404".Redirects are refused. Go copies the previous URL into
Refereron any hop that is not https-to-http, so following one hands the destination the token. Telegram does not redirect; something standing in for it can.Each guard fails against the thing it names:
Referercasego test ./...andgolangci-lint run ./...are clean.One thing this does not change: the HTTP client is still built inside
Request, so a proxy whose TLS material lives on a caller-supplied client cannot be used yet. That is a larger change and I have left it alone.