Server-backed highlights go live: auth hardening + delete-flash fix#294
Open
cameronapak wants to merge 19 commits into
Open
Server-backed highlights go live: auth hardening + delete-flash fix#294cameronapak wants to merge 19 commits into
cameronapak wants to merge 19 commits into
Conversation
🦋 Changeset detectedLatest commit: 00b1ff4 The changes in this PR will be included in the next version bump. This PR includes changesets to release 4 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
This was referenced Jul 20, 2026
cameronapak
marked this pull request as ready for review
July 20, 2026 20:18
cameronapak
force-pushed
the
claude/ype-1034-pr3-recent-colors
branch
from
July 21, 2026 19:46
2f47c5f to
8a6ae88
Compare
A pending highlight resumed after sign-in/data-exchange now routes write failures through the same handling as a user-initiated apply: 401/403 invalidates the permission cache, re-stashes the pending highlight (in its own scope), and re-opens the permission dialog instead of silently dropping the user's tap. No unattended redirect loop: the dialog requires a click. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Correctness: - Gate the color row on auth-provider presence (highlightsInteractive) so copy/share-only integrations don't render inert swatches - Exit permissionDialog to idle and clear pending when auth flips signed-out Efficiency: - Keep serverColors reference-stable across equal refetches (no chapter-wide verse re-sweep per write) - Stabilize useApiData refetch identity by holding fetchFn in a ref Reuse/simplification: - Export getHttpStatus from core; machine consumes it instead of probing the ad-hoc error shape - Extract resolveAuthToken (core) to de-dupe getAuthToken in highlights and data-exchange clients - Add internal useApiClient hook; refactor five hooks onto it - Drop derivable WriteOp.reprompt; share scopesEqual from the machine Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
xstate@5.32.5 was published 2026-07-14, inside the 3-day minimumReleaseAge window, so every CI job failed at pnpm install. Pin to 5.32.4 (published 2026-07-02) and rebuild the lockfile from a fresh resolution, per the policy's own guidance. Bump back once 5.32.5 ages out. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The callback saved granted_permissions for whoever was in localStorage
when the return page loaded — if user B signed in on another tab
mid-redirect, B inherited A's grant and hasPermission('highlights')
skipped B's consent (review finding).
The redirect leg now records the initiating user's id; the callback
reads-and-clears it and only saves the grant when it matches the current
user. Mismatch or missing initiator fails closed: grant discarded, result
downgraded to failure, URL still cleaned — the flow re-prompts instead of
proceeding as granted. Sign-out clears the initiator. The combined
sign-in+permissions path is unaffected: it returns through the sign-in
callback, which binds the grant to the user from the same ID token.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cameronapak
force-pushed
the
claude/ype-1034-pr5-hardening
branch
from
July 21, 2026 20:01
98f5693 to
905e592
Compare
cameronapak
changed the base branch from
claude/ype-1034-pr3-recent-colors
to
main
July 21, 2026 20:01
…ing on 5xx Save the data-exchange initiator before minting the token, and stop clearing the pending stash on apply network/5xx so a sibling permission-lost intent survives. Co-authored-by: Cursor <cursoragent@cursor.com>
cameronapak
changed the base branch from
main
to
claude/ype-1034-pr3-recent-colors
July 22, 2026 16:05
cameronapak
changed the base branch from
claude/ype-1034-pr3-recent-colors
to
main
July 22, 2026 16:05
Keep controlled mode always interactive (YPE-3705) while self-contained still requires HIGHLIGHTS_LIVE and an auth provider. Co-authored-by: Cursor <cursoragent@cursor.com>
Take aria-hidden from main; keep this branch's checkmark-tap remove test. Co-authored-by: Cursor <cursoragent@cursor.com>
Stash granted_permissions across the web OAuth hop and seed from token scope; sync appName/signInPromptMessage onto the UI-bundled core so the sign-in dialog pitch reaches consumers of the UI package. Co-authored-by: Cursor <cursoragent@cursor.com>
An abandoned sign-in flow (consent granted, tab closed before the final code round-trip) left the pre-code granted_permissions stash in localStorage. A later sign-in by a different user would union that stale stash into its own grant set and seed it into the optimistic permission cache under the wrong user. Clear the stash when starting a new flow; it is only ever produced later during the callback pre-code hop and never needs to survive a signIn. Mirrors the data-exchange initiator binding. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Prevent cross-flow permission cache seeding and leftover data-exchange initiator binding after mint failure or a new sign-in. Co-authored-by: Cursor <cursoragent@cursor.com>
The real DELETE /v1/highlights/{id} returns 200 application/json with an
empty body. ApiClient called response.json() on it, which throws, so
every successful delete surfaced as a failed write. The highlights
machine then (correctly, for a real failure) reverted its remove
overlay, repainting the stale highlight until the refetch caught up —
the "removed highlight flashes back" bug.
Read the body as text and treat empty as undefined. Regression coverage
stubs global.fetch and drives the real client + machine + adapter, the
only test shape that can catch response-parsing bugs (client-method
mocks cannot).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Flips the YPE-1034 dark-launch flag. The vapor delete-flash bug is fixed and regression-tested; the optimistic-overlay staleness trade-off (local overlay wins over server truth until a refetch reflects the write) is signed off as acceptable for launch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Highlight fills now render at 1.0 opacity in light mode and 0.3 in dark mode (Swift BibleTextView+Rendering parity), replacing the flat 0.35. Highlighted verse number labels paint white in dark mode so they stay legible over the fill; both fill and label styles clear on removal. ADR YPE-642 corrected to record the as-built values. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The logo component hardcoded an English aria-label default, which bypassed localization. The label is now a required prop; the sign-in dialog already passes the localized string. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The stash payload is already JSON; comma-joining the permissions into a string was a needless round-trip. Unknown shapes fail closed (the stash only lives across a single in-flight OAuth redirect). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The learned-preferences blocks were one developer's session notes, not repo guidance. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Six stacked-PR changesets merge into highlights-server-backed.md: one consumer-facing entry announcing server-backed highlights as live, with an overt callout that earlier releases accidentally shipped a demo-only localStorage implementation. Controlled-mode and aria-label entries stay separate; the aria-label entry now covers the required-label change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
Summary
Server-backed BibleReader highlights go live: the dark-launch flag flips on after a final round of auth hardening, a fix for successful deletes being misread as failures, and Swift-parity UX corrections. Last PR of the YPE-1034 stack (#283 → #285 → #286 → this one); the consolidated changelog announces the launch and the removal of the accidentally shipped localStorage-only highlights.
Changes
HIGHLIGHTS_LIVEon) — signed-in readers' highlights persist to their YouVersion account; six stacked changesets are consolidated into one launch changelog.ApiClienttreated empty-body 2xx JSON responses (the real DELETE shape) as failures, so the machine reverted the successful removal; regression tests now drive the real client through a stubbedfetch.signInalso clears stale permission stashes so an abandoned flow can't leak grants across users.appName/signInPromptMessageprops, Platform wordmark, comma-joinedrequested_permissionswire format, and a permission cache seeded from token scope so a highlight tap doesn't re-prompt right after OAuth.serverColors/refetchidentities, shareduseApiClient), the logo's accessible label is a required localized prop, the pending-permissions stash storesstring[], and personal workflow notes are stripped from AGENTS.md.Start here: change 3 — auth-boundary logic; the fail-closed path is the part worth adversarial review.
Test plan
pnpm build && pnpm typecheck && pnpm lint && pnpm test: all green (972 tests — core 349 / hooks 288 / ui 335)Needs manual check: remove a highlight in the signed-in vite demo and confirm it vanishes with no flash — the fix is test-covered, but the live confirmation was blocked by a signed-out demo session.
Greptile Summary
This PR hardens the highlights feature for production (
HIGHLIGHTS_LIVEflipped totrue) with four targeted fixes on top of the restored stack from #288: cross-user grant isolation in the data-exchange callback (fail-closed via initiator binding), 401/403 re-prompt for resumed pending writes, several code-quality cleanups (reference-stableserverColors, shareduseApiClient, stablerefetchidentity viafetchFnRef), and an xstate rollback from 5.32.5 → 5.32.4.saveDataExchangeInitiator()is called before the token mint, the callback reads-and-clears it atomically, and a user or initiator mismatch degrades tofailure.signInalso clears any stale stash and initiator from a prior abandoned flow so cross-user leakage is prevented on both code paths.repromptfield was removed fromWriteOp; now anyapplyop (user-initiated or resume-applied) re-stashes and re-prompts on 401/403, while removes only invalidate the cache — fixing the silent intent-drop that existed for pending resumes.serverColorsis reference-stable via auseRefequality guard,useApiClientconsolidates client construction across four hooks, andrefetchidentity is stabilized via afetchFnRefpattern inuseApiData(explicitdepsarray still drives re-fetches, so no behavior regression).Confidence Score: 5/5
The auth-boundary hardening is well-implemented and fail-closed; no grant cross-user leakage paths remain.
The core change — binding the data-exchange grant to the initiating user via a read-and-cleared stash — is correctly implemented: the initiator is saved before the async token mint, consumed atomically at callback entry, and any mismatch (different user, missing initiator) degrades to failure rather than silently saving the grant. The stash is also state-bound so a pre-code hop from a different OAuth flow cannot contribute grants. The signIn path correctly purges any prior stash and initiator before starting a new flow. The resume-write re-prompt fix and the signedOut guard in the permission-dialog state are both straightforward and well-tested. The only non-blocking note is that the OIDC_SCOPES denylist used when seeding the permission cache from token scope is a hardcoded set that could grow stale if the auth server adds new standard scopes.
packages/core/src/Users.ts — the OIDC_SCOPES denylist approach for filtering token scope before storing in the permission cache.
Important Files Changed
Sequence Diagram
sequenceDiagram participant User participant SDK as SDK participant Config as Config participant Server as Server Note over User,Server: Data-exchange grant flow with initiator binding User->>SDK: startDataExchange() SDK->>Config: saveDataExchangeInitiator(currentUserId) SDK->>Server: updateToken([highlights]) Server-->>SDK: exchange token SDK->>User: redirect to dataExchangeUrl Note over User,Server: Page navigates — redirect returns Server->>SDK: callback URL with granted status SDK->>Config: read + clear dataExchangeInitiator SDK->>Config: read currentUserId alt initiator equals currentUserId SDK->>Config: saveGrantedPermissions SDK-->>User: status granted else mismatch or no initiator SDK-->>User: status failure - re-prompt end Note over User,Server: Sign-in path with early grant stash User->>SDK: signIn() SDK->>Config: clear stale stash and initiator SDK->>Server: PKCE redirect Server-->>SDK: pre-code callback with granted_permissions SDK->>Config: stashPendingGrantedPermissions(state, grants) SDK->>Server: obtainLocation - final callback Server-->>SDK: code and tokens SDK->>Config: union URL grants + stash + scope minus OIDC SDK->>Config: saveGrantedPermissionsReviews (8): Last reviewed commit: "docs: consolidate highlights changesets ..." | Re-trigger Greptile
Context used: