nativekeel 0.1.20 → 0.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,7 @@ Health check for React Native and Expo apps. One command, no install, no account
6
6
  npx nativekeel
7
7
  ```
8
8
 
9
- **What's new in 0.1.20:** warns about `ImageBackground`, `SafeAreaView`, `DrawerLayoutAndroid` and `UTFSequence`, which React Native has deprecated and will remove (143 of 470 apps we test import one). **0.1.19:** better plans for apps that are far behind: library targets that fit the React Native you have, not the latest. Recent releases added a *Start here* list in every report, hacking risks mapped to OWASP MASVS, crash and performance checks. Every check is tuned on 450+ open-source React Native and Expo apps to keep false alarms out. [All releases →](https://nativekeel.com/changelog)
9
+ **What's new in 0.1.22:** auth tokens in AsyncStorage/MMKV are found by what is stored, not only by the key name. **0.1.21:** corrected 16 KB page dates: Google Play blocks updates without them from 1 February 2027 (high until then). Recent releases added a *Start here* list in every report, hacking risks mapped to OWASP MASVS, crash and performance checks. Every check is tuned on 470 open-source React Native and Expo apps to keep false alarms out ([what we found in them](https://dev.to/keel_alican_akyol/we-scanned-470-open-source-react-native-apps-here-is-what-breaks-them-5072)). [All releases →](https://nativekeel.com/changelog)
10
10
 
11
11
  ```
12
12
  Summary 4 critical · 5 high · 4 medium · 6 low
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nativekeel",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
4
4
  "description": "Health check for React Native and Expo apps: upgrade blockers, store deadlines, crash risks, vulnerable dependencies and leaked secrets, with a step-by-step upgrade plan.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/native.js CHANGED
@@ -55,15 +55,19 @@ export function nativeChecks(project, { rnLatest = null, now = new Date() } = {}
55
55
  const hasAndroid = fs.existsSync(path.join(root, 'android'));
56
56
 
57
57
  // 16 KB page size: React Native itself before 0.77, then the actual built binaries.
58
+ const blocked16k = today >= PAGE_SIZE_16K.blockedFrom;
59
+ const policy16k = blocked16k
60
+ ? `Google Play blocks updates without 16 KB page support since ${PAGE_SIZE_16K.blockedFrom}.`
61
+ : `Google Play requires 16 KB page support for apps targeting Android 15+ (since ${PAGE_SIZE_16K.since}). Play Console warns now, and from ${PAGE_SIZE_16K.blockedFrom} it blocks updates without it.`;
58
62
  if (hasAndroid || project.managed) {
59
63
  if (rn !== null && rn < PAGE_SIZE_16K.firstRnMinor && today >= PAGE_SIZE_16K.since) {
60
64
  add({
61
65
  id: 'android-16kb-rn',
62
- severity: 'critical',
66
+ severity: blocked16k ? 'critical' : 'high',
63
67
  area: 'store',
64
68
  title: `React Native ${project.rnVersion} does not support 16 KB memory pages`,
65
- detail: `Google Play rejects updates without 16 KB page support since ${PAGE_SIZE_16K.since} (extensions ended ${PAGE_SIZE_16K.extensionUntil}). React Native supports it from 0.${PAGE_SIZE_16K.firstRnMinor}${project.expoVersion ? ', which in Expo means SDK 53 or later (SDK 52 ships React Native 0.76)' : ''}.`,
66
- fix: { kind: 'upgrade-rn-min', to: `0.${PAGE_SIZE_16K.firstRnMinor}.0` },
69
+ detail: `${policy16k} React Native supports it from 0.${PAGE_SIZE_16K.firstRnMinor}${project.expoVersion ? ', which in Expo means SDK 53 or later (SDK 52 ships React Native 0.76)' : ''}.`,
70
+ fix: { kind: 'upgrade-rn-min', to: `0.${PAGE_SIZE_16K.firstRnMinor}.0`, blocked: blocked16k, blockedFrom: PAGE_SIZE_16K.blockedFrom },
67
71
  });
68
72
  }
69
73
  const artifact = findBuiltArtifact(root);
@@ -82,11 +86,11 @@ export function nativeChecks(project, { rnLatest = null, now = new Date() } = {}
82
86
  if (relevant.length) {
83
87
  add({
84
88
  id: 'android-16kb-libs',
85
- severity: isRelease ? 'critical' : 'high',
89
+ severity: isRelease && blocked16k ? 'critical' : 'high',
86
90
  area: 'store',
87
91
  title: `${libs.length} native librar${libs.length === 1 ? 'y is' : 'ies are'} not 16 KB aligned (${rel})`,
88
- detail: `${libs.slice(0, 6).join(', ')}${libs.length > 6 ? ', …' : ''}. Google Play rejects updates containing them. Update the package that ships each library, or rebuild it with NDK r28+.`,
89
- fix: { kind: 'align-16kb', libs },
92
+ detail: `${libs.slice(0, 6).join(', ')}${libs.length > 6 ? ', …' : ''}. ${policy16k} Update the package that ships each library, or rebuild it with NDK r28+.`,
93
+ fix: { kind: 'align-16kb', libs, blocked: blocked16k, blockedFrom: PAGE_SIZE_16K.blockedFrom },
90
94
  });
91
95
  } else {
92
96
  const ignored = result.misaligned.length;
package/src/plan.js CHANGED
@@ -258,10 +258,13 @@ export function buildPlan(result) {
258
258
  } else if (privacy) {
259
259
  storeSteps.push('Add `PrivacyInfo.xcprivacy` to the iOS app target. Start from the file in the React Native template and declare the required-reason APIs your app and SDKs use (UserDefaults, file timestamps, system boot time, disk space).');
260
260
  }
261
- if (byKind('upgrade-rn-min').length) {
261
+ const min16k = byKind('upgrade-rn-min')[0];
262
+ const when16k = min16k && min16k.blocked ? 'Google Play rejects updates until' : `From ${min16k ? min16k.blockedFrom : ''} Google Play rejects updates until`;
263
+ const by16k = min16k && min16k.blocked ? 'before your next Play release' : `before ${min16k ? min16k.blockedFrom : ''}`;
264
+ if (min16k) {
262
265
  storeSteps.push(isExpo
263
- ? '**16 KB memory pages:** Google Play rejects updates until the app is on Expo SDK 53 (React Native 0.79) or later. The Expo SDK phase below gets you there; reach SDK 53 before your next Play release.'
264
- : '**16 KB memory pages:** Google Play rejects updates until the app is on React Native 0.77 or later. The React Native phase below gets you there; reach 0.77 before your next Play release.');
266
+ ? `**16 KB memory pages:** ${when16k} the app is on Expo SDK 53 (React Native 0.79) or later. The Expo SDK phase below gets you there; reach SDK 53 ${by16k}.`
267
+ : `**16 KB memory pages:** ${when16k} the app is on React Native 0.77 or later. The React Native phase below gets you there; reach 0.77 ${by16k}.`);
265
268
  }
266
269
  const align = byKind('align-16kb')[0];
267
270
  if (align) {
package/src/rules.js CHANGED
@@ -26,10 +26,11 @@ export const PLAY_TARGET_SDK = {
26
26
  visibility: { minimum: 35, since: '2026-08-31' },
27
27
  };
28
28
 
29
- // Google Play 16 KB memory page size requirement for apps targeting Android 15+.
30
- // https://android-developers.googleblog.com/2025/05/prepare-play-apps-for-devices-with-16kb-page-size.html
29
+ // Google Play 16 KB memory page size requirement for apps targeting Android 15+: policy since
30
+ // 2025-11-01; Play blocks updates without it from 2027-02-01 (checked 2026-10-06 on
31
+ // https://developer.android.com/guide/practices/page-sizes, page updated 2026-09-16).
31
32
  // React Native supports 16 KB pages from 0.77.
32
- export const PAGE_SIZE_16K = { since: '2025-11-01', extensionUntil: '2026-05-31', firstRnMinor: 77 };
33
+ export const PAGE_SIZE_16K = { since: '2025-11-01', blockedFrom: '2027-02-01', firstRnMinor: 77 };
33
34
 
34
35
  // Minimum OS versions of current React Native (since 0.76).
35
36
  // https://reactnative.dev/blog/2024/10/23/release-0.76-new-architecture
package/src/security.js CHANGED
@@ -569,10 +569,24 @@ export function weakCrypto(root) {
569
569
  export function tokenStorage(root) {
570
570
  const hits = [];
571
571
  for (const file of jsFiles(root)) {
572
+ // Web-only files (keychain.web.ts) have no Keychain to use; the mobile file decides.
573
+ if (/\.web\.[cm]?[jt]sx?$/.test(file)) continue;
572
574
  const text = read(file);
573
575
  if (!text || !/AsyncStorage|MMKV/.test(text)) continue;
576
+ const at = (i) => `${path.relative(root, file)}:${text.slice(0, i).split('\n').length}`;
577
+ const seen = new Set();
578
+ // By key name: setItem('access_token', …)
574
579
  for (const m of text.matchAll(/\b(?:AsyncStorage\.setItem|storage\.set|mmkv\.set)\s*\(\s*['"`@]?([\w@.\-:]*?(?:access_?token|refresh_?token|auth_?token|id_?token|jwt|session_?token|bearer)[\w.\-:]*)['"`]?\s*,/gi)) {
575
- hits.push(`${m[1]} (${path.relative(root, file)}:${text.slice(0, m.index).split('\n').length})`);
580
+ seen.add(m.index);
581
+ hits.push(`${m[1]} (${at(m.index)})`);
582
+ }
583
+ // By value: setItem(TOKEN_KEY, token), setItem('user', JSON.stringify({ token })). Push
584
+ // notification and device tokens are not secrets.
585
+ for (const m of text.matchAll(/\b(?:AsyncStorage\.setItem|(?:storage|mmkv)\.set(?:String)?)\s*\(\s*([^,()]{1,60}),\s*([^;\n]{0,120})/g)) {
586
+ if (seen.has(m.index)) continue;
587
+ if (!/\b(?:access_?token|refresh_?token|auth_?token|id_?token|session_?token|login_?token|accessToken|refreshToken|authToken|idToken|sessionToken|loginToken|jwt|token)\b/i.test(m[2])) continue;
588
+ if (/push|fcm|apns|expo_?push|notif|device|firebase|messaging/i.test(m[1] + m[2])) continue;
589
+ hits.push(`${m[1].trim()} (${at(m.index)})`);
576
590
  }
577
591
  }
578
592
  if (!hits.length) return [];