fix(concurrent-keys): reject SDK keys and mobile keys scoped to a view - #795
Draft
aaron-zeisler wants to merge 3 commits into
Draft
fix(concurrent-keys): reject SDK keys and mobile keys scoped to a view#795aaron-zeisler wants to merge 3 commits into
aaron-zeisler wants to merge 3 commits into
Conversation
aaron-zeisler
force-pushed
the
aaronz/SDK-2860/reject-view-scoped-keys
branch
from
August 5, 2026 19:43
76513c7 to
c6bcde9
Compare
aaron-zeisler
force-pushed
the
aaronz/SDK-2860/reject-view-scoped-keys
branch
from
August 5, 2026 20:40
c6bcde9 to
677f834
Compare
aaron-zeisler
commented
Aug 5, 2026
aaron-zeisler
commented
Aug 5, 2026
aaron-zeisler
commented
Aug 5, 2026
aaron-zeisler
commented
Aug 5, 2026
aaron-zeisler
commented
Aug 5, 2026
aaron-zeisler
commented
Aug 5, 2026
aaron-zeisler
commented
Aug 5, 2026
aaron-zeisler
commented
Aug 5, 2026
aaron-zeisler
commented
Aug 6, 2026
aaron-zeisler
commented
Aug 6, 2026
…ial wire format Adds hasViews to each sdkKeys[]/mobileKeys[] entry and carries it onto the accepted-key params. Nothing reads it yet — the filter that consumes it follows in the next commit. A plain bool rather than a pointer: an absent field means "not view-scoped" and there is no third state, unlike expiry where nil genuinely means permanent. Old-format payloads synthesize their accepted keys from the singular sdkKey/mobKey and so never set it, which is correct — the backend forbids views on a default key.
The sdkKeys[]/mobileKeys[] arrays hand relay every non-default credential for an environment, including keys scoped to a view. A view-scoped key is only entitled to a subset of the environment's flags, but relay serves the whole environment payload and has no view support — so accepting one would silently over-deliver every flag to an SDK that must see only a subset. Filter them in BuildAcceptedSet, the single funnel for both the auto-config stream and the offline archive. They never enter the accepted set, so an SDK presenting one is rejected because the credential is absent from the lookup map, and no state for it reaches /status, event forwarding, or the expiry ticker. BuildAcceptedSet returns the identifiers it dropped so the handlers that apply a set can WARN; it takes no logger because the stream parse boundary validates every payload and would double-log each one. Rejection is keyed by credential value rather than evaluated per entry. The accepted-set builder is first-wins, so if two entries carry the same value and only one is marked, per-entry filtering would admit the credential via the unmarked entry while still reporting it rejected. Tainting the value makes one marked entry sufficient whichever position it holds. Excluding the designated key from the taint set is also what keeps the anchor guard structural: the marker is disregarded on the anchor and the primary mobile key, because dropping a designated key would take the whole environment down and the backend forbids views on a default key in the first place. Acquiring a view mid-session needed no new code: reconcileAcceptedKeys already revokes any key absent from the desired set immediately rather than on an expiry timestamp, and RemoveConnectionMapping unmaps before the streams are torn down so a reconnect is rejected.
…ormed-payload error AcceptedSetBuilder.Build returned a bare sentinel when no SDK key survived, while every other rejection was a *MalformedCredentialSetError. That framed an empty set as a caller mistake, which it no longer is: the payload reaches it by combining an undefined anchor with an sdkKeys[] array that is either empty or made up entirely of keys relay excludes — the view-scoped case being new. Return the same type, with a message describing the payload rather than the builder, since that is what an operator reading the log can act on. With the type now uniform, the offline handlers no longer branch on it. Both branches already preserved previous credentials and differed only in message text, and the fallback text was the wrong one: it claimed the environment was auto-configured, and in UpdateEnvironment that the environment failed to initialize when it had already been running. Log the accurate message unconditionally instead. Stop documenting the error type as a contract. Nothing in production inspects it now, and BuildAcceptedSet cannot honor such a promise on its own — the guarantee would depend on a function in another package whose author has no reason to know about it. The taxonomy stays pinned by tests, which is where a claim like this can actually be enforced; TestBuildAcceptedSet_NoSDKKeys now covers both shapes that reach the empty-set path, as it is the only test that would catch this classification regressing.
aaron-zeisler
force-pushed
the
aaronz/SDK-2860/reject-view-scoped-keys
branch
from
August 6, 2026 20:12
706ca7a to
ffc86d5
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.
Summary
Makes the Relay Proxy reject SDK keys and mobile keys that are scoped to a view, so they never enter the accepted credential set. Filtering happens in
envfactory.BuildAcceptedSetso a view-scoped key is absent from the credential lookup map entirely. An SDK presenting one gets a 401.Background
A view-scoped key (payload filtering v2) is only entitled to a subset of its environment's flags. Relay serves the entire environment payload and has no view support, so accepting one would silently over-deliver every flag in the environment to an SDK that only wants a subset.
Commit details
The first two commits are the feature, the third is a cleanup task.
1.
chore: carry the hasViews marker through the credential wire format— 3 files, +81/−13Adds a
hasViewsflag to each SDK key and mobile key the backend sends us, and passes it inward to where relay assembles an environment's credentials. Nothing acts on it yet.2.
fix: reject SDK keys and mobile keys scoped to a view— 5 files, +271/−41Discards view-scoped keys while the payload is being read, so they never join the set of credentials an environment will accept and an SDK presenting one is turned away as unrecognized. An environment's own default SDK key and mobile key are deliberately exempt, since refusing either of those would take the whole environment offline.
3.
refactor: make every credential-set rejection a malformed-payload error— 7 files, +52/−46When a payload left us with no usable SDK key, relay reported that as a different kind of error than it uses for other unusable payloads, which made offline mode log a misleading reason. They are now the same kind, so nothing downstream has to tell them apart.
Integration coverage
Integration tests for this change are stacked in #796 .
Open questions for review
Should
/statusshow the rejected view-scoped keys?, so support can diagnose without asking the customer for logs? Deliberately excluded here: it would require carrying view-scoped values intoEnvContext, reintroducing exactly the state the ingestion filter exists to avoid, and would add a field to a rep that was only just stabilized.