NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
pub.dev · #4337 most downloaded on pub.dev
Approov enabled HttpClient
Last release 1 months ago
21 Aug 2026
Release timing varies
gaps range from 1 weeks to 5 months
Nearly every release is documented
notes for 10 of 10 stable releases
Nothing withdrawn
no release was ever pulled
2 years old
10 releases · first in 2025
Add SwiftPM support, empty-config bypass mode, and isInitialized/isAp…
Add SwiftPM support, empty-config bypass mode, and isInitialized/isAp…
Upgrade notes:
await ApproovService.initialize(config)now completes only after the native initialization attempt finishes, and throwsApproovExceptionon failure — previously it returned immediately and swallowed native failures. Wrap it in try/catch if you want to fall back to bypass mode (see README).- Every successful
initialize()/re-initialize now resets the runtime configuration (custom mutator, header overrides, token binding, substitutions, exclusions, message signing, cached pinning certificates). Apply configuration afterawait initialize(...)returns; configuration applied before it is discarded.- Mixed-quickstart apps: the service layer no longer tolerates the native SDK reporting that it was already initialized with a different configuration. Previously that outcome was caught and ignored, so an app in which another Approov quickstart had already initialized the SDK with different parameters kept working; it now surfaces as an
ApproovExceptionfrominitialize()(TESTING_REQUIREMENTS.md§1, "Cross-Service-Layer Different Config Initialization" — a different config must not be silently accepted). Align the configuration strings, or use areinit...comment.setUseApproovStatusIfNoToken(true)no longer allows a request to proceed. It is now purely a backend-visibility control over the token header value. If you relied on it to keep requests flowing onNO_NETWORK/POOR_NETWORK, install a customApproovServiceMutatorthat overrideshandleInterceptorFetchTokenResultfor the statuses you actually want to allow —MITM_DETECTEDshould not be one of them.NO_APPROOV_SERVICEno longer sends an empty or prefix-only token header. Backends that treated the mere presence ofApproov-Tokenas evidence the layer ran must now either enablesetUseApproovStatusIfNoToken(true)(which sendsNO_APPROOV_SERVICEas the value) or stop relying on the empty header.- Message signing failures now proceed unsigned instead of aborting the request, except for a required body digest that cannot be generated and an unsupported/missing signing algorithm, which still abort (
TESTING_REQUIREMENTS.md§5; matchesapproov-service-okhttp). Backends enforcing signatures remain the enforcement point.
TESTING_REQUIREMENTS.md §3), and previously there was none: a custom mutator declining was silent, and the empty-secure-string case logged at debug, which is below the WARNING default and therefore invisible in the field. UNKNOWN_KEY and an empty secure string now log at warning; any other status can only be skipped because a custom mutator overrode a policy that would otherwise have failed the request, so it logs at error naming the status.NO_APPROOV_SERVICE. Both the header and query paths previously skipped the substitution and forwarded the request with the placeholder still in the header, reporting no error, so the backend received the placeholder as the credential and the app never learned anything had gone wrong. The NO_APPROOV_SERVICE carve-out in TESTING_REQUIREMENTS.md §3 applies to the Approov token fetch, where proceeding without a token is a coherent degraded state that the backend can act on; there is no equivalent for a secret, because the only alternative to failing is sending a wrong secret rather than an absent one. Both paths now throw ApproovException, matching approov-service-okhttp, approov-service-urlsession and approov-service-nsurlsession. Apps that would rather proceed can override the substitution handlers on a custom mutator. Note the consequence: during an Approov outage, requests carrying a configured substitution header now fail instead of going out with the placeholder.ApproovSigningContext lowercases its header keys, but the client staged them case-sensitively, so a custom factory that touched the same header under two spellings (addHeader('X-Foo', …) then setHeader('x-foo', …), in either order) staged two entries whose replay no longer reproduced the single entry the signature covered: the request went out with both the current and the superseded value while the signature covered only the current one, and the backend could never verify it. Staging is now keyed by the lowercased name, preserving the first casing seen for the wire header. Reachable only from a custom SignatureParametersFactory; no effect on the default factory. Covered by a wire-level test rather than a signing-context assertion, since the context was never the broken half.setProceedOnNetworkFail() is now an obsolete no-op (matching approov-service-okhttp). The argument is ignored and no default handler reads it, so NO_NETWORK / POOR_NETWORK / MITM_DETECTED now fail closed on every path: the token fetch and header/query substitution all throw ApproovNetworkException. The flag was a single global switch across every network-related status, so it could not proceed on "no network" without also proceeding on MITM_DETECTED — continuing after the SDK detected interception, potentially before dynamic pins had been received. Express the policy per status instead by overriding ApproovServiceMutator.handleInterceptorFetchTokenResult (or the substitution handlers) and installing it with setServiceMutator. ApproovTokenFetchResult.proceedOnNetworkFail is deprecated and always false.setUseApproovStatusIfNoToken(true) acting as a fail-open escape hatch on network and MITM statuses. The default mutator returned true for NO_NETWORK / POOR_NETWORK / MITM_DETECTED whenever the flag was set, so enabling a backend-visibility feature silently allowed requests to continue after the SDK had reported detected interception. The default mutator now throws ApproovNetworkException for those three statuses unconditionally (TESTING_REQUIREMENTS.md §3, "Default Mutator Behavior": fail-closed for every status except SUCCESS and NO_APPROOV_SERVICE). The flag now only controls what is put in the token header, never whether a request proceeds. The status-fallback injection path is unchanged and remains reachable for a custom mutator that deliberately overrides handleInterceptorFetchTokenResult and returns true. This diverges from approov-service-okhttp, which still has the same escape hatch — that layer needs the matching fix.Approov-Token header being sent on NO_APPROOV_SERVICE. With the status fallback disabled the layer emitted Approov-Token: (or Approov-Token: Bearer with a prefix configured). TESTING_REQUIREMENTS.md §2 ("Missing Artifacts Fallback") requires empty token and trace values to be omitted rather than sent as empty-valued or prefix-only headers, with status evidence provided by setUseApproovStatusIfNoToken(true) instead. The header is now omitted entirely when no token is available, and NO_APPROOV_SERVICE joins the status-fallback allowlist so the status name is injected only when that flag is on. The trace-ID header already omitted itself when empty and is unchanged. This diverges from approov-service-okhttp, whose buildTokenHeaderValue returns prefix + token and so still emits the empty/prefix-only header for this status — that layer needs the matching fix.NO_APPROOV_SERVICE turning an Approov outage into a hard request failure for apps using secure-string substitution. The default mutator continues on this status, so _updateRequest proceeded into the substitution loop, where NO_APPROOV_SERVICE fell through to the default: case and threw ApproovException — every request carrying a configured addSubstitutionHeader / addSubstitutionQueryParam placeholder failed whenever the Approov service was unavailable, where the previous release forwarded the request unmodified. Both substitution handlers now return false for this status, skipping the substitution and leaving the original placeholder in place, so the request is forwarded with only the available artifacts (TESTING_REQUIREMENTS.md §2). This diverges from approov-service-okhttp, whose substitution handlers still throw for this status — that layer needs the matching fix.initialize() published the in-flight attempt as the future every _requireInitialized() caller awaits before that attempt had been resolved, so an app protecting traffic with config A that called initialize(configB) — a config-refresh path, for example — failed every concurrent request with the native rejection of config B, even though the layer remained fully functional under config A. The attempt is now published only when Approov protection is not already active; while it is, _requireInitialized() keeps resolving against the still-valid initialization for the whole duration of the attempt (TESTING_REQUIREMENTS.md §1, "Different Config Failure State"). Two cases are deliberately unchanged and still gate traffic on the attempt: a first initialization, so a failed attempt stays observable and _requireInitialized() rethrows the original root-cause error rather than the generic "has not been initialized" message; and the "empty config → valid config" upgrade out of bypass mode, so requests issued during await initialize(realConfig) wait for it rather than going out unprotected (TESTING_REQUIREMENTS.md §1, "Empty Then Valid Configuration"). Bypass mode reports itself as initialized, so the distinction is protection being active, not the initialized flag._isInitialized / _initialConfig were committed — all inside a try that rethrows as ApproovException, so a failure in that window wiped the runtime configuration while _initialConfig still named the previous config (TESTING_REQUIREMENTS.md §1, "Service-Layer State Only Updated On Success").null as the value — on any non-SUCCESS status that set a token header key. It now fires only when a fallback value was genuinely injected, and names the header it was written to.getInstallMessageSignature(message) publicly, completing the common service-layer interface (TESTING_REQUIREMENTS.md §7). The plumbing already existed privately and in both native plugins; only the public entry point was missing, so callers could not sign with the install key outside the automatic interceptor path. Returns base64 of the raw 64-byte ECDSA r||s form, converted from the platform SDK's DER output, which is what an RFC 9421 ecdsa-p256-sha256 verifier expects. Guarded like its siblings: ApproovException in bypass mode, or when the platform cannot produce a signature. Verified on device on Android and iOS: 64 bytes decoded, and distinct messages produce distinct signatures.approov-service-flutter-httpclient, so attestation records could not distinguish one release from another; it now sends approov-service-flutter-httpclient/<version>, matching how approov-service-okhttp reports. ApproovService.serviceLayerVersion exposes the value, and a unit test fails if it drifts from pubspec.yaml.SUCCESS with no token. Both signing artifacts come from the token itself: install signing is verified against the public key it carries, and account signing uses its mskid claim. Signing headers produced without them cannot be verified by any backend, so the request now proceeds unsigned and logs why (TESTING_REQUIREMENTS.md §2 "Missing Artifacts Fallback"). The token header already required a non-empty token; the signing gate did not.URLSession waited for a disposition and the certificate fetch stalled until it timed out. Unhandled challenges now use NSURLSessionAuthChallengePerformDefaultHandling, the valid fallback (TESTING_REQUIREMENTS.md §4 "Authentication Challenge Dispositions Must Be Valid"). Pre-existing, unrelated to the SwiftPM work.null, so a defined-then-deleted secure string produced an empty or prefix-only header (Bearer on the wire) and rewrote a query parameter to key=, destroying the placeholder the backend needs to see. Both now require a non-empty value and leave the original in place otherwise, logging that the substitution was skipped — matching approov-service-retrofit and TESTING_REQUIREMENTS.md §2 "Missing Artifacts Fallback". Applies to the automatic paths and to substituteQueryParam().TESTING_REQUIREMENTS.md §2, "Unprotected Request Processing"). Two paths were affected: the default mutator returned true for UNPROTECTED_URL, which let header substitution run after the token fetch, and automatic query substitution ran before any token fetch could classify the URL, because dart:io fixes a request's URI at openUrl() time. Either could resolve a secure string into a request bound for a host Approov neither tokenizes nor pins. The default mutator now returns false for UNPROTECTED_URL (joining UNKNOWN_URL, and matching approov-service-okhttp), and query substitution now runs only when a pre-open classification positively confirms the URL is protected — any other outcome, including a network failure or an internal error, suppresses it. A custom mutator overriding handleInterceptorFetchTokenResult can opt header substitution back in, but not automatic query substitution: that is gated by the pre-open URL classification, which runs before any mutator is consulted and requires a confirmed SUCCESS. Apps that need a query parameter substituted regardless can call substituteQueryParam() explicitly.ApproovHttpClient([config]) and ApproovClient([config]) cannot report an initialization failure to the caller: a constructor cannot await, so initialize()'s new asynchronous throw is not catchable around construction. The failure is retained and rethrown from the first request through the client, and the pending error no longer surfaces as an unhandled asynchronous error. Prefer await ApproovService.initialize(config) before constructing the client, which is the pattern the README documents.isInitialized() and isApproovEnabled() public API methods (Dart, Android, iOS).FlutterEngine rather than per process. The plugin instance is created once per engine, so an app with a second engine (background fetch, alarm or geolocation plugins) saw "not initialized" there while the process-wide Approov SDK was initialized and protecting traffic — and the Dart layer read that as "native unprotected" and committed real bypass mode, silently dropping token injection and pinning for every request from that engine. The state is now held in a process-wide static on both platforms (matching approov-service-okhttp), and isInitialized()/isApproovEnabled() are answered from it.isInitialized()/isApproovEnabled() and the empty-config protection probe fall back to a second route rather than treating one failed probe as an established state. This is defence in depth, not a fix for an observed defect: every other native call in the package uses the foreground channel unconditionally and works from background isolates. When neither channel answers, bypass is assumed (per TESTING_REQUIREMENTS.md §1) and now logged at error level rather than passed over silently.REFERENCE.md: the guarded methods do not behave uniformly (throw / empty map / empty string / no-op / pass-through), and that difference is now stated per method.comment argument participates in the native SDK's already-initialized matching, so a same-config re-initialization is accepted only with an identical comment or one starting with reinit (TESTING_REQUIREMENTS.md §1, "Comment Is Part Of The Platform SDK Initialization Identity").initialConfig argument against NSNull, as the comment and updateConfig arguments already are: -[NSNull length] is an unrecognised selector and would crash rather than read as empty.initialize('') (empty configuration string) to actually enter bypass mode — initializes the service layer without calling the native Approov SDK, instead of throwing. Previously this would fail with a native exception surfaced as a Dart PlatformException, contradicting documentation that claimed bypass-mode support.initialize() re-initialization guard to allow the "empty config → valid config" upgrade transition and to silently ignore a "valid config → empty config" downgrade attempt, per the cross-service-layer TESTING_REQUIREMENTS.md spec, instead of throwing in both directions.null comments when forwarding to native, forward every non-empty config to the native SDK, and preserve existing service-layer state when native initialization fails.approov-service-react-native's pattern: the core request pipeline now skips token injection, pinning, and secure string substitution entirely for every request, and every other public method that talks to the native SDK directly (precheck, getDeviceID, fetchToken, getMessageSignature, getAccountMessageSignature, fetchSecureString, fetchCustomJWT, setDevKey, getPins, setDataHashInToken, substituteQueryParam) now fails cleanly with ApproovException("Approov is not enabled") (or a safe no-op/empty default, where that is the correct behavior) instead of crashing with a native exception. Previously only initialize('') itself worked; every other entry point still reached the uninitialized native SDK.initialize write and the foreground-channel isInitialized/isApproovEnabled reads of the same state, matching the equivalent Android fix (volatile fields).setDataHashInToken(...) before fetching the bound Approov token.setUserProperty) failed the whole initialize() after the native SDK had already committed — leaving Dart in bypass while native was protected. The Dart state now commits immediately after native success and the telemetry call is best-effort (matches approov-service-okhttp ordering).initialize('') while the process-wide native layer was already protected would commit bypass mode locally — silently skipping pinning and token injection for that isolate's requests while isApproovEnabled() reported true. The Dart layer now queries native and adopts protected mode.RequiredBodyDigestException, UnsupportedSignatureAlgorithmException, both exported) instead of error-message string matching; a params object with a missing algorithm identifier now also fails closed, matching approov-service-okhttp. Custom SignatureParametersFactory implementations can throw RequiredBodyDigestException to force an abort.approov-service-okhttp, when signing fails open the request goes out without Content-Digest (signing-related headers are staged and only applied on success).logger dependency lower bound to ^2.1.0 (DateTimeFormat is used and was added in 2.1.0).prefetch() — it is now a no-op, matching the rest of the Approov service layer family (approov-service-retrofit, approov-service-urlsession, and others). The Approov SDK manages prefetching automatically; the explicit prefetch call is redundant.One column per month.
Update CHANGELOG.md document
Update CHANGELOG.md document
ApproovServiceMutator support across fetch APIs, request mutation flow, and pinning gate callbacks.ApproovRequestMutations, ApproovRequestSnapshot, ApproovTokenFetchResult, and ApproovTokenFetchStatus.setServiceMutator() / getServiceMutator() plus deprecated alias methods for naming parity.ApproovLogLevel with OFF, ERROR, WARNING, TRACE and setLoggingLevel() / getLoggingLevel().addSubstitutionQueryParam() and removeSubstitutionQueryParam().setUseApproovStatusIfNoToken(bool) and getUseApproovStatusIfNoToken() to control token-header status fallback behavior.NO_NETWORK, POOR_NETWORK, MITM_DETECTED.handleInterceptorFetchTokenResult(...) allows continuation.setApproovHeader(...) apply equally to JWT and status fallback values.USAGE.md and REFERENCE.md with status-fallback behavior, defaults, allowlist, and mutator interaction.README.md, USAGE.md, and REFERENCE.md.Updates Approov IOS SDK to 3.5.3
Ensure compatibility with Flutter 3.29+ threading model changes.
Update Android SDK to version 3.5.3
Update platform SDK to version 3.5.1
Update platform SDK to version 3.5.0
Async service initialize function now returns a future to enable awaits
Support calling Approov from main isolate and any background isolate
Update iOS native pod package to 3.3.1
Your coding agent can read these notes before it upgrades. Set up the MCP server →