Feature/ios optional approov plist - #32
Conversation
The iOS native module no longer raises ApproovPropsNotFound at launch when a bundled approov.plist is absent. With native approov.config-based init this made the properties file mandatory on iOS while Android treats approov.props as optional. It now logs and runs with default properties (leaving _props nil, which is safe: all consumers nil-guard and -valueForKey: on nil returns nil). A present-but-unreadable plist still raises (genuine misconfiguration). Also fixes a nil-pointer log when the file is absent. Reported by PropertyGuru (approov.config-only native integration crashed without an empty placeholder approov.plist).
initialize() reset the token/trace headers, substitution maps, exclusion regexes and other runtime state when the config changed, but never reset the custom service mutator. A mutator installed via setServiceMutator (or the JS setServiceMutatorType wrapper) therefore persisted across an initialization boundary — violating root TESTING_REQUIREMENTS.md section 2 "Service Mutator Reset" and diverging from approov-service-okhttp, which resets the mutator to the default on init. Reset the mutator to a fresh ApproovDefaultMessageSigning (the React Native default, which performs HTTP message signing) inside the config-changed reset block, rather than the no-signing ApproovServiceMutator.DEFAULT that would silently disable default signing on every re-init. Same-config re-inits (React remounts / StrictMode / Fast Refresh) intentionally preserve runtime state and are unaffected. Adds a regression test (initializeWithDifferentConfigResetsCustomServiceMutator). Pre-existing defect, independent of the prebuilt-mutator feature work. iOS needs the equivalent reset when the Android changes are ported.
The ObjC initialize() reset block cleared token/trace headers, substitution maps and exclusion regexes on a config change but never reset the active service mutator, so a custom mutator installed via ApproovServiceMutatorBridge persisted across an initialization boundary — violating root TESTING_REQUIREMENTS.md section 2 "Service Mutator Reset". This is the iOS counterpart of the Android fix (commit 48ee5b6). Adds ApproovServiceMutatorBridge.resetToDefault() (an @objc helper, since the Swift-typed serviceMutator cannot be assigned from Objective-C) and calls it in the config-changed reset block, restoring a fresh ApproovDefaultMessageSigning (the RN default, which performs message signing). Same-config re-inits intentionally preserve state and are unaffected.
Add ApproovService.setServiceMutatorType(mask, { sign }) plus the ApproovService.ReturnDecision status flags and ApproovService.MutatorPreset presets, so an app can choose the per-status proceed/block policy — and whether the outbound request is HTTP Message Signed — entirely from JavaScript, with no native mutator code.
Android: PolicyMutator (bitmask-driven, composes ApproovDefaultMessageSigning) + setServiceMutatorType @ReactMethod. iOS: Swift PolicyMutator + ApproovServiceMutatorBridge.setPolicyMutator + RCT_EXPORT_METHOD setServiceMutatorType, behaviour identical to Android — a not-in-mask failure status blocks the request via ApproovServiceError.permanentError, the same hard failure as Android's IOException. JS: ReturnDecision flags + MutatorPreset presets (DEFAULT, ALWAYS_PROCEED, PROCEED_IF_UNAVAILABLE, PROCEED_DEV_CLEARTEXT). Message signing preserved by default; { sign: false } opts out.
Docs: USAGE.md, REFERENCE.md, CHANGELOG (3.5.16), including the security caveat that proceeding on MITM_DETECTED / REJECTED disables that protection. Tests: Android PolicyMutatorTest (JVM); iOS PolicyMutatorTests (Swift) + run_policy_mutator_tests.sh. PROCEED_DEV_CLEARTEXT is the supported opt-in for the Metro dev BAD_URL symptom (#30).
There was a problem hiding this comment.
Pull request overview
This PR (targeting release 3.5.16) extends the React Native Approov service layer with a JS-selectable request policy mutator (bitmask + presets), fixes service-mutator reset behavior across config-change re-initializations, and makes the iOS bundled approov.plist optional to avoid launch crashes for config-only native integrations.
Changes:
- Add
ApproovService.setServiceMutatorType(mask, { sign })plusReturnDecisionbit flags andMutatorPresetpresets, backed by new nativePolicyMutatorimplementations on Android and iOS. - Reset the active service mutator back to the built-in message-signing default when
initialize()is called with a genuinely different config (Android + iOS). - Make iOS
approov.plistoptional (missing file logs + defaults; unreadable file still raises), and add/extend native/unit test coverage and documentation.
Reviewed changes
Copilot reviewed 20 out of 22 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| USAGE.md | Documents JS-driven policy selection (presets, raw bitmask, signing option) and re-init reset semantics. |
| REFERENCE.md | Adds API reference for setServiceMutatorType, ReturnDecision, and MutatorPreset. |
| CHANGELOG.md | Adds 3.5.16 release notes for the new policy API and the two fixes. |
| package.json | Bumps package version to 3.5.16. |
| package-lock.json | Updates lockfile version fields to 3.5.16. |
| index.js | Adds Proxy wrapper for setServiceMutatorType and exports ReturnDecision/MutatorPreset. |
| index.d.ts | Types the new JS API and the added constants. |
| android/src/main/java/io/approov/reactnative/ApproovService.java | Adds native setServiceMutatorType method and resets mutator on config-change re-init. |
| android/src/main/java/io/approov/reactnative/PolicyMutator.java | New Android native mutator implementing proceed/forward/block policy + optional signing. |
| android/src/test/java/io/approov/reactnative/PolicyMutatorTest.java | New unit tests for Android PolicyMutator decision logic and signing behavior. |
| android/src/test/java/io/approov/reactnative/ApproovServiceRegressionTest.java | Adds regression test to ensure mutator resets on config-change re-init. |
| ios/ApproovProps.m | Makes missing approov.plist optional (log + defaults) instead of raising at launch. |
| ios/ApproovService.m | Resets mutator on config-change re-init and adds exported setServiceMutatorType bridge method. |
| ios/ApproovServiceMutatorBridge.swift | Adds ObjC-callable helpers to install PolicyMutator and reset to default signer. |
| ios/ApproovURLSession/PolicyMutator.swift | New Swift PolicyMutator implementing bitmask decision policy + optional signing via composition. |
| tests/ios/swift/TestSupport/ApproovStub.swift | Extends Swift stub statuses to cover DISABLED and INTERNAL_ERROR. |
| tests/ios/swift/PolicyMutatorTests.swift | New Swift test runner validating bit assignments, blocking semantics, signing delegation, and bridge behavior. |
| tests/ios/run_policy_mutator_tests.sh | New script compiling/running the Swift PolicyMutator test runner via swiftc. |
| tests/ios/run_message_signing_tests.sh | Includes PolicyMutator.swift in the message signing swiftc compilation set. |
| tests/ios/run_native_tests.sh | Adds the Swift PolicyMutator suite to the iOS native test runner. |
| tests/ios/native/TestSupport/ApproovServiceMutatorBridgeStub.m | Adds no-op ObjC stub selectors for new Swift bridge helpers to keep native suites linking. |
| tests/ios/native/TestSupport/approov_service_react_native-Swift.h | Adds stub header declarations for setPolicyMutator and resetToDefault. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The proceed bitmask is bridged from JS as a double and was narrowed to int (Android) / NSInteger (iOS) with an unchecked cast. A fractional, NaN/Infinity, or out-of-32-bit-range value was silently truncated or coerced and could install a proceed policy other than the caller's intent (security-relevant: the mask decides which failure statuses may proceed). Reject such masks before applying the mutator on both platforms. Adds Android and iOS native regression tests.
PolicyMutator's block error interpolated the Swift enum case via \(status); Approov.string(from:) yields the canonical MITM_DETECTED-style name, matching the rest of the iOS layer and Android's enum toString(). Also reword the MITM_DETECTED/REJECTED mask security warning in REFERENCE.md, USAGE.md and CHANGELOG.md to state the request proceeds without proof of attestation (no valid Approov token). (Copilot review, PR #32.)
index.js: options = {} only defaults on undefined; an explicit null
(setServiceMutatorType(mask, null)) threw on options.sign. Treat null
like no options via (options || {}), so sign defaults to true.
ios/ApproovProps.m: correct the optional-props comment - the accessor
is -objectForKey: sent to the nil _props dictionary, not -valueForKey:.
(Copilot review, PR #32.)
| public void initializeWithDifferentConfigResetsCustomServiceMutator() { | ||
| ApproovService service = newService(); | ||
|
|
||
| Promise firstInit = mock(Promise.class); | ||
| service.initialize("config-one", null, firstInit); | ||
| verify(firstInit, timeout(2000)).resolve(null); | ||
|
|
||
| // Install a custom, non-signing mutator (as an app would via setServiceMutator, | ||
| // or the JS setServiceMutatorType wrapper). | ||
| ApproovServiceMutator custom = ApproovServiceMutator.DEFAULT; | ||
| ApproovService.setServiceMutator(custom); | ||
| assertSame(custom, ApproovService.getServiceMutator()); | ||
|
|
||
| // A genuinely different config must reset the mutator so a custom override does | ||
| // not persist across an initialization boundary (root TESTING_REQUIREMENTS.md | ||
| // section 2, "Service Mutator Reset"). | ||
| Promise secondInit = mock(Promise.class); | ||
| service.initialize("config-two", null, secondInit); | ||
| verify(secondInit, timeout(2000)).resolve(null); | ||
|
|
||
| ApproovServiceMutator afterReset = ApproovService.getServiceMutator(); | ||
| assertNotSame("custom mutator must not persist across a config change", custom, afterReset); | ||
| assertTrue("re-init must restore the default message-signing mutator", | ||
| afterReset instanceof ApproovDefaultMessageSigning); | ||
| } |
Prevent custom mutator installed by one test leaking into later tests if an assertion fails before the re-init step resets it. Flagged by Copilot review on PR #32.
charlesoj6205
left a comment
There was a problem hiding this comment.
Blocking — no iOS Swift is compiled against the real Approov SDK, so the new PolicyMutator is unverified. CI being green doesn't cover this. I went through all four suites run_native_tests.sh invokes:
run_mini_sdk_native_tests.sh — ObjC only: mini-SDK Approov.m plus ApproovServiceMutatorBridgeStub.m. No Swift at all.
run_legacy_native_tests.sh — ObjC + stubs.
run_message_signing_tests.sh and run_policy_mutator_tests.sh — swiftc against tests/ios/swift/TestSupport/ApproovStub.swift, which this PR extends (+.disabled, +.internalError) to make the suite build.
npm run build:ios wouldn't help either (and CI doesn't run it): ios/Approov.xcodeproj lists zero .swift files and has no file-system-synchronized group, so it builds no Swift — pre-existing, but it means the podspec glob is the only thing that ever compiles these files.
Net effect: every ApproovTokenFetchStatus case name in bitFor/decideFor is unchecked against the real module, and both switches end in default:, so a wrong-but-plausible spelling compiles against the stub and silently degrades to "no bit → BLOCK" at runtime. approov-ios-sdk 3.5.3 is public, so a swiftc -typecheck with -F against the real xcframework clears this in minutes — or pod lib lint for the thorough version. Worth adding to CI while you're there.
Four cheap asks, all on lines you've already touched:
serviceMutator (ApproovService.java:204) needs to be volatile. The non-volatile field is pre-existing, but the new reset writes it under synchronized (this) with a comment claiming that lock covers the interceptor's reads — it doesn't, they go through the static unsynchronized getServiceMutator(). A stale mutator can survive a config-change re-init.
initializeWithDifferentConfigResetsCustomServiceMutator can't fail: PolicyMutator extends ApproovDefaultMessageSigning, so the instanceof assertion passes even if the custom mutator survived. It also installs ApproovServiceMutator.DEFAULT rather than a PolicyMutator, so the CHANGELOG's scenario is untested.
No iOS test that initialize() does the reset — the ObjC suite stubs resetToDefault as a no-op, so ApproovService.m:436 is uncovered while Android has a test. Needs the stub to record the call rather than swallow it.
setServiceMutatorType(1 << 11) validates fine and installs a silent block-everything policy. Worth rejecting bits outside 0..10 now that you validate the mask's representation.
Address the blocking review on PR #32. setServiceMutatorType now rejects masks with undefined bits on both platforms. Only bits 0-10 name an Approov status, so a mask such as 1 << 11 granted proceed to nothing and installed a silent block-everything policy that looked deliberate. The permitted set is exposed as PolicyMutator.ALL_BITS. ApproovService.serviceMutator is now volatile. The re-initialization reset wrote it under the instance monitor, but the interceptor reads it through the static unsynchronized getServiceMutator(), so that lock ordered nothing for those reads and a stale custom mutator could survive a config change. Add tests/ios/run_real_sdk_typecheck.sh and wire it into the iOS runner. Every Swift suite compiled against ApproovStub.swift, so none could catch an ApproovTokenFetchStatus case name absent from the shipping SDK; since bitFor/decideFor both end in default:, such a name would compile and then degrade to "no bit -> BLOCK" at runtime. The gate compiles the same sources against the real Approov.xcframework. Existing status handling verified correct against SDK 3.5.3. Fix the mutator reset tests. The Android test installed ApproovServiceMutator.DEFAULT and asserted instanceof ApproovDefaultMessageSigning, which PolicyMutator also satisfies, so it could not fail; it now installs a PolicyMutator and asserts the negative. Add same-config counter-tests on both platforms, and make the iOS bridge stub record resetToDefault instead of swallowing it so the iOS initialize() reset path is asserted rather than assumed.
|
Thanks @charlesoj6205 — all five addressed in 47bdc4a. Taking them in your order. 1. Blocking: no iOS Swift compiled against the real Approov SDKYou were right that nothing verified this, and your reading of the four suites matches what I found. Added It resolves a real Result: 0 errors against SDK 3.5.3. Every I verified the gate actually catches the failure mode rather than trusting that it would. Renaming a case consistently across both I did not add 2.
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 23 out of 25 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
tests/ios/run_real_sdk_typecheck.sh:105
run_real_sdk_typecheck.shhard-codesTARGET="arm64-apple-ios13.0-simulator". This can break developers/CI running on Intel macOS (x86_64), whereswiftcmay not be able to typecheck for an arm64 simulator target. Consider selecting the simulator target dynamically based on host architecture so the gate is portable across macOS runners.
SDK_PATH="$(xcrun --sdk iphonesimulator --show-sdk-path)"
TARGET="arm64-apple-ios13.0-simulator"
|
Re the suppressed comment on The comment argues that on Intel macOS Two further reasons it is not worth the branch:
Adding host-arch detection would mean more moving parts in a gate whose whole value is that it is simple and fast. Happy to revisit if an Intel runner ever enters the picture. |
Strict fail-closed signing errors (unsupported algorithm, required body digest) escaped the OkHttp chain as unchecked IllegalStateException, crashing instead of surfacing as a network error. Also correct the USAGE.md tip that pointed custom mutators at the no-signing ApproovServiceMutator.DEFAULT.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 34 out of 36 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
ios/ApproovService.m:405
- PR description says same-config re-initializations “still preserve state”, but the implementation now treats same-config re-init as an initialization boundary that resets runtime configuration and the active service mutator after native success. Please update the PR description (and any release notes text) to match this behavior so consumers don’t rely on state preservation across same-config re-init.
// SDK succeeded (or bypass) — now commit a fresh service-layer state.
// Same-config re-initialization is an initialization boundary too: it is
// forwarded to the SDK first, then runtime configuration and custom mutators
// are reset only after native success.
ios/ApproovService.m:745
@synchronized(approovTokenPrefix)is locking on the value object, but that reference is reassigned inside the critical section. If another thread reads/locks while the prefix changes, reads/writes can be synchronized on different objects (or become effectively unlocked), which defeats the intended thread-safety. Consider introducing a dedicated lock object (e.g.approovTokenPrefixLock) and using it consistently for both reads and writes.
@synchronized(approovTokenHeader) {
approovTokenHeader = header;
}
@synchronized(approovTokenPrefix) {
approovTokenPrefix = prefix ?: @"";
}
|
Addressing the two suppressed comments from the latest Copilot pass. 1.
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 34 out of 36 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
ios/ApproovService.m:422
- The new re-initialization warning says to re-apply
setServiceMutatorType, but a custom service mutator may also have been installed natively (viaApproovServiceMutatorBridge.serviceMutator). This makes the warning misleading for native integrations and doesn’t match Android’s wording (which mentions both paths). Consider updating the message to include the native-mutator case.
if (![[ApproovServiceMutatorBridge shared] isDefaultMutator])
ApproovLogW(@"initialization is discarding a custom service mutator - re-apply "
"setServiceMutatorType after initialize if a custom policy is still required");
The native SDK treats the comment as part of the already-initialized match, so a same-config re-initialization is only accepted when the comment is unchanged or starts with reinit. Verified on device against Approov SDK 3.5.3 on Android and iOS: swapping null for an empty string is reported as a different configuration. Also widen the iOS mutator discard warning to name the native ApproovServiceMutatorBridge path, matching the Android wording.
charlesoj6205
left a comment
There was a problem hiding this comment.
setServiceMutatorType can be silently lost — the documented mitigation doesn't hold
USAGE.md:314 tells consumers to apply the policy in an effect "keyed on approovReady, which re-runs after every successful initialization (including React StrictMode double-mounts and Fast Refresh)". It doesn't. In approov-provider.js approovReady is a boolean latch — it goes false → true on the first success and never changes again — so useEffect(..., [approovReady]) will not re-run when a later initialize() resets the mutator.
That matters specifically because this PR makes same-config re-init a reset boundary. The cases that break:
- an explicit re-
initialize()after the app is ready (account switch, config refresh, the documentedreinit:flow) — policy silently reverts to the built-in default - the headless
ApproovService.initialize()path in Option 2, which has noapproovReadyat all
StrictMode double-mount happens to survive, but by timing — both effect setups run while approovReady is still false, and native module calls are serialized in dispatch order — not by the mechanism the doc describes.
It fails closed (the default is the stricter posture), so this is a functional bug rather than a security hole: requests that the app expected to proceed will start blocking, with only a native log line to go on.
Suggested fix — give the provider a signal that actually changes per initialization:
approov-provider.js
const [status, setStatus] = useState({
approovReady: false,
approovError: null,
approovInitCount: 0,
}) // success path
setStatus((previous) => ({
approovReady: true,
approovError: null,
approovInitCount: previous.approovInitCount + 1,
})) // error path
setStatus((previous) => ({
approovReady: false,
approovError: error,
approovInitCount: previous.approovInitCount,
}))The updater form matters — two in-flight initializeApproov() closures can otherwise clobber the count.
index.d.ts:209
export declare function useApproov(): {
approovReady: boolean;
approovError: any;
approovInitCount: number;
};USAGE.md:314 — replace the paragraph and example with:
Do not set the policy in
ApproovProvider'sonInit— that callback runs beforeinitialize, so the policy is immediately wiped by the reset. Apply it afterwards, keyed onapproovInitCount:const { approovReady, approovInitCount } = useApproov(); useEffect(() => { if (approovReady) { ApproovService.setServiceMutatorType(ApproovService.MutatorPreset.PROCEED_IF_UNAVAILABLE); } }, [approovReady, approovInitCount]);
approovReadyalone is not enough: it latchestrueon the first success and never changes again, so an effect keyed only on it will not re-run when a later re-initialization resets the mutator.approovInitCountincrements on every successful initialization, so the effect re-runs and the policy is re-applied.If you call
ApproovService.initialize()yourself rather than throughApproovProvider(Option 2 above), the counter does not apply — re-applysetServiceMutatorTypeafter each of those calls.
Adding a field to useApproov() is a public-contract addition, so it wants a 3.5.16 CHANGELOG bullet too. approov-monitor.js:26 destructures only approovReady/approovError, so nothing else breaks.
Non-blocking items from the same pass, for a follow-up commit on this branch:
- The re-init reset only warns about the mutator. It also clears
bindingHeader, substitution headers and query params, exclusion regexes, token header/prefix anduseApproovStatusIfNoToken, with no log at all. Token binding silently ceasing to apply is the security-relevant one — worth the same warning the mutator gets. - CHANGELOG gaps. Nothing covers
14ced30(under aPolicyMutator, a blocked status now fails the request during secure-string substitution — a real behavior change) orsetServiceMutator(null)now restoring the signing default instead of the no-opApproovServiceMutator.DEFAULT, which is a public-API change for native integrators.grep -n "substitut" CHANGELOG.mdreturns nothing. - Fail-open vs fail-closed on signing — question rather than a change request.
86beb27moved ASN.1 DER decode failure (both platforms) and the iOS HMAC path fromthrowtoproceedUnsigned, so the request goes out unsigned;07262e5then argues 20 minutes later that strict signing failures must fail closed. Signature unavailability proceeding unsigned matches the established contract, but a DER decode failure is a corruption/integrity failure — should that one stay fail-closed as anIOException? Either way the rule is worth stating explicitly in the code or CHANGELOG so the next change doesn't have to re-derive it.
Verified correct while looking at this, noting it so it doesn't get re-flagged: the true → false flip for noNetwork/poorNetwork/mitmDetected in ios/ApproovURLSession/ApproovServiceMutator.swift is a genuine parity fix — Android's default at ApproovServiceMutator.java:272 already returned false, the documented contract is "true if substitution should proceed", and it's behavior-neutral at the call site because ios/ApproovService.m guards on status == ApproovTokenFetchStatusSuccess before substituting. Likewise 07262e5's IllegalStateException → IOException wrapping is right, and the genuinely strict paths (unsupported algorithm, required body digest) still throw.
approovReady latches true on the first successful initialization and never changes again, so an effect keyed only on it cannot observe a later re-initialization. Since every successful initialization is a reset boundary, a policy applied that way was silently reverted to the built-in default. ApproovProvider now publishes approovInitCount, incremented with the updater form so concurrent initialization closures cannot clobber it, and USAGE.md keys the documented effect on it. Also warn when a re-initialization discards the binding header or the substitution and exclusion configuration, state the message signing fail-open/fail-closed rule on both platforms, and record the previously undocumented setServiceMutator(null) and secure-string substitution behaviour changes in the changelog.
|
Thanks — the blocking finding is correct and is fixed in Blocking:
|
Summary
Ships as 3.5.16 (last published: 3.5.15). Related changes to the React Native service layer, on both Android and iOS:
setServiceMutatorType), no native mutator code.approov.plistis now optional (no launch crash with a config-only native integration).Feature —
ApproovService.setServiceMutatorType(mask, { sign })ApproovService.ReturnDecision(one bit per Approov failure status) +ApproovService.MutatorPresetpresets (DEFAULT,ALWAYS_PROCEED,PROCEED_IF_UNAVAILABLE,PROCEED_DEV_CLEARTEXT).SUCCESS/UNKNOWN_URL/UNPROTECTED_URLalways proceed and are not maskable.{ sign: false }opts out.PolicyMutatoron each platform (Android Java, iOS Swift), composing the default message signer. Behaviour is identical across platforms — a blocked status is a hardfetch()failure on both (AndroidIOException, iOSNSError).ApproovServiceMutator).MITM_DETECTEDorREJECTEDin the mask disables the protection those statuses provide — use presets by default; reach for the raw bitmask deliberately.PROCEED_DEV_CLEARTEXTis the supported dev-only opt-in for the Metro cleartextBAD_URLsymptom (BAD_URL token status crashes network requests instead of being handled gracefully #30).Fix — service mutator reset on re-initialization (Android + iOS)
initialize()cleared the runtime config on a config change but not the active service mutator, so a mutator installed viasetServiceMutator/setServiceMutatorTypepersisted across an initialization boundary. It now resets to the built-in message-signing default on both platforms (root TESTING_REQUIREMENTS §2, "Service Mutator Reset").Every successful initialization is a reset boundary, including same-config re-initialization. Same-config re-init is forwarded to the native SDK, treated as success, and then resets runtime configuration and the active service mutator — it no longer preserves state. A warning is logged whenever a custom mutator is discarded this way. Consumers must re-apply
setServiceMutatorType(and any substitution headers, exclusions, or token-header settings) after each successfulinitialize(). InApproovProvider, apply the policy in an effect keyed onapproovReadyrather than inonInit, which runs beforeinitialize()and would be wiped by the reset.Fix — iOS
approov.plistoptionalWith native
approov.configinit, a missing bundledapproov.plistno longer raisesApproovPropsNotFoundat launch — it logs and runs with defaults, matching Android's optionalapproov.props. A present-but-unreadable plist still raises. (Reported by PropertyGuru.)Review hardening
Mask validation (security-relevant). The proceed bitmask is bridged from JS as a double and is now rejected on both platforms unless it is a finite, integral 32-bit value and sets no bit outside the defined flags (bits 0-10, exposed as
PolicyMutator.ALL_BITS), withMutatorPreset.DEFAULT(-1) excepted. Previously1 << 11validated fine and installed a policy granting proceed to nothing — a silent block-everything policy indistinguishable from a deliberate one. Rejecting undefined bits is a user-visible contract change, soREFERENCE.mdandUSAGE.mdare updated.Stale mutator across re-init (Android, security-relevant).
ApproovService.serviceMutatoris nowvolatile. The reset wrote it under the instance monitor, but the interceptor reads it through the static unsynchronizedgetServiceMutator(), so that lock ordered nothing for those reads.iOS Swift was never compiled against the real SDK. Every Swift suite compiled against a hand-written
ApproovStub, so none could catch anApproovTokenFetchStatuscase name absent from the shipping SDK — and sincePolicyMutator.bitFor/decideForboth end indefault:, such a name would compile and then silently degrade to "no bit → BLOCK" at runtime. Newtests/ios/run_real_sdk_typecheck.shcompiles the same sources against the realApproov.xcframeworkand runs as part ofnpm run test:ios. Existing status handling verified correct against SDK 3.5.3 (0 errors), including the importer renames (.mitmDetected,.badURL). Gate verified to fail on an injected wrong case name.Mutator reset tests strengthened. The Android reset test installed
ApproovServiceMutator.DEFAULTand assertedinstanceof ApproovDefaultMessageSigning, whichPolicyMutatoralso satisfies — so it could not fail. It now installs a realPolicyMutatorand asserts the negative. Same-config counter-tests added on both platforms, and the iOS bridge stub now recordsresetToDefaultinstead of swallowing it, so the iOSinitialize()reset path is asserted rather than assumed.Versioning
Patch 3.5.16 — all changes ship together (last published 3.5.15).
package.json, podspec (s.version = package["version"]) andPackage.swift(git-tag) are consistent; the release tag will be3.5.16.Testing
All run locally on this branch:
npm run test:android— 59 tests, 0 failures.npm run test:ios— exit 0, all five suites: mini-SDK native, legacy native, Swift message-signing, Swift PolicyMutator, real-SDK typecheck.npm run test:js— 15/15.run_mini_sdk_native_tests.shhas now been run locally against the real Approov SDK (3.5.3(7361)), resolving the earlier caveat in this description. It requirescore-service-layers-testingpresent as a sibling directory, which CI provides.Related
BAD_URL;PROCEED_DEV_CLEARTEXTis the supported opt-in.@ReactMethoddropped under New Architecture" report; disproven on-device).