Skip to content

fce834e8 - Report an expired Taro LNURL login as "LNURL login expired" - #1480

Merged
davidleomay merged 5 commits into
DFXswiss:developfrom
Daniel-DFX:fix/taro-lnurl-login-expired
Sep 16, 2026
Merged

davidleomay merged 5 commits into
DFXswiss:developfrom
Daniel-DFX:fix/taro-lnurl-login-expired

Conversation

@Daniel-DFX

@Daniel-DFX Daniel-DFX commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

EN:
When a Taro login challenge is no longer known to the API (404) after its 5-minute lifetime, the login now fails with "LNURL login expired" instead of "Authentication failed"; every other poll failure, including an earlier 404, still reports "Authentication failed". Client error reports can then tell a normal timeout apart from real login failures, apart from the edge cases listed in the details. The status poller now sends one request at a time and ignores responses that arrive after polling stopped, so a stale or overlapping response can no longer drop a successful or retried login. Declared deviations: no Playwright/handbook baseline and no full-stack E2E test for the Taro error state (see details).

DE:
Kennt die API eine Taro-Login-Challenge nach Ablauf ihrer 5-minütigen Gültigkeit nicht mehr (404), schlägt der Login jetzt mit "LNURL login expired" statt "Authentication failed" fehl; alle anderen Poll-Fehler, auch ein früherer 404, melden weiterhin "Authentication failed". Damit lässt sich ein normaler Timeout in den Client-Fehlerberichten von echten Login-Fehlern unterscheiden, bis auf die in den Details genannten Grenzfälle. Der Status-Poller sendet jetzt nur noch eine Anfrage zur Zeit und ignoriert Antworten nach dem Ende des Pollings, sodass eine veraltete oder überlappende Antwort keinen erfolgreichen oder erneuten Login mehr verwirft. Deklarierte Abweichungen: keine Playwright-/Handbook-Baseline und kein Full-Stack-E2E-Test für den Taro-Fehlerzustand (siehe Details).

Details

Problem

ConnectTaro polled getLnurlAuth(k1) with setInterval every second, and its .catch() turned every rejection into new Error('Authentication failed'). The API's cleanup job, which runs every 5 minutes, removes LNURL challenges older than 5 minutes. After that the status call returns 404, so the user sees (and the app reports) "Authentication failed" even though the login simply timed out.

Change (src/components/home/wallet/connect-taro.tsx)

  • A rejection is reported as 'LNURL login expired' only when it is an ApiException with statusCode === 404 and the challenge is at least LNURL_CHALLENGE_LIFETIME_MS (5 minutes) old. Challenge age is measured from when polling for that challenge started.
  • Why the age check: the API also deletes a challenge, and so answers the next status read with 404, when it refuses the wallet callback (for example a blocked IP country, or an invalid tag/action). That can happen well before the 5 minutes are up and is not an expiry. The API's expiry cleanup only removes challenges older than 5 minutes, so a 404 before that stays 'Authentication failed'. LNURL_CHALLENGE_LIFETIME_MS mirrors the API's challenge lifetime and has to be kept in sync by hand.
  • Limits of the age check: the API returns the same 404 for every cause, so the app infers the cause from the challenge age. A 404 with another cause that the app only sees after 5 minutes (e.g. the tab was suspended) is reported as expired. A real expiry seen within the network latency of the challenge creation is reported as 'Authentication failed'. An explicit reason from the status endpoint would remove both edge cases.
  • Every other rejection keeps 'Authentication failed': ApiException with status 0 (network), 5xx, other statuses, non-ApiException errors.
  • ApiException is imported from @dfx.swiss/react. It is the same class the SDK's API hook throws, with statusCode taken from the HTTP response.
  • 'LNURL login expired' is a new string used only for this flow. It deliberately does not reuse the existing "Login link expired" message, which belongs to the email magic-link sign-in. Like the previous 'Authentication failed', it has no entry in src/translations/languages/*.json.

Fix: polling races (pre-existing, found in review of this PR)

  • Overlapping requests for the same challenge. setInterval fired every second without waiting for the previous request. The API deletes the challenge on the first completed status read, so a concurrent request got a 404. If that 404 arrived before the success response, a login that had just succeeded was rejected. Polling is now serial: the next request is scheduled only after the previous one has settled.
  • Stale responses after polling stopped. A request still in flight when polling for a challenge stopped (completed, failed, replaced by a new challenge, or unmounted) could reset the newer login's state or keep polling the old challenge. Each polling run now tracks whether it is still active and ignores late responses.

Reported pre-existing defect outside this repository

The API deletes a completed login's token roughly 30–75 s after the wallet approved it (a 30-second cleanup job plus its start jitter), and deletes the challenge on the first completed status read. If the app only polls again after that window (for example because the browser stayed in the background while the user approved in the Taro app), the status read fails with 404 although the login succeeded. It then shows 'Authentication failed' (or 'LNURL login expired' once the challenge is 5 minutes old). This repository cannot fix it, because the token is already gone server-side; it is reported to the API maintainers with the fix direction (keep completed entries longer and make the completed status read idempotent).

Related change in the API

This PR changes which message a timed-out Taro login reports. Classifying that message at a lower severity in client error reports is part of the API's error ingestion and ships as a separate change there. It needs nothing further from this repository.

Tests (src/__tests__/connect-taro.test.tsx, new)

  • 404 ApiException once the challenge is exactly 5 minutes old → LNURL login expired, and the QR link disappears
  • 404 ApiException 1 ms before that → Authentication failed
  • ApiException 0 / 500, a plain Error, and a non-ApiException object with statusCode: 404 (all after the lifetime) → Authentication failed
  • Polling: continues while the challenge is incomplete and resolves with the access token; the next request is sent only after the previous one settled; stops after a failed check, after a completed challenge and on unmount
  • Replaced challenge: a late failure of the old challenge's request is ignored and the new login completes; a late incomplete response of the old challenge does not keep polling it
  • Reconnect with a session address skips challenge creation; the Taro link, the Open app button and the connect-error retry button are covered
  • Mutation checks against the component: removing the 404 branch, the age check, either late-response guard, or going back to setInterval each makes at least one test fail.

@dfx.swiss/react's ESM build is not loadable under this Jest setup (other tests mock the package too), so the test mock defines a minimal ApiException with the same shape.

Coverage (per touched file, npm run test -- --coverage --collectCoverageFrom='src/components/home/wallet/connect-taro.tsx')

File Statements Branches Functions Lines
src/components/home/wallet/connect-taro.tsx 100 % 100 % 100 % 100 %

Declared deviation: Handbook coverage (CONTRIBUTING.md → Handbook)

This PR changes the error text of one Taro-connect error state (the expired challenge) and adds no Playwright baseline or scripts/handbook/metadata.json entry for it. No committed baseline shows that screen state: the existing /connect spec in e2e/home.spec.ts only checks that the page body is visible and takes no screenshot. Adding one would need a spec (new, or an extension of that one) that fakes the LNURL endpoints and waits past the challenge lifetime, plus a matching reality-declaration entry. That is far more than this change, whose behavior is fully covered by the unit test above. The layout is unchanged; only the message text differs. The reviewer decides whether to accept this deviation.

Declared deviation: Full-stack E2E tests (CONTRIBUTING.md → Full-stack E2E tests)

This PR changes what one screen state shows and adds no case to e2e-stack/. No full-stack spec covers the Taro/LNURL login today: the two /connect cases in e2e-stack/specs/auth.spec.ts (logged out, and with a signature session) check the route, the wallet grid and that no address prompt appears, but never select Taro or run an LNURL login. Reaching the expired state needs one of two things. Either a real wait of more than 5 minutes followed by a signed LNURL login callback, which the API refuses as expired and then removes the challenge. Or time control on both the API and the browser. e2e-stack/ has neither an LNURL login signing helper (its signature helpers sign EVM messages) nor time control, and it runs the API with DISABLED_PROCESSES=*, which switches off process-gated cron jobs (docs/test-architecture.md, Known gaps). A case would therefore add a new LNURL signing helper plus more than 5 minutes of idle waiting to every full run, or new time control on both sides, which is well beyond this change. The unit test above covers the client-side behavior fully; what it does not prove is that the real API answers an expired challenge with HTTP 404 end to end. The reviewer decides whether to accept this deviation.

Reality declaration

No e2e/full-stack fake is added, removed or changed (only Jest module mocks in a unit test), so docs/test-architecture.md is unchanged.

Verification (local, Node 20)

  • npm run lint ✅, Prettier check on changed files ✅
  • CI=true npm test -- --watchAll=false: 145 suites / 2228 tests passed ✅
  • npm run build:dev ✅ (only the existing source-map warnings from node_modules), npm run widget:dev

@Danswar

Danswar commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

@TaprootFreak @davidleomay could please add the label in this PR so the CI runs please?

@Daniel-DFX

Copy link
Copy Markdown
Contributor Author

EN:
@TaprootFreak @davidleomay could you please add the ci label and approve the pending fork CI runs for the current head 68ba53fa?

DE:
@TaprootFreak @davidleomay könnt ihr bitte das ci-Label setzen und die ausstehenden Fork-CI-Runs für den aktuellen Head 68ba53fa freigeben?

Details

This PR comes from a fork (Daniel-DFX/app) and the author account has read-only access, so it cannot add labels or approve workflow runs.

Once CI is green, the PR will be marked ready for review.

@Daniel-DFX

Copy link
Copy Markdown
Contributor Author

EN:
Review status update, not the ready declaration: after 12 review rounds, all four review lanes (quality and logic, in both review stages) report 0 open findings at head 68ba53fa, and the PR stays in draft until CI has run green.

DE:
Review-Statusmeldung, nicht die Ready-Erklärung: Nach 12 Review-Runden melden alle vier Review-Lanes (Qualität und Logik in beiden Review-Stufen) 0 offene Mängel am Head 68ba53fa, und der PR bleibt Draft, bis die CI grün gelaufen ist.

Details

Status

  • Head: 68ba53faa0ef448461cdc7bb3a9e04795a51e767
  • First review stage: 5 rounds (quality + logic in parallel), last round 0 findings on both lanes at this head.
  • Second review stage: 7 rounds (quality + logic in parallel), last round 0 findings on both lanes at this head.
  • This is not the closing ready comment. Per the process, that comment and "Ready for review" only follow once CI has run green on this head. CI has not run yet (fork runs await approval, see the comment above).
  • Still open for the code-owner reviewer: accepting or refusing the two declared deviations (Handbook coverage, Full-stack E2E tests) in the PR description.

Findings by round and how they were resolved

First stage

  1. Quality: PR description lacked per-file coverage numbers → added. Logic: 0.
  2. Logic: a status request still in flight could reset a retried login (pre-existing) → fixed in b3620148. Quality: 0.
  3. 0 / 0 at b3620148.
  4. 0 / 0 at 095e524f.
  5. 0 / 0 at 68ba53fa.

Second stage

  1. Quality: imprecise test types → fixed in 095e524f; two points rejected with evidence (unit-test module mocks are outside the reality-declaration scope; the severity classification lives in the API, not in this repo). Logic: a 404 is not unique to an expired challenge → rule is now 404 and challenge age ≥ 5 minutes; overlapping polls for the same challenge could drop a successful login (pre-existing) → polling is serial, both in 095e524f.
  2. Quality: full-stack E2E deviation not declared → declared; translation point rejected (no connect error string is translated repo-wide). Logic: pre-existing API defect that loses a completed login if its status is not read in time → reported in the description (not fixable in this repo); limits of the age check → documented.
  3. Quality and logic: cleanup-timing wording in a code comment and the description → fixed in 68ba53fa; "no spec yet" wording → corrected. Logic: "double click orphans a login promise" rejected (the retry button is removed on the first click).
  4. Logic: 0. Quality: two inaccuracies in the E2E deviation text → corrected.
  5. Quality: 0. Logic: two inaccuracies in the E2E deviation text → corrected.
  6. Quality: two, logic: three wording/precision points in the description (E2E deviation, token deletion window) → corrected.
  7. 0 / 0 at 68ba53fa.

Local verification (Node 20): lint ✅, 145 suites / 2228 tests ✅, build:dev ✅, widget:dev ✅, 100 % statement/branch/function/line coverage on src/components/home/wallet/connect-taro.tsx. All commits are signed and verified.

@davidleomay davidleomay added the ci Run PR CI on this draft label Sep 11, 2026
@Daniel-DFX

Copy link
Copy Markdown
Contributor Author

EN:
Ready after 12 review passes.
When a Taro LNURL login challenge has expired, the login now fails with "LNURL login expired" instead of "Authentication failed", and the status poller no longer drops successful or retried logins through overlapping or stale responses.

DE:
Bereit nach 12 Review-Durchläufen.
Ist eine Taro-LNURL-Login-Challenge abgelaufen, schlägt der Login jetzt mit "LNURL login expired" statt "Authentication failed" fehl, und der Status-Poller verwirft erfolgreiche oder erneute Logins nicht mehr durch überlappende oder veraltete Antworten.

Details

Head: 68ba53faa0ef448461cdc7bb3a9e04795a51e767

Review passes (quality and logic lanes in parallel per pass; per-round findings and resolutions are listed in the review-status comment above)

  • First stage: 5 passes, last pass 0 findings on both lanes at this head.
  • Second stage: 7 passes, last pass 0 findings on both lanes at this head.
  • Fixed along the way: coverage numbers in the description, stale poll responses after a retry, precise test types, the 404 + challenge-age rule, serial polling, an accurate cleanup comment, and the accuracy of both declared deviations and the reported API defect. Rejected with evidence: reality-declaration entries for unit-test mocks, API-side severity classification as a deferred in-repo fix, translations for connect error strings, and a double-click promise leak that cannot happen (the retry button is removed on the first click).

CI at this head (runs started by the ci label)

  • PR CI: Build and test ✅ (lint, Markdown format check, related tests: 98/98 suites, build:dev, widget:dev) and Full-stack E2E / Full-stack E2E ✅ (develop PR without ci:full: the stack is not brought up, as documented). Run: https://github.com/DFXswiss/app/actions/runs/34638534392
  • CodeQL Advanced: Analyze (javascript-typescript) ✅, Analyze (actions) ✅, CodeQL
  • PR Review Bot: review ✅ (verified commits, ESLint, TypeScript check, security audit, TODO check; the comment step is skipped for fork PRs)
  • The skipped entries at this head belong to the earlier synchronize runs, which started while the PR was a draft without the ci label and are superseded by the runs above. No required status checks are configured on develop.

Comments

  • No reviews, inline comments or review threads.
  • The CI-label request (from @Danswar and in my comment above) is done: the ci label was added and CI ran green.

Mergeability

  • mergeable: MERGEABLE against develop at this head. No rebase was needed.

Still for the code-owner reviewer

  • Accept or refuse the two declared deviations in the description (Handbook coverage, Full-stack E2E tests).

@Daniel-DFX
Daniel-DFX marked this pull request as ready for review September 11, 2026 19:33
@github-actions

github-actions Bot commented Sep 12, 2026

Copy link
Copy Markdown

dfx pr guard

EN: Thanks for your contribution! This repository follows A38. A38 pass: author local-CI report accepted for this head.

DE: Danke für deinen Beitrag! In diesem Repository gilt A38. A38 pass: Autor-Local-CI-Report für diesen Head akzeptiert.

Details

@github-actions

github-actions Bot commented Sep 12, 2026

Copy link
Copy Markdown

EN:
This pull request is back in Draft because A38 is not green.

DE:
Dieser Pull Request steht wieder auf Draft, weil A38 nicht grün ist.

Details
{
  "base": "bfa7d994a010834936fe240dec6c1520942cd4f3",
  "head": "68ba53faa0ef448461cdc7bb3a9e04795a51e767",
  "phase": "applied",
  "pr": 1480,
  "reasons": [
    "Required CI check not green: .github/workflows/pr.yml / Lint and Markdown format",
    "CI status not green: A38 / report (develop) (failure)"
  ],
  "repo": "DFXswiss/app",
  "state": "draft"
}

@github-actions
github-actions Bot marked this pull request as draft September 12, 2026 11:32
When the LNURL challenge is no longer known to the API (404 after its
5-minute TTL), the Taro login poller now rejects with a dedicated
"LNURL login expired" message instead of "Authentication failed".
Every other poll failure (network errors, 5xx, unexpected errors)
still reports "Authentication failed".
Rename the poller test to connect-taro.test.tsx and extend it to the
whole component so the touched file reaches 100 % statement, branch,
function and line coverage: reconnect with a session address, the
Taro link and Open app button, polling until the challenge completes,
cleanup on unmount, and the connect error with its retry button.
The status poller fires every second without waiting for the previous
request. A request still in flight when the login had already failed
could reject after the user retried and reset the new challenge, which
stopped the new poller and left the retry hanging. Responses that
arrive after polling stopped (settled, restarted or unmounted) are now
ignored.
The API deletes an LNURL challenge on the first completed status read,
so an overlapping status request could fail with 404 and drop a login
that had just succeeded. The poller now sends the next request only
after the previous one has settled.

A 404 also follows a wallet callback the API refused (for example a
blocked IP country), which can happen long before the challenge expires.
'LNURL login expired' is therefore reported only when the challenge is
at least 5 minutes old, the API's challenge lifetime; an earlier 404
stays 'Authentication failed'.

The test types now use Blockchain and LnurlAuthStatus instead of plain
string and object.
The API removes a challenge in a cleanup job once it is older than
5 minutes, not exactly 5 minutes after creating it.
@Daniel-DFX
Daniel-DFX force-pushed the fix/taro-lnurl-login-expired branch from 68ba53f to e91d12b Compare September 14, 2026 17:15
@Daniel-DFX

Copy link
Copy Markdown
Contributor Author

EN:
The A38 report below records the checks, results and durations.

DE:
Der A38-Bericht unten dokumentiert die Prüfungen, Ergebnisse und Laufzeiten.

Details
Check / Prüfung Duration / Laufzeit Result / Ergebnis Exit code
lint: Lint and Markdown format 66 s pass 0

Durations rounded up to whole seconds / Laufzeiten auf ganze Sekunden aufgerundet.

Original report / Originalbericht
{
  "head": "e91d12b6f7f2bf6412e9123ee1e1e6e25b77bec0",
  "private": false,
  "recorded_at": "2026-09-14T17:14:52Z",
  "repo": "DFXswiss/app",
  "required": [
    "lint"
  ],
  "runs": [
    {
      "command": "agent a38 job commands --config '{\"env\":{\"CI\":\"true\"},\"npm\":{\"canaries\":[\"react-app-rewired/package.json\",\"react-scripts/package.json\",\"typescript/lib/typescript.js\",\"prettier/package.json\"],\"node_major\":20},\"steps\":[[\"npm\",\"run\",\"lint\"],[\"npm\",\"run\",\"format:md:check\"]]}'",
      "duration_s": 65.51911624986678,
      "exit_code": 0,
      "id": "lint",
      "name": "Lint and Markdown format",
      "result": "pass",
      "timeout_s": 600.0
    }
  ],
  "schema": "dfx-local-ci/v1"
}

@github-actions

github-actions Bot commented Sep 14, 2026

Copy link
Copy Markdown

EN:
I have authorized the recorded CI runs; their results are still pending.

DE:
Ich habe die dokumentierten CI-Läufe freigegeben; ihre Ergebnisse stehen noch aus.

Details
{
  "base": "904a5961bbde2e3b7687f77b1953e30c2fd790b8",
  "head": "e91d12b6f7f2bf6412e9123ee1e1e6e25b77bec0",
  "pr": 1480,
  "repo": "DFXswiss/app",
  "runs": [
    {
      "run_id": 34873605902,
      "workflow": ".github/workflows/codeql.yml"
    },
    {
      "run_id": 34873605986,
      "workflow": ".github/workflows/pr-review-bot.yml"
    },
    {
      "run_id": 34873606123,
      "workflow": ".github/workflows/pr.yml"
    },
    {
      "run_id": 34880894330,
      "workflow": ".github/workflows/ci-on-ready.yaml"
    }
  ]
}

@Daniel-DFX

Copy link
Copy Markdown
Contributor Author

EN:
Ready after 12 review passes.
When a Taro LNURL login challenge has expired, the login now fails with "LNURL login expired" instead of "Authentication failed", and the status poller no longer drops successful or retried logins through overlapping or stale responses.

DE:
Bereit nach 12 Review-Durchläufen.
Ist eine Taro-LNURL-Login-Challenge abgelaufen, schlägt der Login jetzt mit "LNURL login expired" statt "Authentication failed" fehl, und der Status-Poller verwirft erfolgreiche oder erneute Logins nicht mehr durch überlappende oder veraltete Antworten.

Details

This PR was converted back to Draft by the PR guard after develop adopted A38 (#1484), because the previous head 68ba53fa predated the new Lint and Markdown format job and had no A38 author report.

  • Rebase: rebased onto develop 904a5961 with no conflicts and no new commits. The code diff is unchanged from the reviewed head 68ba53fa, so the 12 review passes stay valid and no new pass was run. New head: e91d12b6. All commits are signed and verified.
  • A38: author report for e91d12b6 posted in fce834e8 - Report an expired Taro LNURL login as "LNURL login expired" #1480 (comment) (npm run lint and npm run format:md:check under Node 20: pass). The guard authorized the fork CI runs 34873605902, 34873605986 and 34873606123 for this head.
  • CI at e91d12b6: Lint and Markdown format, Build and test, Full-stack E2E, CodeQL (actions and javascript-typescript), review and dfx pr guard all succeeded. The CI Node version (20.x) matches the local A38 run, and package.json declares no engines.
  • Comments: no review threads and no inline comments. The earlier request to add the ci label and approve fork runs is obsolete, since the guard now authorizes fork CI. An APPROVED review exists at e91d12b6.
  • Mergeability: mergeable: MERGEABLE at e91d12b6.

@Daniel-DFX
Daniel-DFX marked this pull request as ready for review September 14, 2026 18:27
@davidleomay
davidleomay merged commit 1fc6223 into DFXswiss:develop Sep 16, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci Run PR CI on this draft

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants