Skip to content

Feature/ios optional approov plist#32

Open
ivolz wants to merge 7 commits into
mainfrom
feature/ios-optional-approov-plist
Open

Feature/ios optional approov plist#32
ivolz wants to merge 7 commits into
mainfrom
feature/ios-optional-approov-plist

Conversation

@ivolz

@ivolz ivolz commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Ships as 3.5.16 (last published: 3.5.15). Three related changes to the React Native service layer, on both Android and iOS:

  1. feat — select the Approov request policy from JavaScript (setServiceMutatorType), no native mutator code.
  2. fix — the custom service mutator now resets on re-initialization (Android + iOS).
  3. fix — iOS approov.plist is now optional (no launch crash with a config-only native integration).

Feature — ApproovService.setServiceMutatorType(mask, { sign })

  • New JS API + ApproovService.ReturnDecision (one bit per Approov failure status) + ApproovService.MutatorPreset presets (DEFAULT, ALWAYS_PROCEED, PROCEED_IF_UNAVAILABLE, PROCEED_DEV_CLEARTEXT).
  • The mask lists which failure statuses may proceed; a not-in-mask failure status blocks the request. SUCCESS / UNKNOWN_URL / UNPROTECTED_URL always proceed and are not maskable.
  • HTTP Message Signing preserved by default; { sign: false } opts out.
  • Backed by a native PolicyMutator on each platform (Android Java, iOS Swift), composing the default message signer. Behaviour is identical across platforms — a blocked status is a hard fetch() failure on both (Android IOException, iOS NSError).
  • No native app code needed for the common policy cases (previously required hand-writing an ApproovServiceMutator).
  • ⚠️ Security: putting MITM_DETECTED or REJECTED in the mask disables the protection those statuses provide — use presets by default; reach for the raw bitmask deliberately.
  • PROCEED_DEV_CLEARTEXT is the supported dev-only opt-in for the Metro cleartext BAD_URL symptom (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 via setServiceMutator/setServiceMutatorType persisted across an initialization boundary. It now resets to the built-in message-signing default on both platforms whenever the config changes (root TESTING_REQUIREMENTS §2, "Service Mutator Reset"). Same-config re-inits still preserve state.

Fix — iOS approov.plist optional

With native approov.config init, a missing bundled approov.plist no longer raises ApproovPropsNotFound at launch — it logs and runs with defaults, matching Android's optional approov.props. A present-but-unreadable plist still raises. (Reported by PropertyGuru.)

Versioning

Patch 3.5.16 — all three changes ship together (last published 3.5.15). package.json, podspec (s.version = package["version"]) and Package.swift (git-tag) are consistent; the release tag will be 3.5.16.

Testing

  • Android: PolicyMutatorTest + ApproovServiceRegressionTest (mutator-reset regression) — full unit suite 55 tests, all green.
  • iOS: run_policy_mutator_tests.sh (Swift), run_message_signing_tests.sh, run_legacy_native_tests.sh — all pass.
  • ⚠️ Not yet run here: the worker-backed iOS run_mini_sdk_native_tests.sh (needs pod install + the external core-service-layers-testing/mini-sdk/ios checkout) — to run in CI before release.

Related

ivolz added 4 commits July 22, 2026 11:58
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).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 }) plus ReturnDecision bit flags and MutatorPreset presets, backed by new native PolicyMutator implementations 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.plist optional (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.

Comment thread android/src/main/java/io/approov/reactnative/ApproovService.java
Comment thread ios/ApproovService.m
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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 24 changed files in this pull request and generated 1 comment.

Comment thread ios/ApproovURLSession/PolicyMutator.swift
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.)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 24 changed files in this pull request and generated 2 comments.

Comment thread ios/ApproovProps.m
Comment thread index.js
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.)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 24 changed files in this pull request and generated 1 comment.

Comment on lines +323 to +347
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);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants