feat: Auth state management: in-page modal + cross-tab propagation#941
Merged
Conversation
A storage event listener watches nx-ims so disconnected tabs reconnect automatically when the user signs back in elsewhere, instead of waiting for the user to hit back. The inverse signal (sign-out elsewhere) drops the connection so we stop hammering with a token that is about to be revoked. The listener is detached on navigation-flagged disconnects so it does not leak across editor instances. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously, a collab 4401 we couldn't recover from or a daFetch 401 with no token both navigated the tab to IMS sign-in, destroying any unsaved editor state. Now we mount a <da-auth-banner> at the top of the viewport instead. The banner watches the nx-ims storage key and auto-dismisses when another tab signs in, calling refreshToken so pending state can pick up the new session. Manual "Sign in" still triggers handleSignIn for the no-other-tab case. daFetch now also tries window.adobeIMS.refreshToken() + a single retry on 401 before any user-visible disruption, which covers the cross-tab "just signed in" race. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three fixes addressing reported issues: 1. Global storage listener in scripts.js navigates every tab to '/' when any tab clears nx-ims, so an explicit sign-out is never followed by a session-expired dialog elsewhere. 2. The auth UI is now a modal <dialog> opened with showModal() instead of an unobtrusive banner, blocking page interaction until the user signs in or another tab takes them somewhere. 3. On cross-tab sign-in, the dialog now reloads the page rather than trying to refresh imslib state in place — that path didn't reliably update the dialog tab's session, leaving the dialog stuck open. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reverts the global storage listener that navigated every tab to '/' on nx-ims removal. Other tabs now stay on their page and show the session dialog as normal. The originating tab is redirected to '/' by detecting the IMS callback URL after sign-out: returning from IMS without an nx-ims flag means the round-trip was a sign-out (sign-in would have populated nx-ims), so we replace location with '/' before loading the area that the user signed out from. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous logic redirected to '/' whenever a tab returned from IMS without a token, which also fired when the user clicked "Sign in" on the modal but IMS didn't grant a token (e.g. SSO cookie scrubbed by a recent sign-out, or user cancelled the form). Snapshot nx-ims before imsReady touches it so we can tell sign-in (was 'true') from sign-out (was empty) and only redirect for the sign-out case. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1. Add type=\"button\" to the modal's Sign in button. Inside a native <dialog>, a <button> with no type defaults to type=\"submit\" and triggers the dialog's implicit form submission, closing the modal and potentially navigating the page before the click handler can run. 2. Detect sign-in vs sign-out callbacks by inspecting the URL hash for access_token= rather than tracking nx-ims across navigation. nx manages the flag inside handleSignIn/handleSignOut but nx2 does not, so any logic relying on nx-ims breaks under nx2. The access_token presence in the IMS callback URL is a reliable signal in both. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ents The earlier per-feature storage listeners keyed off the nx-ims flag, but nx2's handleSignOut doesn't touch nx-ims — only imslib's internal session keys change. After imsReady, watch every storage event and re-evaluate adobeIMS.getAccessToken(): if we just lost the token, show the modal; if we just gained one, reload to pick up the fresh session. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three storage-event listeners were doing overlapping work: one in createConnection (nx-ims), one in the modal (nx-ims), and one global monitor in scripts.js (live adobeIMS state). The global monitor works under both nx and nx2 and supersedes the other two: - createConnection's listener and its disconnect monkey-patch are gone. The proactive provider.disconnect() it ran on sign-out only saved the brief window before the modal blocks the user anyway, and its sign-in reconnect was already dead code (the global monitor reloads first). - The modal's _onStorage / _reload / _goHome (and the dead _dismiss helper + hideAuthBanner export) are gone. The global monitor handles the same transitions. - The 4401 bail's signed-in check switched from localStorage.nx-ims (managed only by legacy nx) to lastSentToken, which is in scope and reliable under both nx and nx2. Net: ~234 lines removed from the working tree. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two small refinements: 1. The cross-tab auth monitor moved from scripts.js into utils.js, where it sits next to initIms and getAuthToken. initIms attaches it once on first call as a side effect, so every page that touches auth gets the monitor for free without scripts.js needing to know about it. 2. The session-expired dialog now uses autofocus + tabindex=-1 on the dialog itself, so showModal() lands focus on the dialog rather than on the Sign-in button. Tab still reaches the button as before. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The auth monitor in utils.js detects cross-tab sign-out via storage events and mounts the modal, but never touches the WS — and the server only checks the token at WebSocket handshake, not on the established connection. So the prose/index.js connection-close handler's 4401 path never fires from cross-tab sign-out. Dispatch a da-auth-lost custom event from the auth monitor; the createConnection wires a one-line listener that disconnects + reconnects the provider, forcing a fresh handshake that hits the now-401 token and runs the 4401 branch (idempotent with the modal already shown). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The auth monitor in utils.js already covers cross-tab sign-out by mounting the modal, and the prose 4401 handler fires naturally for the scenarios it's designed for (network blip with revoked token, 10-min blur+focus reconnect, initial connect with bad token). Forcing a WS disconnect+reconnect on every cross-tab sign-out just to re-mount the already-mounted modal added complexity without product value — the server-side 4401 can be verified directly via DevTools network toggle. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The modal blocks user input, but the underlying WebSocket stays open and da-collab still uses the cached webSocket.auth (captured at handshake) when writing to da-admin. Other users' edits flowing into the ydoc would be persisted under an auth list that includes the signed-out user's stale bearer. Disconnecting the active provider when the auth monitor detects sign-out closes that window without re-introducing the custom-event or per-component listener layer. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Hello, I'm the AEM Code Sync Bot and I will run some actions to deploy your branch.
|
The "save and restore" afterEach pattern propagated any pre-existing leak — if nx-ims was set when the describe started, restoring it on the way out re-leaked it to subsequent test files whose getAuthToken then tripped initIms()'s dynamic import with an unset getNx. Always remove nx-ims in afterEach of the describe blocks I added or touched (utils.test.js getAuthToken/daFetch, prose/index.test.js createConnection, da-auth-banner.test.js). The flake reproduced in ~10% of full-suite runs; now stable across 15 consecutive runs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Member
auniverseaway
left a comment
There was a problem hiding this comment.
- Looks like there's some test failurs.
- I'd make sure to use da-dialog if possible.
Replace the custom <da-auth-banner> Lit element + CSS with a thin factory that creates a da-dialog (matching da-not-found's pattern): title, body paragraph, action button. Removes ~60 lines of duplicated modal scaffolding and the standalone CSS file. Also wrap initIms's dynamic import inside its try/catch so a stray nx-ims leak under a test session that hasn't called setNx no longer throws "undefined/utils/ims.js". This was the last remaining flake in the full-suite test runs (reproduced ~1-in-12 before, 0-in-30 after). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
da-dialog's close button lives inside its shadow root, so external CSS
can't reach it through the encapsulation boundary. Restore a
da-auth-banner.css with just the .da-dialog-close-btn { display: none }
rule and inject it into the dialog instance's shadow root via
adoptedStyleSheets after updateComplete.
The auth modal is intentionally blocking — the only escape is to sign
in (or be reloaded via the cross-tab auth monitor).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
One CSS declaration is shorter as a CSSStyleSheet literal at module init than as a separate .css file routed through nx's loadStyle. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
auniverseaway
approved these changes
May 15, 2026
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.
Why
Three related gaps in how da-live handles auth state changes:
daFetch401 or an unrecoverable collab4401close calledhandleSignIn(), which redirects to IMS. Any unsaved editor state was lost in the round-trip.handleSignOut(which doesn't touchnx-ims), signing out in Tab A left Tab B's editor fully functional. The user could keep typing — and da-collab kept persisting Tab B's edits to da-admin using the bearer it cached at WS handshake, since access tokens are stateless and survive IMS sign-out until their natural expiry.What
<da-auth-banner>modal. Native<dialog>withshowModal()that blocks page interaction; "Sign in" button triggers the standard IMS flow. Mounted viashowAuthBanner()(idempotent — second call returns the existing instance). Replaces what used to be a redirect.storageevent listener (attached as a side-effect of the firstinitIms()call) that re-readswindow.adobeIMS.getAccessToken()on every change. Works under both nx and nx2 because it inspects live imslib state instead of keying offnx-ims. On auth loss → mount the modal +wsProvider.disconnect()(so da-collab can't keep authoring writes with Tab B's cached bearer). On auth gain →window.location.reload()to pick up the new session cleanly.daFetchrefresh-and-retry on 401. Before any user-visible disruption, ask imslib to refresh the token and retry the failed request once. Rescues the cross-tab "just signed in, but this tab hasn't caught up yet" race. Falls through to the modal only if there's truly no token.prose/index.js4401 handling. When the collab WS reconnect handshake gets4401 'auth'from da-collab (new in da-collab#149), refresh the token and retry. If unrecoverable and the user had a token, mount the modal.scripts.jsinspects the IMS callback URL (old_hash=present,access_token=absent → sign-out return) andwindow.location.replace('/')beforeloadArea()runs, so the sign-out tab doesn't briefly mount the editor + modal for the doc the user just left.Trust model notes
da-collab's auth check is handshake-only —
webSocket.authis cached and never re-verified. The only durable protections against post-sign-out writes are:closeConnon every connection in the room. So token rotation past expiry self-heals — every tab gets force-reconnected with whatever tokengetAuthToken()reads live (which imslib has rotated by then). This is what makes 24+ hour sessions work transparently.wsProvider.disconnect()the moment cross-tab sign-out is detected, so Tab B drops out of the room before any further yjs updates can flow through its still-authed connection.