fix(sdk): verify React Native wallet response delivery - #485
Open
sirdeggen wants to merge 5 commits into
Open
Conversation
The ReactNativeWebView substrate resolved a BRC-100 invocation from any message event whose payload matched the request identifier. It never established that the message came from the React Native bridge, and it compared origins only when a caller configured an exact domain, so the default wildcard target that WalletClient discovery uses accepted a response from any browsing context sharing the page. A React Native host answers by injecting the response into the document that made the call, so the listener now requires that delivery shape before it reads the payload: the event carries no source (a host-synthesized event), comes from this window, or comes from the frame bridging for it, and a browser-attested origin has to belong to this document or to the configured wallet origin. Framed documents, openers, and sandboxed frames reporting an opaque origin can no longer answer an invocation. BRC-100 framing is untouched: the invocation is still one stringified CWI envelope over window.ReactNativeWebView.postMessage, responses are still matched by type, identifier, and isInvocation, and an exact configured domain keeps pinning responses with the same error. Bridge injections without an origin, window.postMessage responses, and host-stamped vendor origins continue to resolve, so hosts need no change. Refs #483 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015aQnVKCDEecajUHTV2RSNG
sirdeggen
requested review from
BraydenLangley,
tonesnotes and
ty-everett
as code owners
August 15, 2026 04:40
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
The delivery check compared a browser-attested origin whenever the event carried a source. A host that synthesizes its response inside the WebView document may set that source to this window while stamping an origin the browser never attested, or leaving it empty, so strict comparison could drop a conforming BRC-100 response and fail wallet discovery on mobile. Origin comparison now applies only where the browser actually attests it: a message relayed by the host frame, which is a separate browsing context. A response injected into this document is same-origin by construction, so the origin the host stamps on it is accepted, exactly as before this branch. An exact configured domain still pins every response. Inlining the delivery predicates back into the listener also keeps the esbuild browser bundle inside its budget: 559,877 raw against a 560,000 limit, where the helper functions had put it 7 bytes over. Refs #483 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015aQnVKCDEecajUHTV2RSNG
The exact-head Sonar analysis reported one new finding on this branch: the delivery guards raised the response listener's cyclomatic complexity from 9 to 15, past the profile's threshold. The guards move into one isBridgeDelivered predicate, which reads better than the inline conjunction chain and returns the listener to 10 with the predicate itself at 5. The tests hoist the origins and the host-frame stub they now repeat into constants so no new duplicated-literal finding takes its place. Behaviour, wire format, and error identities are unchanged, and the esbuild browser bundle stays inside its budget at 559,907 raw bytes against a 560,000 limit. Refs #483 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015aQnVKCDEecajUHTV2RSNG
The wallet client browser contract failed on the exact head: the SDK's delivery check grew the packed @bsv/wallet-toolbox-client bundle past two esbuild ceilings that sat within a few dozen bytes of the measurement. Reading window.origin instead of window.location?.origin is the same document origin per the HTML standard, in fewer bytes, and reclaims most of the growth. What remains is the irreducible cost of verifying that a response reached this document the way the React Native bridge delivers one, so the client budget is ratcheted with its measurement rather than dropping the check or the host-frame relay that iframe consumers depend on. Reformulating the predicate as one expression was measured too and saved nothing, so the clearer early-return form stands. Measured for @bsv/wallet-toolbox-client, esbuild: raw 1252506 against a new 1253000 ceiling, gzip 345035 against a new 345500, brotli 277222 inside its unchanged 277300. Vite raw 1606919, gzip 377313, and brotli 296637 stay inside their unchanged ceilings, as do both wallet mobile profiles. Composition is unchanged at 101 vite modules across @bsv/sdk, @bsv/wallet-toolbox-client, hash-wasm, and idb. Refs #483 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015aQnVKCDEecajUHTV2RSNG
|
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.



Program and scope
ReactNativeWebVieworigin validation)ReactNativeWebViewsubstrate resolved a BRC-100invocation from any
messageevent whose payload matched the request identifier.It never established that the message came from the React Native bridge, and it
compared origins only when a caller configured an exact domain — so the default
wildcard target that
WalletClientdiscovery uses (new ReactNativeWebView('*', …))accepted a wallet response from any browsing context sharing the page. The state
SonarCloud had analysed when ReactNativeWebview origin validation #483 was filed (
55f9eb2, the commit before fix: normalize BRC-100 bytes across wallet transports #482merged) had no origin check at all; fix: normalize BRC-100 bytes across wallet transports #482 added the configured-domain comparison
minutes later, and this change completes the hardening by verifying delivery
before the payload is read.
XDMsubstrate (already verifiesisTrusted,e.source === window.parent, and origin); BRC-100 envelope/wire format; thets2md-generatedpackages/sdk/docs/reference/*.mdfiles, which are stalerepo-wide and would add ~2,500 unrelated lines if regenerated here.
7cebd63cf2b437c60e48a28670a8db14ef72af0bImpact
Affected packages/services and intended patch versions (publication occurs only
through the release workflow after approval):
@bsv/sdk— source2.4.1, published2.4.0, release typepatch(already thepending patch; no further bump required). No exported signature changes: the
constructor,
invoke, error identities, and the CWI envelope are unchanged.@bsv/wallet-toolbox-client— no source change; its browser platform budget isratcheted for the SDK growth (evidence under Verification).
What the listener now requires
A React Native host answers by injecting the response into the document that made
the call, so a response must arrive that way.
isBridgeDeliveredchecks thisbefore
JSON.parse(e.data)— an untrusted context's payload is never read:source(a host-synthesizedMessageEvent),comes from this window (
window.postMessageinjected by the host), or comes fromthe frame bridging for it (
window.parent, which is this window in a top-leveldocument). Framed documents, openers, and sandboxed frames reporting the opaque
"null"origin are ignored.browsing context, so its origin is browser-attested and has to be this document's
origin (
window.origin) or the configured wallet origin. An event synthesizedinside this document is same-origin by construction, and the origin a host stamps
on it is the host's own choice rather than an attestation, so it is accepted as
before.
origin with the same error string, unchanged from
main.Compatibility (BRC-100 / React Native)
Unchanged on the wire: one stringified CWI envelope over
window.ReactNativeWebView.postMessage, responses matched bytype,id, andisInvocation, the sameWalletError(description, code)and byte normalisation.Host behaviours that keep working, each covered by a test:
injectJavaScript+dispatchEvent);window.postMessage(response, '*')injected into this document;sourceto this window with an empty origin or ahost-stamped token such as
react-native;(the existing
WalletClientexpectation that a BRC-100 originator is not aMessageEvent origin);
configured wallet domain;
Verification
pnpm health:check— pass (38 projects, 0 contract findings, 0 control errors),re-run after merging
maininto the branchpnpm lint— pass (oxlint,--deny-warnings)pnpm format:check— passpnpm build && pnpm typecheck— pass across all workspace packagespnpm --filter @bsv/sdk test— 156 suites, 5,924 tests passnode ../../scripts/check-browser-package.mjs .(SDK) — passpnpm --filter @bsv/wallet-toolbox-client run test:browser— passpnpm --filter @bsv/wallet-toolbox-mobile run test:mobile— passimplementation (
3 failed, 20 passed) and pass after it.7cebd63are terminal and successful (run31925984019),
including
merge-gate,Quality gate — zero new Sonar findings,CodeQL,Platform / wallet browser,Platform / wallet mobile,Platform / browser packages, every coverage shard, mutation, and affected-dependent tests. Skips arescope-based and validated by the merge gate.
7cebd63merges currentmainintothe branch to clear a
behindstate;main's only new commit was the publishedversion-sync automation, which touches no file this PR changes. The same 41 checks
were already green on
ec3b250(run31899287547).
Earlier heads each failed once and were diagnosed rather than waived:
20eecdc— quality gate: 1 new Sonar finding. The inline delivery guards raisedthe listener's cyclomatic complexity from 9 to 15, past the profile threshold.
5258384factored them intoisBridgeDelivered(listener back to 10, predicate5) and hoisted repeated test literals into constants so no duplicated-literal
finding replaced it; Sonar then reported 0 new issues.
5258384—Platform / wallet browser:esbuild browser bundle raw size 1252526 exceeds budget 1252500. Reproduced locally byte-for-byte and fixed inec3b250.The BRC-100 byte-boundary contract test for this file still passes.
ReactNativeWebView.tslocally: 98.5 % statements, 91.66 % branches, 100 %functions, 100 % lines.
packages/sdkglobal 94.76 / 87.08 / 95.81 / 95.51against 85/80/85/85 thresholds. 9 tests added.
@bsv/sdkesbuild raw559,731 → 559,887(limit560,000); Vite raw741,120 → 741,345(limit742,000); UMD raw554,475 → 554,625(limit555,000). No SDK budget was raised.@bsv/wallet-toolbox-clientesbuild raw1,252,506and gzip345,035againstprevious ceilings of
1,252,500and345,000— 6 and 35 bytes over. Ceilingsratcheted to
1,253,000and345,500; brotli277,222and all three Vitefigures (
1,606,919/377,313/296,637) stay inside their unchangedceilings. Composition is unchanged: 101 Vite modules across
@bsv/sdk,@bsv/wallet-toolbox-client,hash-wasm,idb; 167 esbuild modules.@bsv/wallet-toolbox-mobilemetro1,660,280and hermes3,366,013raw stayinside unchanged ceilings.
window.origininstead ofwindow.location?.originreclaimed most of thegrowth, reformulating the predicate as a single expression saved zero bytes, and
the only larger saving available was dropping the host-frame relay allowance,
which iframe consumers with a shimmed bridge depend on. Reviewers who prefer the
tighter budget can ask for that allowance to be dropped instead.
compatibility, public API, artifacts, dependencies, docs, and operations
scope-based skip is expected and validated by the merge gate
Security and dependencies
audit results were reviewed — not applicable, no dependency change
(including accepted or false-positive issue states) and zero unreviewed hotspots;
Sonar's aggregate
Quality Gate passedverdict alone is not merge evidence —the repository's own
Quality gate — zero new Sonar findingsjob passed on7cebd63, with 0 new issues, 0 accepted issues, and 0 security hotspotsthe browser budget ratchet is a measured baseline change under
governance/browser-artifact-policy.json, not a suppressionand removal condition — none was added
Note on the Sonar evidence:
sonarcloud.iois blocked by the egress policy of theenvironment this change was developed in, so individual issues could not be opened
from here; findings were reproduced locally with
eslint-plugin-sonarjsandconfirmed against the hosted gate. The finding class behind #483 was reconstructed
from the analysed commit: only
XDM.tsandReactNativeWebView.tsregistermessagelisteners, and at55f9eb2the latter reade.datawith no origin checkwhatsoever — the S2819 "verify the origin of the received message" shape.
Dependency evidence
Not applicable; no dependency, lockfile, or manifest change.
Release and operations
controlling program —
@bsv/sdkalready carries the pending2.4.1patch;@bsv/wallet-toolbox-clientships no source changecontainer, service, or deployment surface is touched
the
@bsv/sdkchangelog,governance/package-release-notes.json, and theregenerated
docs/reference/package-api-migrations.mdare current, and theclass TSDoc documents the new policy.
packages/sdk/docs/reference/wallet.mdis
ts2mdoutput that is already stale againstmainfor unrelated reasons;regenerating it belongs in a dedicated change.
Completion evidence
passing checks, resolved alerts, measurements, or an approved exception —
ReactNativeWebview origin validation #483 will be updated after merge