soroq_flutter 0.3.2
soroq_flutter: ^0.3.2 copied to clipboard
App-facing Flutter package for Soroq hard OTA — Android code-push runtime and the experimental iOS engine-lane hard-OTA state machine (requires the Soroq iOS engine toolchain selected by `soroq setup`).
0.3.2 #
The iOS engine lane can patch an obfuscated app. Adds the freehand_identity_obfuscated_v2 contract,
served for apps built with flutter build ios --obfuscate from a Soroq toolchain that declares
obfuscated_identity_translation_v1. Additive: apps built without --obfuscate, and every
freehand_identity_v1 manifest, behave exactly as in 0.3.1.
- Activation by runtime projection. A v2 ABI entry carries both the source identity and the
obfuscated name the running base actually uses (
runtime_base_identity,runtime_module_class,runtime_module_member). The device installs the runtime half. A v2 entry without a runtime projection fails closed rather than falling back to source names, which would install a redirect that never fires. - Obfuscation binding, checked before download. The app reads the
obfuscationsidecar of its ownsoroq_base_identity.json(the SHA-256 of the base's obfuscation map; never the map). A manifest must declare the samebaseObfuscationMapSha256and anobfuscationBindingDigest, or it is refused withbase_identity.refusedbefore anything is downloaded, staged or activated, and the previous good state is untouched. A non-obfuscated base refuses a v2 manifest, and an obfuscated base refuses a v1 one. - A malformed
obfuscationblock in the identity asset is refused as unreadable instead of ignored.
Device evidence: an obfuscated iPhone 15 Pro base passed the eleven-gate lifecycle (11/0/3) and refused a
patch bound to a different obfuscated base, both on this package's code
(handoff/private-state-r6-device-qualification).
0.3.1 #
A refusal that cannot say what it refused is not evidence. The iOS engine lane's tamper gate rested
on signature.refused active_version=13 — a field naming the version that stayed ACTIVE. Nothing in
the event referred to the candidate, so the only thing tying a refusal to the bytes a harness had
published was that the screen did not change, and an unchanged screen is equally consistent with
nothing ever being published.
manifest.fetchedandsignature.refusednow carrymanifest_shaandsig_sha, twelve hex characters over the bytes as served. The digest is taken BEFORE the bytes are trusted, so it identifies the candidate even when the signature is the thing that fails.signature.refusedalso carriesclaimed_version, parsed from the unverified manifest and named to say so. It is what the candidate claims, not a fact; the digests are what binds. Parsing is fully defensive — the refusal path runs on arbitrary bytes, and telemetry must never be the thing that throws.debugSha256Hex12anddebugClaimedVersionare exported@visibleForTestingbecause the binding is the evidence: if it regresses, the tamper gate quietly goes back to resting on an unchanged screen.test/tamper_refusal_candidate_binding_test.dartpins both, including the empty, truncated, non-JSON, wrong-type and non-UTF-8 candidates.
Packaging corrections. No runtime behaviour depends on these; they are the reason 0.3.0 could not be republished and had drifted from what its own tooling reported.
metais now a direct dependency.lib/src/engine_lane_ota.dartimportspackage:meta/meta.dartfor@visibleForTestingand resolved it transitively through the Flutter SDK, which works untildart pub publishrefuses the package outright.ios/soroq_flutter.podspecsaid0.2.8against a package at0.3.0, three patch versions behind. CocoaPods consumers were being told a version that had not existed for two releases.scripts/check_soroq_published_package_versions.shcompared every file in the package directory against the published archive without applying.pubignore, so it reported the deliberately excludedios/Frameworks/SoroqRuntimeCAPI.xcframeworkas drift. The framework stays excluded — that is the public package policy — and the checker now compares the effective publication set instead.
0.3.0 #
Breaking for the iOS engine lane. An app that cannot verify which base it was built from no longer stages or restores a patch. Previously it fell back to runtime-id-only isolation and patched anyway, which is silent in effect: the patch installs, the app works, and the only trace is one diagnostic line. The runtime id is version-derived, so two structurally different bases sharing app, channel, version and trust both pass it.
What you must do. Rebuild your base with a Soroq CLI that writes the identity asset
(soroq release ios --engine --build emits soroq_base_identity.json into the app bundle after
gen_snapshot). An app built with an older CLI and this package will fetch, verify the signature, and
then refuse to stage, reporting base_identity.unverified. Rolling back to base is deliberately NOT
affected: a signed version-0 manifest still reverts, so the recovery valve stays open for exactly the
app this refuses to patch.
- The base identity is now delivered post-compile, as a file inside the signed app bundle, and carries the runtime id, the app.dill fingerprint, the contract digest and the retention-manifest digest. Two of those are computed from the kernel the bootstrap is compiled into, so they cannot be Dart constants — there is no fixed point.
- It is read synchronously from the bundle root before any restore or network activity, with no method channel. Under activate-before-main that matters: a channel round trip can land after the restore it is meant to guard.
- The asset carries the canonical digest over its own fields and is re-derived on every launch, so an edited asset is refused without relying on the code signature. An asset lifted from another base is refused by the compiled-in runtime id.
SoroqBaseIdentitySourcedistinguishesverified,absentandrejected.baseIdentityCheckedwasbaseIdentity != null, which read the same for "no identity" and "identity REFUSED"; both the source and the reason now appear in theisolation.modeevent.- A REFUSED identity invalidates stored patch state rather than adopting it. An app that cannot say which base it is must not restore the module it happens to find on disk.
0.2.8 #
- Security: redact artifact tickets from the URLs echoed in the patch stage response. An artifact ticket is a bearer credential, and logcat is readable via adb and routinely attached to bug reports. Only the sensitive query VALUE is replaced, so scheme, host and path still survive for diagnosis. Download behaviour is unchanged: Dart resolves the URLs it fetches from the patch record, not from this response.
- Android store upgrades now adopt changed packaged update configuration. Earlier releases copied
soroq/auto_update_config.jsonintoSharedPreferencesonce and then treated any stored endpoint as permanently configured. A Play Store upgrade carrying a corrected endpoint could therefore continue calling the old URL until app data was cleared. Package-owned configuration now records a normalized revision and migrates atomically when the bundled values change. - Explicit developer configuration always wins. Calls to
configureAutoUpdatepersist explicit ownership and are never overwritten by packaged defaults. Legacy installs without an ownership marker migrate only when their client id exactly matches Soroq's generated install identity; an ambiguous or custom client id is preserved fail-closed. - Migration touches only auto-update configuration preferences. Native install identity, downloaded bundles, staged/pending/active patches, rollback history, quarantine state, and runtime identity are unchanged. A failed preferences commit leaves the previous usable configuration active.
- Package metadata now keeps the CocoaPods version, canonical repository URL, and catalog-selected iOS toolchain guidance aligned with the pub package.
0.2.7 #
-
A refused transition is now observable.
transitionByIdentitythrows on any validation failure and changes zero slots, so a refused candidate previously emitted nothing at all — the only evidence was an ABSENT success event, which cannot distinguish "refused atomically" from "never attempted". The runtime now emitstransition.begin(with the batch size it was asked to commit) immediately before the native call, andtransition.failedwhen that call throws or returns a short count. -
transition.failedmeans exactly one thing. It is emitted only for the native transition. AloadModulefailure never reaches a transition, and a failure after the redirects go live leaves them installed; reporting either as a transition refusal would describe a refusal that never happened, and in the post-commit case would claim zero redirects committed while the patch is running. -
A short commit no longer reports success first.
transition.resultwas emitted before the committed-count check, so a refused batch appeared in the transcript as a success immediately followed by a failure. A successful transition now emits exactly onetransition.result; a short commit emits onlytransition.failed. -
stale_clearedcounts identities.staleFlatBaseIdscarries four fields per identity, and the event reported the raw list length — a single cleared redirect announcedstale_cleared=4. -
Docs: the required toolchain is the one the catalog selects. The package described itself as requiring "the Soroq R3 toolchain". The shipped iOS toolchain is the
freehand-step5series, and the engine entrypoints this package depends on (soroqTransitionBatchByIdentity) exist only there — so a developer who installed a literalr3toolchain hitMethod not found: 'soroqTransitionBatchByIdentity', surfacing as a genericCommand PhaseScriptExecution failedthat reads like a signing problem. The README now says to install viasoroq setupand never to pick a toolchain by name.
No behavioural change to activation, refusal or rollback: these are reporting fixes. Zero-slot atomicity and the retention of a previously active patch are unchanged and covered by tests.
0.2.6 #
- Static apps now show patched values. A committed transition invalidates the element tree exactly
once via
invalidatePresentationOnce(). 0.2.5 shipped onlyensureVisualUpdate(), which schedules a frame without marking anything dirty — a frame rebuilds only dirty elements, so a static widget re-rasterized the String it captured during its lastbuild()whether or not the redirect was live. On 0.2.5 that made "patch not activated" and "redirect active, UI never rebuilt" indistinguishable. - Secret-free OTA telemetry, on by default: manifest fetch, signature verification, runtime binding,
staged version, module load,
transitionByIdentitycommitted-vs-required counts, active version, quarantine and errors. Emitted as one greppableSOROQ_OTA …line per event so delivery can be observed independently of whether the UI repainted. Substitute a sink viaSoroq.configure(onEvent:). - Presentation and frame-scheduling are now one centralized primitive pair (
invalidatePresentationOnce/soroqScheduleFrame); nothing outsidepresentation.dartcallsensureVisualUpdatedirectly. - State,
TextEditingControllercontents, scroll offset and pushedNavigatorroutes are preserved across activation — the tree rebuilds, it never remounts.
Changelog #
0.2.5 #
Freehand identity activation for the iOS engine lane, plus the lifecycle and fail-closed guarantees it depends on.
-
Freehand identity activation. Patches now apply by STABLE IDENTITY rather than by numeric slot index.
SoroqFreehandActivatorexposes the identity capability (transitionByIdentity) as an interface separate from the base engine activator, so a freehand build never uses the indexed path and the indexed contract keeps working unchanged for existing releases. -
First-frame lifecycle barrier. Activation is deferred until after the first frame has rasterized (
soroqActivateRestoredAfterFirstFrame), and a patch is marked stable only once a healthy frame has been produced (soroqCommitStableOnHealthyFrame). This is what prevents a restored patch from being applied into a half-built frame, which previously showed as a black window on cold start.restorePrepare()splits candidate preparation from activation so the preparation work happens before the barrier. -
Transactional transitions and rollback. A batch of redirects is applied as ONE transaction: every identity is validated before any slot changes, so a batch that names an unknown identity commits ZERO redirects instead of leaving a partially patched app. The same primitive expresses activation (no stale ids), warm upgrade (set new, clear the ids the new patch does not re-redirect) and rollback (clear everything, set nothing).
-
Cross-base isolation. Patch state inherited from a DIFFERENT App Store base is deleted fail-closed before module restore, so a build can never activate a module compiled against another base.
-
Runtime binding. Every artifact request carries
runtime_id, and a signed manifest for a foreign runtime is rejected even though its signature verifies. The active runtime is persisted alongside the active patch. -
Fail-closed behaviour. Signature, payload-hash and identity failures each refuse at their own stage and are distinguishable in the lifecycle trace: a bad signature never reaches module loading, while a semantically invalid batch loads and is then rejected by the identity transaction. A version that fails activation is quarantined so it is not retried forever, and the previously active patch is retained.
0.2.4 #
- FIX (iOS engine lane): publish the multi-index runtime activation controller. The
_redirectModulestate machine redirects EVERY manifest patch index — for theindexed_selector_v1contract it resolvesreplacement = Function.apply(selector, [index])per index and callsactivator.redirect(index, replacement). A prior published build (0.2.3) shipped an older_activatethat redirected onlypatches.first(index 0) and passed the raw selector module instead ofselector(index), so a four-function OTA patch left every value after index 0 on its base body and made index 0 render blank on device. Activation remains transactional: any single refused/failed redirect rolls back all redirects, quarantines the version, and never reportsactive=patched. - TEST: add R4 regression coverage — a four-function
indexed_selector_v1patch redirects indices 0-3 each to its ownselector(index)replacement, and one refused redirect among four fails closed to base (no false complete activation).
0.2.3 #
- Harden Android Kotlin plugin handling for AGP 9 (supersedes 0.2.2's unconditional apply, which
double-registers the
kotlinextension when AGP built-in Kotlin is enabled).android/build.gradlenow applieskotlin-androidONLY when thekotlinextension is not already registered, and configureskotlin { compilerOptions { ... } }ONLY once that extension exists. Compatible with: AGP < 9 (legacy), AGP 9 +android.builtInKotlin=false, and AGP 9 built-in Kotlin. Validated by building a Soroq-Flutter-frontend app (AGP 9.0.1 / KGP 2.3.20 / Gradle 9.1.0 / builtInKotlin=false) with this package — the exact fresh-user environment. No runtime behavior change. - NOTE (future follow-up): Flutter emits a deprecation warning that this package applies the Kotlin Gradle Plugin; a later release will migrate to Flutter Built-in Kotlin. The build succeeds today.
0.2.2 #
- Fix fresh-user Android build on AGP 9:
android/build.gradlenow applies thekotlin-androidplugin unconditionally. The prior AGP-9 conditional (apply only when AGP major < 9) left fresh AGP 9 apps without the Kotlin Gradle extension, so the always-presentkotlin { compilerOptions { ... } }block failed withCould not find method kotlin() for arguments ... on project ':soroq_flutter'. No runtime behavior change; Android hard-OTA runtime unchanged.
0.2.1 #
- Post-publish metadata/docs/test polish (no runtime behavior change):
- iOS podspec: version 0.2.1; description no longer says iOS is config/data-only — now Android hard OTA + experimental iOS engine-lane hard OTA (Soroq R3 toolchain + activator); still no vendored_frameworks / no SoroqRuntimeCAPI.
- README: removed lower-section contradictions that still framed iOS as config/data OTA only; honest scope (Android hard OTA; iOS experimental engine hard OTA; requires R3 toolchain + minimal activator; no App-Store/Shorebird-parity/arbitrary-Dart claims).
engine_lane.dartdocs: distinguishSoroqEngineLaneClient(read-only status) from the package-ownedSoroqEngineLaneController/Soroqhard-OTA state machine; no longer imply the package does not drive engine-lane apply.ios_package_policy_test: keep the real assertions (no vendored_frameworks, no SoroqRuntimeCAPI, no native staging methods); the.pubignorecheck now skips gracefully when absent (it is publish-time-only, not in the pub.dev archive).
0.2.0 #
- Added the package-driven iOS engine-lane hard-OTA state machine (EXPERIMENTAL):
Soroqfacade +SoroqEngineLaneControllerown fetch → Ed25519 verify → SHA-256 verify → transactional stage → hosted v0 rollback → crash-loop quarantine → client-id/rollout → tamper refusal. The app supplies a thinSoroqEngineActivatorengine binding (bytes→module, index→redirect, rollback) — zero OTA policy. Requires the Soroq R3 experimental engine toolchain (installed by the Soroq CLI). NOT App Store approved; NOT a general arbitrary-Dart hot-patch system (experimental only). - Reframed the package as app-facing hard OTA for both platforms (was "Android + iOS config/data OTA only").
0.1.16 #
- Removed the iOS runtime C API XCFramework from the public podspec/package path so TestFlight/App Store builds using the published plugin remain scoped to config/data OTA.
- Added a package policy regression test that blocks accidental iOS runtime framework vendoring in the public Flutter package.
0.1.15 #
- Added release-scoped config OTA requests and boot reports for iOS config/data harnesses.
- Added config OTA base-restore handling when the control plane reports a rolled-back accepted patch.
0.1.14 #
- Added the iOS plugin bridge for App-Review-safe config/data OTA state, snapshot persistence, and runtime C API launch-plan staging.
- Added
SoroqConfigOtaControllerand config OTA snapshot helpers for Flutter apps that need signed JSON/config payloads with validation and persistence. - Bundled the Soroq runtime C API XCFramework for iOS device and simulator builds.
0.1.13 #
- Recorded Android staged-patch launch success after the first Flutter frame so AOT patches are promoted instead of being treated as incomplete boots on the next cold start.
- Added Android cold-start predicates and regression coverage for runtime managed Dart, AOT shared-library, kernel, and asset override readiness.
0.1.12 #
- Added Android auto-update request scoping from bundled release metadata so
runtime
patch-checkand boot-report calls can sendrelease_idandrelease_versionto the hosted control plane. - Added Android regression coverage for release-scoped patch checks and release-scoped boot health reports.
0.1.11 #
- Packaged the Android runtime JNI library for
armeabi-v7a,arm64-v8a, andx86_64by default so published consumers get the same ABI coverage as the current Android proof lane. - Included the runtime bridge and auto-update coordinator hardening used by the latest hosted release/AOT patch, rollback, and patch-health proofs.
- Refreshed package documentation for the post-
0.1.10public-alpha archive.
0.1.10 #
- Published the hosted operator-control and CLI hardening updates that landed
after the immutable
0.1.9pub.dev archive. - Included the Android auto-update coordinator verification updates for dashboard/device identity and hosted public-alpha proof alignment.
0.1.9 #
- Staged the runtime v15 Android JNI package artifact and public-hygiene test
fixture updates that landed after the immutable
0.1.8pub.dev archive.
0.1.8 #
- Updated the package README to describe the now-published
0.1.8pub.dev install path after the0.1.7publish follow-up exposed immutable archive drift.
0.1.7 #
- Staged Android startup-provider hardening for public-alpha validation: packaged auto-update config now bootstraps before Dart/manual fallback while heavy startup-provider update work is deferred off the cold-start path.
- Added packaged release-metadata seeding and manifest-trust/runtime bridge updates used by the current hosted Android proof lane.
- Added focused Android runtime tests for packaged auto-update config and release metadata seeding.
0.1.6 #
- Updated the package README to describe the now-published pub.dev install path for the current public-alpha Flutter plugin version.
0.1.5 #
- Migrated Android Gradle wiring toward Flutter built-in Kotlin compatibility
without raising the current Flutter
>=3.28.0public-alpha floor. - Removed static
kotlin-androidapplication from the plugin/proof app Gradle files and conditionally apply KGP only for AGP< 9. - Moved Kotlin compiler options and plugin source-set wiring to the built-in-Kotlin-compatible DSL.
0.1.4 #
- Added Android pre-stage bundle integrity preflight for downloaded OTA bundles.
- Surface
bundle_preflight_failedbefore native staging when bundle manifest identity orartifact.binsize/SHA-256 does not match.
0.1.3 #
- Updated the Flutter package to depend on
soroq_sdk0.1.2 so apps get the config OTA helper and kind-scoped patch-check request model through the package re-export. - Documented app-side JSON config patch consumption with
fetchConfigPatch. - Included the current Android runtime bridge, manifest trust, patch download, and device patch identity updates in the next publishable plugin archive.
0.1.2 #
- Bundled the Android arm64 Soroq runtime JNI library in the plugin package so fresh pub.dev consumers no longer need the repository-local Rust runtime checkout during Gradle builds.
- Kept source builds available for repository development with
SOROQ_BUILD_RUST_JNI=1.
0.1.1 #
- Added device patch phase helpers so apps can show whether a device is on the base app, staged patch, booting patch, active patch, or needs attention.
- Kept active patch heartbeats flowing even when there are no queued boot events, allowing the hosted surface to see current device patch state.
- Added public-alpha CLI onboarding commands to the package README.
0.1.0 #
- First public-alpha package surface for the Flutter-facing Soroq plugin.
- Added the shared Dart runtime bridge and Android plugin/bootstrap layer.
- Re-exported the pure Dart
soroq_sdkclient and models. - Documented the current Android-first, pinned-toolchain compatibility story.