Skip to content

feat(release): fingerprint the native surface so an OTA cannot outrun the binary - #2977

Merged
innolope-dev merged 9 commits into
devfrom
chore/native-fingerprint
Sep 4, 2026
Merged

feat(release): fingerprint the native surface so an OTA cannot outrun the binary#2977
innolope-dev merged 9 commits into
devfrom
chore/native-fingerprint

Conversation

@innolope-dev

Copy link
Copy Markdown
Collaborator

Makes "the JS assumes native code the receiving binary doesn't have" a red CI job at OTA time instead of a crash on a user's device.

The gap

A bundle's only identity is a version number, and a version number says nothing about the native surface it needs. 1.2.1 does not encode "requires the 8.51 updater plugin". So JS and native ship on two independent clocks with nothing connecting them, and CI happily publishes a bundle built against new plugins onto binaries built months earlier.

Nothing caught this before:

  • Capgo's min_update_version blocks delivery only, only under the metadata channel strategy (production is on major, so today it is inert), and it lives in a dashboard CI cannot read.
  • Nothing anywhere reported that an incompatible bundle had been built.

The live example: #2968 bumps @capgo/capacitor-updater 8.45.9 → 8.51.14. A plugin's JS wrapper ships in the OTA bundle while its native half stays in the binary, so devices on the 1.1.0 shell would run 8.51.14 JS against 8.45.9 native — and the updater breaking is the worst thing to break, since it is the recovery path.

The check

scripts/native-fingerprint.mjs hashes the JS↔native contract. The load-bearing inputs are Capacitor's two generated plugin manifests, which pin every plugin's resolved version in its dependency path:

project(':capgo-capacitor-updater').projectDir =
  new File('../node_modules/.pnpm/@capgo+capacitor-updater@8.51.14_@capacitor+core@8.2.0/...')

plus capacitor.config.ts, the gradle files, AndroidManifest.xml, project.pbxproj, Info.plist and both entitlements files. Generated web assets are excluded — they are OTA output and would move the fingerprint on every commit.

capgo-deploy.yml recomputes it and compares against the v<major>.<build>.0 tag the bundle's floor targets. On a mismatch the OTA fails and names the file that moved:

native surface changed since v1.1.0: 55e98aa991b5884b -> c59826e04718d7ce
  android/capacitor.settings.gradle: 3f375c804489 -> 628cd3665fa2
  ios/App/CapApp-SPM/Package.swift: cfd61441c036 -> d69faa8977c7
  ...

A bare "refused" would be unactionable; naming the culprit makes the remedy obvious.

Two design choices worth reviewing

No stored state. The fingerprint is a pure function of the tree, so a shipped binary's surface is exactly what its tag describes — any tag can be fingerprinted retroactively (--ref v1.1.0), there is no bootstrap problem, and no second source of truth to drift. This also sidesteps the fact that CI cannot commit to dev (the ruleset has no bypass actors).

MARKETING_VERSION / CURRENT_PROJECT_VERSION are normalised out. native-ios-postsync.js stamps them on every cap sync; left raw, the check would refuse an OTA after every release. A check that cries wolf gets deleted, so this one had to be precise about what counts as the surface. Covered by a test, alongside one asserting a real pbxproj change (the deployment floor) still trips it.

9 tests, spawning the script the way CI does.

Immediate consequence — please read

dev's native surface has already moved past v1.1.0 (the updater bump, the iOS 16.4 floor, #2754's native additions). Once this lands, staging OTAs — auto-published on every dev merge — will fail until v1.2.0 ships.

That is the check working, not a bug: staging testers run production binaries, so today's dev JS genuinely does not fit them. It self-resolves the moment v1.2.0 lands. Flagging it because it will look alarming in the first red run.

There is deliberately no override input. The remedy is to cut a native release; an escape hatch is how guards like this die. If it proves too strict in practice, adding one is a one-line follow-up — but start strict.

Not in this PR

The channel is still on the major strategy, so min_update_version remains inert. Flipping it to metadata needs the client to learn the disableAutoUpdateMetadata rejection code first (NEWER_BINARY_ERRORS only knows to_major/to_minor, so gated devices would trip the 3-strike failure streak and flood Sentry), and it needs a Capgo Super Admin. Tracked separately.

… the binary

A bundle's only identity is a version number, which says nothing about the
native surface it needs. 1.2.1 does not encode 'requires the 8.51 updater
plugin', so CI publishes JS built against new plugins onto binaries built
months earlier and the mismatch first appears on a user's device.

Nothing caught this before. Capgo's min_update_version blocks delivery
only, only under the 'metadata' channel strategy (production is on
'major', so today it is inert), and lives in a dashboard CI cannot read —
it never reports that an incompatible bundle was BUILT.

native-fingerprint.mjs hashes the JS<->native contract: Capacitor's two
generated plugin manifests, which pin each plugin's resolved version in
its dependency path, plus capacitor.config.ts, the gradle files, the
manifest, pbxproj, Info.plist and both entitlements. capgo-deploy.yml
compares the tree it is publishing against the v<major>.<build>.0 tag its
floor targets and fails, naming the file that moved.

It is a pure function of the tree, so nothing is stored and any tag can be
fingerprinted retroactively. MARKETING_VERSION and CURRENT_PROJECT_VERSION
are normalised out: native-ios-postsync.js stamps them on every sync, and
leaving them in would refuse an OTA after every release — a check that
cries wolf gets deleted.

This is docs/NATIVE-RELEASE.md's standing rule, 'bump the native version
whenever you change plugins/native code', with something checking it.
@vercel

vercel Bot commented Sep 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
peanut-wallet Ready Ready Preview Sep 4, 2026 4:16pm UTC

Request Review

@coderabbitai

coderabbitai Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Team

Run ID: 21376ea9-19b5-4c63-b48b-547ac5a9cc05

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 7149.37 → 7149.37 (0)
Findings: 0 net (+0 new, -0 resolved)

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 5809 ran, 0 failed, 0 skipped, 1.7m

📊 Coverage (unit)

metric %
statements 74.5%
branches 59.8%
functions 68.1%
lines 75.5%
⏱ 10 slowest test cases
time test
🐢 9.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Network failure keeps loading while retries remain, then shows the generic error
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › MANTECA_SOURCE_OVER_MONTHLY_CAP fails fast with copy that names the real cause
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › MANTECA_MERCHANT_RECENT_REFUND fails fast with copy that names the real cause
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › User KYC not approved fails fast with copy that names the real cause
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › routes the KYC rejection on its wire code, and does not retry it
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › MANTECA_MERCHANT_VOLUME_NEAR_CAP fails fast with copy that names the real cause
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › MANTECA_USER_NOT_PROVISIONED fails fast with copy that names the real cause
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › a refused idempotency key tells the user to scan again, not to contact support
3.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Going offline blames the connection, and reconnecting clears it for the recovered scan
3.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Scan that recovers on the retry lands on the payment screen, not an error
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@chip-peanut-bot chip-peanut-bot Bot 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.

Chip review — changes requested

The new OTA check fails closed for its listed files, but it can still report an unchanged native surface when either an in-repo native bridge implementation or the authoritative resolved plugin dependency changes.

Findings

  • BLOCKING · scripts/native-fingerprint.mjs:68 · Include the in-repo native bridges in the fingerprint
    The allowlist ends at configuration files and omits the native code that JavaScript actually calls, including android/app/src/main/java/me/peanut/wallet/* and ios/App/App/*Plugin.swift plus their registration code. This is already a live edit pattern: commits have changed MainActivity.java alongside OTA JavaScript, and PushProvisioningPlugin.swift has changed independently without touching any listed input. If a later bundle calls a new method or relies on corrected behavior in one of those files, --diff returns unchanged against the old release tag and the bundle is published to a binary with the old bridge. Hash the relevant native source/registration directories (with deterministic path ordering), and add a test that mutates a representative Java or Swift bridge file.

  • BLOCKING · scripts/native-fingerprint.mjs:52 · Derive plugin versions from authoritative dependency state
    The check trusts two committed generated manifests, but the OTA workflow only runs pnpm install and native-build.js; it never regenerates either manifest. A PR can therefore bump a Capacitor plugin in package.json/pnpm-lock.yaml while accidentally leaving these generated files unchanged. The install supplies the new JS wrapper to the OTA build, both fingerprints still see the old manifest bytes, and the bundle is published to a binary containing the old native plugin—the exact mismatch this guard is meant to stop. Fingerprint the resolved native-plugin entries from the lockfile/package metadata (or deterministically regenerate and verify the manifests before comparing), and cover a lockfile-only plugin bump in the tests.

Checked clean

  • Confirmed the detached worktree head, trusted author, base ref, supplied base SHA, and merge base all match the dispatcher metadata.
  • Reviewed the workflow trigger, checkout depth and tags, native-floor resolution, release-tag timing, Capgo upload boundary, and the new fail-closed step.
  • Exercised the CLI at the exact head: the working tree matches HEAD, while comparison with v1.1.0 exits 1 and names all changed listed inputs.
  • Traced native build and release workflows to distinguish committed source state from generated and release-time native state.
  • Reviewed native-source history and confirmed Java/Swift bridge implementations can change without any current fingerprint input changing.
  • Exact-head ci-success, unit, native-export, typecheck, eslint, format, analyze, design-system, and provenance checks passed; Deploy Preview remained in progress and is not relevant to this deployment guard.
  • A local Jest run was unavailable because dependencies are not installed in the detached worktree; the exact-head unit check completed successfully.

Security review by moonshotai/kimi-k3: 0 finding(s), marked with the model name. It reads the diff only and answers only security, privacy and money, so treat its findings as advice.

Third opinion by claude-opus: 0 finding(s), marked with the model name. It answers only product truth, missing tests and the cross-repo contract, so treat its findings as advice.

Exact head: f43a34c99ea3 · Context: repo, ci · Took 9m

Comment thread scripts/native-fingerprint.mjs Outdated
Comment thread scripts/native-fingerprint.mjs Outdated
Two holes Chip found, both of which let the exact mismatch this guard
exists to stop through unnoticed.

The allowlist stopped at config files, so the native code JS actually
calls was invisible. MainActivity.java registers the app-local plugins and
the *Plugin.java / *Plugin.swift files are the methods themselves; a
bundle calling a new method, or relying on corrected behaviour in one of
them, moved no listed input and --diff reported unchanged. Both are live
edit patterns in this repo's history. Hash the bridge sources, path and
content, so a rename with identical bytes still counts.

The plugin versions were read only from Capacitor's committed generated
manifests, which the OTA workflow never regenerates — it runs pnpm install
and native-build.js. A plugin bumped in package.json + pnpm-lock.yaml
without a cap sync therefore ships the NEW JS wrapper to the bundle while
both manifests still hash to the old bytes. The lockfile is the
authoritative record of what install produced, so read the resolved
versions from it; the manifests stay as the complementary check on the
plugin SET.

Also fixes a silent-empty bug found while testing the above:  matches nothing and exits 0, so the first cut of
the bridge globs reported an empty set at EVERY ref and would have
compared nothing against nothing while passing. List the directory and
filter by extension instead, with a test that the sets resolve at a ref.

13 tests, covering an Android bridge edit, an iOS bridge edit and a
lockfile-only bump.

@chip-peanut-bot chip-peanut-bot Bot 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.

Chip review — changes requested

The bridge coverage is fixed, but the fingerprint still has two fail-open gaps and the new ref test leaves exact-head CI red.

Findings

  • BLOCKING · scripts/native-fingerprint.mjs:57 · Discover native plugins beyond four namespaces
    The lockfile now supplies resolved versions, but this matcher decides which dependencies are native by four scopes plus cordova. A direct Capacitor plugin such as @capacitor-community/in-app-review or @transistorsoft/capacitor-background-geolocation is excluded. If one is added or bumped without cap sync, package.json and pnpm-lock.yaml move while both generated manifests remain stale, so the fingerprint reports unchanged and an OTA can ship a JS wrapper that the binary lacks. Derive the native-plugin set from installed package metadata or cover generic Capacitor plugin names, and add a regression outside the current scopes.

  • BLOCKING · scripts/native-fingerprint.mjs:76 · Fingerprint native resource contracts too
    The Android list hashes AndroidManifest.xml but omits native resources it delegates to, including android/app/src/main/res/values/capacitor-passkey.xml; the iOS list likewise omits the Wallet and notification extension Info.plists and entitlements. Changing the passkey asset statement or an extension entitlement leaves every current input unchanged, so JS that relies on the corrected native configuration can be OTA'd onto an older binary. Add the relevant text resource/config files (or narrow globs that exclude generated web assets and images) and regression-test one Android resource plus one extension plist/entitlement.

  • MAJOR · scripts/tests/native-fingerprint.test.js:168 · Do not require release tags in the shallow unit checkout
    The exact-head unit check fails here: that job uses actions/checkout's default shallow history, so v1.1.0 is absent and the script returns <absent> for the bridge set; line 170 then fails and ci-success is red. The two earlier v1.1.0 tests also pass for the wrong reason because an unknown ref is treated as an all-absent tree. Make these tests self-contained with HEAD plus a temporary working-tree change (or explicitly fetch tags), and add an assertion that an unknown ref is rejected rather than hashed.

Checked clean

  • Verified the supplied head, base SHA, merge base, trusted author, and four-file diff.
  • Java/Kotlin and Swift bridge sets resolve in the full-history checkout at HEAD and v1.1.0; prior P1 is fixed.
  • The current native dependencies all resolve to concrete pnpm-lock versions, then the namespace matcher was checked against additional Capacitor plugin names.
  • The CLI manifest and --diff HEAD paths passed directly; workflow inputs use execFile argument arrays and do not add shell interpolation.
  • Exact-head format, typecheck, ESLint, native-export, design-system, analysis, and preview checks passed; unit and aggregate ci-success failed on the new ref test.

Security review by moonshotai/kimi-k3: 0 finding(s), marked with the model name. It reads the diff only and answers only security, privacy and money, so treat its findings as advice.

Third opinion by claude-opus: 0 finding(s), marked with the model name. It answers only product truth, missing tests and the cross-repo contract, so treat its findings as advice.

Exact head: 5fe6817f1495 · Context: repo · Took 12m

Comment thread scripts/native-fingerprint.mjs Outdated
Comment thread scripts/native-fingerprint.mjs
Comment thread scripts/__tests__/native-fingerprint.test.js Outdated
The unit job checks out at depth 1 with no tags, so `--ref v1.1.0` resolved
to nothing in CI. The regression guard added in 5fe6817 caught that
honestly and went red — but the two older ref-based tests had been passing
for entirely the wrong reason: with no tag, every input reads <absent>,
which still differs from the working tree, so "the fingerprint differs
across refs" and "--diff exits 1" both held while checking nothing at all.

Use HEAD, which always resolves, and create the difference by mutating the
working tree instead of relying on history. The suite now asserts the
inverse too — that the working tree equals HEAD when clean — which is what
actually proves the ref path reads content rather than silently returning
absence for everything.

Same failure mode as the `git ls-tree` glob bug this suite already guards:
an empty read that looks exactly like a passing check.

The workflow itself is unaffected — capgo-deploy.yml checks out with
fetch-depth: 0 and fails closed with an explicit message when the release
tag is missing.

@chip-peanut-bot chip-peanut-bot Bot 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.

Chip review — changes requested

The hermetic test fix is green, but two native-surface omissions still let incompatible OTAs pass the fingerprint check.

Findings

  • BLOCKING · scripts/native-fingerprint.mjs:57 · Discover native plugins beyond four namespaces
    This matcher still recognizes native packages only under @capacitor, @Capgo, @OneSignal, @sumsub, or names containing cordova. Add or bump a Capacitor plugin such as @capacitor-community/in-app-review without running cap sync: package.json and pnpm-lock.yaml move, both generated manifests remain stale, the dependency is excluded here, and the fingerprint stays unchanged, allowing an OTA wrapper onto a binary without its native half. Derive the plugin set from package metadata or Capacitor discovery and cover a plugin outside these namespaces.

  • BLOCKING · scripts/native-fingerprint.mjs:76 · Fingerprint native resource contracts too
    The input list hashes AndroidManifest.xml but not the resource it delegates passkey trust to: android/app/src/main/res/values/capacitor-passkey.xml. Correcting that asset statement changes whether native passkey authentication works while every current fingerprint input remains unchanged, so a later OTA can be published to binaries carrying the old association. The Wallet and notification extension Info.plists and entitlements are omitted similarly. Include the relevant text resource/config files with focused Android and iOS extension regression tests.

Checked clean

  • Verified the supplied repository, PR, trusted author, exact head, base SHA, merge base, and four-file diff.
  • Java/Kotlin and Swift bridge sets are hashed by path and content in both working-tree and git-ref modes; prior P1 is fixed.
  • Selected native dependency versions come from pnpm-lock.yaml and a lockfile-only bump moves the fingerprint; prior P2 is fixed independently of the remaining discovery gap.
  • The workflow uses full history, verifies the resolved release tag, quotes the floor ref, and runs the fingerprint before the Capgo upload.
  • Direct manifest and --diff HEAD checks passed; exact-head aggregate, unit, native-export, typecheck, lint, format, analysis, design-system, provenance, and report checks are green, with preview still in progress.
  • Reviewed workflow secret handling and process-spawn inputs; no new injection, credential disclosure, or privilege-escalation path was found.

Security review by moonshotai/kimi-k3: 0 finding(s), marked with the model name. It reads the diff only and answers only security, privacy and money, so treat its findings as advice.

Third opinion by claude-opus: 0 finding(s), marked with the model name. It answers only product truth, missing tests and the cross-repo contract, so treat its findings as advice.

Exact head: f1f31e174235 · Context: repo · Took 9m

Comment thread scripts/native-fingerprint.mjs Outdated
Comment thread scripts/native-fingerprint.mjs
Three more holes Chip found, all the same shape: an input the config files
delegate to, which nothing in the allowlist moved for.

Native resources. AndroidManifest.xml was hashed but not the res/values it
points at, so editing capacitor-passkey.xml — the asset statement passkeys
are validated against — left every input unchanged. On iOS only the app's
own Info.plist and entitlements were covered, not the extensions', though
an extension's capabilities are just as much part of the shell a bundle
lands on. Both are now globs, and the two iOS single-file inputs fold into
one that also covers OneSignal's and the Wallet extensions'.

Community plugins. The native dependency matcher was a four-scope
allowlist, so @capacitor-community/in-app-review and
@transistorsoft/capacitor-background-geolocation were invisible: bumping
one without a cap sync moved package.json and the lockfile while both
generated manifests stayed stale, and the fingerprint reported unchanged.
Match the substring instead, and take the union with the plugin names
Capacitor itself generated — the declared dependencies catch a plugin that
was never synced, the generated manifests catch one whose package name
says nothing about being native.

Unresolvable refs. git show and git ls-tree both fail quietly for an
unknown ref, so every input read <absent> and produced a well-formed
fingerprint of nothing. It differs from any real tree, so a --diff against
a missing tag reported "changed" and looked like the check had run. Now an
error. Same shape as the ls-tree glob bug and the tag-dependent tests
before it: silence that reads as a result.

17 tests, including the passkey asset statement, an extension entitlement,
a community plugin outside every first-party scope, and an unknown ref.

@chip-peanut-bot chip-peanut-bot Bot 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.

Chip review — changes requested

Most direct prior omissions are fixed, but the release guard can still false-green for an unrecognized native plugin, a conditionally compiled bridge, or same-version patched native code. The new tests also mutate files shared with parallel Jest workers.

Findings

  • BLOCKING · scripts/native-fingerprint.mjs:60 · Discover native plugins from metadata, not their names
    The P3/P6 discovery gap remains in a narrower form: a newly declared Capacitor plugin whose package name contains none of capacitor, cordova, onesignal, or sumsub is filtered out here. If its generated manifests are stale—the exact failure this guard is meant to catch—generatedPluginNames cannot recover it either, so adding its JS wrapper leaves every fingerprint input unchanged and an OTA can reach a binary with no native implementation. Discover plugins from package metadata/plugin.xml or maintain an exhaustive native-dependency declaration instead of a package-name heuristic.

  • BLOCKING · scripts/native-fingerprint.mjs:105 · Fingerprint the bridge set actually compiled into the binary
    This glob hashes PushProvisioningPlugin.java even when the Android release intentionally omits src/meawallet/java because its optional build inputs are absent; the release still succeeds and the tag therefore claims a bridge the binary never registered. A later OTA can rely on PushProvisioning while comparing equal to that tag and pass this guard. Make that build variant fail closed for releases, or record and compare a non-sensitive compiled-capability/build-variant manifest rather than treating every tracked source file as present in the binary.

  • BLOCKING · scripts/native-fingerprint.mjs:248 · Include patched native package identity
    Only version-looking tokens are retained from pnpm-lock.yaml. Adding or changing a pnpm patch for a native plugin can change both its JS wrapper and native implementation while leaving the package version and generated Capacitor manifests unchanged; the patch file and lockfile patch hash are not inputs, so the OTA fingerprint still matches the old binary. Hash the relevant resolved package entries, including patch/integrity identity and patch-file contents, not just their semantic versions.

  • MINOR · scripts/tests/native-fingerprint.test.js:27 · Isolate fingerprint fixtures from the live checkout
    withPatchedInput rewrites tracked repository files while Jest runs test files in parallel. For example, this suite holds project.pbxproj at MARKETING_VERSION 9.9.9 while spawning the CLI, and marketing-version.test.js independently reads that same file and expects the package version, creating a timing-dependent unit failure despite the finally restore. Run the fingerprint CLI against a temporary repository fixture or otherwise isolate these mutations from the shared checkout.

Checked clean

  • P1 is fixed: Android and iOS in-repo bridge sources are hashed by path and content.
  • P2 is fixed for resolved version changes: declared and generated native plugin names now derive versions from pnpm-lock.yaml, including a lockfile-only bump.
  • P4 and P7 are fixed for the cited tracked resources: Android XML resources and all iOS plist and entitlement files are included.
  • P5 is fixed: ref-based unit coverage uses HEAD and no longer requires release tags in the shallow unit checkout.
  • The CLI manifest and --diff HEAD paths completed successfully against the pinned checkout.
  • Exact-head unit, typecheck, format, lint, native-export, analyze, preview, and aggregate CI checks completed successfully.

Security review by moonshotai/kimi-k3: 0 finding(s), marked with the model name. It reads the diff only and answers only security, privacy and money, so treat its findings as advice.

Third opinion by claude-opus: 0 finding(s), marked with the model name. It answers only product truth, missing tests and the cross-repo contract, so treat its findings as advice.

Exact head: a4f2a49c6510 · Context: repo · Took 15m

Comment thread scripts/native-fingerprint.mjs
Comment thread scripts/native-fingerprint.mjs
Comment thread scripts/native-fingerprint.mjs
Comment thread scripts/__tests__/native-fingerprint.test.js Outdated
…e tests

Four findings, all real.

The name heuristic could still miss a plugin called after neither capacitor
nor a first-party scope, and if its generated manifests were stale too —
the exact case this guard exists for — nothing recovered it, because
package.json and pnpm-lock.yaml are not inputs on their own. Hash the full
dependency NAME set. Names and not versions deliberately: adding or
removing a dependency is rare and worth re-checking the native surface,
while bumping a JS-only library is not, and a check that refuses an OTA on
every lockfile churn gets switched off.

The Android bridge glob hashed every source set, including src/meawallet,
which build.gradle adds only when the MeaWallet Nexus credentials are
present. A release without them still succeeds, so the tag claimed a bridge
the binary never registered — worse than not claiming it, since a later OTA
could rely on the claim. Hash src/main only. This narrows rather than
solves: whether a secret-gated variant is in a given binary is not visible
in the tree at all, so the fingerprint now neither asserts nor denies it.

A pnpm patch rewrites a package's JS wrapper and its native sources with no
version change, leaving the lockfile version and both generated manifests
identical. Added patches/ and the patchedDependencies map.

The tests rewrote tracked files around a spawned CLI and restored them in a
finally. That is not isolation — Jest runs suites in parallel, and
marketing-version.test.js reads the same project.pbxproj, so holding it at
9.9.9 for the length of a subprocess made an unrelated suite fail on
timing. They now build a throwaway git fixture and point the CLI at it with
a new --root flag. 18 tests, and the two suites now pass together.
@innolope-dev

Copy link
Copy Markdown
Collaborator Author

All four findings addressed in b682a6b96. Replies are on the two threads that still had live anchors; the other two had none, so they are here.

Fingerprint the bridge set actually compiled into the binary. Correct — android/app/build.gradle adds src/meawallet only when the MeaWallet Nexus credentials are present, the release still succeeds without them, and the glob was hashing PushProvisioningPlugin.java regardless. The tag claimed a bridge the binary may never have registered, which is worse than not claiming it because a later OTA could rely on the claim.

The glob is now android/app/src/main/**.{java,kt}. I want to be straight that this narrows rather than solves it: whether a secret-gated variant is in a given binary is not visible in the tree at all, so the fingerprint now neither asserts nor denies it. I did not take the fail-closed option because it would break every release built without the MeaWallet credentials, which is currently all of them. A compiled-capability manifest emitted by the build and recorded on the tag is the real answer, and it is a bigger change than this PR should carry — happy to file it as a follow-up if you agree that is the right shape.

Isolate fingerprint fixtures from the live checkout. Entirely right, and it was already a latent flake rather than a hypothetical: the suite held project.pbxproj at MARKETING_VERSION 9.9.9 across a spawned subprocess while marketing-version.test.js read the same file expecting the package version.

The tests now build a throwaway git repo from copies of the real inputs and point the CLI at it with a new --root flag; nothing mutates this checkout. Verified by running native-fingerprint and marketing-version together — 29 tests, both suites green, which they could not reliably have been before.

@chip-peanut-bot chip-peanut-bot Bot 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.

Chip review — changes requested

The latest patch fixes patch identity and test isolation, but the guard can still false-green for an already-declared generically named plugin and for the credential-gated Android bridge compiled by release CI.

Findings

  • BLOCKING · scripts/native-fingerprint.mjs:271 · Track generic native plugin version changes
    Hashing every dependency name catches the first declaration, but it does not catch a later version bump. If a generically named plugin was released while its committed Capacitor manifests were stale (the release workflows run cap sync only in their build workspaces), the tag still has no generated name for it. A later 1.x to 2.x package/lockfile bump leaves allDependencies unchanged, the name heuristic still excludes it, and the OTA passes against the old binary. Classify native plugins from package metadata or an explicit native-dependency map, then hash their resolved versions.

  • BLOCKING · scripts/native-fingerprint.mjs:119 · Fingerprint the credential-gated bridge when releases compile it
    The new src/main-only glob drops PushProvisioningPlugin.java even though android-release.yml supplies the MeaWallet credentials and app/build.gradle compiles src/meawallet into release binaries. After a tagged release, changing that bridge and OTA JS to rely on its new method or behavior leaves this input and build.gradle unchanged, so the OTA can pass against the older binary. Make release builds fail closed on the capability or compare a variant-specific compiled-capability/artifact manifest; omitting the conditional source creates a false green for the normal release variant.

Checked clean

  • P1 is fixed: path-and-content hashes cover the in-repo Android main and iOS bridge sources.
  • P2 is fixed for identified plugins: resolved versions come from pnpm-lock.yaml rather than committed generated manifests.
  • P3 and P6 are fixed for the cited namespace allowlist: discovery now combines a broad name pattern with both generated Capacitor manifests; P8 records the remaining generic-name version gap.
  • P4 and P7 are fixed: Android XML resources and iOS plist and entitlement files are fingerprinted.
  • P5 is fixed: ref coverage uses a temporary repository's HEAD and errors on an unknown ref.
  • P10 is fixed: patch paths, contents, and the patchedDependencies mapping now move the fingerprint.
  • P11 is fixed: every mutation occurs in a unique temporary git fixture rather than the live checkout.
  • The CLI manifest and --diff HEAD paths completed successfully at the pinned head; the focused local Jest suite was unavailable because this detached worktree has no node_modules.
  • Exact-head aggregate CI, unit, native-export, typecheck, lint, format, analysis, design-system, and preview checks completed successfully.

Security review by moonshotai/kimi-k3: 0 finding(s), marked with the model name. It reads the diff only and answers only security, privacy and money, so treat its findings as advice.

Third opinion by claude-opus: 0 finding(s), marked with the model name. It answers only product truth, missing tests and the cross-repo contract, so treat its findings as advice.

Exact head: b682a6b96c09 · Context: repo · Took 8m

Comment thread scripts/native-fingerprint.mjs Outdated
Comment thread scripts/native-fingerprint.mjs Outdated
… set

Two re-raises, both right.

Hashing dependency NAMES caught a plugin's first declaration but not a later
version bump: a generically-named plugin released while its committed
manifests were stale is invisible to the name heuristic AND to Capacitor's
generated list, so a 1.x to 2.x bump moved nothing. Added an explicit
NATIVE_DEPENDENCIES list as a third source, and a test asserting every
plugin Capacitor generated appears in it — so a synced plugin cannot drop
out of the list silently, which is what makes the list trustworthy for the
unsynced case it exists to cover.

Reverted the Android glob to every source set. Whether src/meawallet reaches
a binary depends on a CI secret, so it is not knowable from the tree and
both choices are unsound: including it lets a tag claim a bridge the binary
may lack, excluding it lets a bridge change go untracked when the binary
does have it. Including is the conservative error — it can only force an
unnecessary native release, while excluding fails silently and ships JS
against a binary without the method.

Worth recording: the premise that releases currently compile that variant
is not true today — MEAWALLET_NEXUS_USER_ANDROID exists neither at repo
level nor in the Production environment, so build.gradle takes the else
branch. The hole is real the moment those secrets are provisioned, which is
why this errs toward refusal rather than leaving it out.
@innolope-dev

Copy link
Copy Markdown
Collaborator Author

Fingerprint the credential-gated bridge when releases compile it — reverted in 7fd94622e; the Android glob is back to every source set, src/meawallet included.

One correction to the premise, for the record: releases do not compile that variant today. MEAWALLET_NEXUS_USER_ANDROID exists neither at repo level nor in the Production environment, so app/build.gradle takes the else branch. I checked before changing direction.

That does not save the exclusion, though — the team confirms those secrets are being provisioned, just not immediately. So the hole you describe becomes real, and tracking the source set is correct rather than merely cautious.

The reasoning I settled on: whether that bridge reaches a binary is not knowable from the tree, so both choices are unsound. Including it lets a tag claim a bridge the binary may lack; excluding it lets a bridge change go untracked once the binary does carry it. Including is the conservative error — it can only force an unnecessary native release, while excluding fails silently and ships JS against a binary with no such method.

The real fix is for the variant to stop depending on a CI secret, or for the build to emit a compiled-capability manifest recorded on the tag. Both are larger than this PR, and I'd rather land the guard erring toward refusal than hold it for a build change. Happy to file that follow-up.

@chip-peanut-bot chip-peanut-bot Bot 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.

Chip review — changes requested

The latest patch closes the direct bridge-list gap, but the guard still false-greens for generic native-plugin version bumps, credential-dependent Android binaries, and iOS native dependencies injected only by the release post-sync script.

Findings

  • BLOCKING · scripts/native-fingerprint.mjs:302 · Track generic native plugin version changes
    The explicit list covers today's plugins, but hashing every other dependency only by name still misses its later version changes. If a generically named native plugin is added while the committed Capacitor manifests are stale, the first addition forces a native release through the dependency-name hash, yet that release tag still does not classify the package as native. A later 1.x to 2.x package/lockfile bump leaves the name set unchanged and produces the same fingerprint; I reproduced that exact transition with some-native-thing. Require a native declaration or installed-package metadata for every such dependency, then hash its resolved lockfile version.

  • BLOCKING · scripts/native-fingerprint.mjs:153 · Fingerprint the binary's actual Android variant
    Hashing src/meawallet fixes source edits when that source is compiled, but it still cannot distinguish the binary that was actually cut. android-release.yml can succeed without both MeaWallet Nexus secrets; app/build.gradle then omits this source set, while the tag receives the same source hash as a fully featured build. A later OTA can start calling an existing PushProvisioning method without changing native files and compare equal to a tag whose binary never registered the plugin. Make release CI fail closed for this capability or persist and fingerprint a non-secret manifest of the compiled variant.

  • BLOCKING · scripts/native-fingerprint.mjs:118 · Include release-time iOS native inputs
    The iOS input list omits scripts/native-ios-postsync.js, even though the release workflow runs it after cap sync to inject proprietary frameworks into the compiled Package.swift and it pins MPP_VERSION. Changing that version from 2.0.0 to 2.1.0 leaves this fingerprint unchanged; OTA JavaScript can therefore rely on the newer native SDK while still passing against a binary built with 2.0.0. Hash the post-sync script and authoritative resolved iOS dependency state, or fingerprint a canonical manifest produced from the release workspace.

Checked clean

  • Verified the supplied trusted author, exact head SHA, base ref, base SHA, merge base, and four-file diff.
  • P1 is fixed: path-and-content hashes cover the in-repo Android and iOS bridge sources.
  • P2 is fixed for classified plugins: their resolved versions come from pnpm-lock.yaml rather than generated manifests.
  • P3, P6, and P8 are fixed for newly declared plugins: broad discovery plus the complete dependency-name set makes an addition move the fingerprint; P12 records the remaining later-version gap.
  • P4 and P7 are fixed for the cited resource contracts: Android XML resources and all iOS plist and entitlement files are included.
  • P5 is fixed: fixture ref tests use HEAD and unknown refs fail closed.
  • P10 is fixed: patch contents and patchedDependencies identity are included.
  • P11 is fixed: mutations run in an isolated temporary repository.
  • P13 is fixed for bridge source changes when release CI compiles the credential-gated source set; P9 records the unresolved actual-build-variant mismatch.
  • The CLI manifest and --diff HEAD paths passed at the pinned head; isolated reproductions confirmed unchanged fingerprints for both a generic native-plugin 1.x to 2.x bump and an iOS MPP SDK 2.0.0 to 2.1.0 bump.
  • Exact-head aggregate CI, unit, native-export, typecheck, lint, format, analysis, design-system, provenance, and preview checks completed successfully.
  • Reviewed workflow secret handling, ref validation, and git process spawning; no new injection or credential-disclosure path was found.

Security review by moonshotai/kimi-k3: 0 finding(s), marked with the model name. It reads the diff only and answers only security, privacy and money, so treat its findings as advice.

Third opinion by claude-opus: 0 finding(s), marked with the model name. It answers only product truth, missing tests and the cross-repo contract, so treat its findings as advice.

Exact head: 7fd94622e729 · Context: repo, ci · Took 12m

Comment thread scripts/native-fingerprint.mjs Outdated
Comment thread scripts/native-fingerprint.mjs
Comment thread scripts/native-fingerprint.mjs
scripts/native-ios-postsync.js runs after cap sync in the release workflow
and vendors proprietary frameworks whose versions are string constants
inside it — SUMSUB_VERSION 1.42.0 and MPP_VERSION 2.0.0. Nothing else in
the manifest records them, so bumping the MeaWallet SDK changed what the
binary links while every fingerprint input stayed identical, and OTA JS
relying on the newer SDK compared equal to a tag built against the old one.

Hash the script itself. A test bumps MPP_VERSION and asserts the
fingerprint moves.
@innolope-dev

Copy link
Copy Markdown
Collaborator Author

Round 5. One fixed, two I want to argue rather than patch again.

Include release-time iOS native inputs — fixed in dd00451b8. Straightforwardly right: SUMSUB_VERSION = '1.42.0' and MPP_VERSION = '2.0.0' are string constants inside native-ios-postsync.js, the release workflow runs it after cap sync to vendor those frameworks, and nothing else in the manifest recorded them. The script is now an input, with a test that bumps MPP_VERSION.

Track generic native plugin version changes — I don't think I should fix this the way it's framed, and here is the number that decides it. Hashing every dependency's resolved version is the only way to catch a later bump of an unclassified package. On this repo that is 73 commits touching package.json in the last 60 days against 4 native releases. The guard would refuse essentially every OTA, and a guard that blocks the normal path gets deleted within a week — which costs more safety than the case it closes.

The alternative you suggest, requiring a native declaration for every dependency, means classifying ~200 packages and touching that list on every dependency addition. That is a real option, but it is a policy decision about how this repo takes dependencies, not a defect in this script, and it should be someone's explicit call rather than something I slip into a fingerprint PR.

What is in place: an explicit NATIVE_DEPENDENCIES list whose completeness is enforced against Capacitor's generated manifests by a test, the name heuristic as a backstop, and the dependency name set so a new plugin forces a native release on first appearance. The residue is precisely: a generically-named native plugin, added while the manifests are stale, bumped later. Three simultaneous conditions, the first of which already forced a release.

Fingerprint the binary's actual Android variant — agreed, and not solvable here. Whether src/meawallet was compiled depends on a CI secret, so no function of the tree can distinguish those two binaries; the fingerprint now errs toward refusing an OTA rather than toward a false green. Your fix is the right one — release CI failing closed on the capability, or the build emitting a non-secret compiled-variant manifest that the tag records — and both are build changes outside this PR. Confirmed with the team that those secrets are being provisioned, so this does need doing; I would rather file it than fake it here.

Happy to be overruled on either. If the call is that the OTA lane should block on every dependency bump, or that the full classification is worth maintaining, say so and I will implement it.

@chip-peanut-bot chip-peanut-bot Bot 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.

Chip review — changes requested

The post-sync SDK pin is now covered and exact-head CI is green, but the guard still false-greens for later generic-plugin bumps, secret-dependent release variants, and the resolved iOS package graph.

Findings

  • BLOCKING · scripts/native-fingerprint.mjs:309 · Track generic native plugin version changes
    The explicit list still does not close the stale-manifest case. Add a generically named native plugin without committing cap-sync output: the dependency-name digest forces that first native release, but the test only checks the still-stale generated manifests and does not require adding the package to NATIVE_DEPENDENCIES. A later package/lockfile bump from 1.x to 2.x leaves the name set unchanged, remains outside the matcher and manifests, and produces the identical fingerprint; I reproduced that transition at this head. Require an authoritative checked native-dependency declaration or package-metadata discovery for every such plugin, then hash its resolved version.

  • BLOCKING · scripts/native-fingerprint.mjs:152 · Fingerprint the binary's actual release variant
    Hashing every source file is not conservative when the release can omit that source. Both native release lanes succeed without the MeaWallet Nexus credentials: Android then excludes src/meawallet and its SDK, while iOS leaves the canImport-fenced stub, yet either tag receives the same tree fingerprint as a fully featured binary. A later OTA can start calling an existing PushProvisioning method and compare equal to a tag whose shipped binary never had that implementation. Fail native releases closed for this capability, or record a non-secret per-platform capability manifest from the built artifact and compare against that.

  • BLOCKING · scripts/native-fingerprint.mjs:125 · Include the resolved iOS package graph
    The tracked ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved is consumed by the release xcodebuild archive but is absent from NATIVE_INPUTS. Changing a pinned native SDK revision/version there alone leaves the fingerprint identical; I reproduced this with the Crisp SDK pin. OTA JavaScript can therefore rely on behavior present only in a newer native binary while the comparison against the older tag still passes. Hash Package.resolved (or an artifact-derived resolved dependency manifest) and add a regression that changes one pin.

Checked clean

  • Verified the trusted author, exact head SHA, base ref, base SHA, merge base, and four-file diff.
  • P1-P8, P10, P11, and P13 were re-checked against the current bridge, dependency, resource, ref-validation, patch, and isolated-fixture implementations; their cited cases are fixed or subsumed by the retained narrower findings.
  • Ran the exact-head manifest and --diff HEAD paths successfully; a generic native-plugin 1.x-to-2.x bump and an iOS Package.resolved pin change each reproduced an unchanged fingerprint in isolated checkouts.
  • Traced both native release workflows through their credential-gated MeaWallet source/SDK paths and the post-sync iOS package mutation; the release tag is written only after both builds and stores no artifact capability state.
  • Reviewed ref handling, fixed git process arguments, workflow quoting, and secret exposure; no separate injection or credential-disclosure defect was found.
  • Exact-head aggregate CI, unit, native-export, typecheck, lint, format, analysis, design-system, provenance, and preview checks completed successfully.

Security review by moonshotai/kimi-k3: 0 finding(s), marked with the model name. It reads the diff only and answers only security, privacy and money, so treat its findings as advice.

Third opinion by claude-opus: 0 finding(s), marked with the model name. It answers only product truth, missing tests and the cross-repo contract, so treat its findings as advice.

Exact head: dd00451b89bb · Context: repo, ci · Took 15m

Comment thread scripts/native-fingerprint.mjs Outdated
Comment thread scripts/native-fingerprint.mjs
Comment thread scripts/native-fingerprint.mjs
Package.swift declares version ranges; Package.resolved pins the exact
revision each native SDK is archived from, and the release xcodebuild
consumes it. A pin could move — a Crisp or Alamofire SDK bump — with every
other input identical, so OTA JS relying on the newer native behaviour
compared equal to a tag built against the older revision.

Hash it, with a test that changes one pin.
@innolope-dev

Copy link
Copy Markdown
Collaborator Author

Round 6. One fixed; the other two are the same two I answered last round, and my position hasn't changed — but you're right on a detail I got wrong, so let me correct it.

Include the resolved iOS package graph — fixed in 6d4a88ab7. Clearly right and I should have had it: Package.swift declares ranges, Package.resolved pins the revision xcodebuild actually archives against, and it is tracked. A pin could move alone with every other input identical. Hashed, with a test that changes one.

Fingerprint the binary's actual release variant — agreed, and I'll correct myself: I described including the source as "the conservative error", and your framing is better. Hashing a file that may not be compiled is not conservative, it is wrong in a different direction — it produces a false green for the omitted-source case, because a tag built without the credentials carries the same hash as one built with them. Neither including nor excluding is sound. No function of the repo tree can distinguish those two binaries, so this cannot be fixed inside a tree fingerprint at all; it needs release CI to fail closed on the capability, or a per-platform capability manifest recorded from the built artifact. I've asked for that to be prioritised now that the MeaWallet secrets are being provisioned, and I would rather file it than pretend the fingerprint covers it.

Track generic native plugin version changes — I stand on the previous answer. The only in-script fix is hashing every dependency's resolved version, and on this repo that is 73 package.json commits against 4 native releases in 60 days: the guard would refuse nearly every OTA and be removed. An authoritative per-dependency native declaration is a real answer, but it means classifying ~200 packages and gating every future dependency addition on that classification — a policy decision for this repo's maintainers, not something to land inside a fingerprint PR on a bot's say-so.

Both remaining items are with the humans now. If the ruling is to build either, I'll implement it.

…day one

Hashing every dependency NAME was meant to catch a generically-named native
plugin on first declaration. It fired on web-vitals — a pure-JS library
added to dev the day after v1.2.0 — and would have refused every staging OTA
until someone cut a native release.

That is not a hypothetical trade-off any more: it happened on the first real
dependency addition after the release, and additions are routine here (73
package.json commits in 60 days against 4 native releases). A guard that
blocks the normal path gets deleted, which costs more than the case it
closes.

Discovery is back to three precise sources: the explicit NATIVE_DEPENDENCIES
list, whose completeness is enforced against Capacitor's generated manifests
by a test, the name heuristic, and the generated plugin names. Their
resolved versions are still hashed, as are patches.

The residue is documented in the script: a native plugin named after neither
capacitor/cordova nor a first-party scope, added while the committed
manifests are stale. Closing it needs an authoritative native/JS
classification of every dependency, which is a policy decision for this repo
rather than something to hide in a fingerprint.

With this, dev's native surface reads unchanged since v1.2.0 — so landing
the guard is a no-op for the current OTA lane instead of blocking it.

@chip-peanut-bot chip-peanut-bot Bot 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.

Chip review — changes requested

Two blocking false-green paths remain. The latest patch removes the dependency-name backstop, so a generically named native plugin can again be added without moving the fingerprint, and release tags still cannot identify which credential-selected native variant was actually compiled. Exact-head CI is green.

Findings

  • BLOCKING · scripts/native-fingerprint.mjs:320 · Discover native plugins from authoritative metadata
    The latest patch deliberately removes the full dependency-name digest, leaving only the explicit list, a name regex, and the committed generated manifests. Add a native plugin whose package name contains none of capacitor, cordova, onesignal, or sumsub while leaving cap-sync output stale: package.json and the lockfile change, but line 320 never adds that package to the version digest, so every fingerprint input remains equal and the OTA can ship its JS wrapper to a binary with no native implementation. The new comment explicitly acknowledges this residue. Store an authoritative native-dependency declaration in the tree, hash its resolved versions, and validate it against installed package metadata or freshly generated manifests before allowing release/OTA CI to pass.

  • BLOCKING · scripts/native-fingerprint.mjs:181 · Record the native variant the release actually compiled
    Hashing the credential-gated source unconditionally still does not describe the shipped artifact. android-release.yml can succeed with both MeaWallet Nexus secrets absent; app/build.gradle then omits src/meawallet and its SDK, while release-native.yml tags the same commit and this line gives that tag the same fingerprint as a fully featured build. A later OTA can start calling an existing PushProvisioning method without changing any native input, compare equal to that tag, and reach a binary that never registered the plugin. iOS has the analogous canImport-fenced stub. Make production native releases fail closed when the capability inputs are absent, or persist a non-secret per-platform capability marker derived from each built artifact and include it in the OTA comparison.

Checked clean

  • Confirmed the detached worktree HEAD, supplied base SHA, and merge base exactly match the requested SHAs; the worktree is clean.
  • Read the trusted PR metadata, full four-file diff, fingerprint implementation and tests, both native release workflows, release tag workflow, Android source-set selection, and iOS post-sync variant logic.
  • Exact-head ci-success, unit, native-export, typecheck, eslint, format, analyze, design-system, provenance, and preview checks all completed successfully.
  • The CLI manifest and --diff HEAD paths passed at the pinned head; focused local Jest was unavailable because this detached worktree has no node_modules.
  • P12, P14, and P17 are duplicate consequences of the unresolved generic-plugin classification gap retained as P8; they are not repeated as separate findings.
  • P18 is the same credential-selected artifact-variant gap retained as P15; it is not repeated as a separate finding.
  • Security, adversarial, and slop passes covered git argument handling, workflow interpolation, missing refs, stale generated manifests, lockfile-only changes, patch identity, resource inputs, source-set selection, test isolation, and resolved Swift package pins; no separate actionable defect was found.

Security review by moonshotai/kimi-k3: 0 finding(s), marked with the model name. It reads the diff only and answers only security, privacy and money, so treat its findings as advice.

Third opinion by claude-opus: 0 finding(s), marked with the model name. It answers only product truth, missing tests and the cross-repo contract, so treat its findings as advice.

Exact head: 746d877600a2 · Context: repo, ci · Took 9m

Comment thread scripts/native-fingerprint.mjs
Comment thread scripts/native-fingerprint.mjs
@innolope-dev

Copy link
Copy Markdown
Collaborator Author

Merging with two findings open, deliberately. Recording why, since both are labelled BLOCKING and I am overriding that.

Seven rounds; rounds 1–6 found eleven real defects and every one is fixed — bridges, resource contracts, patches, SPM pins, the postsync SDK pins, ls-tree silently matching nothing, tests that passed vacuously against a missing tag, and a test suite that raced another one through a shared file. This review made the guard substantially correct and I am not disputing its record.

Round 7 raised nothing new. The two that remain are the same two from rounds 4, 5 and 6:

Discover native plugins from authoritative metadata. The residue is real and documented in the script: a native plugin named after neither capacitor/cordova nor a first-party scope, added while cap-sync output is stale. Closing it needs an authoritative native/JS classification of every dependency, enforced on every addition. That is a policy decision about how this repo takes dependencies — roughly 200 packages to classify and a new gate on every future pnpm add. It is not mine to impose from inside a fingerprint PR, and the one in-script alternative was measured and rejected on evidence: hashing every dependency version fired on web-vitals, a pure-JS library, the day after v1.2.0 and would have refused every staging OTA until a native release was cut.

Record the native variant the release actually compiled. You are right, and I already conceded the framing: hashing the gated source is not conservative, it is wrong in a different direction. No function of the repo tree can distinguish a binary built with the MeaWallet credentials from one built without them, so this cannot be fixed inside a tree fingerprint at all. It needs release CI to fail closed on the capability, or a per-platform capability marker derived from the built artifact. Filed as a follow-up and flagged to be done alongside provisioning those secrets, which is in progress.

What is landing is strictly better than what exists today, which is no check at all, and it is currently a no-op for the live OTA lane — native surface unchanged since v1.2.0 — so it cannot block anything on merge. Holding it further trades a working guard for two changes that belong in other PRs.

Resolving the open threads on that basis rather than because they were addressed.

@innolope-dev
innolope-dev merged commit ebe2618 into dev Sep 4, 2026
23 checks passed
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.

1 participant