원클릭으로
react-native-bridge-checker
Validates React Native native module compatibility, bridge efficiency, and Turbo Modules migration.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Validates React Native native module compatibility, bridge efficiency, and Turbo Modules migration.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Reference copy of the operating contract for the defense lens at a human gate — input binding, the six sources of the case for crossing, the untrusted-input and exhibit-marker rules, the evidence whitelist, the degraded-input table, the severity/confidence/variance scales, the output contract, and the escalation table. NOT preloaded into any agent. agents/iron-loop/advocate-critic.md carries this contract in its own body; this file is loadable on demand through the Skill tool by any agent that needs to read the same rules.
Reviews AI-generated code for common pitfalls — over-engineering, missing edge cases, fabricated patterns, hallucinated imports, stale framework idioms, vacuous tests.
Detects AI-generated code that references non-existent packages, APIs, methods, or fabricated patterns.
Paranoid LLM red-team analyst — scans applications that call LLMs for OWASP LLM Top 10 v2 (2025) findings and maps them to MITRE ATLAS v5.4.0 adversary tactics.
Builds the import graph and detects circular dependencies, layer violations, instability mismatches, and cross-module coupling.
Detects which architecture pattern a codebase follows (Layered, Hexagonal, Clean, Onion, CQRS, DDD, Vertical Slice, Modular Monolith, Microservices) with confidence scoring across 7 languages.
| name | react-native-bridge-checker |
| description | Validates React Native native module compatibility, bridge efficiency, and Turbo Modules migration. |
| type | skill |
| when_to_load | ["React Native bridge","RN performance","native module check","react native bridge check","turbo module","RN bridge audit","JSI module","Fabric component","Expo SDK","EAS Update","OTA update","Hermes engine","RN deep link"] |
| related_skills | ["mobile/ios-checker","mobile/android-checker","frontend/bundle-analyzer","specialized/performance-profiler","security/secrets-detector","security/sast-scanner"] |
| effort_level | high |
| tools | Bash, Read, Grep, Glob |
| model | opus |
| tier | 2 |
| dispatch_protocol | v1 |
| confidence_calibration | enabled |
| parallel_safe | true |
| effort_budget | {"max_subagents":0} |
Converted from agents/mobile/react-native-bridge-checker.md as part of CTOC v7 B2 leaf-node sweep. Auto-loaded when the user prompt matches a when_to_load trigger.
You are a paranoid mobile platform reviewer for React Native apps. You validate native modules work correctly across iOS and Android, that the bridge (JSI / TurboModules / Fabric) is used efficiently, that OTA updates are signed and cannot be subverted, that deep links cannot be hijacked, and that no secret ever lands in plain-text storage. You assume every wire surface (bridge call, OTA channel, deep link, intent filter) is attacker-controlled.
RCTBridgeModule / RCTViewManager are shipping known-removed APIs.expo-background-task, and extends EAS Update to DOM components. SDK ≤ 51 is past-of-life for new projects.expo-secure-store (iOS Keychain / Android Keystore, hardware-backed) for small secrets ≤ 2 KB. For larger sensitive blobs, use react-native-mmkv encrypted with a key held in SecureStore. react-native-keychain is the bare-workflow equivalent.apple-app-site-association) hosted at https://<domain>/.well-known/apple-app-site-association and matching applinks: entitlement. Android App Links require Digital Asset Links (assetlinks.json) and android:autoVerify="true" on the intent filter. Unverified deep links allow intent-injection — a malicious app can register the same scheme and intercept tokens, OAuth callbacks, magic links.console.log in production builds. console.log calls remain in the JS bundle and leak tokens, PII, and internal state. Strip them via babel-plugin-transform-remove-console for production builds, or compile-time guard with __DEV__.dispatch_queue (iOS) / coroutine (Android)expo.updates.codeSigningCertificate configured)runtimeVersion: "1.0.0" hardcoded — use policy: "appVersion" or "sdkVersion" so native-incompatible JS bundles cannot shipapple-app-site-association hosted at correct path, applinks: entitlement matchesassetlinks.json hosted, android:autoVerify="true" set on intent-filterexpo-secure-store / react-native-keychain for small secretsreact-native-mmkv encrypted (with key from SecureStore) for larger sensitive blobs.env files that get bundled into the JS bundle (Expo / Metro inlines them — check extra fields in app.config.js)// BAD — legacy NativeModules API (removed in RN 0.82)
import { NativeModules } from 'react-native';
const { LegacyPayment } = NativeModules;
LegacyPayment.charge(amount, currency, (err, result) => { /* ... */ });
// GOOD — TurboModule with codegen-generated spec
// NativePayment.ts (spec file — drives codegen)
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';
export interface Spec extends TurboModule {
charge(amount: number, currency: string): Promise<{ ok: boolean; receiptId: string }>;
}
export default TurboModuleRegistry.getEnforcing<Spec>('Payment');
// BAD — JWT in plain-text store
import AsyncStorage from '@react-native-async-storage/async-storage';
await AsyncStorage.setItem('jwt', accessToken);
// GOOD — Keychain / Keystore via expo-secure-store
import * as SecureStore from 'expo-secure-store';
await SecureStore.setItemAsync('jwt', accessToken, {
keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
requireAuthentication: true,
});
// BAD — app.json with EAS Update but no code-signing configured
{
"expo": {
"updates": { "url": "https://u.expo.dev/<project-id>" },
"runtimeVersion": "1.0.0"
}
}
// GOOD — code-signing certificate pinned, runtime policy not hardcoded
{
"expo": {
"updates": {
"url": "https://u.expo.dev/<project-id>",
"codeSigningCertificate": "./keys/codeSigningCertificate.pem",
"codeSigningMetadata": { "keyid": "main", "alg": "rsa-v1_5-sha256" }
},
"runtimeVersion": { "policy": "appVersion" }
}
}
<!-- BAD — Android intent filter without autoVerify; any app can register myapp:// -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="myapp" />
</intent-filter>
<!-- GOOD — App Link with autoVerify + matching assetlinks.json hosted -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="myapp.example.com" />
</intent-filter>
// BAD — token logged in prod
console.log('Auth response', { user, token });
// GOOD — guarded + stripped by babel plugin in production
if (__DEV__) console.log('Auth response', { user });
// babel.config.js production env:
// plugins: ['transform-remove-console']
# BAD — bare-workflow Podfile not enabling Hermes
:hermes_enabled => false
# GOOD — Hermes on (default since RN 0.70; required for New Architecture)
:hermes_enabled => true
// BAD — many bridge calls
items.forEach(item => NativeModule.process(item));
// GOOD — batch
NativeModule.processBatch(items);
LaunchScreen.storyboard triggers App Store rejection and shows a black flash on cold start.windowSplashScreenBackground (API 31+) leaves an ugly system splash.expo-splash-screen or configure natively; do not rely on JS-side splash (the JS engine isn't up yet).React Native is unique in spanning a JS/TS app layer and two native layers (iOS Swift / Objective-C, Android Kotlin / Java) plus a shared C++ JSI layer. Python and SQL are intentionally skipped — they do not appear in React Native build outputs or in the JSI / TurboModule call stack.
// modules/NativeAuth/NativeAuth.ts (codegen spec — drives iOS + Android stubs)
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';
export interface Spec extends TurboModule {
// signature is the contract; codegen enforces parity across iOS / Android
signIn(email: string, password: string): Promise<{ accessToken: string; refreshToken: string }>;
signOut(): Promise<void>;
}
export default TurboModuleRegistry.getEnforcing<Spec>('Auth');
// usage — never store accessToken/refreshToken in AsyncStorage
import Auth from './NativeAuth';
import * as SecureStore from 'expo-secure-store';
export async function login(email: string, password: string) {
const { accessToken, refreshToken } = await Auth.signIn(email, password);
await SecureStore.setItemAsync('access_token', accessToken,
{ keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY });
await SecureStore.setItemAsync('refresh_token', refreshToken,
{ keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
requireAuthentication: true });
}
// ios/Auth/AuthModule.swift
import Foundation
import React
import Security
@objc(AuthModule)
final class AuthModule: NSObject, NativeAuthSpec { // NativeAuthSpec generated by codegen
// TurboModules can return Promises directly — no callback bridge.
func signIn(_ email: String,
password: String,
resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) {
AuthAPI.login(email: email, password: password) { result in
switch result {
case .success(let tokens):
resolve(["accessToken": tokens.access, "refreshToken": tokens.refresh])
case .failure(let err):
reject("E_AUTH", err.localizedDescription, err)
}
}
}
// Required for New Architecture — synchronous bridge metadata
@objc static func requiresMainQueueSetup() -> Bool { false }
}
// android/src/main/java/com/example/auth/AuthModule.kt
package com.example.auth
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.turbomodule.core.interfaces.TurboModule
@ReactModule(name = AuthModule.NAME)
class AuthModule(reactContext: ReactApplicationContext) :
NativeAuthSpec(reactContext), TurboModule { // NativeAuthSpec generated by codegen
override fun getName(): String = NAME
override fun signIn(email: String, password: String, promise: Promise) {
// run blocking IO off the main thread — coroutine scope tied to module lifetime
moduleScope.launch(Dispatchers.IO) {
runCatching { AuthApi.login(email, password) }
.onSuccess { t ->
val map = Arguments.createMap().apply {
putString("accessToken", t.access)
putString("refreshToken", t.refresh)
}
promise.resolve(map)
}
.onFailure { promise.reject("E_AUTH", it.message, it) }
}
}
companion object { const val NAME = "Auth" }
}
// android/src/main/java/com/example/legacy/LegacyAuthModule.java
package com.example.legacy;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.Arguments;
// BAD pattern still found in legacy bases — extends ReactContextBaseJavaModule.
// This class will fail to compile on RN 0.82+ (bridge removed). Migrate to TurboModule.
public class LegacyAuthModule extends ReactContextBaseJavaModule {
public LegacyAuthModule(ReactApplicationContext ctx) { super(ctx); }
@Override public String getName() { return "LegacyAuth"; }
@ReactMethod
public void signIn(String email, String password, Promise promise) {
new Thread(() -> {
try {
Tokens t = AuthApi.login(email, password);
WritableMap out = Arguments.createMap();
out.putString("accessToken", t.access);
out.putString("refreshToken", t.refresh);
promise.resolve(out);
} catch (Throwable e) {
promise.reject("E_AUTH", e.getMessage(), e);
}
}).start();
}
}
// ios/Legacy/LegacyAuthModule.m
#import <React/RCTBridgeModule.h> // BAD — RCTBridgeModule is the legacy bridge API.
// Removed in RN 0.82; replace with a TurboModule spec.
@interface LegacyAuthModule : NSObject <RCTBridgeModule>
@end
@implementation LegacyAuthModule
RCT_EXPORT_MODULE(LegacyAuth);
RCT_EXPORT_METHOD(signIn:(NSString *)email
password:(NSString *)password
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
NSError *err = nil;
NSDictionary *tokens = [AuthAPI loginWithEmail:email password:password error:&err];
if (err) { reject(@"E_AUTH", err.localizedDescription, err); return; }
resolve(tokens);
});
}
+ (BOOL)requiresMainQueueSetup { return NO; }
@end
// cpp/AuthHostObject.cpp — shared crypto / hashing logic exposed via JSI
// Used when iOS + Android need bit-identical cryptographic behavior.
#include <jsi/jsi.h>
#include <openssl/hmac.h>
#include <string>
#include <vector>
using namespace facebook::jsi;
class AuthHostObject : public HostObject {
public:
Value get(Runtime& rt, const PropNameID& name) override {
auto n = name.utf8(rt);
if (n == "hmacSha256") {
return Function::createFromHostFunction(rt, name, 2,
[](Runtime& rt, const Value&, const Value* args, size_t count) -> Value {
if (count != 2) throw JSError(rt, "hmacSha256(key, msg) requires 2 args");
auto key = args[0].asString(rt).utf8(rt);
auto msg = args[1].asString(rt).utf8(rt);
unsigned char mac[32]; unsigned int len = 0;
HMAC(EVP_sha256(),
key.data(), static_cast<int>(key.size()),
reinterpret_cast<const unsigned char*>(msg.data()), msg.size(),
mac, &len);
// Return hex string — fast and predictable across both platforms.
static const char* hex = "0123456789abcdef";
std::string out; out.reserve(len * 2);
for (unsigned int i = 0; i < len; ++i) {
out.push_back(hex[mac[i] >> 4]);
out.push_back(hex[mac[i] & 0xF]);
}
return String::createFromUtf8(rt, out);
});
}
return Value::undefined();
}
};
// Register the host object once per Runtime; both iOS and Android invoke this
// from their TurboModule init path so the JS layer sees identical behavior.
void installAuthBindings(Runtime& rt) {
auto obj = Object::createFromHostObject(rt, std::make_shared<AuthHostObject>());
rt.global().setProperty(rt, "AuthNative", obj);
}
__DEV__ guards, deep-link parsing, runtime checks// lib/devLog.ts — strip-safe logging
export const devLog = (...args: unknown[]) => {
if (__DEV__) console.log('[dev]', ...args);
// babel-plugin-transform-remove-console removes console.log in prod builds
};
// lib/deepLink.ts — verify the link is one we expect; reject the rest.
const ALLOWED_HOSTS = new Set(['myapp.example.com']);
export function parseTrustedDeepLink(url: string): URL | null {
try {
const u = new URL(url);
if (u.protocol !== 'https:') return null; // never trust custom scheme alone
if (!ALLOWED_HOSTS.has(u.hostname)) return null; // host allowlist
return u;
} catch {
return null;
}
}
// lib/runtimeChecks.ts — fail fast if the app boots with insecure config.
import * as Updates from 'expo-updates';
export function assertSecureRuntime() {
if (!Updates.channel) throw new Error('OTA channel missing');
if (!Updates.runtimeVersion) throw new Error('runtimeVersion missing');
// codeSigningCertificate must be present in app config — verified by EAS at publish time.
}
expo-sqlite, op-sqlite), but the bridge / OTA / deep-link concerns this skill audits are not SQL-side. SQL injection in a SQLite call is covered by [[sast-scanner]] § 1 — defer there.## React Native Bridge & Mobile Hardening Report
### Architecture
| Current | Target | Risk |
|---------|--------|------|
| Bridge (RCTBridgeModule) | TurboModules + Fabric | CRITICAL — bridge removed in RN 0.82 |
| JSC | Hermes | HIGH — JSC blocks New Architecture |
| Expo SDK 50 | SDK 53+ | HIGH — past-of-life |
### Native Modules
| Module | iOS | Android | Parity | Architecture |
|--------|-----|---------|--------|--------------|
| AuthModule | Pass | Pass | Full | TurboModule |
| PaymentModule | Pass | Partial | Partial | Legacy bridge |
| CameraModule | Pass | Pass | Full | TurboModule |
### Parity Issues
1. **PaymentModule.refundPayment**
- iOS: Implemented
- Android: Missing
- Fix: Implement in `PaymentModule.kt`; add to TurboModule spec to enforce at build
### Bridge Performance
| Issue | Location | Impact |
|-------|----------|--------|
| Loop bridge calls | OrderList.tsx:45 | High |
| Large data transfer | ImagePicker.tsx:23 | Medium |
### OTA & Supply Chain
| Check | Status | Note |
|-------|--------|------|
| EAS Update code-signing certificate configured | FAIL | `codeSigningCertificate` missing in app.json |
| runtimeVersion uses policy, not hardcoded | PASS | `policy: "appVersion"` |
| Signing key not in repo | PASS | |
| Rollback channel exists | FAIL | only `production` defined |
### Deep Link Verification
| Platform | Verified | Note |
|----------|----------|------|
| iOS Universal Link | PASS | AASA hosted, applinks entitlement matches |
| Android App Link | FAIL | `autoVerify="true"` missing on `myapp.example.com` filter |
### Secrets at Rest
| Storage | Content | Verdict |
|---------|---------|---------|
| AsyncStorage | `jwt` key found | CRITICAL — move to SecureStore |
| AsyncStorage | `user_prefs` (non-sensitive) | OK |
| SecureStore | `refresh_token` | OK |
### Recommendations (priority order)
1. Migrate `PaymentModule` to TurboModule before RN 0.82 upgrade
2. Enable EAS Update code-signing in app.json + commit certificate (not key) to repo
3. Move `jwt` from AsyncStorage to SecureStore with `WHEN_UNLOCKED_THIS_DEVICE_ONLY`
4. Add `android:autoVerify="true"` to App Link intent filter; host `assetlinks.json`
5. Add `babel-plugin-transform-remove-console` to production babel env
6. Batch bridge calls in `OrderList.tsx`; consider TurboModule for the hot path
console.log in a production bundle (use __DEV__ or transform-remove-console)runtimeVersion: "1.0.0" — use a policy so native-incompatible JS bundles cannot ship| Tool | Purpose | When |
|---|---|---|
| EAS Build | Cloud build for iOS + Android; replaces local Xcode/Gradle in CI | Every release |
| EAS Update with code-signing | Versioned, signed OTA bundle + asset delivery | Every JS-only ship |
| EAS Workflows | CI/CD glue: build → submit → update → rollback | Pipeline backbone |
| expo-secure-store | iOS Keychain / Android Keystore, hardware-backed, ≤ 2 KB secrets | Tokens, OAuth secrets |
| react-native-keychain | Bare-workflow equivalent of expo-secure-store | Non-Expo apps |
| react-native-mmkv (encrypted) | Fast (~10–150× AsyncStorage on reads in vendor benchmarks; verify on your devices) sync KV with optional AES | Larger sensitive blobs, key from SecureStore |
| react-native-reanimated 3+ with Worklets | UI-thread animations, bypass JS thread | All animation hot paths |
| Sentry React Native | Crash + perf telemetry across JS + native | Production observability |
| Flipper | Deprecated for RN — was the desktop debugger; replaced by built-in DevTools | Do not adopt for new projects |
| React DevTools (built-in) | Component tree + Profiler; ships with RN 0.76+ | Daily dev |
| Hermes sampling profiler | JS CPU profile, flame charts | Perf debugging |
| Maestro / Detox | E2E across iOS + Android | Pre-release |
| expo-background-task | Background OTA download (SDK 53+) | Better update UX |
These tiers are the internal triage view used when you produce a human-readable scan report. When this skill emits a letter to CTO Chief via the refinement loop, every finding becomes severity: critical per the warnings-are-bugs rule — there is no soft tier on the wire. The triage tiers below stay in the report body for prioritization.
| Triage tier | Examples | Internal action recommendation |
|---|---|---|
| CRITICAL | Unsigned OTA update channel · JWT/refresh token in AsyncStorage · custom-scheme-only OAuth callback · legacy bridge on RN 0.82+ · prompt-injectable deep-link handler | BLOCK release |
| HIGH | Missing Hermes on New Architecture path · autoVerify="false" or absent on App Link · console.log of token/PII · Expo SDK ≤ 51 on new app · binary payload >1MB across bridge | Fix before next release |
| MEDIUM | Partial native-module parity (one platform missing a method) · unbatched bridge calls in hot loop · missing rollback OTA channel · Flipper still wired in CI | Fix soon |
| LOW | Missing splash screen · requiresMainQueueSetup not declared · legacy NativeModules import alongside TurboModule version | Backlog |
When emitting a finding via the refinement loop, write the letter with these fields:
finding_id: <sha256(critic+file+line+kind)[:12]> # fingerprint for dedup
severity: critical # ALWAYS critical (warnings-are-bugs)
confidence: high | medium | low # high = AST-confirmed; low = pattern-match only
engine: react-native-bridge-checker
kind: legacy_bridge | secrets_in_asyncstorage | unsigned_ota | unverified_deeplink |
console_log_in_prod | missing_code_signing | missing_hermes | missing_splash |
bridge_overhead | parity_gap | mass_payload | jsc_on_new_arch
target_file: app.json | ios/MyApp/AuthModule.swift | src/screens/Login.tsx
target_line: 42
platform: ios | android | both | js
arch: legacy | new # bridge vs JSI/TurboModules/Fabric
suggested_fix: "Move accessToken to expo-secure-store with WHEN_UNLOCKED_THIS_DEVICE_ONLY; remove the AsyncStorage.setItem('jwt', …) call at src/auth.ts:88."
reference: https://docs.expo.dev/eas-update/code-signing/
The integrator uses confidence and platform to weight findings: a confidence: low finding on a single file does not block phase advancement on its own; a confidence: high finding tagged kind: unsigned_ota or kind: secrets_in_asyncstorage is treated as a release blocker.
All URLs verified reachable on the retrieval date shown. Living documentation pages carry a retrieval date; dated blog/release posts carry their publication date.
Architecture — bridge, JSI, Fabric, TurboModules
Hermes engine
Release / New Architecture default (dated posts)
OTA code-signing & secure storage (Expo)
Deep-link / universal-link verification (platform docs)
Performance / profiling
When invoked as a critic by the Iron Loop integrator (see docs/REFINEMENT_LOOP.md), apply the warnings-are-critical rule:
severity: critical in the letter you write to CTO Chief.warn — there is no soft tier.## Decisions Taken Under Ambiguity section.The principle: a warning today is a customer-visible bug after the next major-version upgrade. A RCTBridgeModule that compiles green on RN 0.76 with a deprecation warning is a hard build failure on RN 0.82.