fix: inspect a pasted Meta token with the app access token and never renew a permanent one (#740) - #744
Merged
Merged
Conversation
…renew a permanent one (#740) The debug_token inspection added in #726 has never worked in the field. Three facts, established against a real Business Manager system-user token, explain why and decide the fix: 1. POST /debug_token is refused by Graph ("Unsupported post request", subcode 33). The edge is GET-only. #726 chose POST to keep the token out of a URL httpx logs at INFO, so nothing was ever recorded: no token_type, no token_expires_at, no expiry on the configure card, and every install left on the 53-day age clock the issue was raised to replace. 2. GET /debug_token?input_token=T authenticated with that same token T is refused too ("(#100) You must provide an app access token, or a user access token that is an owner or developer of the app"). A system-user token cannot inspect itself; the call needs the app access token "<app_id>|<app_secret>" of the app that issued the token. 3. Graph reports a token issued without an expiry as expires_at: 0 — the Access Token Debugger's "Expires: Never". mureo read that as "unknown", indistinguishable from an absent field, so with the app pair stored a permanent token fell through to the age clock and was exchanged for a 60-day one: a strict downgrade that then kept the treadmill going. What changed: - inspect_meta_access_token sends a GET with input_token in the query and the app access token in an Authorization header, takes the app pair as keyword arguments, and raises the new MetaTokenInspectUnavailable when there is no pair to inspect with — before any request. The inspected token now travels in the URL, so the httpx/httpcore loggers are filtered for the duration of the call and restored in a finally. The returned dict gains never_expires, true only for a literal expires_at: 0. - validate_meta_access_token forwards the pair and reports token_inspect_skipped alongside token_inspect_error: "mureo could not ask" and "Meta refused" are different sentences for the operator. - MetaAdsCredentials gains token_never_expires, loaded from the meta_ads section, and _should_refresh returns False on it before the app-pair gate and both clocks. auth_setup carries the flag forward as a token-bound field (and writes no expiry beside it); the advanced env-var writer clears it with the rest of the clock when a token is entered by hand. - The paste route resolves the app pair from the payload or from what is already on disk, read before the save, and passes it to the probe. New token_expiry_untracked warning replaces token_inspect_failed when there was no pair; token_expiry_unknown is left for an inspection that ran and found neither a date nor a "never". A permanent token gets no expiry warning, no auto_refresh_unavailable, and token_never_expires on the wire and on disk. - Surfaces: the status snapshot reports access_token_never_expires and suppresses both expiry warnings for it; the configure card and the dashboard Meta row state the fact instead of a countdown; mureo auth status appends "(token does not expire)" or "(expires YYYY-MM-DD)". New en/ja strings for the never-expires line, the untracked-expiry warning and the dashboard row; the inspect-failed and app-pair copy is reworded, and the card intro and setup guide no longer claim every system-user token expires after 60 days. - Docs: authentication.md, architecture.md and the daily-check skill now say the app pair is what lets mureo read the expiry as well as renew a 60-day token, and that a token issued without an expiry is left alone.
…s given (#740) With no app ID and secret anywhere, the paste card said two contradictory things at once: the expiry line rendered wizard.auth.meta_token_expiry_unknown ("Meta did not report an expiry for this token"), while the warning right under the save button said mureo had never asked Meta at all. Only the second is true — without the app access token there is no debug_token call to make. renderExpiry now branches on the response's own warning code: when warnings carries token_expiry_untracked it renders the new wizard.auth.meta_token_expiry_untracked ("Expiry not checked — see the note below.") and points at the warning that explains what to enter. The branch sits ahead of the unknown one because an untracked expiry has no date either, so the old branch would otherwise keep answering for it. meta_token_expiry_unknown is unchanged and still used where it is accurate: an inspection that ran and either failed or came back with no date.
…er-expires flag (#740) Review follow-ups on the token inspection landed earlier in this branch. The httpcore half of the log filter was inert. Logger.addFilter is consulted only for records logged through that exact logger object — unlike handlers and levels, filters are not inherited by child loggers — and httpcore never logs through "httpcore" itself: every trace record comes from httpcore.connection, httpcore.http11, httpcore.http2, httpcore.proxy or httpcore.socks. A host application running httpcore at DEBUG would still have got the inspected token in its log. _HTTP_LOGGER_NAMES now lists the five emitters alongside the two package roots, with the reason written down where the list is defined, and a parametrised test emits a token-bearing record through each emitter while the call is in flight: it must not reach a capturing handler, and the same record must reach it once the call has returned. The never-expires flag is now validated at the boundary instead of coerced. bool("false") is True, so a hand-edited file or a third-party credential store that wrote the string "false" would have disabled the Meta token refresh forever, silently. The new auth._coerce_never_expires accepts only a JSON boolean, logs anything else once and reads it as "not established" — the pre-#740 behaviour, never worse. Both readers go through it: the credentials loader and the status snapshot's Meta row, so the dashboard and the refresh path cannot disagree about what counts as permanent. Also: a regression test that a transport error quoting the failed request — the URL carries the inspected token, the Authorization header carries the app access token — is scrubbed of both, on the inspect path and on the validate path whose token_inspect_error reaches the configure card. And one paragraph each in the changelog and the auth docs saying that a credential store outside mureo's own file has to carry token_never_expires through for the no-renewal guarantee to apply; without it the field defaults to False.
…spection # Conflicts: # CHANGELOG.md
Merged
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.
Closes #740.
What
The
debug_tokeninspection added in #726 never worked in the field, and its fallback was actively dangerous for a permanent token. Three facts, established against a real Business Manager system-user token (see the issue thread):POST /debug_tokenis refused by Graph (Unsupported post request, subcode 33). The edge is GET-only, so Meta pasted system-user tokens expire in 60–90 days; mureo assumes they never do #726 recorded nothing: notoken_type, notoken_expires_at, every install left on the 53-day age clock.GET /debug_token?input_token=Tauthenticated withTitself is refused ((#100) You must provide an app access token…). A system-user token cannot inspect itself; the call needs the app access tokenapp_id|app_secretof the issuing app.expires_at: 0(Debugger: "Expires: Never"). mureo read that as "unknown", so with the app pair stored_should_refreshfell to the age clock and would have exchanged a permanent token for a 60-day one.Changes
mureo/meta_ads/accounts.py:inspect_meta_access_tokensends GET withinput_tokenin the query and the app access token in anAuthorization: Bearerheader; raises the newMetaTokenInspectUnavailablebefore any request when there is no app pair. Because the token now travels in the URL, alogging.Filteris installed onhttpxand on eachhttpcore.*emitter logger (filters are not inherited by child loggers) for the duration of the call and removed infinally. The result gainsnever_expires(true only for a literalexpires_at: 0).validate_meta_access_tokenforwards the pair and reportstoken_inspect_skippedseparately fromtoken_inspect_error.mureo/auth.py:MetaAdsCredentials.token_never_expires, loaded only from a JSON boolean (anything else is logged and ignored, sincebool("false")isTrue);_should_refreshreturnsFalseon it before the app-pair gate and both clocks.mureo/auth_setup.pycarries it as a token-bound field and writes no expiry beside it;mureo/web/env_var_writer.pyclears it with the rest of the clock when a token is entered by hand.mureo/web/handlers.pypaste route: resolves the app pair from the payload or from the credentials already on disk (read before the save). Warning matrix: no pair anywhere →token_expiry_untracked(new; replaces the misleadingtoken_inspect_failed); Graph refused →token_inspect_failed; ran but returned neither a date nor "never" →token_expiry_unknown; permanent → no warning,token_never_expires: true,auto_refresh: falsewithoutauto_refresh_unavailable.access_token_never_expiresand suppresses both expiry warnings for it; the configure card says "This token does not expire" (or "Expiry not checked — see the note below" when no pair was given); the dashboard Meta row states the fact;mureo auth statusappends(token does not expire)/(expires YYYY-MM-DD). en/ja strings added; the card intro, guide step 3 and the app-pair hint no longer claim every system-user token expires after 60 days.docs/authentication.md,docs/architecture.md, daily-check skill), CHANGELOG[Unreleased]/ Fixed. Plugins that buildMetaAdsCredentialsthemselves must carrytoken_never_expiresthrough to get the guarantee; without it the field defaults toFalse, the pre-Meta debug_token is sent as POST and Graph rejects it, so #726 expiry tracking never records anything #740 behaviour.Verification
Clean venv, no plugins, no real Graph calls:
10546 passed, 9 skipped;black --check,ruff checkclean;mypy mureo/unchanged (pre-existing missing-stub errors only);node --test tests/js/*.test.js695/695. Tests pin: GET not POST, the Bearer header,input_tokenin the query, no request without the pair, the token never reaching an httpx or httpcore.* log record while the call is in flight (and host logging restored after), transport errors scrubbed of both secrets,expires_at: 0→never_expires, refresh refused for a permanent token even with the app pair and an expired-looking clock, the non-boolean flag rejected, the paste-route warning matrix, and the env-var writer clearing the flag.Captured against a mock home carrying
token_never_expires: true(light + dark, full page) — attached in the review thread.