Repository navigation
feat: [SDK-5465] broadcast a worker 401 to the page so the page invalidates the JWT - #1528
Conversation
abdulraqeeb33
left a comment
There was a problem hiding this comment.
The compare-before-delete rule matches the page, and the JWT stays out of the logs. The broadcast itself is not part of the service worker event, so the invalidation can be dropped.
ec64916 to
f9f61ea
Compare
f9f61ea to
6a8d18f
Compare
abdulraqeeb33
left a comment
There was a problem hiding this comment.
The await and the per-client catch close the earlier bug. The new test does not reach the throwing client, and a matchAll rejection on the duration path skips the outcome.
6a8d18f to
944fd78
Compare
|
Don't address, this just a note: |
After the first page view the session traffic (on_session upsert, session_time deactivate) runs in the worker only. On a 401 the worker only logged an error, because it has no token store, so an expired token could get a 401 on every focus, blur, and unload while the page never removed it and never fired userJwtInvalidated. The worker now posts WorkerMessengerCommand._JwtUnauthorized with the externalId and the rejected jwt to all window clients. The page handler in _establishServiceWorkerChannel removes the token only when it still matches the stored one, the same rule as the page on_session path and the operation queue, so a newer token is kept. broadcastToPages is a standalone function so api/sw.ts can call it without a messenger instance; WorkerMessengerSW._broadcast uses it. The page receive log now redacts a jwt in the payload, as the worker side does.
…b gets it notifyPagesIfUnauthorized is awaited from both session requests, so the broadcast finishes inside the promise the event holds open. broadcastToPages catches each client.postMessage, so a tab in the middle of closing does not stop the report to the other tabs.
sendSessionDuration awaits the broadcast in the same try as the outcome request, so a rejection from matchAll skipped the outcome. The broadcast now catches it and returns. The closing-client test queues a list for the focus check first, so the throwing client reaches the broadcast.
Every tab that got the broadcast read, compared, and deleted the token on its own. Tabs in separate processes do not see each other's localStorage delete in time, so one 401 fired userJwtInvalidated once in every tab and the app asked for N tokens. postToOnePage sends the report to the focused tab, then a visible tab, then the first tab. If the chosen tab throws, for example while it closes, the next tab in the order gets the report. Other commands still go to all tabs.
b841ca4 to
b946e46
Compare
Description
1 Line Summary
The service worker tells the page which JWT the server rejected, and the page removes that token and fires
userJwtInvalidated.Details
Stacked on #1527 (SDK-5466). Review only the last 7 commits.
After the first page view the session traffic (
on_sessionupsert,session_timedeactivate) runs in the worker only. On a 401 the worker only logged an error, because it has no token store. An expired or revoked token could get a 401 on every focus, blur, and unload, while the page never removed it and never fireduserJwtInvalidated. The app got no signal to refresh the token until a page request also got a 401.Worker (
api/sw.ts):notifyPageIfUnauthorizedreplaceslogIfUnauthorized. On a 401 for a signed request it still logs the error, and it postsWorkerMessengerCommand._JwtUnauthorizedwith{ externalId, jwt }to one window client: the focused one, then a visible one, then the first one. One tab only, because tabs in separate processes do not see each other's localStorage delete in time, so a broadcast fireduserJwtInvalidatedonce in every tab. Both the session upsert and the session duration update await it, so the post finishes inside the promise the event holds open and the browser does not stop the worker beforepostMessage.postToOnePageandbroadcastToPagesinworkerMessenger/sw.tsare standalone functions soapi/sw.tscan call them without a messenger instance;WorkerMessengerSW._broadcastnow callsbroadcastToPages, and other commands still go to all tabs. Both catch aclients.matchAllrejection, so a failed post on the duration path does not skip the outcome request. If the chosen tab throws onpostMessage, for example while it closes,postToOnePagesends the report to the next tab in the order.Page (
ServiceWorkerManager._establishServiceWorkerChannel): the handler removes the token only whenJwtTokenStore._getJwt(externalId)equals the rejectedjwt, the same rule as the pageon_sessionpath andOperationRepo. A newer stored token is kept._invalidateJwtis idempotent, so a repeated report in one tab firesuserJwtInvalidatedonce.Logs: the page receive log (
[WM] Page received:) now redacts ajwtin the payload, as the worker receive log already does. The broadcast log prints the command and the client URL only.Out of scope, as in the ticket: a retry of the session request with the new token. SDK-5466 covers the next
on_sessiononce a token arrives.Bundle size
Measured with size-limit against the SDK-5466 head:
OneSignalSDK.page.es6.jsOneSignalSDK.sw.jsThe page cost is the handler, the command string, and
redactJwt, which the page bundle did not include before. Thebuild:commit raises the limits to 45.50 kB (page) and 13.01 kB (worker).Systems Affected
Validation
Tests
Info
vp test run— 72 files, 881 tests pass.vp check— 0 errors, 0 warnings in the changed files.vp run build:prod— passes with the raised limits.New tests:
ServiceWorker.test.ts: a 401 on a signed upsert logs the error and posts_JwtUnauthorizedwith the external id and the token; the session request does not settle before the page gets the report; the report goes to one tab only (focused before visible before hidden, the first of several hidden tabs, and the next tab if the chosen one throws onpostMessage); amatchAllrejection on the duration path does not skip the outcome; a 200 on a signed request and a 401 on an unsigned request post nothing; a 401 on the signed session duration update posts the same message. The existing "the jwt never reaches a log line" test still passes.ServiceWorkerManager.test.ts: the page handler removes the rejected token and firesuserJwtInvalidatedonce for 2 reports; a newer stored token is kept and nothing fires; the receive log showsjwt: '[redacted]'and no log line contains the token.Checklist
Programming Checklist
Interfaces:
Functions:
Typescript:
Other:
elem of arraysyntax. PreferforEachor usemap(thefor...ofinbroadcastToPagesis the existing loop moved out of the class)contextif possible. Instead, we can pass it to function/constructor so that we don't callOneSignal.contextScreenshots
Info
Not needed: no UI change.
Checklist
Related Tickets