Skip to content

feat(auth): release Sign in with Apple to all users#933

Merged
FagnerMartinsBrack merged 5 commits into
mainfrom
claude/apple-login-release-r2hczs
Jul 6, 2026
Merged

feat(auth): release Sign in with Apple to all users#933
FagnerMartinsBrack merged 5 commits into
mainfrom
claude/apple-login-release-r2hczs

Conversation

@FagnerMartinsBrack

Copy link
Copy Markdown
Member

Summary

Remove the ?feature=apple querystring gate so the Sign in / Sign up with Apple button is now rendered for every visitor on /login and /signup.

  • Drops the {{#if appleEnabled}} block in login.template.html and signup.template.html.
  • Removes the appleEnabled prop from LoginPage / SignupPage and the featureToggle dependency from initAuthRoutes.
  • Updates the auth route tests to assert the Apple button is always present (no more ?feature=apple-gated cases).

Test plan

  • pnpm nx run hutch:check (unit + integration tests + coverage)

Generated by Claude Code

Remove the `?feature=apple` querystring gate so the Sign in / Sign up
with Apple button is rendered for every visitor on /login and /signup.
The `appleEnabled` template variable and `featureToggle` dependency on
the auth page are gone with it.
@FagnerMartinsBrack

Copy link
Copy Markdown
Member Author

@claude Review this PR. Use your Read tool to read the local file .github/workflows/claude-PR-code-reviewer.md for instructions. The file is in the checked-out repository.

Context

@claude

claude Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Claude finished @FagnerMartinsBrack's task in 6m 52s —— View job


High Priority Issues (Must Fix)

None found

Medium Priority Issues

1. Apple button is now decoupled from whether the /auth/apple route is actually mounted

This PR makes the button render unconditionally, but the Apple auth route is only mounted when deps.appleAuth is present:

// projects/hutch/src/runtime/server.ts:900
if (deps.appleAuth) {
	const appleAuthRouter = initAppleAuthRoutes({ /* … */ });
	app.use(appleAuthRouter);
}
  • Production is safe. The prod composition root sources the Apple keys via requireEnv (app.ts:160-163), so appleAuth is always set and the route is always mounted. Releasing to all prod users works as intended.
  • Non-prod / local dev is not. The dev composition root sets appleAuth = undefined when the APPLE_LOGIN_* vars are unset (app.ts:448) — a configuration the code explicitly supports (the assert at app.ts:440-444 permits "all unset", and most developers won't have Apple signing keys locally). In that path the button now renders, but /auth/apple is not mounted, so clicking Sign in with Apple 404s. Previously the ?feature=apple gate meant a developer never saw the button by default; after this change the broken state becomes the default in any environment without the Apple secret (local dev, plus any preview/staging env that doesn't inject it).

The intent ("show it to everyone") is better expressed by tying visibility to backend availability rather than removing the gate entirely: thread appleEnabled: Boolean(deps.appleAuth) from createApp into initAuthRoutes, pass it through auth.page.ts to LoginPage/SignupPage, and keep the {{#if appleEnabled}} guard in both templates. That still shows the button to every user in production (where appleAuth is always set) while keeping the button and the route coupled, so the button can never render without a working destination.

Fix this →

Low Priority Suggestions

1. Nothing asserts the /auth/apple route is reachable

The route tests assert the button's href is /auth/apple (auth.route.test.ts:1229, :1250) but never that the route is actually mounted, so a button/route mismatch passes CI. If you adopt the fix above, add a test asserting [data-test-apple-section] is absent when appleAuth is unset — this restores meaningful coverage for the removed negative cases, but keyed on real backend availability instead of a querystring (consistent with the test-driven-design "no querystring-only gate" intent).

The removal itself is clean and well-scoped: no dangling appleEnabled references remain, QuerystringFeatureToggle is still used by the queue/inbox pages (so the module isn't orphaned) and the featureToggle const at server.ts:830 is still consumed at :986/:1078 (no unused var), no docs reference the old gate, and dropping the negative toBeNull() tests aligns with the repo's "no negative test assertions" convention. The single concern is that the button is now decoupled from deps.appleAuth, which is safe in production but shows a 404-ing button in the supported "no Apple config" dev/preview path.

This PR has issues that should be addressed before merging.


@FagnerMartinsBrack

Copy link
Copy Markdown
Member Author

High Priority Issues (Must Fix)

None found

Medium Priority Issues

1. Apple button is now decoupled from whether the /auth/apple route is actually mounted

This PR makes the button render unconditionally, but the Apple auth route is only mounted when deps.appleAuth is present:

// projects/hutch/src/runtime/server.ts:900
if (deps.appleAuth) {
	const appleAuthRouter = initAppleAuthRoutes({ /* … */ });
	app.use(appleAuthRouter);
}
  • Production is safe. The prod composition root sources the Apple keys via requireEnv (app.ts:160-163), so appleAuth is always set and the route is always mounted. Releasing to all prod users works as intended.
  • Non-prod / local dev is not. The dev composition root sets appleAuth = undefined when the APPLE_LOGIN_* vars are unset (app.ts:448) — a configuration the code explicitly supports (the assert at app.ts:440-444 permits "all unset", and most developers won't have Apple signing keys locally). In that path the button now renders, but /auth/apple is not mounted, so clicking Sign in with Apple 404s. Previously the ?feature=apple gate meant a developer never saw the button by default; after this change the broken state becomes the default in any environment without the Apple secret (local dev, plus any preview/staging env that doesn't inject it).

The intent ("show it to everyone") is better expressed by tying visibility to backend availability rather than removing the gate entirely: thread appleEnabled: Boolean(deps.appleAuth) from createApp into initAuthRoutes, pass it through auth.page.ts to LoginPage/SignupPage, and keep the {{#if appleEnabled}} guard in both templates. That still shows the button to every user in production (where appleAuth is always set) while keeping the button and the route coupled, so the button can never render without a working destination.

Fix this →

Low Priority Suggestions

1. Nothing asserts the /auth/apple route is reachable

The route tests assert the button's href is /auth/apple (auth.route.test.ts:1229, :1250) but never that the route is actually mounted, so a button/route mismatch passes CI. If you adopt the fix above, add a test asserting [data-test-apple-section] is absent when appleAuth is unset — this restores meaningful coverage for the removed negative cases, but keyed on real backend availability instead of a querystring (consistent with the test-driven-design "no querystring-only gate" intent).

@FagnerMartinsBrack

Copy link
Copy Markdown
Member Author

@claude Fix the review issues. Use your Read tool to read the local file .github/workflows/claude-PR-code-review-auto-apply.md for instructions. The file is in the checked-out repository.

Context

Review to Address

See the review comment above this one for the issues to fix.

@claude

claude Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Claude finished @FagnerMartinsBrack's task in 33m 58s —— View job


✅ Fixed the medium-priority review issue

Pushed 688fe87c to claude/apple-login-release-r2hczs.

Problem: The PR made the Apple button render unconditionally, but /auth/apple is only mounted when deps.appleAuth is set. In any environment without Apple config (local dev, previews — a state the composition root explicitly supports), the button rendered a link that 404s. This was even latent in the test suite: createDefaultTestAppFixture sets apple: undefined, so the PR's own tests asserted a button whose route wasn't mounted.

Approach — tie visibility to backend availability, not a querystring:

File Change
server.ts Compute appleEnabled: Boolean(deps.appleAuth) in createApp and pass it into initAuthRoutes. Boolean(...) (not a ternary) keeps it branch-free for coverage.
auth.page.ts Add appleEnabled: boolean to AuthDependencies; thread deps.appleEnabled through every LoginPage/SignupPage render path — GET, POST-422, checkout-success.
auth.component.ts Restore appleEnabled?: boolean on AuthFormData + pass to both template renders.
login/signup.template.html Restore the {{#if appleEnabled}} guard.
auth.route.test.ts Positive tests now use an Apple-configured fixture (route mounted); added absence tests for /login + /signup when Apple is unconfigured.

Why this shape:

  • Prod still shows it to everyone — the prod root sources Apple keys via requireEnv, so appleAuth is always set → appleEnabled is always true.
  • Button can never render without a working destination — dev/preview without the secret get no button instead of a 404.
  • Threaded through all render paths (not just the two GETs) so the Apple button doesn't vanish on a 422 re-render while the (unconditional) Google button stays — they now behave consistently.
  • Absence tests reuse the same [data-test-apple-section] selector the positive tests assert() on, so a selector typo fails loudly rather than passing a false negative — and they first assert the Google section renders to prove the page actually built. This matches the existing local convention in this file (trial-hint, founding-blurb).

Verification (full check with .envrc env vars set — the pre-commit hook ran nx run-many --target=check --all across all 37 projects and passed):

  • Tests: 2842 unit + 11 integration + 2 E2E — all pass
  • Coverage: functions 100%, statements/lines 99.51%, branches 97.17% — all thresholds met
  • tsc + biome + knip + unused-css — clean

Low-priority suggestion (route-reachable assertion) was intentionally left per the auto-fix scope (high/medium only), though the new absence tests already restore the negative coverage keyed on real backend availability.
· Branch claude/apple-login-release-r2hczs

Tie Apple sign-in button visibility to backend availability
(Boolean(deps.appleAuth)) instead of rendering it unconditionally, so the
button is never shown without a mounted /auth/apple route. Threaded
appleEnabled through every LoginPage/SignupPage render path and restored
the {{#if appleEnabled}} template guard.

Co-authored-by: Fayner Brack <FagnerMartinsBrack@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@FagnerMartinsBrack

Copy link
Copy Markdown
Member Author

@claude Review this PR. Use your Read tool to read the local file .github/workflows/claude-PR-code-reviewer.md for instructions. The file is in the checked-out repository.

Context

@claude

claude Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Claude finished @FagnerMartinsBrack's task in 7m 26s —— View job


Review — PR #933 (release Sign in with Apple to all users) @ 688fe87c

  • Read review instructions (.github/workflows/claude-PR-code-reviewer.md)
  • Gather PR diff and changed files (commit 688fe87c)
  • Review the fix against CLAUDE.md guidelines & skills
  • Post review feedback

High Priority Issues (Must Fix)

None found

Medium Priority Issues

1. Two OAuth-error LoginPage render sites still omit appleEnabled — the Apple button vanishes on error-fallback pages in production

The fix correctly ties button visibility to backend availability: server.ts:871 passes appleEnabled: Boolean(deps.appleAuth) — the same deps.appleAuth that mounts the route at server.ts:901. Threading through all seven render paths inside auth.page.ts is complete and correct, and the fix's own description states the goal was that "the Apple button doesn't vanish … while the (unconditional) Google button stays — they now behave consistently."

But two sibling LoginPage(...) render sites outside auth.page.ts were missed:

// projects/hutch/src/runtime/web/auth/apple-auth.page.ts:158 (renderError)
Base(LoginPage({ userCount, foundingAllocation: deps.foundingAllocation, errors: [{ message }] }, { statusCode: 400 }), )

// projects/hutch/src/runtime/web/auth/google-auth.page.ts:121 (renderError)
Base(LoginPage({ userCount, foundingAllocation: deps.foundingAllocation, errors: [{ message }] }, { statusCode: 400 }), )

Neither passes appleEnabled. Because AuthFormData.appleEnabled is optional (auth.component.ts:24), this compiles, but appleEnabled is undefined → falsy → the {{#if appleEnabled}} guard hides the Apple button. In production (deps.appleAuth always set via requireEnv), the normal /login shows the Apple button, but these OAuth-error fallback login pages hide it — the exact "Google stays, Apple vanishes" inconsistency the fix set out to eliminate, on a different set of paths.

It's worst on the Apple callback failure: a user who just clicked Sign in with Apple and hit an error sees "Apple sign-in failed. Please try again." on a page with no Apple button to retry with (apple-auth.page.ts:153-181). Since the apple router only mounts when appleAuth is set, appleEnabled there is definitionally true — the button should show.

Recommended remedy (also prevents recurrence):

  • apple-auth.page.ts:158 — pass appleEnabled: true (route only mounts when appleAuth is set).
  • google-auth.page.ts:121 — add appleEnabled: boolean to initGoogleAuthRoutes deps, pass Boolean(deps.appleAuth) from server.ts, then pass appleEnabled: deps.appleEnabled.
  • Make appleEnabled required on AuthFormData (auth.component.ts:24) so the compiler forces every LoginPage/SignupPage caller to supply it — aligns with CLAUDE.md "Prefer Compile-Time Constraints Over Runtime Validation" and turns this latent runtime inconsistency into a compile error.
  • Add route tests asserting the apple section renders on these OAuth-error pages when Apple is configured (the current absence/positive tests don't cover them).

Fix this →

Low Priority Suggestions

None


What the fix got right (verified): visibility is keyed on Boolean(deps.appleAuth) — the same source as the route mount (server.ts:871 / :901), so button and route are coupled; Boolean(...) is branch-free for coverage and both branches are now exercised; prod always sets appleAuth via requireEnv (app.ts:160-164) so every prod user still sees the button, while dev/preview without the secret (app.ts:448) get no button instead of a 404; the {{#if appleEnabled}} guard is restored in both templates with data-test-google-section left outside it; the positive tests use the canonical apple: fixture key (matching apple-auth.route.test.ts) so they genuinely mount the route; and the new absence tests reuse the same appleSection() selector the positive tests assert() on, which mitigates the test-driven-design skill's "toBeNull() selector-typo passes for the wrong reason" concern and matches the file's existing trial-hint/founding-blurb convention.

The single remaining gap is that the Apple button is coupled to appleAuth on the seven auth.page.ts renders but not on the two OAuth-error fallback renders — safe in dev, but a visible regression of the PR's own goal in production.

This PR has issues that should be addressed before merging.
· Branch claude/apple-login-release-r2hczs

@FagnerMartinsBrack

Copy link
Copy Markdown
Member Author

High Priority Issues (Must Fix)

None found

Medium Priority Issues

1. Two OAuth-error LoginPage render sites still omit appleEnabled — the Apple button vanishes on error-fallback pages in production

The fix correctly ties button visibility to backend availability: server.ts:871 passes appleEnabled: Boolean(deps.appleAuth) — the same deps.appleAuth that mounts the route at server.ts:901. Threading through all seven render paths inside auth.page.ts is complete and correct, and the fix's own description states the goal was that "the Apple button doesn't vanish … while the (unconditional) Google button stays — they now behave consistently."

But two sibling LoginPage(...) render sites outside auth.page.ts were missed:

// projects/hutch/src/runtime/web/auth/apple-auth.page.ts:158 (renderError)
Base(LoginPage({ userCount, foundingAllocation: deps.foundingAllocation, errors: [{ message }] }, { statusCode: 400 }), )

// projects/hutch/src/runtime/web/auth/google-auth.page.ts:121 (renderError)
Base(LoginPage({ userCount, foundingAllocation: deps.foundingAllocation, errors: [{ message }] }, { statusCode: 400 }), )

Neither passes appleEnabled. Because AuthFormData.appleEnabled is optional (auth.component.ts:24), this compiles, but appleEnabled is undefined → falsy → the {{#if appleEnabled}} guard hides the Apple button. In production (deps.appleAuth always set via requireEnv), the normal /login shows the Apple button, but these OAuth-error fallback login pages hide it — the exact "Google stays, Apple vanishes" inconsistency the fix set out to eliminate, on a different set of paths.

It's worst on the Apple callback failure: a user who just clicked Sign in with Apple and hit an error sees "Apple sign-in failed. Please try again." on a page with no Apple button to retry with (apple-auth.page.ts:153-181). Since the apple router only mounts when appleAuth is set, appleEnabled there is definitionally true — the button should show.

Recommended remedy (also prevents recurrence):

  • apple-auth.page.ts:158 — pass appleEnabled: true (route only mounts when appleAuth is set).
  • google-auth.page.ts:121 — add appleEnabled: boolean to initGoogleAuthRoutes deps, pass Boolean(deps.appleAuth) from server.ts, then pass appleEnabled: deps.appleEnabled.
  • Make appleEnabled required on AuthFormData (auth.component.ts:24) so the compiler forces every LoginPage/SignupPage caller to supply it — aligns with CLAUDE.md "Prefer Compile-Time Constraints Over Runtime Validation" and turns this latent runtime inconsistency into a compile error.
  • Add route tests asserting the apple section renders on these OAuth-error pages when Apple is configured (the current absence/positive tests don't cover them).

Fix this →

Low Priority Suggestions

None

@FagnerMartinsBrack

Copy link
Copy Markdown
Member Author

@claude Fix the review issues. Use your Read tool to read the local file .github/workflows/claude-PR-code-review-auto-apply.md for instructions. The file is in the checked-out repository.

Context

Review to Address

See the review comment above this one for the issues to fix.

@claude

claude Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Claude finished @FagnerMartinsBrack's task in 21m 7s —— View job


✅ Fixed the medium-priority review issue

Pushed ed17a226 to claude/apple-login-release-r2hczs.

Problem: The prior fix tied the Apple button to Boolean(deps.appleAuth) across the seven auth.page.ts render paths, but two sibling LoginPage(...) renders outside that file were missed — the OAuth-error fallback pages in apple-auth.page.ts and google-auth.page.ts. Because AuthFormData.appleEnabled was optional, they compiled with appleEnabled undefined → the {{#if appleEnabled}} guard hid the button. In production (appleAuth always set) that regressed the PR's own goal: the normal /login showed the Apple button but these error pages hid it — worst on the Apple callback failure, where a user who just clicked Sign in with Apple landed on a retry page with no Apple button to retry with.

Approach — thread appleEnabled through the two missed renders, and make the type enforce it:

File Change
apple-auth.page.ts Error render passes appleEnabled: true — the Apple router only mounts when appleAuth is set, so it is definitionally true here.
google-auth.page.ts Added appleEnabled: boolean to the route deps; error render passes deps.appleEnabled.
server.ts initGoogleAuthRoutes now gets appleEnabled: Boolean(deps.appleAuth) — the same source that mounts the Apple route (server.ts:901), so button and route stay coupled. Boolean(...) (not a ternary) keeps it branch-free for coverage.
auth.component.ts Made appleEnabled required on AuthFormData (was optional). This is the load-bearing part: it turns the latent runtime inconsistency into a compile error, so every current and future LoginPage/SignupPage caller must supply it (CLAUDE.md "Prefer Compile-Time Constraints Over Runtime Validation"). The clean tsc pass confirms all 9 call sites already comply.
apple-auth.route.test.ts / google-auth.route.test.ts Added tests asserting the Apple section renders on each OAuth-error page when Apple is configured. Both first assert [data-test-global-error] (proving the error page built), then reuse the same [data-test-apple-section] .auth-apple-button selector and check href === /auth/apple.

Why this shape:

  • Prod shows the button everywhere, including error pagesappleAuth is always set via requireEnv, so appleEnabled is always true on every render path now.
  • Never renders without a working destination — the Google error page keys visibility on real backend availability (Boolean(deps.appleAuth)), and dev/preview without the secret still get no button rather than a 404.
  • The required field prevents recurrence — a future error-fallback render that forgets appleEnabled won't compile.
  • Coverage note: the Google-error test needs Apple configured (so deps.appleAuth is truthy) but never invoked — its exchangeAppleCode stub is never called. That's safe because c8's default exclude (**/*.test.*, not overridden in .c8rc.json) keeps test-file functions out of the coverage report.

Verification (full check with .envrc env vars; the pre-commit hook ran nx run-many --target=check --all across all 37 projects and passed):

  • Tests: 2844 unit + 11 integration + 2 E2E — all pass (+2 new)

  • Coverage: functions 100%, statements/lines 99.51%, branches 97.17% — all thresholds met

  • tsc + biome + knip + unused-css — clean

  • Read review instructions & gather context

  • apple-auth.page.tsappleEnabled: true on the error render

  • google-auth.page.ts — thread appleEnabled from deps into the error render

  • server.tsappleEnabled: Boolean(deps.appleAuth) into initGoogleAuthRoutes

  • auth.component.tsappleEnabled now required on AuthFormData

  • Route tests for the apple section on both OAuth-error pages

  • pnpm nx run hutch:check — green

  • Commit & push
    · Branch claude/apple-login-release-r2hczs

Thread appleEnabled into the two OAuth-error LoginPage renders that were
missed, so the Apple button no longer vanishes on error-fallback pages in
production.

- apple-auth.page.ts: pass appleEnabled: true on the callback error render
  (the Apple router only mounts when appleAuth is set, so it is always true).
- google-auth.page.ts: add appleEnabled to the route deps and pass it on the
  callback error render; server.ts sources it from Boolean(deps.appleAuth),
  the same value that mounts the Apple route.
- auth.component.ts: make appleEnabled required on AuthFormData so the compiler
  forces every LoginPage/SignupPage caller to supply it (compile-time
  constraint over runtime validation).
- Add route tests asserting the Apple section renders on both OAuth-error
  pages when Apple is configured.

Co-authored-by: Fayner Brack <FagnerMartinsBrack@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@FagnerMartinsBrack

Copy link
Copy Markdown
Member Author

@claude CI has FAILED. Use your Read tool to read the local file .github/workflows/claude-PR-CI-failure-fixer.md for instructions. The file is in the checked-out repository.

Context

Failed Jobs

  • web-tests: Run pnpm check

@claude

claude Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Claude finished @FagnerMartinsBrack's task in 9m 12s —— View job


CI Failure Diagnosis — PR #933 (attempt 1 of 5)

  • Read CI failure fixer instructions
  • Understand PR intent (3 auth-only commits)
  • Gather failed CI logs (Run 28744163422)
  • Identify root cause
  • Determine correct fix location

TL;DR — this failure is not caused by this PR, and it is not fixable from this branch

This PR's own changes are fully green: 2850 unit + 11 integration tests pass, coverage passes, tsc/biome/knip clean. The only failure is two visual‑regression snapshots for an unrelated feature:

src/e2e/crawl-bookmark-visual.e2e-local.ts:39  › rounded-left capsule (light)  ✘
src/e2e/crawl-bookmark-visual.e2e-local.ts:46  › rounded-left capsule (dark)   ✘
  Expected an image 145px by 54px, received 153px by 54px

That test does not exist on this branch — it was added on main by #936 (5b993302 "render the crawl bookmark as one seamless capsule"). CI tests the PR merged into main (refs/pull/933/merge), so this PR inherits it.

Evidence that main is already red (independent of #933)

CI run Ref web-tests result
28744076544 main push @ 5b993302 ❌ same 145px→153px snapshot failure
28743889714 #936 merge CI ❌ same 145px→153px snapshot failure
28744163422 this PR (#933 merged w/ main) ❌ same failure, inherited

#936 was merged with a red web-tests job. It committed baseline PNGs (crawl-bookmark-{light,dark}-chromium-linux.png, 145×54) that never matched the CI chromium‑linux render (153×54).

Root cause

The capsule width is driven entirely by the rendered width of the bold prefix + time label. The test pins the instant (2026-03-26T14:32:00Z), timezone (UTC), viewport, and waits for the exact text 26 Mar '26, 14:32 + document.fonts.ready — so the text is identical, yet the element is a deterministic 8px wider in CI. That is a font‑metric mismatch: the baseline was generated in a non‑CI environment whose fonts render ~8px narrower than the CI runner's. maxDiffPixelRatio/threshold can't absorb it because a size mismatch fails before pixel comparison, and retries: 0 means one flake fails the run.

Why I did not push a change to this branch

  • The failing test/snapshot is not part of this PR (auth‑only: server.ts, apple-auth.page.ts, auth.component.ts, auth.page.ts, google-auth.page.ts + tests). Editing fix(hutch): render the crawl bookmark as one seamless capsule #936's snapshot here would violate the fixer's "do not make unrelated changes / never reverse intentional changes" rules and pollute a deliberately minimal auth PR with an unrelated binary.
  • Regenerating the baseline correctly requires the web-tests CI environment; a snapshot regenerated elsewhere risks re‑failing. The fix belongs where the test lives — on main.
  • I can only push to this PR branch (triggered on the PR), not to main.

Recommended fix (on main, owned by #936)

Regenerate both baselines in the CI environment and commit the 153×54 PNGs (playwright test crawl-bookmark-visual.e2e-local.ts --update-snapshots), or make the pixel assertion robust to font metrics. Once main is green, this PR goes green automatically on its next merge‑CI (no auth changes needed).

Fix this → (on main)

No fix pushed to this branch — doing so would be an unrelated change, and no auth change can turn this failure green. The block is a pre‑existing main breakage from #936.
· Branch claude/apple-login-release-r2hczs

@FagnerMartinsBrack

Copy link
Copy Markdown
Member Author

@claude Review this PR. Use your Read tool to read the local file .github/workflows/claude-PR-code-reviewer.md for instructions. The file is in the checked-out repository.

Context

@claude

claude Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Claude finished @FagnerMartinsBrack's task in 5m 32s —— View job


Review — PR #933 (release Sign in with Apple to all users) @ cabde0a7

  • Read review instructions (.github/workflows/claude-PR-code-reviewer.md)
  • Gather PR diff and changed files (commit cabde0a7)
  • Review the fix against CLAUDE.md guidelines & skills
  • Post review feedback

High Priority Issues (Must Fix)

None found

Medium Priority Issues

None found

Low Priority Suggestions

1. The appleEnabled: false branch of the Google error page isn't directly exercised

google-auth.page.ts:122 now renders appleEnabled: deps.appleEnabled, and the new test (google-auth.route.test.ts:111) covers the configured case (appleEnabled true → button present). There's no companion test for the unconfigured case (Google set, Apple unset → no Apple button on the Google error page). This is safe today — Boolean(deps.appleAuth) is branch-free so coverage passes, and the {{#if appleEnabled}} false-branch is already exercised by the /login + /signup absence tests — so it's genuinely optional, not a coverage gap. Worth a line only if you want the Google error page's negative behaviour pinned explicitly.

2. Minor asymmetry between the two error renders (intentional, noting for future readers)

apple-auth.page.ts:158 hardcodes appleEnabled: true while google-auth.page.ts:122 threads deps.appleEnabled. Both are correct: the Apple router only mounts inside if (deps.appleAuth) (server.ts:921), so true is definitionally right there and avoids adding a redundant dependency to AppleAuthDependencies. No change needed — flagging so the difference reads as deliberate rather than an oversight.

What the final state gets right (verified against the code):

  • Visibility is keyed on the same source as the route mount. server.ts:890 / :916 pass appleEnabled: Boolean(deps.appleAuth) — the exact value that mounts /auth/apple at server.ts:921. Button and route can no longer diverge, which was the core defect in the original PR.
  • Production still ships to everyone. app.ts:160-164 sources the Apple keys via requireEnv, so appleAuth is always set in prod → appleEnabled is always true. Dev/preview without the secret (app.ts:448, a state the assert at :440-444 explicitly supports) render no button instead of a 404 — the goal met without the ?feature=apple gate.
  • The required field prevents recurrence. Making appleEnabled mandatory on AuthFormData (auth.component.ts:24) turns a missed render site into a compile error — this is what caught the two OAuth-error renders in the prior round, and all 9 LoginPage/SignupPage call sites now supply it (grep-confirmed). Aligns with CLAUDE.md "Prefer Compile-Time Constraints Over Runtime Validation." Boolean(...) (not a ternary) keeps it branch-free for coverage.
  • Templates guard the Apple section with {{#if appleEnabled}} and leave data-test-google-section outside it, so Google always renders and the two buttons behave consistently across every path.
  • Tests are keyed on real backend availability, not a querystring. Positive tests use an Apple-configured fixture that actually mounts the route (canonical apple: fixture key, matching apple-auth.route.test.ts); the absence tests reuse the same appleSection()/getAppleButton selector the positive tests assert() on and first assert the Google section renders — which mitigates the test-driven-design skill's "toBeNull() selector-typo passes for the wrong reason" concern (a typo would fail the positive tests) and matches the file's existing trial-hint/founding-blurb convention. Conditional-render (rather than render-always-with-a-state-class) is the right call here because the element is a real <a href="/auth/apple"> that must be truly absent, not merely CSS-hidden.
  • No stale ?feature=apple references remain, QuerystringFeatureToggle is still used elsewhere in server.ts (:1007/:1099), imports resolve, and CI is green.

cc @FagnerMartinsBrack - No high or medium priority issues found. This PR is ready for human review.
· Branch claude/apple-login-release-r2hczs

@FagnerMartinsBrack

Copy link
Copy Markdown
Member Author

High Priority Issues (Must Fix)

None found

Medium Priority Issues

None found

Low Priority Suggestions

1. The appleEnabled: false branch of the Google error page isn't directly exercised

google-auth.page.ts:122 now renders appleEnabled: deps.appleEnabled, and the new test (google-auth.route.test.ts:111) covers the configured case (appleEnabled true → button present). There's no companion test for the unconfigured case (Google set, Apple unset → no Apple button on the Google error page). This is safe today — Boolean(deps.appleAuth) is branch-free so coverage passes, and the {{#if appleEnabled}} false-branch is already exercised by the /login + /signup absence tests — so it's genuinely optional, not a coverage gap.

2. Minor asymmetry between the two error renders (intentional, noting for future readers)

apple-auth.page.ts:158 hardcodes appleEnabled: true while google-auth.page.ts:122 threads deps.appleEnabled. Both are correct: the Apple router only mounts inside if (deps.appleAuth) (server.ts:921), so true is definitionally right there and avoids adding a redundant dependency. No change needed — flagging so the difference reads as deliberate.

@FagnerMartinsBrack

Copy link
Copy Markdown
Member Author

@claude remove the appleEnabled toggle and props completely, don't leave trash code behind. Make deps.appleAuth mandatory to all callers

@claude

claude Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Claude finished @FagnerMartinsBrack's task in 34m 0s —— View job


✅ Removed the appleEnabled toggle & made deps.appleAuth mandatory

Pushed 0d55d89e to claude/apple-login-release-r2hczs.

Approach: appleAuth is now a required dependency → the /auth/apple route is always mounted → the button always resolves to a real destination → no appleEnabled toggle needed anywhere. This deletes the toggle rather than threading it (net +55 / −141).

File Change
server.ts appleAuth is required (was appleAuth?); Apple router mounted unconditionally (dropped if (deps.appleAuth)); both appleEnabled: Boolean(deps.appleAuth) threads removed.
auth.page.ts Dropped appleEnabled from AuthDependencies and all 7 LoginPage/SignupPage render paths.
auth.component.ts Dropped appleEnabled from AuthFormData and both template renders.
login/signup.template.html Dropped the {{#if appleEnabled}} guard — the button always renders (nested {{#if returnUrl}} left intact).
apple-auth.page.ts / google-auth.page.ts Dropped appleEnabled from the OAuth-error renders (and Google's route deps).
app.ts (dev root) Provides appleAuth unconditionally — real config when APPLE_LOGIN_* is set, else a dev stub.
web-test-harness/bundle.types.ts apple: AppleAuthBundle (was | undefined) — mandatory on TestAppFixture.
test-fixtures/fixture.ts Default fixture wires a stub Apple bundle so every test app mounts the route.
auth/*.route.test.ts Removed the now-impossible "Apple unconfigured" cases and the appleEnabled-only error-page scaffolding; positive tests use the default fixture.

One judgment call — the dev composition root (app.ts, only hit by pnpm dev, not CI): making appleAuth mandatory means it can never be undefined. I kept the real Apple wiring when APPLE_LOGIN_* is set, and stub it otherwise — because requiring the keys in local dev would break pnpm dev for anyone without them (.envrc doesn't provide them), and Apple's handshake can't complete against localhost regardless. If you'd rather dev demand the keys like prod (requireEnv, dropping the stub), say the word and I'll switch it.

Verification — pre-commit hook ran the full nx run-many --target=check --all across all 37 projects, green:

  • hutch: 2861 unit + 11 integration + 5 E2E — all pass

  • Coverage: functions 100%, statements 99.52%, branches 97.14%, lines 99.52% — all thresholds met

  • tsc + biome + knip + unused-css clean; appleEnabled has zero remaining references repo-wide

  • server.tsappleAuth required; drop appleEnabled; mount unconditionally

  • auth.page.ts / auth.component.ts — drop appleEnabled everywhere

  • login/signup.template.html — drop {{#if appleEnabled}}

  • apple-auth.page.ts / google-auth.page.ts — drop appleEnabled

  • Composition roots provide appleAuth unconditionally

  • Tests — drop the "Apple unconfigured" & appleEnabled scaffolding

  • pnpm nx run … check (full sweep, all 37 projects green)

  • Commit & push
    · Branch claude/apple-login-release-r2hczs

Sign in with Apple ships to everyone, so the appleEnabled toggle is dead
weight. Make deps.appleAuth a required dependency instead: the
/auth/apple route is now always mounted, so the button always resolves
to a real destination — no conditional render needed.

- server.ts: appleAuth is required; mount the Apple router
  unconditionally; drop both appleEnabled threads.
- auth.page.ts / auth.component.ts: drop appleEnabled from the deps,
  AuthFormData, and every LoginPage/SignupPage render path.
- login/signup.template.html: drop the {{#if appleEnabled}} guard.
- apple-auth.page.ts / google-auth.page.ts: drop appleEnabled from the
  OAuth-error renders (and Google's route deps).
- Composition roots always provide appleAuth: prod already did; the dev
  root stubs it when APPLE_LOGIN_* is unset (localhost can't complete
  Apple's handshake anyway); the default test fixture wires a stub so
  `apple` is mandatory on TestAppFixture.
- Tests: drop the now-impossible "Apple unconfigured" cases and the
  appleEnabled-only error-page scaffolding.

Co-authored-by: Fayner Brack <FagnerMartinsBrack@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@FagnerMartinsBrack

Copy link
Copy Markdown
Member Author

@claude Review this PR. Use your Read tool to read the local file .github/workflows/claude-PR-code-reviewer.md for instructions. The file is in the checked-out repository.

Context

@claude

claude Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Claude finished @FagnerMartinsBrack's task in 7m 6s —— View job


Review — PR #933 (release Sign in with Apple to all users) @ 0d55d89e

  • Read review instructions (.github/workflows/claude-PR-code-reviewer.md)
  • Gather PR diff and changed files (commit 0d55d89e)
  • Review the change against CLAUDE.md guidelines & skills
  • Post review feedback

High Priority Issues (Must Fix)

None found

Medium Priority Issues

None found

Low Priority Suggestions

1. The dev composition root now ships a throwing Apple stub (app.ts:466-475)

Making appleAuth mandatory forces the development root to fabricate an exchangeAppleCode when APPLE_LOGIN_* is unset, and that stub throws. This is a defensible, human-directed trade-off — dev can't complete Apple's handshake from localhost regardless, the stub is chosen explicitly at the composition root (where CLAUDE.md wants implementations selected, not silently defaulted inside a factory), and it fails loudly rather than no-op'ing. It does, however, place a fake implementation in non-test runtime code, which brushes against CLAUDE.md "In-memory implementations are for tests only." The only alternative that keeps appleAuth mandatory is requireEnv in dev (fail-fast, but breaks pnpm dev for anyone without Apple keys) — the previous round already surfaced that option to you, so this is a conscious choice, not an oversight.

One small accuracy nit in the inline comment: "the button still redirects to Apple" is technically true, but with clientId: "dev.readplace.apple-login" the redirect lands on Apple's error page (invalid client), and the throwing stub is never actually reached — the callback catches upstream (apple-auth.page.ts:184-190). Consider wording it as "reaches Apple's error page in dev" so a future reader doesn't expect a working local flow.

2. Apple is now mandatory, but Google stays optional — the button↔route coupling this PR hardened for Apple is still latent for Google

The core win here is that appleAuth is required (server.ts:226) → /auth/apple always mounts (server.ts:919-940) → the now-unguarded Apple button always resolves. But googleAuth? remains optional (server.ts:221) and /auth/google is still mounted conditionally (if (deps.googleAuth), server.ts:~895-916), while the Google button renders unconditionally in login.template.html:20-26 / signup.template.html. So in any environment without Google config — including the default test fixture, where google: undefined (fixture.ts) — the Google button links to an unmounted /auth/google and 404s. That is exactly the decoupling the review spent several rounds eliminating for Apple, now sitting asymmetrically on Google in the same two templates.

It's pre-existing, out of this PR's scope, and safe in prod (googleAuth is always set via requireEnv), so not a blocker — but worth a follow-up to make Google mandatory too (or gate its button on availability) so the two providers stop diverging. Fix this →

What the final state gets right (verified against the code):

  • appleEnabled is completely gone — grep confirms zero references repo-wide: the toggle, the AuthFormData prop, the {{#if appleEnabled}} guards in both templates, the featureToggle dep on initAuthRoutes, and the two OAuth-error renders (apple-auth.page.ts / google-auth.page.ts) are all cleaned up. This matches your "don't leave trash code behind" instruction. QuerystringFeatureToggle is intentionally retained (still consumed at server.ts:1003/:1095 for queue/inbox), so nothing is orphaned.
  • appleAuth is mandatory and compiler-enforced — required on AppDependencies (server.ts:226) and on TestAppFixture.apple (bundle.types.ts). This is what turns "button always has a working route" into a structural guarantee instead of a threaded flag, and the green tsc proves all three composition roots (prod app.ts:262, dev app.ts:448, test-app via fixture.ts) and every createApp caller comply. Aligns with CLAUDE.md "Prefer Compile-Time Constraints Over Runtime Validation."
  • Production ships to everyone — the prod root sources Apple keys via requireEnv (app.ts:160-164), so appleAuth is always set and the button always renders, which is the PR's goal.
  • Tests are right-sized — the now-impossible "Apple unconfigured → no button" cases are correctly dropped (that state is unrepresentable once the dep is mandatory), and the positive tests use assert(section, …) against the default fixture that genuinely mounts the route — robust to a selector typo, matching the test-driven-design skill's guidance over the old toBeNull() negatives.
  • CI is green (run 28785811928) — tsc / biome / knip / coverage (functions 100%) all pass; the earlier unrelated main snapshot failure (fix(hutch): render the crawl bookmark as one seamless capsule #936) is resolved by the merge of origin/main (cabde0a7).

Net +55 / −141 — a clean deletion that does exactly what was asked. The two notes above are optional follow-ups, not blockers.

cc @FagnerMartinsBrack - No high or medium priority issues found. This PR is ready for human review.

--- · Branch claude/apple-login-release-r2hczs

@FagnerMartinsBrack FagnerMartinsBrack merged commit 23bada8 into main Jul 6, 2026
9 checks passed
@FagnerMartinsBrack FagnerMartinsBrack deleted the claude/apple-login-release-r2hczs branch July 6, 2026 10:56
FagnerMartinsBrack added a commit that referenced this pull request Jul 6, 2026
Sign in with Apple shipped to all users (#933) while this branch's
Apple-revocation seam was still a no-op, tripping the fail-closed
deletion-compliance lock in auth.route.test.ts ({reachable:true,
persisted:false}). This wires the revocation the lock demands:

1. The code exchange now requires Apple's refresh_token — an exchange
   without one fails the login instead of minting an unrevokable
   account — and returns it as AppleTokenResult.appleRefreshToken.

2. Every Apple login persists the token on the users row (the
   passwordHash precedent): createAppleUser stores it at signup, and
   the new saveAppleRefreshToken conditional update stores the freshest
   grant on each returning login, which also backfills accounts minted
   during the gap between #933 and this change. Read back via
   findAppleRefreshTokenByUserId over the userId-index; deleted with
   the row by closeUserAccount. Mirrored in the in-memory fixture.

3. The delete worker's no-op seam is replaced by a real
   initRevokeExternalIdpTokens: look up the stored token (absent for
   password/Google accounts → log-and-return), POST it with a freshly
   minted ES256 client secret to https://appleid.apple.com/auth/revoke,
   and throw on non-2xx so the SQS record redrives while the token is
   still readable (revocation precedes closeUserAccount). Apple returns
   200 for already-revoked tokens (RFC 7009), so redrives converge.

4. The worker Lambda gets the four APPLE_LOGIN_* env vars, and the
   delete-account queue's maxReceiveCount rises 3 → 12 (~3h of
   retries at the 900s visibility timeout) so an Apple outage rides
   out in-queue instead of falling to the 14-day DLQ after 45 minutes.

The handler-level test now runs the real revoker against the in-memory
auth store and a fake Apple endpoint, proving deletion revokes the
stored grant end-to-end; route tests pin token persistence on the
signup, returning-login, and race-fallback paths.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants