22phase : 12-cloudflare-login-restoration
33fixed_at : 2026-07-23T00:00:00Z
44review_path : .planning/phases/12-cloudflare-login-restoration/12-REVIEW.md
5- iteration : 1
6- findings_in_scope : 3
7- fixed : 3
5+ iteration : 2
6+ findings_in_scope : 7
7+ fixed : 7
88skipped : 0
99status : all_fixed
1010---
@@ -78,8 +78,62 @@ Added a regression test, `loginRequestPercentEncodesStructuralCharactersInCreden
7878decoder splits a pair at its first ` = ` and leaves ` + ` untouched, so it reads a corrupted body as
7979intact — which is why the existing baselines never caught this.
8080
81+ ## Fixed Issues — iteration 2 (Info tier, at the user's direction)
82+
83+ Iteration 1 covered the ` critical_warning ` scope. The user then selected IN-01, IN-03, IN-04 and
84+ IN-05 for fixing and left IN-02 and IN-06 alone. Each landed as its own commit.
85+
86+ ### IN-01: Comment overstates what the rejection throw prevents
87+
88+ ` httpShouldHandleCookies = false ` was set only on the clearance-carrying retry, so on the bare path
89+ URLSession filed the response's ` Set-Cookie ` automatically, before and regardless of the throw. A
90+ user with a live session who mistyped a re-login could have it clobbered by the failure page's
91+ tombstones. Rather than correct the comment, the stronger of the review's two options was taken: the
92+ jar is suppressed on both paths, which makes them symmetric and makes the comment true.
93+
94+ ** Behavior change:** the bare login POST no longer sends jar cookies. Nothing is lost — credentials
95+ arrive on the * response* and ` setCredentials ` applies them on success — and the clearance path had
96+ already shipped on the same reasoning. ` CloudflareChallengeDetectionTests ` pinned the old value and
97+ was updated with the reason.
98+
99+ ### IN-03: The four-attempt transport retry applies to the credential POST
100+
101+ ` fetch ` now takes an ` attempts ` count defaulting to the existing four, and ` LoginRequest ` asks for
102+ one. A POST the forum received but whose response was lost is otherwise replayed, and each replay
103+ spends another of the account's login attempts against the forum's own lockout — the lockout this
104+ phase taught the parser to surface.
105+
106+ The account-layer baseline pinning four attempts read them through ` LoginRequest ` , so it moved to
107+ ` VoteGalleryTagRequest ` and still guards the default; the login exception has a case of its own.
108+
109+ ### IN-04: ` redactedCredentialHeader ` splits on ` , ` only
110+
111+ Hand-splitting the coalesced header was the root cause, so it is gone: ` HTTPCookie ` parses the
112+ header and the names come from the cookies it recognises. This closes both the reported noise
113+ (` expires ` fragments printed as names) and the residual risk the review flagged — a non-compliant
114+ comma inside a value can no longer surface a fragment of that value, because a parser either
115+ recognises a cookie or yields nothing. The function now takes the response URL; without one it names
116+ nothing rather than gambling with a value.
117+
118+ ### IN-05: ` parseLoginErrorMessage ` scans the entire page
119+
120+ The markers now count only as the text of the forum's own error-box label (` pformstrip ` /
121+ ` formsubtitle ` ), which is where both real shapes put them.
122+
123+ ** Deviation from the review's suggested fix:** it proposed anchoring "before falling back to the
124+ page-wide scan". The fallback was deliberately not implemented — keeping it would preserve the exact
125+ false positive being removed, since a page with no anchor still reaches the page-wide scan.
126+ Unrecognised markup now degrades to the unlabelled generic failure the caller already handles, which
127+ leaves a live session intact; that is the safer of the two failure modes, because the misfire being
128+ removed drops session cookies on a login that actually succeeded.
129+
130+ One existing fixture (` markupAndEntitiesBetweenTheMarkerAndTheMessageAreIgnored ` ) used a class-less
131+ ` <div> ` and was updated to the real markup; three cases were added for the new contract.
132+
81133## Verification performed
82134
135+ Iteration 1 (as recorded when written, before the iOS scheme was available to the fixer):
136+
83137- Every modified file re-read after editing; ` xcrun swiftc -parse ` clean on all four.
84138- No line exceeds the 120-character ` line_length ` limit; no ` swiftlint:disable ` was added and no rule
85139 was suppressed. SwiftLint itself is not installed on this machine, so rule conformance was checked
@@ -88,19 +142,34 @@ intact — which is why the existing baselines never caught this.
88142- The new ` dictString() ` was type-checked and executed standalone: it emits
89143 ` PassWord=p%26w%3Dd%2Bq%20x%25y&UserName=baseline-user ` and ` commenttext_new=first%0Asecond ` ,
90144 matching the updated test expectations exactly.
91- - Full compilation was ** not** run: ` AppPackage ` is iOS-only and ` swift build ` on macOS fails at
92- manifest resolution (platform floors), before reaching any source. The test suite must be run
93- through the iOS scheme during verification.
145+ - Full compilation was ** not** run at the time: ` AppPackage ` is iOS-only and ` swift build ` on macOS
146+ fails at manifest resolution (platform floors), before reaching any source.
147+
148+ Iteration 2 closed that gap for both iterations, using the same scheme and flags as CI
149+ (` .github/workflows/test.yml ` ):
150+
151+ - ` xcodebuild build -scheme EhPanda ` against an iOS Simulator destination: ** BUILD SUCCEEDED** , no
152+ errors and no compiler warnings. SwiftLint runs as a build-tool plugin in this project, so a clean
153+ build is also a clean lint — no rule was suppressed and no ` swiftlint:disable ` was added.
154+ - ` xcodebuild test -scheme EhPanda ` : ** TEST SUCCEEDED** , all suites green. The pre-existing
155+ ` withKnownIssue ` blocks remain the only reported known issues.
156+ - Every test added or changed across both iterations was confirmed by name in the run output.
157+ - ` scripts/check-cookie-logging.sh ` passes.
158+ - ` -skipMacroValidation ` was required: three macro packages need re-approval after version bumps.
159+ This is a local Xcode trust prompt, not a code issue, and CI passes the same flag.
94160
95161## Out of scope
96162
97- IN-01 through IN-06 are Info-tier and were not addressed under ` fix_scope: critical_warning ` .
98- IN-01 (the comment overstating what the rejection throw prevents) and IN-04 (the DEBUG redactor's
99- ` , ` -only split) sit in ` Request+Account.swift ` , which this run touched for WR-03; they were left
100- untouched so each commit stays scoped to its finding.
163+ IN-02 and IN-06 were reviewed and deliberately left alone.
164+
165+ - ** IN-02** (stale dismissal echo misread as a swipe) — the review concludes the window is
166+ unreachable in production and records it as a known limitation rather than a defect. Closing it
167+ would mean threading a challenge ID through the presentation for no observable benefit.
168+ - ** IN-06** (locale-dependent assertions in ` AppErrorStructuredTests ` ) — environment-sensitive
169+ rather than wrong, pre-existing test style, and CI runners are English so it never fires there.
101170
102171---
103172
104173_ Fixed: 2026-07-23_
105- _ Fixer: Claude (gsd-code-fixer)_
106- _ Iteration: 1 _
174+ _ Fixer: Claude (gsd-code-fixer, iteration 1) / Claude Opus 4.8 (iteration 2 )_
175+ _ Iteration: 2 _
0 commit comments