feat(rn): Capgo React Native updater + CLI with file-level delta#2703
feat(rn): Capgo React Native updater + CLI with file-level delta#2703riderx wants to merge 5 commits into
Conversation
📝 WalkthroughWalkthroughThis change adds a React Native updater package with Android and iOS native implementations, a React Native bundling/upload CLI, updater-package detection in the existing CLI, related tests and documentation, and new Supabase user username typings. ChangesReact Native updater package
CLI updater integration
React Native CLI
Supabase user typings
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant App as React Native app
participant Bridge as CapgoUpdater bridge
participant Server as Capgo update API
participant Storage as Bundle storage
App->>Bridge: getLatest(options)
Bridge->>Server: POST update metadata
Server-->>Bridge: version, URL, checksum, manifest
App->>Bridge: download(options)
Bridge->>Server: GET bundle files
Bridge->>Storage: validate and persist bundle
Storage-->>Bridge: bundle record
Bridge-->>App: download result and events
App->>Bridge: set({ id })
Bridge->>Storage: select current bundle
Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches📝 Generate docstrings
Comment |
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
Warning Review the following alerts detected in dependencies. According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.
|
Merging this PR will not alter performance
Comparing Footnotes
|
Introduce @capgo/react-native-updater and @capgo/rn-cli so RN apps can use the same Capgo /updates + manifest delta path as Capacitor, plus CLI upload wiring that detects the RN updater package. Co-authored-by: Cursor <cursoragent@cursor.com>
Knip flagged CapgoUpdater + default as duplicate exports. Co-authored-by: Cursor <cursoragent@cursor.com>
3384aae to
8c4465f
Compare
Auto-sync on main regenerated types without the columns from the social usernames migration, breaking backend typecheck against updated tests. Co-authored-by: Cursor <cursoragent@cursor.com>
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_8c8360e0-fd25-481c-9e2c-3ea5610f5dc1) |
|
We've triggered an ultrareview automatically — This PR introduces a new React Native native updater with file-level delta manifests, Brotli decompression, and state management across 42 files, modifying core CLI upload logic and package resolution.... I'll post findings when complete. An ultrareview is cubic's deepest review, catching hard-to-find bugs in the most critical PRs. It runs a longer, multi-pass analysis using cubic's most capable review models, and typically takes around 30 minutes. It consumes your team's reviewed-lines allowance at 3× the standard rate. Automated ultrareviews are disabled by default. We triggered this run as part of your trial. Want cubic to do this for every high-risk PR? Enable auto-ultrareview in your settings. |
Pin Android deps with verification metadata, simplify download APIs, handle file op return values, and clean unused Swift/TS parameters. Co-authored-by: Cursor <cursoragent@cursor.com>
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_66f14708-5dc4-45e1-ac77-164d0302f6fb) |
There was a problem hiding this comment.
All reported issues were addressed across 8 files (changes from recent commits).
Tip: instead of fixing issues one by one fix them all with cubic
Re-trigger cubic
Reject encrypted Capgo session keys until crypto is ported, canonicalize manifest/zip paths under the bundle dir, and drop the ineffective verification-metadata file. Co-authored-by: Cursor <cursoragent@cursor.com>
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_0ba567b7-28ae-41c1-bc19-952cc8722523) |
Review follow-up (AI generated)Addressed cubic P1/P2 findings in
|
There was a problem hiding this comment.
Risk: high. Not approving: Cursor Bugbot skipped (usage limit reached), so the required automated review did not complete. Human review is still required for this new native React Native updater and CLI changes; two reviewers are already assigned.
Sent by Cursor Approval Agent: Pull Request Approver External
There was a problem hiding this comment.
Risk: high. Not approving: Cursor Bugbot skipped (usage limit reached), so the required automated review did not complete. Human review is still required for this native React Native updater and CLI changes; Dalanir and WcaleNieWolny are already assigned.
Sent by Cursor Approval Agent: Pull Request Approver
|
There was a problem hiding this comment.
Actionable comments posted: 28
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/react-native-updater/android/src/main/AndroidManifest.xml`:
- Around line 1-3: Add the android.permission.INTERNET uses-permission
declaration to the AndroidManifest manifest so the updater can perform HTTP
update checks and downloads when the host application does not declare network
access.
In
`@packages/react-native-updater/android/src/main/java/app/capgo/rnupdater/BundleStore.kt`:
- Around line 55-85: Update BundleStore’s list, save, and upsert methods to
synchronize index mutations so concurrent list→modify→save operations cannot
overwrite each other. Make save write the JSON to a temporary file in the same
directory, then atomically replace bundles.json, preserving the existing index
contents on write failures and preventing readers from seeing partial JSON.
In
`@packages/react-native-updater/android/src/main/java/app/capgo/rnupdater/CapgoDownloader.kt`:
- Around line 56-59: Update the download flow around request.url and downloadZip
to pass request.checksum through, then compute the downloaded ZIP’s SHA-256 and
compare it with that checksum before any extraction or activation. Abort the
update on mismatch, preserving the existing success stats only for verified
archives, and update all downloadZip call sites including the additional
referenced path.
- Around line 131-139: Refactor the download flow around findCachedByHash so the
existing cached bundles are recursively scanned and hashed once per download,
producing a hash-to-file index. Pass and reuse this index for each manifest
entry instead of rescanning the cache on every call, while preserving the
existing reuse and writeDownloadedFile behavior.
In
`@packages/react-native-updater/android/src/main/java/app/capgo/rnupdater/CapgoHttp.kt`:
- Around line 42-55: Update postJson to check response.isSuccessful before
parsing the body as a JSONObject update payload. For non-2xx responses, return
or surface an error result containing the HTTP status and response details so
getLatest cannot interpret the failure as updateAvailable; preserve the existing
JSON parsing and invalid_json fallback for successful responses.
In
`@packages/react-native-updater/android/src/main/java/app/capgo/rnupdater/CapgoUpdaterModule.kt`:
- Around line 177-181: Reorder the operations in CapgoUpdaterModule: resolve the
promise with bundleToMap(record) before invoking CapgoUpdater.reload for the
selected-bundle flow at
packages/react-native-updater/android/src/main/java/app/capgo/rnupdater/CapgoUpdaterModule.kt
lines 177-181, and apply the same resolve-before-reload ordering for the
builtin-bundle flow at lines 208-210.
- Around line 134-164: Enforce successful terminal status before activation: in
packages/react-native-updater/android/src/main/java/app/capgo/rnupdater/CapgoUpdaterModule.kt:134-164,
update the download failure path to persist the record as error and remove
partial files; at :172-180, require record.status == "success" in set; at
:193-197, require successful status and complete files in next; and in
packages/react-native-updater/android/src/main/java/app/capgo/rnupdater/CapgoUpdater.kt:27-36,
allow promotion only for successful records.
- Around line 182-184: Read and store the nullable option ID before entering the
try/catch surrounding the set operation, using the existing options access
pattern and guarding missing or null values as needed. Update the catch block to
reuse that captured ID in CapgoHttp.sendStats instead of calling
options.getString("id") again, ensuring the failure path can always reach
promise.reject.
In `@packages/react-native-updater/ios/CapgoBrotli.swift`:
- Around line 13-32: Update decodeBrotli to avoid relying on the fixed 6×/64 KiB
dstCapacity guess. Use compression_stream or a retry loop that enlarges the
output buffer when compression_decode_buffer cannot fit the decoded payload,
while preserving successful Data output and the existing failure error.
In `@packages/react-native-updater/ios/CapgoUpdater.swift`:
- Around line 364-389: Update set and next to validate the requested bundle ID
using the index and confirm its bundle file exists before writing
capgo_current_bundle_id or capgo_next_bundle_id. Reject unknown or incomplete
IDs and return without persisting preferences or sending stats; preserve the
existing success flow for valid bundles.
- Around line 255-274: Update the ZIP download flow in the updater method
containing the manifest and url branches to compute the downloaded archive’s
SHA-256 checksum and compare it with the supplied checksum before extraction or
marking the bundle successful. Reject the download on mismatch, while preserving
the existing behavior for valid ZIP downloads and the manifest branch.
- Around line 375-377: Remove the exit(0) termination from the set and reset
activation paths in CapgoUpdater.swift. Replace it with React Native bridge
reload behavior, or defer activation until the next normal app launch, while
preserving the existing update/reset flow and avoiding host-process termination.
- Around line 40-49: Update applyPendingNext() to check capgo_app_ready before
promoting the pending bundle. Persist the current bundle ID as the previous
bundle when applying an update, and if the prior launch was not marked ready,
restore that previous bundle instead of promoting the pending one; ensure the
relevant UserDefaults keys are updated consistently.
- Around line 193-196: Replace the whole-file loading in sha256(path:) and the
related download/archive/install flows with file-backed URLSessionDownloadTask
handling and incremental SHA-256 updates. Stream response data directly to disk,
then hash files in chunks rather than loading them into Data, preserving the
existing checksum and processing behavior while avoiding simultaneous full-size
allocations.
- Around line 161-183: Refactor postJson to avoid semaphore-based synchronous
waiting and expose an asynchronous completion or async/throws result to its
callers. Configure explicit request and resource timeouts on the
URLSession/request, and update every caller of postJson to await or handle the
asynchronous result while preserving JSON parsing and error propagation.
- Around line 218-233: Update the response maps in the updater callback around
sendEvent and resolve to avoid storing Swift optionals as Any: conditionally add
optional error fields so missing values are omitted, and validate required
version, URL, and session_key fields before constructing or resolving the
success map. Preserve the existing noNeedUpdate event and resolution behavior
while ensuring all emitted values are React Native-compatible.
- Around line 118-121: Update saveIndex(_:) to write the serialized bundle index
atomically, using a temporary file and replacing or renaming the existing index
only after the write succeeds. Stop suppressing serialization and file-write
errors; propagate failures through the saveIndex(_:) API and its callers so
unsuccessful updates are observable.
In `@packages/react-native-updater/ios/CapgoZip.swift`:
- Around line 24-33: Update the ZIP entry parsing flow around nameData and
payload in CapgoZip.swift to validate every computed range before calling
Data.subdata(in:), including the filename range and payload range derived from
nameLen, extraLen, and compSize. Throw the existing extraction NSError with a
clear invalid or truncated entry message when any range exceeds data.count,
while preserving normal parsing for valid entries.
- Around line 82-101: Update the decompression logic around the stream
processing in CapgoZip to decode in bounded output chunks, enforce a maximum
extracted-size limit independent of the ZIP header’s expectedSize, and continue
processing until the stream reaches COMPRESSION_STATUS_END. Treat
COMPRESSION_STATUS_OK as incomplete rather than success, rejecting streams that
exceed the cap or cannot reach END, and only return the fully decoded output
after successful completion.
In `@packages/react-native-updater/README.md`:
- Around line 34-40: Update the AppDelegate release bundle URL guidance to fall
back to the packaged bundle URL when CapgoUpdater.getJSBundleURL() returns nil,
preserving the Capgo URL when available. Reference CapgoUpdater.getJSBundleURL()
and the existing packaged-bundle URL implementation, and ensure first and reset
installations always return a valid release bundle.
In `@packages/react-native-updater/src/definitions.ts`:
- Around line 1-4: Keep ManifestEntry nullable for server responses, and
introduce a separate request manifest type with required file_name and
download_url while omitting file_hash. Update DownloadOptions.manifest and the
download flow to use this request-only type, ensuring LatestVersion.manifest is
validated or transformed before being passed to download rather than forwarding
nullable fields to the Android bridge.
In `@packages/react-native-updater/src/index.ts`:
- Around line 68-73: Update addListener in the unlinked-emitter branch to throw
the existing LINKING_ERROR instead of returning a no-op remove subscription.
Preserve the current emitter.addListener registration and removal behavior when
the native module is linked.
In `@packages/rn-cli/README.md`:
- Around line 27-32: Add the text language identifier to the fenced
export-layout example in the README, changing the opening fence before
.capgo-rn/export/ while leaving the example contents unchanged.
In `@packages/rn-cli/src/bundle.ts`:
- Around line 66-95: Wrap the export loop and its bundle-output validation in a
try/catch so failures from run or the missing-output check stop the spinner
before propagating. In the catch associated with the spinner started by the
export flow, stop it with a failure message, then rethrow the original error;
preserve the existing success stop and completion logging.
- Around line 15-21: Update the run function’s spawn options to avoid the
Windows shell fallback: keep shell disabled and invoke a direct Windows-safe
executable path so --entry-file and other args remain literal argv values.
Preserve the existing exit and error handling behavior.
In `@packages/rn-cli/src/upload.ts`:
- Around line 60-70: Validate the delta options in the upload flow before
invoking Capgo: reject configurations where `options.delta === false` and
`options.deltaOnly` is enabled. Keep the existing argument construction for
valid combinations, ensuring `--delta-only` is never forwarded without
`--delta`.
- Around line 23-30: Update the run function’s spawn invocation to avoid shell
parsing on Windows by removing the platform-based shell option or otherwise
using a Windows-safe direct executable/.cmd launcher. Preserve the existing cwd,
stdio, environment, and exit/error handling behavior.
In `@packages/rn-cli/test/export-layout.test.ts`:
- Around line 7-18: Update the test expected Capgo delta folder shape to
exercise production behavior through runBundle rather than creating the asserted
files directly. Mock the Metro executable and invoke runBundle, then retain
assertions for index.android.bundle, main.jsbundle, and assets/img.png;
alternatively, test the production layout validator if that is the exposed
implementation path.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: df7fe8be-4676-4afc-8d1e-e6db7da07d62
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (41)
cli/src/bundle/partial.tscli/src/bundle/upload.tscli/src/utils.tscli/test/test-rn-updater-version.test.mjspackage.jsonpackages/react-native-updater/.gitignorepackages/react-native-updater/CapgoReactNativeUpdater.podspecpackages/react-native-updater/README.mdpackages/react-native-updater/android/build.gradlepackages/react-native-updater/android/gradle.propertiespackages/react-native-updater/android/src/main/AndroidManifest.xmlpackages/react-native-updater/android/src/main/java/app/capgo/rnupdater/BundleStore.ktpackages/react-native-updater/android/src/main/java/app/capgo/rnupdater/CapgoConfig.ktpackages/react-native-updater/android/src/main/java/app/capgo/rnupdater/CapgoDownloader.ktpackages/react-native-updater/android/src/main/java/app/capgo/rnupdater/CapgoHttp.ktpackages/react-native-updater/android/src/main/java/app/capgo/rnupdater/CapgoUpdater.ktpackages/react-native-updater/android/src/main/java/app/capgo/rnupdater/CapgoUpdaterModule.ktpackages/react-native-updater/android/src/main/java/app/capgo/rnupdater/CapgoUpdaterPackage.ktpackages/react-native-updater/ios/CapgoBrotli.swiftpackages/react-native-updater/ios/CapgoUpdater.mpackages/react-native-updater/ios/CapgoUpdater.swiftpackages/react-native-updater/ios/CapgoZip.swiftpackages/react-native-updater/package.jsonpackages/react-native-updater/react-native.config.jspackages/react-native-updater/src/__tests__/api.test.tspackages/react-native-updater/src/definitions.tspackages/react-native-updater/src/index.tspackages/react-native-updater/src/version.tspackages/react-native-updater/tsconfig.build.jsonpackages/react-native-updater/tsconfig.jsonpackages/rn-cli/.gitignorepackages/rn-cli/README.mdpackages/rn-cli/package.jsonpackages/rn-cli/src/bundle.tspackages/rn-cli/src/index.tspackages/rn-cli/src/init.tspackages/rn-cli/src/upload.tspackages/rn-cli/test/export-layout.test.tspackages/rn-cli/tsconfig.jsonsrc/types/supabase.types.tssupabase/functions/_backend/utils/supabase.types.ts
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
Cap-go/capacitor-updater(manual)
| <manifest xmlns:android="http://schemas.android.com/apk/res/android" | ||
| package="app.capgo.rnupdater"> | ||
| </manifest> |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Declare the network permission required by the updater.
The module performs HTTP downloads, but its merged manifest contributes no android.permission.INTERNET; host apps that do not already declare it cannot check or download updates.
Proposed fix
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="app.capgo.rnupdater">
+ <uses-permission android:name="android.permission.INTERNET" />
</manifest>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <manifest xmlns:android="http://schemas.android.com/apk/res/android" | |
| package="app.capgo.rnupdater"> | |
| </manifest> | |
| <manifest xmlns:android="http://schemas.android.com/apk/res/android" | |
| package="app.capgo.rnupdater"> | |
| <uses-permission android:name="android.permission.INTERNET" /> | |
| </manifest> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/react-native-updater/android/src/main/AndroidManifest.xml` around
lines 1 - 3, Add the android.permission.INTERNET uses-permission declaration to
the AndroidManifest manifest so the updater can perform HTTP update checks and
downloads when the host application does not declare network access.
| fun list(context: Context): List<BundleRecord> { | ||
| val file = File(root(context), INDEX) | ||
| if (!file.exists()) return emptyList() | ||
| return try { | ||
| val arr = JSONArray(file.readText()) | ||
| (0 until arr.length()).map { i -> | ||
| val o = arr.getJSONObject(i) | ||
| BundleRecord( | ||
| id = o.getString("id"), | ||
| version = o.optString("version", ""), | ||
| status = o.optString("status", "success"), | ||
| checksum = o.optString("checksum", ""), | ||
| downloaded = o.optString("downloaded", ""), | ||
| ) | ||
| } | ||
| } catch (_: Exception) { | ||
| emptyList() | ||
| } | ||
| } | ||
|
|
||
| fun save(context: Context, bundles: List<BundleRecord>) { | ||
| val arr = JSONArray() | ||
| bundles.forEach { arr.put(it.toJson()) } | ||
| File(root(context), INDEX).writeText(arr.toString()) | ||
| } | ||
|
|
||
| fun upsert(context: Context, record: BundleRecord) { | ||
| val all = list(context).filter { it.id != record.id }.toMutableList() | ||
| all.add(record) | ||
| save(context, all) | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Make bundle-index updates synchronized and atomic.
Concurrent downloads execute list → modify → save on a cached thread pool, so one upsert can overwrite another. Directly writing bundles.json also lets readers observe truncated JSON; Line 70 then silently converts that corruption into an empty index, enabling subsequent permanent data loss.
Serialize index mutations and write a temporary file followed by an atomic replacement.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@packages/react-native-updater/android/src/main/java/app/capgo/rnupdater/BundleStore.kt`
around lines 55 - 85, Update BundleStore’s list, save, and upsert methods to
synchronize index mutations so concurrent list→modify→save operations cannot
overwrite each other. Make save write the JSON to a temporary file in the same
directory, then atomically replace bundles.json, preserving the existing index
contents on write failures and preventing readers from seeing partial JSON.
| request.url.isNotEmpty() && !request.url.contains("404.capgo.app") -> { | ||
| CapgoHttp.sendStats(context, "download_zip_start", request.version) | ||
| downloadZip(canonicalDest, request.url, progress) | ||
| CapgoHttp.sendStats(context, "download_zip_complete", request.version) |
There was a problem hiding this comment.
🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win
Verify the full ZIP against request.checksum before extraction.
The ZIP path never consumes the supplied checksum, yet the downloaded JavaScript is subsequently activated. A corrupted or tampered archive can therefore be installed despite the advertised SHA-256 contract.
Pass the checksum into downloadZip, compare it with sha256(zipFile), and abort before extraction on mismatch.
Also applies to: 201-217
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@packages/react-native-updater/android/src/main/java/app/capgo/rnupdater/CapgoDownloader.kt`
around lines 56 - 59, Update the download flow around request.url and
downloadZip to pass request.checksum through, then compute the downloaded ZIP’s
SHA-256 and compare it with that checksum before any extraction or activation.
Abort the update on mismatch, preserving the existing success stats only for
verified archives, and update all downloadZip call sites including the
additional referenced path.
| val reused = findCachedByHash(context, fileHash, dest) | ||
| if (reused != null) { | ||
| reused.copyTo(target, overwrite = true) | ||
| } else { | ||
| writeDownloadedFile(context, version, fileName, downloadUrl, target, isBrotli) | ||
| } | ||
|
|
||
| verifyChecksum(context, version, fileName, fileHash, target) | ||
| } |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift
Build the cache hash index once per download.
For every manifest entry, findCachedByHash recursively rereads and hashes every file in every existing bundle. A manifest and cached bundle containing (N) files require roughly (N²) full-file reads, making large delta updates unnecessarily slow and battery-intensive.
Also applies to: 183-199
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@packages/react-native-updater/android/src/main/java/app/capgo/rnupdater/CapgoDownloader.kt`
around lines 131 - 139, Refactor the download flow around findCachedByHash so
the existing cached bundles are recursively scanned and hashed once per
download, producing a hash-to-file index. Pass and reuse this index for each
manifest entry instead of rescanning the cache on every call, while preserving
the existing reuse and writeDownloadedFile behavior.
| fun postJson(url: String, body: JSONObject): JSONObject { | ||
| val request = Request.Builder() | ||
| .url(url) | ||
| .post(body.toString().toRequestBody("application/json".toMediaType())) | ||
| .header("User-Agent", "CapgoRNUpdater/${CapgoConfig.PLUGIN_VERSION}") | ||
| .build() | ||
| client.newCall(request).execute().use { response -> | ||
| val text = response.body?.string() ?: "{}" | ||
| return try { | ||
| JSONObject(text) | ||
| } catch (_: Exception) { | ||
| JSONObject().put("error", "invalid_json").put("message", text.take(200)) | ||
| } | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify every postJson caller and whether HTTP status is handled elsewhere.
rg -n -C4 '\bpostJson\s*\(|\bisSuccessful\b' packages/react-native-updater/androidRepository: Cap-go/capgo.app
Length of output: 5044
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- CapgoHttp.kt ---'
sed -n '1,220p' packages/react-native-updater/android/src/main/java/app/capgo/rnupdater/CapgoHttp.kt
echo
echo '--- CapgoUpdaterModule.kt ---'
sed -n '1,180p' packages/react-native-updater/android/src/main/java/app/capgo/rnupdater/CapgoUpdaterModule.ktRepository: Cap-go/capgo.app
Length of output: 9488
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- updateUrl / response contract references ---'
rg -n -C3 'updateUrl|session_key|noNeedUpdate|updateAvailable|invalid_json|manifest' packages/react-native-updater ../../Cap-go/capacitor-updater 2>/dev/null || true
echo
echo '--- README / docs mentions ---'
rg -n -C2 'session_key|noNeedUpdate|updateAvailable|updateUrl|manifest' . 2>/dev/null | head -n 200Repository: Cap-go/capgo.app
Length of output: 50372
Handle non-2xx responses before treating the body as an update payload. postJson always parses the response body, so a 4xx/5xx JSON response without an error field can still flow through getLatest as updateAvailable with empty or invalid fields. Check response.isSuccessful and surface the HTTP failure instead.
🧰 Tools
🪛 ast-grep (0.44.1)
[error] 42-43: An outbound HTTP request is built from a non-literal URL: URL(...).openConnection(), OkHttp's Request.Builder().url(...), or a Spring RestTemplate call (getForObject/getForEntity/postForObject/postForEntity/exchange/execute). When the URL is derived from a variable, method call, concatenation, or interpolation, an attacker can point the request at internal services, cloud metadata endpoints (e.g. 169.254.169.254), or arbitrary hosts (Server-Side Request Forgery). Do not build request targets from untrusted input: validate the URL against a strict allowlist of permitted hosts/schemes, resolve and re-check the host after DNS resolution, and reject internal/link-local/loopback addresses before connecting.
Context: Request.Builder()
.url(url)
Note: [CWE-918] Server-Side Request Forgery (SSRF).
(ssrf-outbound-request-tainted-url-kotlin)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@packages/react-native-updater/android/src/main/java/app/capgo/rnupdater/CapgoHttp.kt`
around lines 42 - 55, Update postJson to check response.isSuccessful before
parsing the body as a JSONObject update payload. For non-2xx responses, return
or surface an error result containing the HTTP status and response details so
getLatest cannot interpret the failure as updateAvailable; preserve the existing
JSON parsing and invalid_json fallback for successful responses.
| function run(cmd: string, args: string[], cwd: string): Promise<void> { | ||
| return new Promise((resolvePromise, reject) => { | ||
| const child = spawn(cmd, args, { cwd, stdio: 'inherit', shell: process.platform === 'win32' }) | ||
| child.on('error', reject) | ||
| child.on('exit', (code) => { | ||
| if (code === 0) resolvePromise() | ||
| else reject(new Error(`${cmd} ${args.join(' ')} exited with ${code}`)) |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n## packages/rn-cli/src/bundle.ts\n'
cat -n packages/rn-cli/src/bundle.ts | sed -n '1,220p'
printf '\n## usages of run(\n'
rg -n "spawn\\(|shell: process\\.platform === 'win32'|new Spinner|spinner\\." packages/rn-cli/src -n
printf '\n## nearby package metadata\n'
cat -n packages/rn-cli/package.json | sed -n '1,220p'Repository: Cap-go/capgo.app
Length of output: 5838
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n## runBundle usages\n'
rg -n "runBundle\\(" packages/rn-cli/src
printf '\n## full bundle.ts with line numbers around the loop\n'
cat -n packages/rn-cli/src/bundle.ts | sed -n '50,120p'
printf '\n## upload.ts for the same spawn pattern\n'
cat -n packages/rn-cli/src/upload.ts | sed -n '1,120p'Repository: Cap-go/capgo.app
Length of output: 5884
Avoid the Windows shell fallback here. shell: true on line 17 lets cmd.exe parse --entry-file and the other CLI values as shell syntax instead of literal argv items, so user-controlled input can become command injection. Keep shell: false and use a direct Windows-safe executable path instead.
🧰 Tools
🪛 ast-grep (0.44.1)
[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawn } from 'node:child_process'
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(detect-child-process-typescript)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/rn-cli/src/bundle.ts` around lines 15 - 21, Update the run
function’s spawn options to avoid the Windows shell fallback: keep shell
disabled and invoke a direct Windows-safe executable path so --entry-file and
other args remain literal argv values. Preserve the existing exit and error
handling behavior.
Source: Linters/SAST tools
| const s = spinner() | ||
| s.start(`Exporting Metro bundles (${platforms.join(', ')})`) | ||
|
|
||
| const metroBin = findMetroBin(project) | ||
| const useNode = metroBin.endsWith('.js') | ||
|
|
||
| for (const platform of platforms) { | ||
| const bundleOut = platform === 'ios' | ||
| ? join(out, 'main.jsbundle') | ||
| : join(out, 'index.android.bundle') | ||
| const assetsDest = join(out, 'assets') | ||
| const args = [ | ||
| ...(useNode ? [metroBin] : []), | ||
| 'bundle', | ||
| '--platform', platform, | ||
| '--dev', options.dev ? 'true' : 'false', | ||
| '--entry-file', entryFile, | ||
| '--bundle-output', bundleOut, | ||
| '--assets-dest', assetsDest, | ||
| '--reset-cache', | ||
| ] | ||
| const cmd = useNode ? process.execPath : metroBin | ||
| await run(cmd, args, project) | ||
| if (!existsSync(bundleOut)) { | ||
| throw new Error(`Metro did not produce ${bundleOut}`) | ||
| } | ||
| } | ||
|
|
||
| s.stop(color.green(`Export ready at ${out}`)) | ||
| log.info(`Files: ${readdirSync(out).join(', ')}`) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Stop the spinner on export failures.
A rejected run() call or the missing-output error bypasses Line 94, leaving an active spinner before the top-level error is printed. Wrap the export loop in try/catch, stop it with a failure message, then rethrow.
Proposed fix
const s = spinner()
s.start(`Exporting Metro bundles (${platforms.join(', ')})`)
- for (const platform of platforms) {
- // export work
+ try {
+ for (const platform of platforms) {
+ // export work
+ }
+ s.stop(color.green(`Export ready at ${out}`))
+ }
+ catch (error) {
+ s.stop(color.red('Export failed'))
+ throw error
}
-
- s.stop(color.green(`Export ready at ${out}`))📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const s = spinner() | |
| s.start(`Exporting Metro bundles (${platforms.join(', ')})`) | |
| const metroBin = findMetroBin(project) | |
| const useNode = metroBin.endsWith('.js') | |
| for (const platform of platforms) { | |
| const bundleOut = platform === 'ios' | |
| ? join(out, 'main.jsbundle') | |
| : join(out, 'index.android.bundle') | |
| const assetsDest = join(out, 'assets') | |
| const args = [ | |
| ...(useNode ? [metroBin] : []), | |
| 'bundle', | |
| '--platform', platform, | |
| '--dev', options.dev ? 'true' : 'false', | |
| '--entry-file', entryFile, | |
| '--bundle-output', bundleOut, | |
| '--assets-dest', assetsDest, | |
| '--reset-cache', | |
| ] | |
| const cmd = useNode ? process.execPath : metroBin | |
| await run(cmd, args, project) | |
| if (!existsSync(bundleOut)) { | |
| throw new Error(`Metro did not produce ${bundleOut}`) | |
| } | |
| } | |
| s.stop(color.green(`Export ready at ${out}`)) | |
| log.info(`Files: ${readdirSync(out).join(', ')}`) | |
| const s = spinner() | |
| s.start(`Exporting Metro bundles (${platforms.join(', ')})`) | |
| const metroBin = findMetroBin(project) | |
| const useNode = metroBin.endsWith('.js') | |
| try { | |
| for (const platform of platforms) { | |
| const bundleOut = platform === 'ios' | |
| ? join(out, 'main.jsbundle') | |
| : join(out, 'index.android.bundle') | |
| const assetsDest = join(out, 'assets') | |
| const args = [ | |
| ...(useNode ? [metroBin] : []), | |
| 'bundle', | |
| '--platform', platform, | |
| '--dev', options.dev ? 'true' : 'false', | |
| '--entry-file', entryFile, | |
| '--bundle-output', bundleOut, | |
| '--assets-dest', assetsDest, | |
| '--reset-cache', | |
| ] | |
| const cmd = useNode ? process.execPath : metroBin | |
| await run(cmd, args, project) | |
| if (!existsSync(bundleOut)) { | |
| throw new Error(`Metro did not produce ${bundleOut}`) | |
| } | |
| } | |
| s.stop(color.green(`Export ready at ${out}`)) | |
| } | |
| catch (error) { | |
| s.stop(color.red('Export failed')) | |
| throw error | |
| } | |
| log.info(`Files: ${readdirSync(out).join(', ')}`) |
🧰 Tools
🪛 ast-grep (0.44.1)
[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawn } from 'node:child_process'
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(detect-child-process-typescript)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/rn-cli/src/bundle.ts` around lines 66 - 95, Wrap the export loop and
its bundle-output validation in a try/catch so failures from run or the
missing-output check stop the spinner before propagating. In the catch
associated with the spinner started by the export flow, stop it with a failure
message, then rethrow the original error; preserve the existing success stop and
completion logging.
| function run(cmd: string, args: string[], cwd: string): Promise<void> { | ||
| return new Promise((resolvePromise, reject) => { | ||
| const child = spawn(cmd, args, { cwd, stdio: 'inherit', shell: process.platform === 'win32', env: process.env }) | ||
| child.on('error', reject) | ||
| child.on('exit', (code) => { | ||
| if (code === 0) resolvePromise() | ||
| else reject(new Error(`${cmd} exited with ${code}`)) | ||
| }) |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
ast-grep outline packages/rn-cli/src/upload.ts --view expanded
rg -n -C3 "shell:|runUpload\\s*\\(|capgoCli" packages/rn-cliRepository: Cap-go/capgo.app
Length of output: 3412
Do not route this spawn through cmd.exe on Windows. shell: process.platform === 'win32' lets CLI-controlled paths and flags be parsed by the shell, so legitimate values with metacharacters can break or redirect the upload. Spawn the executable directly, or use a Windows-safe .cmd launcher without shell parsing.
🧰 Tools
🪛 ast-grep (0.44.1)
[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawn } from 'node:child_process'
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(detect-child-process-typescript)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/rn-cli/src/upload.ts` around lines 23 - 30, Update the run
function’s spawn invocation to avoid shell parsing on Windows by removing the
platform-based shell option or otherwise using a Windows-safe direct
executable/.cmd launcher. Preserve the existing cwd, stdio, environment, and
exit/error handling behavior.
Source: Linters/SAST tools
| const useDelta = options.delta !== false | ||
| const args = [ | ||
| 'bundle', 'upload', | ||
| appId, | ||
| '--path', exportPath, | ||
| '--channel', options.channel, | ||
| '--no-code-check', | ||
| ] | ||
|
|
||
| if (useDelta) args.push('--delta') | ||
| if (options.deltaOnly) args.push('--delta-only') |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Reject conflicting --no-delta and --delta-only options.
The current logic can omit --delta while still forwarding --delta-only. Validate these options as mutually exclusive before invoking Capgo.
Proposed fix
const useDelta = options.delta !== false
+ if (!useDelta && options.deltaOnly) {
+ throw new Error('--delta-only cannot be combined with --no-delta')
+ }
+
const args = [📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const useDelta = options.delta !== false | |
| const args = [ | |
| 'bundle', 'upload', | |
| appId, | |
| '--path', exportPath, | |
| '--channel', options.channel, | |
| '--no-code-check', | |
| ] | |
| if (useDelta) args.push('--delta') | |
| if (options.deltaOnly) args.push('--delta-only') | |
| const useDelta = options.delta !== false | |
| if (!useDelta && options.deltaOnly) { | |
| throw new Error('--delta-only cannot be combined with --no-delta') | |
| } | |
| const args = [ | |
| 'bundle', 'upload', | |
| appId, | |
| '--path', exportPath, | |
| '--channel', options.channel, | |
| '--no-code-check', | |
| ] | |
| if (useDelta) args.push('--delta') | |
| if (options.deltaOnly) args.push('--delta-only') |
🧰 Tools
🪛 ast-grep (0.44.1)
[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawn } from 'node:child_process'
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(detect-child-process-typescript)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/rn-cli/src/upload.ts` around lines 60 - 70, Validate the delta
options in the upload flow before invoking Capgo: reject configurations where
`options.delta === false` and `options.deltaOnly` is enabled. Keep the existing
argument construction for valid combinations, ensuring `--delta-only` is never
forwarded without `--delta`.
| test('expected Capgo delta folder shape', () => { | ||
| const dir = join(tmpdir(), `capgo-rn-export-${Date.now()}`) | ||
| mkdirSync(join(dir, 'assets'), { recursive: true }) | ||
| writeFileSync(join(dir, 'index.android.bundle'), 'android') | ||
| writeFileSync(join(dir, 'main.jsbundle'), 'ios') | ||
| writeFileSync(join(dir, 'assets', 'img.png'), 'x') | ||
|
|
||
| expect(existsSync(join(dir, 'index.android.bundle'))).toBe(true) | ||
| expect(existsSync(join(dir, 'main.jsbundle'))).toBe(true) | ||
| expect(existsSync(join(dir, 'assets', 'img.png'))).toBe(true) | ||
|
|
||
| rmSync(dir, { recursive: true, force: true }) |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
Exercise the production export logic instead of recreating its expected output.
This test writes every asserted file itself, so changes that break runBundle cannot fail the test. Invoke runBundle with a mocked Metro executable, or extract and test the production layout validator.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/rn-cli/test/export-layout.test.ts` around lines 7 - 18, Update the
test expected Capgo delta folder shape to exercise production behavior through
runBundle rather than creating the asserted files directly. Mock the Metro
executable and invoke runBundle, then retain assertions for
index.android.bundle, main.jsbundle, and assets/img.png; alternatively, test the
production layout validator if that is the exposed implementation path.
There was a problem hiding this comment.
1 issue found across 5 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/react-native-updater/android/build.gradle">
<violation number="1" location="packages/react-native-updater/android/build.gradle:26">
P3: This comment does not suppress the intended `text:S8569` finding: `NOSONAR` applies only to issues raised on the comment's own line, not subsequent Gradle declarations. Consider a scoped `sonar.issue.ignore.multicriteria` entry for this rule and file instead.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Fix all with cubic | Re-trigger cubic
| } | ||
| } | ||
|
|
||
| // NOSONAR text:S8569 - library pins versions; consuming app owns Gradle dependency locking |
There was a problem hiding this comment.
P3: This comment does not suppress the intended text:S8569 finding: NOSONAR applies only to issues raised on the comment's own line, not subsequent Gradle declarations. Consider a scoped sonar.issue.ignore.multicriteria entry for this rule and file instead.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/react-native-updater/android/build.gradle, line 26:
<comment>This comment does not suppress the intended `text:S8569` finding: `NOSONAR` applies only to issues raised on the comment's own line, not subsequent Gradle declarations. Consider a scoped `sonar.issue.ignore.multicriteria` entry for this rule and file instead.</comment>
<file context>
@@ -23,6 +23,7 @@ android {
}
}
+// NOSONAR text:S8569 - library pins versions; consuming app owns Gradle dependency locking
configurations.configureEach {
resolutionStrategy {
</file context>




Summary (AI generated)
@capgo/react-native-updater: React Native native module (iOS/Android) that speaks Capgo/updates+/statsand applies Capgo file-level delta manifests (SHA-256 + optional Brotli), same backend contract as@capgo/capacitor-updater.@capgo/rn-cli(capgo-rn): Metro export ofindex.android.bundle+main.jsbundle+ assets, then upload via@capgo/cli bundle upload --delta.@capgo/cliso upload/delta/brotli detection accepts@capgo/react-native-updater, and skip Capacitorindex.html/notifyAppReadychecks for RN export folders.Motivation (AI generated)
React Native teams need Capgo-style OTA with the same Capgo Cloud delta system (not binary bspatch). This mirrors the RN integration pattern from zepto-labs/react-native-delta (JS bundle path override + check/download/apply), while reusing Capgo’s existing file-manifest delta pipeline end to end.
Business Impact (AI generated)
Opens Capgo live updates to React Native without forking storage/backend. One cloud, one delta format, new client + CLI surface.
Test Plan (AI generated)
bun run --cwd packages/rn-cli testbun run --cwd packages/react-native-updater testbun test ./cli/test/test-rn-updater-version.test.mjsgetJSBundleFile/getJSBundleURL, callnotifyAppReady(), upload withcapgo-rn upload <appId> --channel production, verify/updatesreturnsmanifestand only changed files download@capgo/capacitor-updaterand requireindex.html/notifyAppReadyunless--no-code-checkGenerated with AI
Made with Cursor
Summary by CodeRabbit