shiplint 0.1.1 → 1.0.0

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.
Files changed (41) hide show
  1. package/README.md +267 -62
  2. package/dist/cli/index.js +0 -0
  3. package/dist/rules/config/index.d.ts +2 -0
  4. package/dist/rules/config/index.d.ts.map +1 -1
  5. package/dist/rules/config/index.js +5 -1
  6. package/dist/rules/config/index.js.map +1 -1
  7. package/dist/rules/config/missing-encryption-flag.d.ts +12 -0
  8. package/dist/rules/config/missing-encryption-flag.d.ts.map +1 -0
  9. package/dist/rules/config/missing-encryption-flag.js +40 -0
  10. package/dist/rules/config/missing-encryption-flag.js.map +1 -0
  11. package/dist/rules/config/missing-launch-storyboard.d.ts +12 -0
  12. package/dist/rules/config/missing-launch-storyboard.d.ts.map +1 -0
  13. package/dist/rules/config/missing-launch-storyboard.js +42 -0
  14. package/dist/rules/config/missing-launch-storyboard.js.map +1 -0
  15. package/dist/rules/index.d.ts.map +1 -1
  16. package/dist/rules/index.js +10 -0
  17. package/dist/rules/index.js.map +1 -1
  18. package/dist/rules/metadata/index.d.ts +1 -0
  19. package/dist/rules/metadata/index.d.ts.map +1 -1
  20. package/dist/rules/metadata/index.js +3 -1
  21. package/dist/rules/metadata/index.js.map +1 -1
  22. package/dist/rules/metadata/missing-supported-orientations.d.ts +12 -0
  23. package/dist/rules/metadata/missing-supported-orientations.d.ts.map +1 -0
  24. package/dist/rules/metadata/missing-supported-orientations.js +68 -0
  25. package/dist/rules/metadata/missing-supported-orientations.js.map +1 -0
  26. package/dist/rules/privacy/index.d.ts +2 -0
  27. package/dist/rules/privacy/index.d.ts.map +1 -1
  28. package/dist/rules/privacy/index.js +5 -1
  29. package/dist/rules/privacy/index.js.map +1 -1
  30. package/dist/rules/privacy/missing-bluetooth-purpose.d.ts +11 -0
  31. package/dist/rules/privacy/missing-bluetooth-purpose.d.ts.map +1 -0
  32. package/dist/rules/privacy/missing-bluetooth-purpose.js +80 -0
  33. package/dist/rules/privacy/missing-bluetooth-purpose.js.map +1 -0
  34. package/dist/rules/privacy/missing-camera-purpose.d.ts.map +1 -1
  35. package/dist/rules/privacy/missing-camera-purpose.js +5 -1
  36. package/dist/rules/privacy/missing-camera-purpose.js.map +1 -1
  37. package/dist/rules/privacy/missing-face-id-purpose.d.ts +11 -0
  38. package/dist/rules/privacy/missing-face-id-purpose.d.ts.map +1 -0
  39. package/dist/rules/privacy/missing-face-id-purpose.js +80 -0
  40. package/dist/rules/privacy/missing-face-id-purpose.js.map +1 -0
  41. package/package.json +1 -1
package/README.md CHANGED
@@ -1,107 +1,312 @@
1
1
  # ShipLint
2
2
 
3
- 🛡️ Catch App Store rejections before they happen.
3
+ **ShipLint is a pre-submission linter for iOS apps that catches App Store rejection reasons before you upload.** It scans your `Info.plist`, entitlements, privacy manifests (`PrivacyInfo.xcprivacy`), and `project.pbxproj` files for issues that would trigger ITMS errors or App Review violations — in seconds, from the command line, with zero configuration.
4
4
 
5
- ShipLint scans your iOS project for App Store Review Guideline violations and tells you exactly how to fix them.
5
+ [![npm version](https://img.shields.io/npm/v/shiplint.svg)](https://www.npmjs.com/package/shiplint)
6
+ [![License](https://img.shields.io/npm/l/shiplint.svg)](https://github.com/Signal26AI/ShipLint/blob/main/LICENSE)
6
7
 
7
- ## Installation
8
+ ---
8
9
 
9
- ```bash
10
- npm install -g shiplint
11
- ```
10
+ ## The Problem
12
11
 
13
- Or with npx (no install required):
12
+ According to [Apple's 2024 App Store Transparency Report](https://www.apple.com/legal/more-resources/docs/2024-App-Store-Transparency-Report.pdf), over **7.7 million app submissions** were reviewed in 2024, and approximately **1.9 million were rejected** — roughly 25% of all submissions. The most common reasons? Missing privacy usage descriptions, incomplete entitlements, and absent privacy manifests. These are configuration issues, not code bugs.
14
13
 
15
- ```bash
16
- npx shiplint scan ./MyApp.xcodeproj
17
- ```
14
+ > "I spent three days trying to figure out why my app kept getting rejected. Turned out I was missing `NSCameraUsageDescription` in my Info.plist. Three days for a one-line fix." — iOS indie developer on r/iOSProgramming
18
15
 
19
- ## The Problem
16
+ The rejection feedback loop is brutal: **build → archive → upload to App Store Connect → wait for processing → receive rejection email → fix → rebuild → re-upload**. Each cycle takes 10–30 minutes even when you know the fix. When you don't, it can take days.
17
+
18
+ ### Specific ITMS Errors Developers Hit
19
+
20
+ - **ITMS-90683**: Missing purpose string (e.g., `NSCameraUsageDescription`, `NSMicrophoneUsageDescription`). This is the single most common first-time submission error. [Apple Technical Note TN2151](https://developer.apple.com/documentation/technotes/tn2151-understanding-and-resolving-issues-with-app-distribution)
21
+ - **ITMS-91053**: Missing `PrivacyInfo.xcprivacy` privacy manifest, required since Spring 2024 for apps using [required reason APIs](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api). [WWDC23 Session 10060 — "Get started with privacy manifests"](https://developer.apple.com/videos/play/wwdc2023/10060/)
22
+ - **ITMS-90078**: Missing entitlements for declared capabilities (e.g., Sign in with Apple not configured despite using third-party login). [App Store Review Guideline 4.8](https://developer.apple.com/app-store/review/guidelines/#sign-in-with-apple)
23
+
24
+ ### If You Built Your iOS App with AI Assistance, You're Especially at Risk
25
+
26
+ Tools like **Cursor**, **GitHub Copilot**, and **Claude** generate syntactically correct Swift code — but they frequently miss platform-level configuration. AI doesn't add `NSCameraUsageDescription` to your `Info.plist` when it generates camera code. It doesn't create a `PrivacyInfo.xcprivacy` when it imports an analytics SDK. The code compiles and runs fine in the simulator, but App Store Connect will reject it.
20
27
 
21
- App Store rejections cost time and money:
22
- - 2-7 day review delays
23
- - Missed launch windows
24
- - Frustrated users waiting for updates
28
+ ShipLint catches exactly these gaps.
25
29
 
26
- Common culprits: missing privacy descriptions, Sign in with Apple requirements, tracking compliance — all preventable.
30
+ ---
31
+
32
+ ## Quick Start
33
+
34
+ **No install needed** — run directly with npx:
35
+
36
+ ```bash
37
+ npx shiplint scan ./YourApp
38
+ ```
27
39
 
28
- ## The Solution
40
+ **Global install** for regular use:
29
41
 
30
- ShipLint catches these issues **before** you submit:
42
+ ```bash
43
+ npm install -g shiplint
44
+ ```
45
+
46
+ Then scan any iOS project directory or `.xcodeproj`:
31
47
 
32
48
  ```bash
33
- $ shiplint scan ./MyApp.xcodeproj
49
+ shiplint scan ./MyApp.xcodeproj
50
+ ```
34
51
 
52
+ ### Example Output
53
+
54
+ ```
35
55
  🛡️ ShipLint Scan Results
36
56
 
37
- 🔍 Found 2 issue(s):
57
+ 🔍 Found 3 issue(s):
38
58
 
39
- 1. [CRITICAL] Missing Camera Usage Description
40
- 📍 Info.plist • 📋 Guideline 5.1.1
59
+ 1. [CRITICAL] missing-camera-purpose
60
+ 📍 Info.plist • 📋 Guideline 5.1.1 • ⚠️ ITMS-90683
41
61
 
42
- Your app uses AVFoundation but Info.plist is missing
43
- NSCameraUsageDescription...
62
+ Your app references AVFoundation but Info.plist is missing
63
+ NSCameraUsageDescription. Apple requires a human-readable
64
+ explanation of why your app needs camera access.
44
65
 
45
66
  How to fix:
46
- Add NSCameraUsageDescription to your Info.plist...
67
+ Add to Info.plist:
68
+ <key>NSCameraUsageDescription</key>
69
+ <string>This app uses the camera to scan QR codes.</string>
70
+
71
+ 2. [CRITICAL] missing-privacy-manifest
72
+ 📍 Project • 📋 Required Reason API • ⚠️ ITMS-91053
73
+
74
+ Your project uses APIs that require a privacy manifest
75
+ (PrivacyInfo.xcprivacy) as of Spring 2024. Without it,
76
+ App Store Connect will reject your binary.
47
77
 
48
- 2. [CRITICAL] Third-Party Login Without Sign in with Apple
78
+ 3. [WARNING] third-party-login-no-siwa
49
79
  📍 Entitlements • 📋 Guideline 4.8
50
80
 
51
- Your app includes Google Sign-In but Sign in with Apple
52
- is not configured...
81
+ Your app includes Google Sign-In but Sign in with Apple
82
+ is not configured in your entitlements.
53
83
  ```
54
84
 
55
- ## What We Check
85
+ ---
86
+
87
+ ## What ShipLint Catches
88
+
89
+ ShipLint ships with **15 rules** covering the most common App Store rejection reasons. Each rule maps directly to an Apple guideline and specific ITMS error code.
90
+
91
+ ### Privacy Usage Descriptions — [App Store Review Guideline 5.1.1](https://developer.apple.com/app-store/review/guidelines/#data-collection-and-storage)
92
+
93
+ These rules prevent **ITMS-90683** ("Missing purpose string in Info.plist"). Apple requires every app that accesses protected resources to declare a human-readable usage description. If you use the API but don't include the string, App Store Connect rejects your binary automatically — no human review needed.
94
+
95
+ | Rule | Info.plist Key | What It Catches |
96
+ |------|---------------|-----------------|
97
+ | `missing-camera-purpose` | `NSCameraUsageDescription` | App uses AVFoundation/camera APIs without declaring why |
98
+ | `missing-microphone-purpose` | `NSMicrophoneUsageDescription` | App uses audio recording APIs without declaring why |
99
+ | `missing-location-purpose` | `NSLocationWhenInUseUsageDescription` | App uses CoreLocation without declaring why |
100
+ | `missing-photo-library-purpose` | `NSPhotoLibraryUsageDescription` | App accesses Photos without declaring why |
101
+ | `missing-contacts-purpose` | `NSContactsUsageDescription` | App accesses Contacts without declaring why |
102
+ | `missing-bluetooth-purpose` | `NSBluetoothAlwaysUsageDescription` | App uses CoreBluetooth without declaring why |
103
+ | `missing-face-id-purpose` | `NSFaceIDUsageDescription` | App uses LocalAuthentication (Face ID) without declaring why |
104
+ | `location-always-unjustified` | `NSLocationAlwaysAndWhenInUseUsageDescription` | App requests "Always" location without sufficient justification — almost always rejected per [Guideline 5.1.2](https://developer.apple.com/app-store/review/guidelines/#data-use-and-sharing) |
105
+
106
+ ### App Tracking Transparency — [Guideline 5.1.2](https://developer.apple.com/app-store/review/guidelines/#data-use-and-sharing)
107
+
108
+ | Rule | What It Catches |
109
+ |------|-----------------|
110
+ | `att-tracking-mismatch` | App imports `AdSupport` or `AppTrackingTransparency` framework but `Info.plist` is missing `NSUserTrackingUsageDescription`. Required since iOS 14.5. [Apple ATT documentation](https://developer.apple.com/documentation/apptrackingtransparency) |
56
111
 
57
- | Category | Rules |
58
- |----------|-------|
59
- | **Privacy** | Camera, Location, Microphone, Photos, Contacts usage descriptions |
60
- | **Tracking** | ATT compliance, tracking SDK detection |
61
- | **Authentication** | Sign in with Apple requirements |
62
- | **Security** | App Transport Security, Privacy Manifests (iOS 17+) |
112
+ ### Sign in with Apple — [Guideline 4.8](https://developer.apple.com/app-store/review/guidelines/#sign-in-with-apple)
63
113
 
64
- 10 rules today, more coming weekly.
114
+ | Rule | What It Catches |
115
+ |------|-----------------|
116
+ | `third-party-login-no-siwa` | App uses a third-party login SDK (Google, Facebook, etc.) but Sign in with Apple is not configured. Required since [WWDC19](https://developer.apple.com/videos/play/wwdc2019/706/) for apps offering third-party sign-in. |
65
117
 
66
- ## Usage
118
+ ### App Transport Security — [Guideline 2.1](https://developer.apple.com/app-store/review/guidelines/#performance)
119
+
120
+ | Rule | What It Catches |
121
+ |------|-----------------|
122
+ | `ats-exception-without-justification` | App sets `NSAllowsArbitraryLoads = YES` or declares ATS exceptions without justification. Apple expects all network traffic to use HTTPS. [Apple ATS documentation](https://developer.apple.com/documentation/bundleresources/information_property_list/nsapptransportsecurity) |
123
+
124
+ ### Privacy Manifests (iOS 17+) — [WWDC23](https://developer.apple.com/videos/play/wwdc2023/10060/)
125
+
126
+ | Rule | What It Catches |
127
+ |------|-----------------|
128
+ | `missing-privacy-manifest` | Project uses [required reason APIs](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api) (UserDefaults, file timestamps, etc.) or third-party SDKs that require a `PrivacyInfo.xcprivacy` file. Enforced by App Store Connect since Spring 2024 via **ITMS-91053**. |
129
+
130
+ ### Export Compliance — [Apple Export Compliance](https://developer.apple.com/documentation/bundleresources/information_property_list/itsappusesnonexemptencryption)
131
+
132
+ | Rule | Info.plist Key | What It Catches |
133
+ |------|---------------|-----------------|
134
+ | `missing-encryption-flag` | `ITSAppUsesNonExemptEncryption` | Missing export compliance declaration. Without this key, App Store Connect prompts for manual compliance answers on every upload — adding friction and potential delays. Set to `false` if your app only uses HTTPS or standard iOS encryption. |
135
+
136
+ ### Launch Configuration — [Guideline 4.0 (Design)](https://developer.apple.com/app-store/review/guidelines/#design)
137
+
138
+ | Rule | Info.plist Key | What It Catches |
139
+ |------|---------------|-----------------|
140
+ | `missing-launch-storyboard` | `UILaunchStoryboardName` | Missing launch storyboard. Required since April 2020 for all iOS apps to support all screen sizes. Apps without this key are rejected. |
141
+
142
+ ### App Configuration — [Guideline 4.0 (Design)](https://developer.apple.com/app-store/review/guidelines/#design)
143
+
144
+ | Rule | Info.plist Key | What It Catches |
145
+ |------|---------------|-----------------|
146
+ | `missing-supported-orientations` | `UISupportedInterfaceOrientations` | Missing interface orientation declaration. Apps should explicitly declare which orientations they support to avoid UI issues on different devices. |
147
+
148
+ ---
149
+
150
+ ## How It Works
151
+
152
+ ShipLint is a **static analysis tool** — it parses your project files directly without building or running your app. A scan completes in under 2 seconds.
153
+
154
+ **Files ShipLint reads:**
155
+
156
+ | File | What ShipLint Looks For |
157
+ |------|------------------------|
158
+ | `Info.plist` | Privacy usage descriptions (`NS*UsageDescription`), ATS configuration, tracking declarations |
159
+ | `*.entitlements` | Sign in with Apple capability, associated domains |
160
+ | `project.pbxproj` | Framework imports (AVFoundation, CoreLocation, AdSupport), build settings |
161
+ | `PrivacyInfo.xcprivacy` | Privacy manifest existence and required reason API declarations |
162
+ | `Podfile.lock` / `Package.resolved` | Third-party SDK detection (analytics, login, tracking SDKs) |
163
+
164
+ **Output formats:**
67
165
 
68
166
  ```bash
69
- # Scan an Xcode project
70
- shiplint scan ./MyApp.xcodeproj
167
+ # Human-readable (default)
168
+ shiplint scan ./MyApp
169
+
170
+ # JSON (for scripting)
171
+ shiplint scan ./MyApp --format json
172
+
173
+ # SARIF (for GitHub Code Scanning integration)
174
+ shiplint scan ./MyApp --format sarif
175
+ ```
176
+
177
+ ---
178
+
179
+ ## CI/CD Integration
180
+
181
+ ### GitHub Actions
182
+
183
+ Add ShipLint to your CI pipeline to catch rejection-causing issues on every push:
184
+
185
+ ```yaml
186
+ name: ShipLint
187
+ on: [push, pull_request]
188
+
189
+ jobs:
190
+ shiplint:
191
+ runs-on: ubuntu-latest
192
+ steps:
193
+ - uses: actions/checkout@v4
194
+
195
+ - name: Setup Node.js
196
+ uses: actions/setup-node@v4
197
+ with:
198
+ node-version: '18'
199
+
200
+ - name: Run ShipLint
201
+ run: npx shiplint scan ./ios --format sarif > shiplint.sarif
202
+
203
+ - name: Upload SARIF
204
+ if: always()
205
+ uses: github/codeql-action/upload-sarif@v3
206
+ with:
207
+ sarif_file: shiplint.sarif
208
+ ```
209
+
210
+ ### Generic CI
211
+
212
+ ```bash
213
+ # Fails with exit code 1 if critical issues found
214
+ npx shiplint scan ./ios --format json
215
+
216
+ # Use in any CI system
217
+ if ! npx shiplint scan ./ios; then
218
+ echo "ShipLint found App Store rejection risks. Fix before merging."
219
+ exit 1
220
+ fi
221
+ ```
222
+
223
+ ---
224
+
225
+ ## FAQ
226
+
227
+ ### How is ShipLint different from Fastlane Precheck?
228
+
229
+ ShipLint and [Fastlane Precheck](https://docs.fastlane.tools/actions/precheck/) solve different problems and work well together. **Fastlane Precheck** validates your App Store Connect metadata — app descriptions, keywords, screenshots, and age ratings. It checks what users *see* on your store listing. **ShipLint** validates your actual Xcode project files — `Info.plist`, entitlements, `PrivacyInfo.xcprivacy`, and `project.pbxproj`. It checks what Apple's automated systems scan in your *binary*. You should use both: ShipLint before you build, Precheck before you submit.
230
+
231
+ ### Does ShipLint replace Xcode's built-in validation?
71
232
 
72
- # Scan a directory
73
- shiplint scan ./ios
233
+ No. Xcode's "Validate App" runs at archive/upload time, after you've already spent 5–15 minutes building and archiving. It also only catches a subset of issues — it validates the binary but won't flag missing `NSCameraUsageDescription` until App Review. **ShipLint runs in under 2 seconds** against your source files, catching issues before you even open Xcode. Think of it as a linter (like ESLint for JavaScript) versus a compiler — you want both, but the linter saves you from slow feedback loops.
74
234
 
75
- # Output as JSON
76
- shiplint scan ./MyApp.xcodeproj --format json
235
+ ### Can ShipLint check apps built with Cursor, Copilot, or other AI coding tools?
236
+
237
+ Yes — and this is one of ShipLint's most valuable use cases. AI code generation tools like **Cursor**, **GitHub Copilot**, and **Claude** produce syntactically valid Swift and SwiftUI code, but they routinely miss iOS platform configuration requirements. A typical pattern: the AI generates camera capture code using `AVCaptureSession`, but doesn't add `NSCameraUsageDescription` to `Info.plist`. The code compiles, runs in the simulator, and looks perfect — until App Store Connect rejects it with ITMS-90683. ShipLint catches these configuration gaps automatically.
238
+
239
+ ### What ITMS errors does ShipLint prevent?
240
+
241
+ ShipLint's rules are designed to prevent the most common automated rejection errors from App Store Connect:
242
+
243
+ - **ITMS-90683** — Missing purpose string (`NSCameraUsageDescription`, `NSMicrophoneUsageDescription`, `NSLocationWhenInUseUsageDescription`, `NSPhotoLibraryUsageDescription`, `NSContactsUsageDescription`, `NSUserTrackingUsageDescription`). Prevented by ShipLint's six `missing-*-purpose` rules and the `att-tracking-mismatch` rule.
244
+ - **ITMS-91053** — Missing privacy manifest (`PrivacyInfo.xcprivacy`). Prevented by the `missing-privacy-manifest` rule.
245
+ - **ITMS-90078** — Missing or misconfigured entitlements. Prevented by the `third-party-login-no-siwa` rule.
246
+
247
+ ShipLint also catches issues that trigger human reviewer rejections under Guidelines [2.1](https://developer.apple.com/app-store/review/guidelines/#performance), [4.8](https://developer.apple.com/app-store/review/guidelines/#sign-in-with-apple), and [5.1.1](https://developer.apple.com/app-store/review/guidelines/#data-collection-and-storage).
248
+
249
+ ### Does ShipLint work with React Native, Flutter, or Expo?
250
+
251
+ ShipLint scans the native iOS project files regardless of what generated them. If your React Native, Flutter, or Expo project has an `ios/` directory with `Info.plist` and `project.pbxproj`, ShipLint can scan it. Point it at your `ios/` folder:
252
+
253
+ ```bash
254
+ npx shiplint scan ./ios
77
255
  ```
78
256
 
79
- ## Rules (10 and growing)
257
+ ### Is ShipLint open source?
80
258
 
81
- **Privacy (Guideline 5.1.1)**
82
- - `missing-camera-purpose` - Camera usage without NSCameraUsageDescription
83
- - `missing-microphone-purpose` - Microphone usage without NSMicrophoneUsageDescription
84
- - `missing-location-purpose` - Location usage without NSLocationUsageDescription
85
- - `missing-photo-library-purpose` - Photo library usage without NSPhotoLibraryUsageDescription
86
- - `missing-contacts-purpose` - Contacts usage without NSContactsUsageDescription
87
- - `location-always-unjustified` - "Always" location without proper justification
88
- - `att-tracking-mismatch` - ATT framework without NSUserTrackingUsageDescription
259
+ ShipLint's CLI scanner is distributed via npm. The rule definitions and scanning engine are designed to be transparent — you can see exactly what each rule checks and why. Visit the [GitHub repository](https://github.com/Signal26AI/ShipLint) for source code and issue tracking.
89
260
 
90
- **Authentication (Guideline 4.8)**
91
- - `third-party-login-no-siwa` - Third-party login without Sign in with Apple
261
+ ---
92
262
 
93
- **Security (Guideline 2.1)**
94
- - `ats-exception-without-justification` - App Transport Security exceptions
263
+ ## Comparison: iOS Submission Checking Tools
95
264
 
96
- **Metadata (iOS 17+)**
97
- - `missing-privacy-manifest` - Missing PrivacyInfo.xcprivacy for required APIs
265
+ Different tools cover different layers of the submission process. Here's where ShipLint fits:
98
266
 
99
- ## Coming Soon
267
+ | Capability | ShipLint | Fastlane Precheck | Xcode Validate | Manual Review |
268
+ |---|---|---|---|---|
269
+ | **When it runs** | Before build (2s) | Before submission | At upload (10-30 min) | After upload (1-7 days) |
270
+ | **Privacy usage descriptions** (NSCameraUsageDescription, etc.) | ✅ | ❌ | ❌¹ | ✅ |
271
+ | **Privacy manifest** (PrivacyInfo.xcprivacy) | ✅ | ❌ | ✅ | ✅ |
272
+ | **Sign in with Apple** requirement | ✅ | ❌ | ❌ | ✅ |
273
+ | **App Transport Security** | ✅ | ❌ | ❌ | ✅ |
274
+ | **ATT / Tracking compliance** | ✅ | ❌ | ❌ | ✅ |
275
+ | **App Store metadata** (descriptions, keywords) | ❌ | ✅ | ❌ | ✅ |
276
+ | **Screenshot validation** | ❌ | ✅ | ❌ | ✅ |
277
+ | **Binary architecture** | ❌ | ❌ | ✅ | ❌ |
278
+ | **Code signing** | ❌ | ❌ | ✅ | ❌ |
279
+ | **CI/CD integration** | ✅ | ✅ | ❌ | ❌ |
280
+ | **Works without Xcode** | ✅ | ✅ | ❌ | N/A |
100
281
 
101
- **ShipLint GitHub App** automatic PR checks, no setup required.
282
+ ¹ *Xcode shows warnings at build time for some missing keys but does not block the archive/upload process.*
102
283
 
103
- Join the waitlist: [shiplint.dev](https://shiplint.dev)
284
+ **The ideal pipeline:** ShipLint (project files) → Xcode Validate (binary) → Fastlane Precheck (metadata) → Submit.
104
285
 
105
286
  ---
106
287
 
107
- © 2026 Signal26. All rights reserved.
288
+ ## Requirements
289
+
290
+ - **Node.js** 18 or later
291
+ - Works on macOS, Linux, and Windows (CI-friendly — no Xcode required)
292
+
293
+ ---
294
+
295
+ ## Links
296
+
297
+ - 🌐 **Website:** [shiplint.app](https://shiplint.app)
298
+ - 📦 **npm:** [npmjs.com/package/shiplint](https://www.npmjs.com/package/shiplint)
299
+ - 💻 **GitHub:** [github.com/Signal26AI/ShipLint](https://github.com/Signal26AI/ShipLint)
300
+ - 🐛 **Issues:** [github.com/Signal26AI/ShipLint/issues](https://github.com/Signal26AI/ShipLint/issues)
301
+
302
+ ---
303
+
304
+ ## Contributing
305
+
306
+ Found a rule that's missing? An ITMS error you keep hitting? [Open an issue](https://github.com/Signal26AI/ShipLint/issues) — we add new rules based on real-world rejection patterns.
307
+
308
+ ---
309
+
310
+ ## License
311
+
312
+ © 2025–2026 [Signal26](https://signal26.dev). All rights reserved.
package/dist/cli/index.js CHANGED
File without changes
@@ -2,4 +2,6 @@
2
2
  * Config rules exports
3
3
  */
4
4
  export { ATSExceptionWithoutJustificationRule } from './ats-exception-without-justification.js';
5
+ export { MissingEncryptionFlagRule } from './missing-encryption-flag.js';
6
+ export { MissingLaunchStoryboardRule } from './missing-launch-storyboard.js';
5
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/rules/config/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,oCAAoC,EAAE,MAAM,0CAA0C,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/rules/config/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,oCAAoC,EAAE,MAAM,0CAA0C,CAAC;AAChG,OAAO,EAAE,yBAAyB,EAAE,MAAM,8BAA8B,CAAC;AACzE,OAAO,EAAE,2BAA2B,EAAE,MAAM,gCAAgC,CAAC"}
@@ -1,9 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ATSExceptionWithoutJustificationRule = void 0;
3
+ exports.MissingLaunchStoryboardRule = exports.MissingEncryptionFlagRule = exports.ATSExceptionWithoutJustificationRule = void 0;
4
4
  /**
5
5
  * Config rules exports
6
6
  */
7
7
  var ats_exception_without_justification_js_1 = require("./ats-exception-without-justification.js");
8
8
  Object.defineProperty(exports, "ATSExceptionWithoutJustificationRule", { enumerable: true, get: function () { return ats_exception_without_justification_js_1.ATSExceptionWithoutJustificationRule; } });
9
+ var missing_encryption_flag_js_1 = require("./missing-encryption-flag.js");
10
+ Object.defineProperty(exports, "MissingEncryptionFlagRule", { enumerable: true, get: function () { return missing_encryption_flag_js_1.MissingEncryptionFlagRule; } });
11
+ var missing_launch_storyboard_js_1 = require("./missing-launch-storyboard.js");
12
+ Object.defineProperty(exports, "MissingLaunchStoryboardRule", { enumerable: true, get: function () { return missing_launch_storyboard_js_1.MissingLaunchStoryboardRule; } });
9
13
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/rules/config/index.ts"],"names":[],"mappings":";;;AAAA;;GAEG;AACH,mGAAgG;AAAvF,8JAAA,oCAAoC,OAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/rules/config/index.ts"],"names":[],"mappings":";;;AAAA;;GAEG;AACH,mGAAgG;AAAvF,8JAAA,oCAAoC,OAAA;AAC7C,2EAAyE;AAAhE,uIAAA,yBAAyB,OAAA;AAClC,+EAA6E;AAApE,2IAAA,2BAA2B,OAAA"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Rule: Missing Export Compliance Flag
3
+ *
4
+ * Detects when an app is missing the ITSAppUsesNonExemptEncryption key
5
+ * in Info.plist. Without this key, App Store Connect prompts for export
6
+ * compliance information on every upload, causing friction and delays.
7
+ *
8
+ * Export Compliance Documentation
9
+ */
10
+ import type { Rule } from '../../types/index.js';
11
+ export declare const MissingEncryptionFlagRule: Rule;
12
+ //# sourceMappingURL=missing-encryption-flag.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"missing-encryption-flag.d.ts","sourceRoot":"","sources":["../../../src/rules/config/missing-encryption-flag.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,IAAI,EAAwB,MAAM,sBAAsB,CAAC;AAIvE,eAAO,MAAM,yBAAyB,EAAE,IAmCvC,CAAC"}
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MissingEncryptionFlagRule = void 0;
4
+ const index_js_1 = require("../../types/index.js");
5
+ const base_js_1 = require("../base.js");
6
+ exports.MissingEncryptionFlagRule = {
7
+ id: 'config-002-missing-encryption-flag',
8
+ name: 'Missing Export Compliance Flag',
9
+ description: 'Checks for missing ITSAppUsesNonExemptEncryption in Info.plist',
10
+ category: index_js_1.RuleCategory.Config,
11
+ severity: index_js_1.Severity.Medium,
12
+ confidence: index_js_1.Confidence.High,
13
+ guidelineReference: 'Export Compliance',
14
+ async evaluate(context) {
15
+ // If the key exists (true or false), the developer has declared their intent
16
+ if (context.hasPlistKey('ITSAppUsesNonExemptEncryption')) {
17
+ return [];
18
+ }
19
+ return [
20
+ (0, base_js_1.makeFinding)(this, {
21
+ description: `Your Info.plist is missing the ITSAppUsesNonExemptEncryption key. ` +
22
+ `Without this key, App Store Connect will prompt you to answer export compliance ` +
23
+ `questions manually on every single upload. This causes friction and potential delays ` +
24
+ `in your submission workflow.`,
25
+ location: 'Info.plist',
26
+ fixGuidance: `Add the ITSAppUsesNonExemptEncryption key to your Info.plist. If your app ` +
27
+ `only uses HTTPS/URLSession or standard iOS encryption (most apps), set it to false:
28
+
29
+ <key>ITSAppUsesNonExemptEncryption</key>
30
+ <false/>
31
+
32
+ Set it to true only if your app uses custom encryption algorithms beyond standard HTTPS ` +
33
+ `(e.g., proprietary encryption, custom TLS implementations). In that case, you'll also ` +
34
+ `need to submit export compliance documentation to Apple.`,
35
+ documentationURL: 'https://developer.apple.com/documentation/bundleresources/information_property_list/itsappusesnonexemptencryption',
36
+ }),
37
+ ];
38
+ },
39
+ };
40
+ //# sourceMappingURL=missing-encryption-flag.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"missing-encryption-flag.js","sourceRoot":"","sources":["../../../src/rules/config/missing-encryption-flag.ts"],"names":[],"mappings":";;;AAUA,mDAA0E;AAC1E,wCAAyC;AAE5B,QAAA,yBAAyB,GAAS;IAC7C,EAAE,EAAE,oCAAoC;IACxC,IAAI,EAAE,gCAAgC;IACtC,WAAW,EAAE,gEAAgE;IAC7E,QAAQ,EAAE,uBAAY,CAAC,MAAM;IAC7B,QAAQ,EAAE,mBAAQ,CAAC,MAAM;IACzB,UAAU,EAAE,qBAAU,CAAC,IAAI;IAC3B,kBAAkB,EAAE,mBAAmB;IAEvC,KAAK,CAAC,QAAQ,CAAC,OAAoB;QACjC,6EAA6E;QAC7E,IAAI,OAAO,CAAC,WAAW,CAAC,+BAA+B,CAAC,EAAE,CAAC;YACzD,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,OAAO;YACL,IAAA,qBAAW,EAAC,IAAI,EAAE;gBAChB,WAAW,EAAE,oEAAoE;oBAC/E,kFAAkF;oBAClF,uFAAuF;oBACvF,8BAA8B;gBAChC,QAAQ,EAAE,YAAY;gBACtB,WAAW,EAAE,4EAA4E;oBACvF;;;;;yFAK+E;oBAC/E,wFAAwF;oBACxF,0DAA0D;gBAC5D,gBAAgB,EAAE,mHAAmH;aACtI,CAAC;SACH,CAAC;IACJ,CAAC;CACF,CAAC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Rule: Missing Launch Storyboard
3
+ *
4
+ * Detects when an app is missing UILaunchStoryboardName in Info.plist.
5
+ * Since April 2020, all apps submitted to the App Store must use a
6
+ * launch storyboard to support all screen sizes.
7
+ *
8
+ * App Store Review Guideline: 4.0 (Design)
9
+ */
10
+ import type { Rule } from '../../types/index.js';
11
+ export declare const MissingLaunchStoryboardRule: Rule;
12
+ //# sourceMappingURL=missing-launch-storyboard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"missing-launch-storyboard.d.ts","sourceRoot":"","sources":["../../../src/rules/config/missing-launch-storyboard.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,IAAI,EAAwB,MAAM,sBAAsB,CAAC;AAIvE,eAAO,MAAM,2BAA2B,EAAE,IAqCzC,CAAC"}
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MissingLaunchStoryboardRule = void 0;
4
+ const index_js_1 = require("../../types/index.js");
5
+ const base_js_1 = require("../base.js");
6
+ exports.MissingLaunchStoryboardRule = {
7
+ id: 'config-003-missing-launch-storyboard',
8
+ name: 'Missing Launch Storyboard',
9
+ description: 'Checks for missing UILaunchStoryboardName in Info.plist',
10
+ category: index_js_1.RuleCategory.Config,
11
+ severity: index_js_1.Severity.Critical,
12
+ confidence: index_js_1.Confidence.High,
13
+ guidelineReference: '4.0',
14
+ async evaluate(context) {
15
+ // Only flag if the key is completely absent.
16
+ // An empty string is valid (SwiftUI lifecycle apps use empty UILaunchStoryboardName).
17
+ if (context.hasPlistKey('UILaunchStoryboardName')) {
18
+ return [];
19
+ }
20
+ return [
21
+ (0, base_js_1.makeFinding)(this, {
22
+ description: `Your Info.plist is missing the UILaunchStoryboardName key. Since April 2020, ` +
23
+ `all apps submitted to the App Store must include a launch storyboard to support ` +
24
+ `all device screen sizes. Apps without a launch storyboard will be rejected.`,
25
+ location: 'Info.plist',
26
+ fixGuidance: `Add UILaunchStoryboardName to your Info.plist pointing to your launch storyboard:
27
+
28
+ <key>UILaunchStoryboardName</key>
29
+ <string>LaunchScreen</string>
30
+
31
+ If you're using a SwiftUI app lifecycle, set it to an empty string — Xcode typically ` +
32
+ `handles this automatically. Make sure a corresponding LaunchScreen.storyboard file ` +
33
+ `exists in your project.
34
+
35
+ Note: Launch images (UILaunchImages / asset catalog launch images) are no longer accepted ` +
36
+ `as a substitute for launch storyboards.`,
37
+ documentationURL: 'https://developer.apple.com/documentation/bundleresources/information_property_list/uilaunchstoryboardname',
38
+ }),
39
+ ];
40
+ },
41
+ };
42
+ //# sourceMappingURL=missing-launch-storyboard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"missing-launch-storyboard.js","sourceRoot":"","sources":["../../../src/rules/config/missing-launch-storyboard.ts"],"names":[],"mappings":";;;AAUA,mDAA0E;AAC1E,wCAAyC;AAE5B,QAAA,2BAA2B,GAAS;IAC/C,EAAE,EAAE,sCAAsC;IAC1C,IAAI,EAAE,2BAA2B;IACjC,WAAW,EAAE,yDAAyD;IACtE,QAAQ,EAAE,uBAAY,CAAC,MAAM;IAC7B,QAAQ,EAAE,mBAAQ,CAAC,QAAQ;IAC3B,UAAU,EAAE,qBAAU,CAAC,IAAI;IAC3B,kBAAkB,EAAE,KAAK;IAEzB,KAAK,CAAC,QAAQ,CAAC,OAAoB;QACjC,6CAA6C;QAC7C,sFAAsF;QACtF,IAAI,OAAO,CAAC,WAAW,CAAC,wBAAwB,CAAC,EAAE,CAAC;YAClD,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,OAAO;YACL,IAAA,qBAAW,EAAC,IAAI,EAAE;gBAChB,WAAW,EAAE,+EAA+E;oBAC1F,kFAAkF;oBAClF,6EAA6E;gBAC/E,QAAQ,EAAE,YAAY;gBACtB,WAAW,EAAE;;;;;sFAKiE;oBAC5E,qFAAqF;oBACrF;;2FAEiF;oBACjF,yCAAyC;gBAC3C,gBAAgB,EAAE,4GAA4G;aAC/H,CAAC;SACH,CAAC;IACJ,CAAC;CACF,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/rules/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAG9C,cAAc,oBAAoB,CAAC;AAGnC,cAAc,iBAAiB,CAAC;AAGhC,cAAc,qBAAqB,CAAC;AAGpC,cAAc,mBAAmB,CAAC;AAGlC,cAAc,WAAW,CAAC;AAc1B;;GAEG;AACH,eAAO,MAAM,QAAQ,EAAE,IAAI,EAW1B,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,CAE1C,CAAC;AAEF;;GAEG;AACH,wBAAgB,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAEpD;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,CAAC,EAAE,MAAM,EAAE,GAAG,gBAAgB,CAkBvE;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,CAE/C;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,CAG9D"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/rules/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAG9C,cAAc,oBAAoB,CAAC;AAGnC,cAAc,iBAAiB,CAAC;AAGhC,cAAc,qBAAqB,CAAC;AAGpC,cAAc,mBAAmB,CAAC;AAGlC,cAAc,WAAW,CAAC;AAmB1B;;GAEG;AACH,eAAO,MAAM,QAAQ,EAAE,IAAI,EAgB1B,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,CAE1C,CAAC;AAEF;;GAEG;AACH,wBAAgB,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAEpD;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,CAAC,EAAE,MAAM,EAAE,GAAG,gBAAgB,CAkBvE;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,CAE/C;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,CAG9D"}
@@ -40,6 +40,11 @@ const missing_contacts_purpose_js_1 = require("./privacy/missing-contacts-purpos
40
40
  const third_party_login_no_siwa_js_1 = require("./auth/third-party-login-no-siwa.js");
41
41
  const missing_privacy_manifest_js_1 = require("./metadata/missing-privacy-manifest.js");
42
42
  const ats_exception_without_justification_js_1 = require("./config/ats-exception-without-justification.js");
43
+ const missing_encryption_flag_js_1 = require("./config/missing-encryption-flag.js");
44
+ const missing_launch_storyboard_js_1 = require("./config/missing-launch-storyboard.js");
45
+ const missing_bluetooth_purpose_js_1 = require("./privacy/missing-bluetooth-purpose.js");
46
+ const missing_face_id_purpose_js_1 = require("./privacy/missing-face-id-purpose.js");
47
+ const missing_supported_orientations_js_1 = require("./metadata/missing-supported-orientations.js");
43
48
  /**
44
49
  * All available rules
45
50
  */
@@ -51,9 +56,14 @@ exports.allRules = [
51
56
  missing_photo_library_purpose_js_1.MissingPhotoLibraryPurposeRule,
52
57
  missing_microphone_purpose_js_1.MissingMicrophonePurposeRule,
53
58
  missing_contacts_purpose_js_1.MissingContactsPurposeRule,
59
+ missing_bluetooth_purpose_js_1.MissingBluetoothPurposeRule,
60
+ missing_face_id_purpose_js_1.MissingFaceIdPurposeRule,
54
61
  third_party_login_no_siwa_js_1.ThirdPartyLoginNoSIWARule,
55
62
  missing_privacy_manifest_js_1.MissingPrivacyManifestRule,
63
+ missing_supported_orientations_js_1.MissingSupportedOrientationsRule,
56
64
  ats_exception_without_justification_js_1.ATSExceptionWithoutJustificationRule,
65
+ missing_encryption_flag_js_1.MissingEncryptionFlagRule,
66
+ missing_launch_storyboard_js_1.MissingLaunchStoryboardRule,
57
67
  ];
58
68
  /**
59
69
  * Rule registry - maps rule IDs to rule instances
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/rules/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AA0DA,0BAEC;AAcD,wDAkBC;AAMD,4BAEC;AAKD,8CAGC;AAvGD,gBAAgB;AAChB,qDAAmC;AAEnC,aAAa;AACb,kDAAgC;AAEhC,iBAAiB;AACjB,sDAAoC;AAEpC,eAAe;AACf,oDAAkC;AAElC,iBAAiB;AACjB,4CAA0B;AAE1B,gCAAgC;AAChC,mFAA+E;AAC/E,uFAAmF;AACnF,6FAAyF;AACzF,iFAA6E;AAC7E,iGAA4F;AAC5F,2FAAuF;AACvF,uFAAmF;AACnF,sFAAgF;AAChF,wFAAoF;AACpF,4GAAuG;AAEvG;;GAEG;AACU,QAAA,QAAQ,GAAW;IAC9B,oDAAwB;IACxB,wDAA0B;IAC1B,8DAA6B;IAC7B,kDAAuB;IACvB,iEAA8B;IAC9B,4DAA4B;IAC5B,wDAA0B;IAC1B,wDAAyB;IACzB,wDAA0B;IAC1B,6EAAoC;CACrC,CAAC;AAEF;;GAEG;AACU,QAAA,YAAY,GAAsB,IAAI,GAAG,CACpD,gBAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC,CACtC,CAAC;AAEF;;GAEG;AACH,SAAgB,OAAO,CAAC,EAAU;IAChC,OAAO,oBAAY,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;AAC9B,CAAC;AAUD;;;GAGG;AACH,SAAgB,sBAAsB,CAAC,GAAc;IACnD,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC7B,OAAO,EAAE,KAAK,EAAE,gBAAQ,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC;IAC7C,CAAC;IAED,MAAM,KAAK,GAAW,EAAE,CAAC;IACzB,MAAM,UAAU,GAAa,EAAE,CAAC;IAEhC,KAAK,MAAM,EAAE,IAAI,GAAG,EAAE,CAAC;QACrB,MAAM,IAAI,GAAG,oBAAY,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAClC,IAAI,IAAI,EAAE,CAAC;YACT,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnB,CAAC;aAAM,CAAC;YACN,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACtB,CAAC;IACH,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC;AAC/B,CAAC;AAED;;;GAGG;AACH,SAAgB,QAAQ,CAAC,GAAc;IACrC,OAAO,sBAAsB,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC;AAC3C,CAAC;AAED;;GAEG;AACH,SAAgB,iBAAiB,CAAC,UAAoB;IACpD,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC;IACvC,OAAO,gBAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;AAC3D,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/rules/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAoEA,0BAEC;AAcD,wDAkBC;AAMD,4BAEC;AAKD,8CAGC;AAjHD,gBAAgB;AAChB,qDAAmC;AAEnC,aAAa;AACb,kDAAgC;AAEhC,iBAAiB;AACjB,sDAAoC;AAEpC,eAAe;AACf,oDAAkC;AAElC,iBAAiB;AACjB,4CAA0B;AAE1B,gCAAgC;AAChC,mFAA+E;AAC/E,uFAAmF;AACnF,6FAAyF;AACzF,iFAA6E;AAC7E,iGAA4F;AAC5F,2FAAuF;AACvF,uFAAmF;AACnF,sFAAgF;AAChF,wFAAoF;AACpF,4GAAuG;AACvG,oFAAgF;AAChF,wFAAoF;AACpF,yFAAqF;AACrF,qFAAgF;AAChF,oGAAgG;AAEhG;;GAEG;AACU,QAAA,QAAQ,GAAW;IAC9B,oDAAwB;IACxB,wDAA0B;IAC1B,8DAA6B;IAC7B,kDAAuB;IACvB,iEAA8B;IAC9B,4DAA4B;IAC5B,wDAA0B;IAC1B,0DAA2B;IAC3B,qDAAwB;IACxB,wDAAyB;IACzB,wDAA0B;IAC1B,oEAAgC;IAChC,6EAAoC;IACpC,sDAAyB;IACzB,0DAA2B;CAC5B,CAAC;AAEF;;GAEG;AACU,QAAA,YAAY,GAAsB,IAAI,GAAG,CACpD,gBAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC,CACtC,CAAC;AAEF;;GAEG;AACH,SAAgB,OAAO,CAAC,EAAU;IAChC,OAAO,oBAAY,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;AAC9B,CAAC;AAUD;;;GAGG;AACH,SAAgB,sBAAsB,CAAC,GAAc;IACnD,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC7B,OAAO,EAAE,KAAK,EAAE,gBAAQ,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC;IAC7C,CAAC;IAED,MAAM,KAAK,GAAW,EAAE,CAAC;IACzB,MAAM,UAAU,GAAa,EAAE,CAAC;IAEhC,KAAK,MAAM,EAAE,IAAI,GAAG,EAAE,CAAC;QACrB,MAAM,IAAI,GAAG,oBAAY,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAClC,IAAI,IAAI,EAAE,CAAC;YACT,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnB,CAAC;aAAM,CAAC;YACN,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACtB,CAAC;IACH,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC;AAC/B,CAAC;AAED;;;GAGG;AACH,SAAgB,QAAQ,CAAC,GAAc;IACrC,OAAO,sBAAsB,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC;AAC3C,CAAC;AAED;;GAEG;AACH,SAAgB,iBAAiB,CAAC,UAAoB;IACpD,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC;IACvC,OAAO,gBAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;AAC3D,CAAC"}
@@ -2,4 +2,5 @@
2
2
  * Metadata rules exports
3
3
  */
4
4
  export { MissingPrivacyManifestRule } from './missing-privacy-manifest.js';
5
+ export { MissingSupportedOrientationsRule } from './missing-supported-orientations.js';
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/rules/metadata/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/rules/metadata/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAC3E,OAAO,EAAE,gCAAgC,EAAE,MAAM,qCAAqC,CAAC"}
@@ -1,9 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.MissingPrivacyManifestRule = void 0;
3
+ exports.MissingSupportedOrientationsRule = exports.MissingPrivacyManifestRule = void 0;
4
4
  /**
5
5
  * Metadata rules exports
6
6
  */
7
7
  var missing_privacy_manifest_js_1 = require("./missing-privacy-manifest.js");
8
8
  Object.defineProperty(exports, "MissingPrivacyManifestRule", { enumerable: true, get: function () { return missing_privacy_manifest_js_1.MissingPrivacyManifestRule; } });
9
+ var missing_supported_orientations_js_1 = require("./missing-supported-orientations.js");
10
+ Object.defineProperty(exports, "MissingSupportedOrientationsRule", { enumerable: true, get: function () { return missing_supported_orientations_js_1.MissingSupportedOrientationsRule; } });
9
11
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/rules/metadata/index.ts"],"names":[],"mappings":";;;AAAA;;GAEG;AACH,6EAA2E;AAAlE,yIAAA,0BAA0B,OAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/rules/metadata/index.ts"],"names":[],"mappings":";;;AAAA;;GAEG;AACH,6EAA2E;AAAlE,yIAAA,0BAA0B,OAAA;AACnC,yFAAuF;AAA9E,qJAAA,gCAAgC,OAAA"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Rule: Missing Supported Orientations
3
+ *
4
+ * Detects when an app is missing UISupportedInterfaceOrientations in Info.plist.
5
+ * Apps must declare supported orientations to ensure correct behavior across
6
+ * all device types and screen sizes.
7
+ *
8
+ * App Store Review Guideline: 4.0 (Design)
9
+ */
10
+ import type { Rule } from '../../types/index.js';
11
+ export declare const MissingSupportedOrientationsRule: Rule;
12
+ //# sourceMappingURL=missing-supported-orientations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"missing-supported-orientations.d.ts","sourceRoot":"","sources":["../../../src/rules/metadata/missing-supported-orientations.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,IAAI,EAAwB,MAAM,sBAAsB,CAAC;AAIvE,eAAO,MAAM,gCAAgC,EAAE,IAiE9C,CAAC"}
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MissingSupportedOrientationsRule = void 0;
4
+ const index_js_1 = require("../../types/index.js");
5
+ const base_js_1 = require("../base.js");
6
+ exports.MissingSupportedOrientationsRule = {
7
+ id: 'metadata-002-missing-supported-orientations',
8
+ name: 'Missing Supported Orientations',
9
+ description: 'Checks for missing UISupportedInterfaceOrientations in Info.plist',
10
+ category: index_js_1.RuleCategory.Metadata,
11
+ severity: index_js_1.Severity.Medium,
12
+ confidence: index_js_1.Confidence.High,
13
+ guidelineReference: '4.0',
14
+ async evaluate(context) {
15
+ const orientations = context.plistArray('UISupportedInterfaceOrientations');
16
+ // Case 1: Key completely missing
17
+ if (orientations === undefined) {
18
+ return [
19
+ (0, base_js_1.makeFinding)(this, {
20
+ description: `Your Info.plist is missing the UISupportedInterfaceOrientations key. ` +
21
+ `Apps should declare which interface orientations they support to ensure correct ` +
22
+ `behavior across all device types. Without this key, Apple's defaults may not match ` +
23
+ `your app's intended behavior, which can lead to UI issues and potential rejection.`,
24
+ location: 'Info.plist',
25
+ fixGuidance: `Add UISupportedInterfaceOrientations to your Info.plist with the orientations ` +
26
+ `your app supports:
27
+
28
+ <key>UISupportedInterfaceOrientations</key>
29
+ <array>
30
+ <string>UIInterfaceOrientationPortrait</string>
31
+ </array>
32
+
33
+ Common orientation values:
34
+ - UIInterfaceOrientationPortrait
35
+ - UIInterfaceOrientationPortraitUpsideDown
36
+ - UIInterfaceOrientationLandscapeLeft
37
+ - UIInterfaceOrientationLandscapeRight
38
+
39
+ For iPad, also consider adding UISupportedInterfaceOrientations~ipad with ` +
40
+ `all four orientations (iPad apps are expected to support all orientations).`,
41
+ documentationURL: 'https://developer.apple.com/documentation/bundleresources/information_property_list/uisupportedinterfaceorientations',
42
+ }),
43
+ ];
44
+ }
45
+ // Case 2: Empty array
46
+ if (orientations.length === 0) {
47
+ return [
48
+ (0, base_js_1.makeFinding)(this, {
49
+ title: 'Empty Supported Orientations',
50
+ description: `UISupportedInterfaceOrientations exists in Info.plist but is an empty array. ` +
51
+ `Your app must declare at least one supported orientation.`,
52
+ location: 'Info.plist',
53
+ fixGuidance: `Add at least one orientation to UISupportedInterfaceOrientations:
54
+
55
+ <key>UISupportedInterfaceOrientations</key>
56
+ <array>
57
+ <string>UIInterfaceOrientationPortrait</string>
58
+ </array>
59
+
60
+ Most apps should support at minimum UIInterfaceOrientationPortrait.`,
61
+ documentationURL: 'https://developer.apple.com/documentation/bundleresources/information_property_list/uisupportedinterfaceorientations',
62
+ }),
63
+ ];
64
+ }
65
+ return [];
66
+ },
67
+ };
68
+ //# sourceMappingURL=missing-supported-orientations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"missing-supported-orientations.js","sourceRoot":"","sources":["../../../src/rules/metadata/missing-supported-orientations.ts"],"names":[],"mappings":";;;AAUA,mDAA0E;AAC1E,wCAAyC;AAE5B,QAAA,gCAAgC,GAAS;IACpD,EAAE,EAAE,6CAA6C;IACjD,IAAI,EAAE,gCAAgC;IACtC,WAAW,EAAE,mEAAmE;IAChF,QAAQ,EAAE,uBAAY,CAAC,QAAQ;IAC/B,QAAQ,EAAE,mBAAQ,CAAC,MAAM;IACzB,UAAU,EAAE,qBAAU,CAAC,IAAI;IAC3B,kBAAkB,EAAE,KAAK;IAEzB,KAAK,CAAC,QAAQ,CAAC,OAAoB;QACjC,MAAM,YAAY,GAAG,OAAO,CAAC,UAAU,CAAC,kCAAkC,CAAC,CAAC;QAE5E,iCAAiC;QACjC,IAAI,YAAY,KAAK,SAAS,EAAE,CAAC;YAC/B,OAAO;gBACL,IAAA,qBAAW,EAAC,IAAI,EAAE;oBAChB,WAAW,EAAE,uEAAuE;wBAClF,kFAAkF;wBAClF,qFAAqF;wBACrF,oFAAoF;oBACtF,QAAQ,EAAE,YAAY;oBACtB,WAAW,EAAE,gFAAgF;wBAC3F;;;;;;;;;;;;;2EAa+D;wBAC/D,6EAA6E;oBAC/E,gBAAgB,EAAE,sHAAsH;iBACzI,CAAC;aACH,CAAC;QACJ,CAAC;QAED,sBAAsB;QACtB,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC9B,OAAO;gBACL,IAAA,qBAAW,EAAC,IAAI,EAAE;oBAChB,KAAK,EAAE,8BAA8B;oBACrC,WAAW,EAAE,+EAA+E;wBAC1F,2DAA2D;oBAC7D,QAAQ,EAAE,YAAY;oBACtB,WAAW,EAAE;;;;;;;oEAO6C;oBAC1D,gBAAgB,EAAE,sHAAsH;iBACzI,CAAC;aACH,CAAC;QACJ,CAAC;QAED,OAAO,EAAE,CAAC;IACZ,CAAC;CACF,CAAC"}
@@ -8,4 +8,6 @@ export { ATTTrackingMismatchRule } from './att-tracking-mismatch.js';
8
8
  export { MissingPhotoLibraryPurposeRule } from './missing-photo-library-purpose.js';
9
9
  export { MissingMicrophonePurposeRule } from './missing-microphone-purpose.js';
10
10
  export { MissingContactsPurposeRule } from './missing-contacts-purpose.js';
11
+ export { MissingBluetoothPurposeRule } from './missing-bluetooth-purpose.js';
12
+ export { MissingFaceIdPurposeRule } from './missing-face-id-purpose.js';
11
13
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/rules/privacy/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,wBAAwB,EAAE,MAAM,6BAA6B,CAAC;AACvE,OAAO,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAC3E,OAAO,EAAE,6BAA6B,EAAE,MAAM,kCAAkC,CAAC;AACjF,OAAO,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AACrE,OAAO,EAAE,8BAA8B,EAAE,MAAM,oCAAoC,CAAC;AACpF,OAAO,EAAE,4BAA4B,EAAE,MAAM,iCAAiC,CAAC;AAC/E,OAAO,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/rules/privacy/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,wBAAwB,EAAE,MAAM,6BAA6B,CAAC;AACvE,OAAO,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAC3E,OAAO,EAAE,6BAA6B,EAAE,MAAM,kCAAkC,CAAC;AACjF,OAAO,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AACrE,OAAO,EAAE,8BAA8B,EAAE,MAAM,oCAAoC,CAAC;AACpF,OAAO,EAAE,4BAA4B,EAAE,MAAM,iCAAiC,CAAC;AAC/E,OAAO,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAC3E,OAAO,EAAE,2BAA2B,EAAE,MAAM,gCAAgC,CAAC;AAC7E,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC"}
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.MissingContactsPurposeRule = exports.MissingMicrophonePurposeRule = exports.MissingPhotoLibraryPurposeRule = exports.ATTTrackingMismatchRule = exports.LocationAlwaysUnjustifiedRule = exports.MissingLocationPurposeRule = exports.MissingCameraPurposeRule = void 0;
3
+ exports.MissingFaceIdPurposeRule = exports.MissingBluetoothPurposeRule = exports.MissingContactsPurposeRule = exports.MissingMicrophonePurposeRule = exports.MissingPhotoLibraryPurposeRule = exports.ATTTrackingMismatchRule = exports.LocationAlwaysUnjustifiedRule = exports.MissingLocationPurposeRule = exports.MissingCameraPurposeRule = void 0;
4
4
  /**
5
5
  * Privacy rules exports
6
6
  */
@@ -18,4 +18,8 @@ var missing_microphone_purpose_js_1 = require("./missing-microphone-purpose.js")
18
18
  Object.defineProperty(exports, "MissingMicrophonePurposeRule", { enumerable: true, get: function () { return missing_microphone_purpose_js_1.MissingMicrophonePurposeRule; } });
19
19
  var missing_contacts_purpose_js_1 = require("./missing-contacts-purpose.js");
20
20
  Object.defineProperty(exports, "MissingContactsPurposeRule", { enumerable: true, get: function () { return missing_contacts_purpose_js_1.MissingContactsPurposeRule; } });
21
+ var missing_bluetooth_purpose_js_1 = require("./missing-bluetooth-purpose.js");
22
+ Object.defineProperty(exports, "MissingBluetoothPurposeRule", { enumerable: true, get: function () { return missing_bluetooth_purpose_js_1.MissingBluetoothPurposeRule; } });
23
+ var missing_face_id_purpose_js_1 = require("./missing-face-id-purpose.js");
24
+ Object.defineProperty(exports, "MissingFaceIdPurposeRule", { enumerable: true, get: function () { return missing_face_id_purpose_js_1.MissingFaceIdPurposeRule; } });
21
25
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/rules/privacy/index.ts"],"names":[],"mappings":";;;AAAA;;GAEG;AACH,yEAAuE;AAA9D,qIAAA,wBAAwB,OAAA;AACjC,6EAA2E;AAAlE,yIAAA,0BAA0B,OAAA;AACnC,mFAAiF;AAAxE,+IAAA,6BAA6B,OAAA;AACtC,uEAAqE;AAA5D,mIAAA,uBAAuB,OAAA;AAChC,uFAAoF;AAA3E,kJAAA,8BAA8B,OAAA;AACvC,iFAA+E;AAAtE,6IAAA,4BAA4B,OAAA;AACrC,6EAA2E;AAAlE,yIAAA,0BAA0B,OAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/rules/privacy/index.ts"],"names":[],"mappings":";;;AAAA;;GAEG;AACH,yEAAuE;AAA9D,qIAAA,wBAAwB,OAAA;AACjC,6EAA2E;AAAlE,yIAAA,0BAA0B,OAAA;AACnC,mFAAiF;AAAxE,+IAAA,6BAA6B,OAAA;AACtC,uEAAqE;AAA5D,mIAAA,uBAAuB,OAAA;AAChC,uFAAoF;AAA3E,kJAAA,8BAA8B,OAAA;AACvC,iFAA+E;AAAtE,6IAAA,4BAA4B,OAAA;AACrC,6EAA2E;AAAlE,yIAAA,0BAA0B,OAAA;AACnC,+EAA6E;AAApE,2IAAA,2BAA2B,OAAA;AACpC,2EAAwE;AAA/D,sIAAA,wBAAwB,OAAA"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Rule: Missing Bluetooth Usage Description
3
+ *
4
+ * Detects when an app uses CoreBluetooth but is missing
5
+ * the required NSBluetoothAlwaysUsageDescription in Info.plist.
6
+ *
7
+ * App Store Review Guideline: 5.1.1
8
+ */
9
+ import type { Rule } from '../../types/index.js';
10
+ export declare const MissingBluetoothPurposeRule: Rule;
11
+ //# sourceMappingURL=missing-bluetooth-purpose.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"missing-bluetooth-purpose.d.ts","sourceRoot":"","sources":["../../../src/rules/privacy/missing-bluetooth-purpose.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,IAAI,EAAwB,MAAM,sBAAsB,CAAC;AAOvE,eAAO,MAAM,2BAA2B,EAAE,IA8EzC,CAAC"}
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MissingBluetoothPurposeRule = void 0;
4
+ const index_js_1 = require("../../types/index.js");
5
+ const plist_parser_js_1 = require("../../parsers/plist-parser.js");
6
+ const base_js_1 = require("../base.js");
7
+ const BLUETOOTH_FRAMEWORKS = ['CoreBluetooth'];
8
+ exports.MissingBluetoothPurposeRule = {
9
+ id: 'privacy-008-missing-bluetooth-purpose',
10
+ name: 'Missing Bluetooth Usage Description',
11
+ description: 'Checks for Bluetooth framework usage without NSBluetoothAlwaysUsageDescription',
12
+ category: index_js_1.RuleCategory.Privacy,
13
+ severity: index_js_1.Severity.Critical,
14
+ confidence: index_js_1.Confidence.High,
15
+ guidelineReference: '5.1.1',
16
+ async evaluate(context) {
17
+ const detectedFrameworks = BLUETOOTH_FRAMEWORKS.filter(f => context.hasFramework(f));
18
+ if (detectedFrameworks.length === 0) {
19
+ return [];
20
+ }
21
+ const bluetoothDescription = context.plistString('NSBluetoothAlwaysUsageDescription');
22
+ // Case 1: Completely missing
23
+ if (bluetoothDescription === undefined) {
24
+ return [
25
+ (0, base_js_1.makeFinding)(this, {
26
+ description: `Your app links against Bluetooth frameworks (${detectedFrameworks.join(', ')}) ` +
27
+ `but Info.plist is missing NSBluetoothAlwaysUsageDescription. Apps that access Bluetooth ` +
28
+ `must provide a purpose string explaining why access is needed.`,
29
+ location: 'Info.plist',
30
+ fixGuidance: `Add NSBluetoothAlwaysUsageDescription to your Info.plist with a clear, user-facing ` +
31
+ `explanation of why your app needs Bluetooth access. For example:
32
+
33
+ <key>NSBluetoothAlwaysUsageDescription</key>
34
+ <string>We use Bluetooth to connect to your fitness tracker and sync workout data.</string>
35
+
36
+ The description should explain the specific feature that uses Bluetooth and ` +
37
+ `be written from the user's perspective.`,
38
+ documentationURL: 'https://developer.apple.com/documentation/bundleresources/information_property_list/nsbluetoothalwaysusagedescription',
39
+ }),
40
+ ];
41
+ }
42
+ // Case 2: Empty or whitespace only
43
+ if (bluetoothDescription.trim() === '') {
44
+ return [
45
+ (0, base_js_1.makeFinding)(this, {
46
+ title: 'Empty Bluetooth Usage Description',
47
+ description: `NSBluetoothAlwaysUsageDescription exists in Info.plist but is empty. ` +
48
+ `Apple requires a meaningful description explaining why your app needs Bluetooth access.`,
49
+ location: 'Info.plist',
50
+ fixGuidance: `Update NSBluetoothAlwaysUsageDescription with a clear, specific explanation of why ` +
51
+ `your app needs Bluetooth access. Generic or empty descriptions may be rejected.
52
+
53
+ Good example: "We use Bluetooth to connect to your heart rate monitor."
54
+ Bad example: "Bluetooth access required" or ""`,
55
+ documentationURL: 'https://developer.apple.com/documentation/bundleresources/information_property_list/nsbluetoothalwaysusagedescription',
56
+ }),
57
+ ];
58
+ }
59
+ // Case 3: Placeholder text detected
60
+ if ((0, plist_parser_js_1.isPlaceholder)(bluetoothDescription)) {
61
+ return [
62
+ (0, base_js_1.makeFinding)(this, {
63
+ title: 'Placeholder Bluetooth Usage Description',
64
+ description: `NSBluetoothAlwaysUsageDescription appears to contain placeholder text: "${bluetoothDescription}". ` +
65
+ `Apple requires meaningful, user-facing descriptions.`,
66
+ location: 'Info.plist',
67
+ fixGuidance: `Replace the placeholder text with a clear explanation of why your app needs Bluetooth access. ` +
68
+ `The description should be specific to your app's features.
69
+
70
+ Current value: "${bluetoothDescription}"
71
+
72
+ Write a description that helps users understand what feature uses Bluetooth and why.`,
73
+ documentationURL: 'https://developer.apple.com/documentation/bundleresources/information_property_list/nsbluetoothalwaysusagedescription',
74
+ }),
75
+ ];
76
+ }
77
+ return [];
78
+ },
79
+ };
80
+ //# sourceMappingURL=missing-bluetooth-purpose.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"missing-bluetooth-purpose.js","sourceRoot":"","sources":["../../../src/rules/privacy/missing-bluetooth-purpose.ts"],"names":[],"mappings":";;;AASA,mDAA0E;AAC1E,mEAA8D;AAC9D,wCAAyC;AAEzC,MAAM,oBAAoB,GAAG,CAAC,eAAe,CAAC,CAAC;AAElC,QAAA,2BAA2B,GAAS;IAC/C,EAAE,EAAE,uCAAuC;IAC3C,IAAI,EAAE,qCAAqC;IAC3C,WAAW,EAAE,gFAAgF;IAC7F,QAAQ,EAAE,uBAAY,CAAC,OAAO;IAC9B,QAAQ,EAAE,mBAAQ,CAAC,QAAQ;IAC3B,UAAU,EAAE,qBAAU,CAAC,IAAI;IAC3B,kBAAkB,EAAE,OAAO;IAE3B,KAAK,CAAC,QAAQ,CAAC,OAAoB;QACjC,MAAM,kBAAkB,GAAG,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;QAErF,IAAI,kBAAkB,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACpC,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,MAAM,oBAAoB,GAAG,OAAO,CAAC,WAAW,CAAC,mCAAmC,CAAC,CAAC;QAEtF,6BAA6B;QAC7B,IAAI,oBAAoB,KAAK,SAAS,EAAE,CAAC;YACvC,OAAO;gBACL,IAAA,qBAAW,EAAC,IAAI,EAAE;oBAChB,WAAW,EAAE,gDAAgD,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI;wBAC5F,0FAA0F;wBAC1F,gEAAgE;oBAClE,QAAQ,EAAE,YAAY;oBACtB,WAAW,EAAE,qFAAqF;wBAChG;;;;;6EAKiE;wBACjE,yCAAyC;oBAC3C,gBAAgB,EAAE,uHAAuH;iBAC1I,CAAC;aACH,CAAC;QACJ,CAAC;QAED,mCAAmC;QACnC,IAAI,oBAAoB,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;YACvC,OAAO;gBACL,IAAA,qBAAW,EAAC,IAAI,EAAE;oBAChB,KAAK,EAAE,mCAAmC;oBAC1C,WAAW,EAAE,uEAAuE;wBAClF,yFAAyF;oBAC3F,QAAQ,EAAE,YAAY;oBACtB,WAAW,EAAE,qFAAqF;wBAChG;;;+CAGmC;oBACrC,gBAAgB,EAAE,uHAAuH;iBAC1I,CAAC;aACH,CAAC;QACJ,CAAC;QAED,oCAAoC;QACpC,IAAI,IAAA,+BAAa,EAAC,oBAAoB,CAAC,EAAE,CAAC;YACxC,OAAO;gBACL,IAAA,qBAAW,EAAC,IAAI,EAAE;oBAChB,KAAK,EAAE,yCAAyC;oBAChD,WAAW,EAAE,2EAA2E,oBAAoB,KAAK;wBAC/G,sDAAsD;oBACxD,QAAQ,EAAE,YAAY;oBACtB,WAAW,EAAE,gGAAgG;wBAC3G;;kBAEM,oBAAoB;;qFAE+C;oBAC3E,gBAAgB,EAAE,uHAAuH;iBAC1I,CAAC;aACH,CAAC;QACJ,CAAC;QAED,OAAO,EAAE,CAAC;IACZ,CAAC;CACF,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"missing-camera-purpose.d.ts","sourceRoot":"","sources":["../../../src/rules/privacy/missing-camera-purpose.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,IAAI,EAAwB,MAAM,sBAAsB,CAAC;AAOvE,eAAO,MAAM,wBAAwB,EAAE,IAiFtC,CAAC"}
1
+ {"version":3,"file":"missing-camera-purpose.d.ts","sourceRoot":"","sources":["../../../src/rules/privacy/missing-camera-purpose.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,IAAI,EAAwB,MAAM,sBAAsB,CAAC;AAWvE,eAAO,MAAM,wBAAwB,EAAE,IAiFtC,CAAC"}
@@ -4,7 +4,11 @@ exports.MissingCameraPurposeRule = void 0;
4
4
  const index_js_1 = require("../../types/index.js");
5
5
  const plist_parser_js_1 = require("../../parsers/plist-parser.js");
6
6
  const base_js_1 = require("../base.js");
7
- const CAMERA_FRAMEWORKS = ['AVFoundation', 'AVKit', 'VisionKit'];
7
+ // Note: VisionKit is NOT included here because it's often used for ImageAnalyzer
8
+ // (Live Text on existing images) which doesn't require camera access. Only
9
+ // DataScannerViewController and VNDocumentCameraViewController in VisionKit
10
+ // require camera permission, and detecting those requires deeper source analysis.
11
+ const CAMERA_FRAMEWORKS = ['AVFoundation', 'AVKit'];
8
12
  exports.MissingCameraPurposeRule = {
9
13
  id: 'privacy-001-missing-camera-purpose',
10
14
  name: 'Missing Camera Usage Description',
@@ -1 +1 @@
1
- {"version":3,"file":"missing-camera-purpose.js","sourceRoot":"","sources":["../../../src/rules/privacy/missing-camera-purpose.ts"],"names":[],"mappings":";;;AASA,mDAA0E;AAC1E,mEAA8D;AAC9D,wCAAyC;AAEzC,MAAM,iBAAiB,GAAG,CAAC,cAAc,EAAE,OAAO,EAAE,WAAW,CAAC,CAAC;AAEpD,QAAA,wBAAwB,GAAS;IAC5C,EAAE,EAAE,oCAAoC;IACxC,IAAI,EAAE,kCAAkC;IACxC,WAAW,EAAE,oEAAoE;IACjF,QAAQ,EAAE,uBAAY,CAAC,OAAO;IAC9B,QAAQ,EAAE,mBAAQ,CAAC,QAAQ;IAC3B,UAAU,EAAE,qBAAU,CAAC,IAAI;IAC3B,kBAAkB,EAAE,OAAO;IAE3B,KAAK,CAAC,QAAQ,CAAC,OAAoB;QACjC,kDAAkD;QAClD,MAAM,kBAAkB,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;QAElF,IAAI,kBAAkB,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACpC,mDAAmD;YACnD,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,MAAM,iBAAiB,GAAG,OAAO,CAAC,WAAW,CAAC,0BAA0B,CAAC,CAAC;QAE1E,6BAA6B;QAC7B,IAAI,iBAAiB,KAAK,SAAS,EAAE,CAAC;YACpC,OAAO;gBACL,IAAA,qBAAW,EAAC,IAAI,EAAE;oBAChB,WAAW,EAAE,qDAAqD,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI;wBACjG,uFAAuF;wBACvF,2DAA2D;oBAC7D,QAAQ,EAAE,YAAY;oBACtB,WAAW,EAAE,wFAAwF;wBACnG;;;;;8EAKkE;wBAClE,yCAAyC;oBAC3C,gBAAgB,EAAE,8GAA8G;iBACjI,CAAC;aACH,CAAC;QACJ,CAAC;QAED,mCAAmC;QACnC,IAAI,iBAAiB,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;YACpC,OAAO;gBACL,IAAA,qBAAW,EAAC,IAAI,EAAE;oBAChB,KAAK,EAAE,gCAAgC;oBACvC,WAAW,EAAE,8DAA8D;wBACzE,sFAAsF;oBACxF,QAAQ,EAAE,YAAY;oBACtB,WAAW,EAAE,qFAAqF;wBAChG;;;4CAGgC;oBAClC,gBAAgB,EAAE,8GAA8G;iBACjI,CAAC;aACH,CAAC;QACJ,CAAC;QAED,oCAAoC;QACpC,IAAI,IAAA,+BAAa,EAAC,iBAAiB,CAAC,EAAE,CAAC;YACrC,OAAO;gBACL,IAAA,qBAAW,EAAC,IAAI,EAAE;oBAChB,KAAK,EAAE,sCAAsC;oBAC7C,WAAW,EAAE,kEAAkE,iBAAiB,KAAK;wBACnG,sDAAsD;oBACxD,QAAQ,EAAE,YAAY;oBACtB,WAAW,EAAE,6FAA6F;wBACxG;;kBAEM,iBAAiB;;sFAEmD;oBAC5E,gBAAgB,EAAE,8GAA8G;iBACjI,CAAC;aACH,CAAC;QACJ,CAAC;QAED,oBAAoB;QACpB,OAAO,EAAE,CAAC;IACZ,CAAC;CACF,CAAC"}
1
+ {"version":3,"file":"missing-camera-purpose.js","sourceRoot":"","sources":["../../../src/rules/privacy/missing-camera-purpose.ts"],"names":[],"mappings":";;;AASA,mDAA0E;AAC1E,mEAA8D;AAC9D,wCAAyC;AAEzC,iFAAiF;AACjF,2EAA2E;AAC3E,4EAA4E;AAC5E,kFAAkF;AAClF,MAAM,iBAAiB,GAAG,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC;AAEvC,QAAA,wBAAwB,GAAS;IAC5C,EAAE,EAAE,oCAAoC;IACxC,IAAI,EAAE,kCAAkC;IACxC,WAAW,EAAE,oEAAoE;IACjF,QAAQ,EAAE,uBAAY,CAAC,OAAO;IAC9B,QAAQ,EAAE,mBAAQ,CAAC,QAAQ;IAC3B,UAAU,EAAE,qBAAU,CAAC,IAAI;IAC3B,kBAAkB,EAAE,OAAO;IAE3B,KAAK,CAAC,QAAQ,CAAC,OAAoB;QACjC,kDAAkD;QAClD,MAAM,kBAAkB,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;QAElF,IAAI,kBAAkB,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACpC,mDAAmD;YACnD,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,MAAM,iBAAiB,GAAG,OAAO,CAAC,WAAW,CAAC,0BAA0B,CAAC,CAAC;QAE1E,6BAA6B;QAC7B,IAAI,iBAAiB,KAAK,SAAS,EAAE,CAAC;YACpC,OAAO;gBACL,IAAA,qBAAW,EAAC,IAAI,EAAE;oBAChB,WAAW,EAAE,qDAAqD,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI;wBACjG,uFAAuF;wBACvF,2DAA2D;oBAC7D,QAAQ,EAAE,YAAY;oBACtB,WAAW,EAAE,wFAAwF;wBACnG;;;;;8EAKkE;wBAClE,yCAAyC;oBAC3C,gBAAgB,EAAE,8GAA8G;iBACjI,CAAC;aACH,CAAC;QACJ,CAAC;QAED,mCAAmC;QACnC,IAAI,iBAAiB,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;YACpC,OAAO;gBACL,IAAA,qBAAW,EAAC,IAAI,EAAE;oBAChB,KAAK,EAAE,gCAAgC;oBACvC,WAAW,EAAE,8DAA8D;wBACzE,sFAAsF;oBACxF,QAAQ,EAAE,YAAY;oBACtB,WAAW,EAAE,qFAAqF;wBAChG;;;4CAGgC;oBAClC,gBAAgB,EAAE,8GAA8G;iBACjI,CAAC;aACH,CAAC;QACJ,CAAC;QAED,oCAAoC;QACpC,IAAI,IAAA,+BAAa,EAAC,iBAAiB,CAAC,EAAE,CAAC;YACrC,OAAO;gBACL,IAAA,qBAAW,EAAC,IAAI,EAAE;oBAChB,KAAK,EAAE,sCAAsC;oBAC7C,WAAW,EAAE,kEAAkE,iBAAiB,KAAK;wBACnG,sDAAsD;oBACxD,QAAQ,EAAE,YAAY;oBACtB,WAAW,EAAE,6FAA6F;wBACxG;;kBAEM,iBAAiB;;sFAEmD;oBAC5E,gBAAgB,EAAE,8GAA8G;iBACjI,CAAC;aACH,CAAC;QACJ,CAAC;QAED,oBAAoB;QACpB,OAAO,EAAE,CAAC;IACZ,CAAC;CACF,CAAC"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Rule: Missing Face ID Usage Description
3
+ *
4
+ * Detects when an app uses LocalAuthentication but is missing
5
+ * the required NSFaceIDUsageDescription in Info.plist.
6
+ *
7
+ * App Store Review Guideline: 5.1.1
8
+ */
9
+ import type { Rule } from '../../types/index.js';
10
+ export declare const MissingFaceIdPurposeRule: Rule;
11
+ //# sourceMappingURL=missing-face-id-purpose.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"missing-face-id-purpose.d.ts","sourceRoot":"","sources":["../../../src/rules/privacy/missing-face-id-purpose.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,IAAI,EAAwB,MAAM,sBAAsB,CAAC;AAOvE,eAAO,MAAM,wBAAwB,EAAE,IA8EtC,CAAC"}
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MissingFaceIdPurposeRule = void 0;
4
+ const index_js_1 = require("../../types/index.js");
5
+ const plist_parser_js_1 = require("../../parsers/plist-parser.js");
6
+ const base_js_1 = require("../base.js");
7
+ const FACE_ID_FRAMEWORKS = ['LocalAuthentication'];
8
+ exports.MissingFaceIdPurposeRule = {
9
+ id: 'privacy-009-missing-face-id-purpose',
10
+ name: 'Missing Face ID Usage Description',
11
+ description: 'Checks for LocalAuthentication framework usage without NSFaceIDUsageDescription',
12
+ category: index_js_1.RuleCategory.Privacy,
13
+ severity: index_js_1.Severity.Critical,
14
+ confidence: index_js_1.Confidence.High,
15
+ guidelineReference: '5.1.1',
16
+ async evaluate(context) {
17
+ const detectedFrameworks = FACE_ID_FRAMEWORKS.filter(f => context.hasFramework(f));
18
+ if (detectedFrameworks.length === 0) {
19
+ return [];
20
+ }
21
+ const faceIdDescription = context.plistString('NSFaceIDUsageDescription');
22
+ // Case 1: Completely missing
23
+ if (faceIdDescription === undefined) {
24
+ return [
25
+ (0, base_js_1.makeFinding)(this, {
26
+ description: `Your app links against biometric authentication frameworks (${detectedFrameworks.join(', ')}) ` +
27
+ `but Info.plist is missing NSFaceIDUsageDescription. Apps that use Face ID ` +
28
+ `must provide a purpose string explaining why biometric authentication is needed.`,
29
+ location: 'Info.plist',
30
+ fixGuidance: `Add NSFaceIDUsageDescription to your Info.plist with a clear, user-facing ` +
31
+ `explanation of why your app needs Face ID access. For example:
32
+
33
+ <key>NSFaceIDUsageDescription</key>
34
+ <string>We use Face ID to securely authenticate you for quick access to your account.</string>
35
+
36
+ The description should explain what feature uses Face ID and ` +
37
+ `be written from the user's perspective.`,
38
+ documentationURL: 'https://developer.apple.com/documentation/bundleresources/information_property_list/nsfaceidusagedescription',
39
+ }),
40
+ ];
41
+ }
42
+ // Case 2: Empty or whitespace only
43
+ if (faceIdDescription.trim() === '') {
44
+ return [
45
+ (0, base_js_1.makeFinding)(this, {
46
+ title: 'Empty Face ID Usage Description',
47
+ description: `NSFaceIDUsageDescription exists in Info.plist but is empty. ` +
48
+ `Apple requires a meaningful description explaining why your app needs Face ID access.`,
49
+ location: 'Info.plist',
50
+ fixGuidance: `Update NSFaceIDUsageDescription with a clear, specific explanation of why ` +
51
+ `your app needs Face ID access. Generic or empty descriptions may be rejected.
52
+
53
+ Good example: "We use Face ID to securely log you in without a password."
54
+ Bad example: "Face ID access required" or ""`,
55
+ documentationURL: 'https://developer.apple.com/documentation/bundleresources/information_property_list/nsfaceidusagedescription',
56
+ }),
57
+ ];
58
+ }
59
+ // Case 3: Placeholder text detected
60
+ if ((0, plist_parser_js_1.isPlaceholder)(faceIdDescription)) {
61
+ return [
62
+ (0, base_js_1.makeFinding)(this, {
63
+ title: 'Placeholder Face ID Usage Description',
64
+ description: `NSFaceIDUsageDescription appears to contain placeholder text: "${faceIdDescription}". ` +
65
+ `Apple requires meaningful, user-facing descriptions.`,
66
+ location: 'Info.plist',
67
+ fixGuidance: `Replace the placeholder text with a clear explanation of why your app needs Face ID access. ` +
68
+ `The description should be specific to your app's features.
69
+
70
+ Current value: "${faceIdDescription}"
71
+
72
+ Write a description that helps users understand what feature uses Face ID and why.`,
73
+ documentationURL: 'https://developer.apple.com/documentation/bundleresources/information_property_list/nsfaceidusagedescription',
74
+ }),
75
+ ];
76
+ }
77
+ return [];
78
+ },
79
+ };
80
+ //# sourceMappingURL=missing-face-id-purpose.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"missing-face-id-purpose.js","sourceRoot":"","sources":["../../../src/rules/privacy/missing-face-id-purpose.ts"],"names":[],"mappings":";;;AASA,mDAA0E;AAC1E,mEAA8D;AAC9D,wCAAyC;AAEzC,MAAM,kBAAkB,GAAG,CAAC,qBAAqB,CAAC,CAAC;AAEtC,QAAA,wBAAwB,GAAS;IAC5C,EAAE,EAAE,qCAAqC;IACzC,IAAI,EAAE,mCAAmC;IACzC,WAAW,EAAE,iFAAiF;IAC9F,QAAQ,EAAE,uBAAY,CAAC,OAAO;IAC9B,QAAQ,EAAE,mBAAQ,CAAC,QAAQ;IAC3B,UAAU,EAAE,qBAAU,CAAC,IAAI;IAC3B,kBAAkB,EAAE,OAAO;IAE3B,KAAK,CAAC,QAAQ,CAAC,OAAoB;QACjC,MAAM,kBAAkB,GAAG,kBAAkB,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;QAEnF,IAAI,kBAAkB,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACpC,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,MAAM,iBAAiB,GAAG,OAAO,CAAC,WAAW,CAAC,0BAA0B,CAAC,CAAC;QAE1E,6BAA6B;QAC7B,IAAI,iBAAiB,KAAK,SAAS,EAAE,CAAC;YACpC,OAAO;gBACL,IAAA,qBAAW,EAAC,IAAI,EAAE;oBAChB,WAAW,EAAE,+DAA+D,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI;wBAC3G,4EAA4E;wBAC5E,kFAAkF;oBACpF,QAAQ,EAAE,YAAY;oBACtB,WAAW,EAAE,4EAA4E;wBACvF;;;;;8DAKkD;wBAClD,yCAAyC;oBAC3C,gBAAgB,EAAE,8GAA8G;iBACjI,CAAC;aACH,CAAC;QACJ,CAAC;QAED,mCAAmC;QACnC,IAAI,iBAAiB,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;YACpC,OAAO;gBACL,IAAA,qBAAW,EAAC,IAAI,EAAE;oBAChB,KAAK,EAAE,iCAAiC;oBACxC,WAAW,EAAE,8DAA8D;wBACzE,uFAAuF;oBACzF,QAAQ,EAAE,YAAY;oBACtB,WAAW,EAAE,4EAA4E;wBACvF;;;6CAGiC;oBACnC,gBAAgB,EAAE,8GAA8G;iBACjI,CAAC;aACH,CAAC;QACJ,CAAC;QAED,oCAAoC;QACpC,IAAI,IAAA,+BAAa,EAAC,iBAAiB,CAAC,EAAE,CAAC;YACrC,OAAO;gBACL,IAAA,qBAAW,EAAC,IAAI,EAAE;oBAChB,KAAK,EAAE,uCAAuC;oBAC9C,WAAW,EAAE,kEAAkE,iBAAiB,KAAK;wBACnG,sDAAsD;oBACxD,QAAQ,EAAE,YAAY;oBACtB,WAAW,EAAE,8FAA8F;wBACzG;;kBAEM,iBAAiB;;mFAEgD;oBACzE,gBAAgB,EAAE,8GAA8G;iBACjI,CAAC;aACH,CAAC;QACJ,CAAC;QAED,OAAO,EAAE,CAAC;IACZ,CAAC;CACF,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shiplint",
3
- "version": "0.1.1",
3
+ "version": "1.0.0",
4
4
  "description": "Catch App Store rejections before they happen. Pre-submission linter for iOS apps.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {