Repository navigation
feat(notifications): bootstrap an authenticated WebView session for member pages - #19
Merged
Aalv3 merged 1 commit intoSep 9, 2026
Conversation
…ember pages First-party member pages could not open in the app because the WebView carries cookies, not the User API key, and WebViewComponent's navigation policy correctly refuses to load a canonical page into an unauthenticated Discourse session. PR #16 routed there anyway, which produced a blank screen stuck on "Still loading...". The supported contract closes the gap. webViewSession posts the app's existing RSA public key to /user-api-key/otp with the governed auth_redirect and pkcs1 padding, using site.jsonApi so the existing User-Api-Key and User-Api-Client-Id headers, rate-limit buckets and cooldowns all apply. The returned redirect_url is parsed with the same helper the authorization callback uses and the one-time password is decrypted with the same JSEncrypt private key. No second cryptographic implementation, and no new server endpoint. The WebView then loads /session/otp/<otp>, the member completes the existing confirmation form, and only then is the originally requested destination loaded. The confirmation step is never bypassed: it is what sets the session cookie. An existing session is reused rather than spending an OTP: a live Discourse _t cookie means the destination loads directly. The cookie package is required lazily so importing this module does not pull a native dependency into every suite that reaches Discourse.js. The WebView policy relaxation is scoped to a bootstrap the app itself started. Authorization requires a pending destination, and the window closes the moment that destination loads, so this is not a standing "any internal page opens" rule. The original guard is untouched for everything else. Safety holds throughout. The decrypted OTP must match the route's hex constraint before it is interpolated into a path. Off-origin destinations are refused before an OTP is minted. Non-staff /admin, malformed payloads, unknown types and unauthenticated callers remain denied. Any failure or cancellation ends in a bounded explicit state rather than a blank WebView. Native Topic and MemberProfile routing, notification read-marking and the private member-photo credential boundary are unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fk48MTrNBBSZeLvcJmc8SR
Aalv3
merged commit Sep 9, 2026
335419e
into
fix/auth-failure-classification-20260830
4 checks passed
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.
Backlog #9. Implements the authenticated
first_party_webpath using the supported OTP contract. JS-only, native-only — no server change. Supersedes PR #18 (fallback), fixes the destination-render FAIL from PR #16.Auth / session state flow
classifyFirstPartyMemberRoute→first_party_webdestinationPresentation→{ kind: 'web', url }Discourse._openFirstPartyWeb→resolveWebSessionEntry:_tcookie? → load the destination directly, no OTP mintedPOST /user-api-key/otp(public_key,auth_redirect,padding=pkcs1) viasite.jsonApi→ parseredirect_url→ RSA-decryptoneTimePassword→/session/otp/<otp>with the destination rememberedReuse, not reimplementation
ensureRSAKeys/rsaKeys.public/decryptHelperare the same machinery as the authorization flow, andparseAuthCallbackParametersis the same redirect parser.site.jsonApicarries the existingUser-Api-Key/User-Api-Client-Idheaders and inherits the rate-limit buckets and cooldowns.Security
/^[0-9a-f]+$/) before path interpolation —../../admin,abc/defand empties are refused./admin, malformed payloads, unknown types, unauthenticated callers: all still denied.Tests — 96 suites / 780 tests
New
webViewSession.test.js(25) covers: OTP request shape and credential reuse; RSA decrypt reuse; unauthenticated and missing-key refusal before any request; missing/off-originredirect_url; non-hex OTP rejection; 429 propagation; bootstrap URL constraints; session reuse skipping the OTP; empty/absent/unreadable cookie handling; off-origin destination refusal; presentation mapping for all six classes; native routing unchanged; denied cases; bounded failure copy; and wiring assertions thatopenUrlnever opens the WebView without session resolution and that the policy relaxation is bootstrap-scoped.validate:system: format/lint clean,verify:ota17/17,verify:ios-auth12/12, native Release BUILD SUCCEEDED.One pre-existing PR #16 test asserted the old
route.dispositionshape insideopenUrl; it now asserts the refactoredpresentation.kindform.Scope
js/webViewSession.js(new),js/notificationDestination.js(new),js/Discourse.js(+62/−17),js/screens/WebViewScreen.js(+1),js/screens/WebViewScreenComponents/WebViewComponent.js(+32), tests. Noios/,android/,vendor/, config, dependency or lockfile change. Runtimean-ios-android-1.0.0-native-2unchanged; JS-only OTA feasible.Not device-validated. No OTA published.