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.
- package/README.md +267 -62
- package/dist/cli/index.js +0 -0
- package/dist/rules/config/index.d.ts +2 -0
- package/dist/rules/config/index.d.ts.map +1 -1
- package/dist/rules/config/index.js +5 -1
- package/dist/rules/config/index.js.map +1 -1
- package/dist/rules/config/missing-encryption-flag.d.ts +12 -0
- package/dist/rules/config/missing-encryption-flag.d.ts.map +1 -0
- package/dist/rules/config/missing-encryption-flag.js +40 -0
- package/dist/rules/config/missing-encryption-flag.js.map +1 -0
- package/dist/rules/config/missing-launch-storyboard.d.ts +12 -0
- package/dist/rules/config/missing-launch-storyboard.d.ts.map +1 -0
- package/dist/rules/config/missing-launch-storyboard.js +42 -0
- package/dist/rules/config/missing-launch-storyboard.js.map +1 -0
- package/dist/rules/index.d.ts.map +1 -1
- package/dist/rules/index.js +10 -0
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/metadata/index.d.ts +1 -0
- package/dist/rules/metadata/index.d.ts.map +1 -1
- package/dist/rules/metadata/index.js +3 -1
- package/dist/rules/metadata/index.js.map +1 -1
- package/dist/rules/metadata/missing-supported-orientations.d.ts +12 -0
- package/dist/rules/metadata/missing-supported-orientations.d.ts.map +1 -0
- package/dist/rules/metadata/missing-supported-orientations.js +68 -0
- package/dist/rules/metadata/missing-supported-orientations.js.map +1 -0
- package/dist/rules/privacy/index.d.ts +2 -0
- package/dist/rules/privacy/index.d.ts.map +1 -1
- package/dist/rules/privacy/index.js +5 -1
- package/dist/rules/privacy/index.js.map +1 -1
- package/dist/rules/privacy/missing-bluetooth-purpose.d.ts +11 -0
- package/dist/rules/privacy/missing-bluetooth-purpose.d.ts.map +1 -0
- package/dist/rules/privacy/missing-bluetooth-purpose.js +80 -0
- package/dist/rules/privacy/missing-bluetooth-purpose.js.map +1 -0
- package/dist/rules/privacy/missing-camera-purpose.d.ts.map +1 -1
- package/dist/rules/privacy/missing-camera-purpose.js +5 -1
- package/dist/rules/privacy/missing-camera-purpose.js.map +1 -1
- package/dist/rules/privacy/missing-face-id-purpose.d.ts +11 -0
- package/dist/rules/privacy/missing-face-id-purpose.d.ts.map +1 -0
- package/dist/rules/privacy/missing-face-id-purpose.js +80 -0
- package/dist/rules/privacy/missing-face-id-purpose.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,107 +1,312 @@
|
|
|
1
1
|
# ShipLint
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/shiplint)
|
|
6
|
+
[](https://github.com/Signal26AI/ShipLint/blob/main/LICENSE)
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
---
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
npm install -g shiplint
|
|
11
|
-
```
|
|
10
|
+
## The Problem
|
|
12
11
|
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40
|
+
**Global install** for regular use:
|
|
29
41
|
|
|
30
|
-
|
|
42
|
+
```bash
|
|
43
|
+
npm install -g shiplint
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Then scan any iOS project directory or `.xcodeproj`:
|
|
31
47
|
|
|
32
48
|
```bash
|
|
33
|
-
|
|
49
|
+
shiplint scan ./MyApp.xcodeproj
|
|
50
|
+
```
|
|
34
51
|
|
|
52
|
+
### Example Output
|
|
53
|
+
|
|
54
|
+
```
|
|
35
55
|
🛡️ ShipLint Scan Results
|
|
36
56
|
|
|
37
|
-
🔍 Found
|
|
57
|
+
🔍 Found 3 issue(s):
|
|
38
58
|
|
|
39
|
-
1. [CRITICAL]
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
70
|
-
shiplint scan ./MyApp
|
|
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
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
257
|
+
### Is ShipLint open source?
|
|
80
258
|
|
|
81
|
-
|
|
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
|
-
|
|
91
|
-
- `third-party-login-no-siwa` - Third-party login without Sign in with Apple
|
|
261
|
+
---
|
|
92
262
|
|
|
93
|
-
|
|
94
|
-
- `ats-exception-without-justification` - App Transport Security exceptions
|
|
263
|
+
## Comparison: iOS Submission Checking Tools
|
|
95
264
|
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
|
|
282
|
+
¹ *Xcode shows warnings at build time for some missing keys but does not block the archive/upload process.*
|
|
102
283
|
|
|
103
|
-
|
|
284
|
+
**The ideal pipeline:** ShipLint (project files) → Xcode Validate (binary) → Fastlane Precheck (metadata) → Submit.
|
|
104
285
|
|
|
105
286
|
---
|
|
106
287
|
|
|
107
|
-
|
|
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;
|
|
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"}
|
package/dist/rules/index.js
CHANGED
|
@@ -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
|
package/dist/rules/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/rules/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;
|
|
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"}
|
|
@@ -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;
|
|
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
|
-
|
|
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,
|
|
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"}
|