feat: [SDK-5237] never sign the subscription-id routes and do not gate them on a token - #1506
Merged
Merged
Conversation
sherwinski
added this pull request to stack #1507
September 22, 2026 02:31
16 tasks
sherwinski
force-pushed
the
sherwin/sdk-5237
branch
from
September 22, 2026 21:28
35bc9d8 to
824d9cc
Compare
fadi-george
approved these changes
Sep 22, 2026
… token
The server rejects a bearer on PATCH subscriptions/{id} with 401 and
accepts the request without one. It ignores a bearer on DELETE
subscriptions/{id}. updateSubscriptionById and deleteSubscriptionById
now refuse a jwt at the type level.
UpdateSubscriptionOperation and DeleteSubscriptionOperation set
_requiresJwt to false, so the dispatch gate does not hold them for a
token. The anonymous suppression at enqueue and the start-up purge
exempt them too; the gate lets them through, so a pending remove or
opt-out after logout still goes out.
The page bundle grows by 13 B gzipped to 44.57 kB.
…no token A 401 on a request that sent no bearer says nothing about the stored token. The unauthorized handler now drops such operations instead of invalidating the token and re-queueing them, which would loop on every new token the app supplies. page.es6.js grows by 2 B gzipped.
The SDK never signs PATCH or DELETE subscriptions/{id}. A 401 there
means the server contract changed, and the operation is dropped, so
the loss must be visible. page.es6.js grows by 51 B gzipped to 44.58 kB.
sherwinski
force-pushed
the
sherwin/sdk-5237
branch
from
September 22, 2026 22:04
824d9cc to
ce2962e
Compare
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.
Description
1 Line Summary
PATCH subscriptions/{id}andDELETE subscriptions/{id}are never signed, and their operations do not wait for a token.Details
Last pull request of the M3 stack for Identity Verification for Web SDK. Stacked on SDK-5238.
Re-scope. The ticket asked to forward the token per the server answer. The SDK-5220 matrix says the server rejects a bearer on
PATCH subscriptions/{id}(401auth-2on the legacy Turbine path, the request proceeds without a header) and ignores a bearer onDELETE subscriptions/{id}(handlerWithoutAuth). So the answer is: do not sign either route, and do not hold either operation for a token. iOS made the same call (SDK-5137, item 13). Android attaches the bearer anyway; web does not follow it here.src/core/requests/api.tsupdateSubscriptionByIdanddeleteSubscriptionByIdtakeOmit<RequestMetadata, 'jwt'>, so a caller cannot pass a token by accident. The doc comments say why.src/core/operations/UpdateSubscriptionOperation.ts,DeleteSubscriptionOperation.ts_requiresJwtreturnsfalse. M2 added this hook onOperationfor endpoints that do not check auth; these are the first 2 uses. The dispatch gate inOperationRepo._hasValidJwtIfRequiredlets them through without a stored token.src/core/operationRepo/OperationRepo.ts_requiresJwt === false. Both exist because "an anonymous operation can never dispatch: the gate needs a token". For these 2 operations the gate does not need a token, so the reason does not hold. Without the exemption, an opt-out or a subscription removal after logout would be dropped with a "call login first" warning, and the server would keep the subscription._handleFailUnauthorizedreturnsfalsefor an operation with_requiresJwt === false, so the caller drops it. A 401 on a request that sent no bearer says nothing about the stored token. Without this guard, such a 401 would invalidate a valid token, ask the app for a new one, and retry unsigned in a loop (review finding on SDK-5236).src/core/executors/SubscriptionOperationExecutor.ts_updateSubscriptionand_deleteSubscriptionlog an error on a 401 and return_FailNoretry. The SDK never signs these routes, so a 401 means the server contract changed (for exampleupdate_subscription_block_unauthorizedturned on). The operation is dropped either way; the log makes the loss visible instead of silent.Bundle size.
page.es6.jsgrows by 91 B gzipped to 44.58 kB (2_requiresJwtgetters, 3 gate exemptions, the 401 log). The limit moves to the measured size.Systems Affected
Validation
Tests
Info
src/core/executors/executorsIv.test.ts: IV active with a stored token, update and delete subscription send noAuthorizationheader and hit/apps/{appId}/subscriptions/{id}; a 401 on either stays_FailNoretryand logs an error.src/core/operations/Operation.test.ts:_requiresJwtistruefor every operation except update and delete subscription.src/core/operationRepo/OperationRepo.test.ts: under IV, an anonymousDeleteSubscriptionOperationis queued at enqueue with no warning and passes the dispatch gate; the start-up purge keeps it and still drops the other anonymous operations; a 401 on an operation that needs no JWT is dropped, keeps the stored token, and fires nouserJwtInvalidatedevent.vp check,vp test(764 tests), andvp run build:prodpass.Checklist
Programming Checklist
Interfaces:
Functions:
Typescript:
Other:
elem of arraysyntax. PreferforEachor usemapcontextif possible. Instead, we can pass it to function/constructor so that we don't callOneSignal.contextScreenshots
Info
Not needed. No user-visible change.
Checklist
Related Tickets