store-preflight-mcp 0.1.0__tar.gz
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.
- store_preflight_mcp-0.1.0/.gitignore +10 -0
- store_preflight_mcp-0.1.0/LICENSE +21 -0
- store_preflight_mcp-0.1.0/PKG-INFO +275 -0
- store_preflight_mcp-0.1.0/README.md +252 -0
- store_preflight_mcp-0.1.0/RULES.md +242 -0
- store_preflight_mcp-0.1.0/pyproject.toml +53 -0
- store_preflight_mcp-0.1.0/store_preflight_mcp/__init__.py +3 -0
- store_preflight_mcp-0.1.0/store_preflight_mcp/cli.py +141 -0
- store_preflight_mcp-0.1.0/store_preflight_mcp/engine.py +440 -0
- store_preflight_mcp-0.1.0/store_preflight_mcp/models.py +261 -0
- store_preflight_mcp-0.1.0/store_preflight_mcp/rules/__init__.py +94 -0
- store_preflight_mcp-0.1.0/store_preflight_mcp/rules/apple.py +742 -0
- store_preflight_mcp-0.1.0/store_preflight_mcp/rules/play.py +584 -0
- store_preflight_mcp-0.1.0/store_preflight_mcp/scan/__init__.py +4 -0
- store_preflight_mcp-0.1.0/store_preflight_mcp/scan/manifests.py +153 -0
- store_preflight_mcp-0.1.0/store_preflight_mcp/scan/project.py +144 -0
- store_preflight_mcp-0.1.0/store_preflight_mcp/scan/source.py +88 -0
- store_preflight_mcp-0.1.0/store_preflight_mcp/server.py +342 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Asif
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: store-preflight-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: App Store / Play rejection preflight for agents โ maps API usage to the declaration it obliges, before the binary is rejected
|
|
5
|
+
Project-URL: Homepage, https://github.com/asif786ka/store-preflight-mcp
|
|
6
|
+
Project-URL: Repository, https://github.com/asif786ka/store-preflight-mcp
|
|
7
|
+
Project-URL: Issues, https://github.com/asif786ka/store-preflight-mcp/issues
|
|
8
|
+
Author: Asif
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: android,app-store,compliance,flutter,google-play,ios,mcp,privacy-manifest,react-native
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: mcp>=1.2.0
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# store-preflight-mcp
|
|
25
|
+
|
|
26
|
+
**Your agent just wrote code that will get your app rejected. It has no idea.**
|
|
27
|
+
|
|
28
|
+
An MCP server that maps *API usage โ required declaration โ dated policy*, so a
|
|
29
|
+
coding agent finds out at write time instead of from an App Store Connect email
|
|
30
|
+
three days after the PR merged.
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
$ store-preflight scan . --date 2026-07-15
|
|
34
|
+
|
|
35
|
+
๐ UserDefaults requires a declared reason in PrivacyInfo.xcprivacy
|
|
36
|
+
ITMS-91053
|
|
37
|
+
No PrivacyInfo.xcprivacy exists anywhere in this project. Code touches
|
|
38
|
+
UserDefaults APIs, so App Store Connect will refuse the upload.
|
|
39
|
+
โ ios/RejectedApp/AnalyticsModule.swift:7 private let defaults = UserDefaults.standard
|
|
40
|
+
fix: PrivacyInfo.xcprivacy โ NSPrivacyAccessedAPICategoryUserDefaults
|
|
41
|
+
|
|
42
|
+
โ failing: 7 finding(s) at or above blocker
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
That's the whole product. An agent adds an analytics module, touches
|
|
46
|
+
`UserDefaults`, and nothing in the world tells it that Apple has required a
|
|
47
|
+
`CA92.1` entry in a file it has never heard of since **1 May 2024**. The binary
|
|
48
|
+
is rejected days later, by which point nobody remembers which PR did it.
|
|
49
|
+
|
|
50
|
+
Doc MCPs describe APIs. None of them map an API to the declaration it obliges, or
|
|
51
|
+
to the date that obligation acquired teeth.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Why dates are the whole design
|
|
56
|
+
|
|
57
|
+
This is the sibling of [mobile-docs-mcp](https://github.com/asif786ka/mobile-docs-mcp),
|
|
58
|
+
and it reuses its core insight. There, a symbol isn't "real" โ it's real *within a
|
|
59
|
+
version range* (`since` / `deprecated` / `removed`). Here, a rule isn't "true" โ
|
|
60
|
+
it's true *within a date window*:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
PolicyWindow(
|
|
64
|
+
announced = "2023-06-05", # WWDC23
|
|
65
|
+
warning_from = "2024-03-13", # email only, upload still succeeds
|
|
66
|
+
enforced_from = "2024-05-01", # hard block
|
|
67
|
+
)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`PolicyWindow.status_on(date)` is the direct analogue of
|
|
71
|
+
`VersionRange.status_at(version)`. Same model, different axis โ because store
|
|
72
|
+
policy ships on deadlines, not releases.
|
|
73
|
+
|
|
74
|
+
This isn't decoration. It's what makes the tool usable:
|
|
75
|
+
|
|
76
|
+
- **Severity follows the calendar.** A blocker that doesn't bite until October is
|
|
77
|
+
reported as a *warning* in July, automatically. Nobody hand-edits severities as
|
|
78
|
+
deadlines pass.
|
|
79
|
+
- **You can scan the future.** `--date 2026-10-28` tells you what breaks when a
|
|
80
|
+
deadline lands, before it lands.
|
|
81
|
+
- **The tests don't rot.** Every assertion is pinned to a fixed date, so the suite
|
|
82
|
+
doesn't silently change verdict when a deadline passes.
|
|
83
|
+
|
|
84
|
+
The same unchanged repo, scanned on two dates:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
$ store-preflight scan . --date 2026-07-15
|
|
88
|
+
โน๏ธ Geofencing is no longer an approved use case for a location foreground service
|
|
89
|
+
โ ๏ธ READ_CONTACTS requires a declaration for apps targeting Android 17+
|
|
90
|
+
|
|
91
|
+
$ store-preflight scan . --date 2026-10-28
|
|
92
|
+
โ ๏ธ Geofencing is no longer an approved use case for a location foreground service
|
|
93
|
+
๐ READ_CONTACTS requires a declaration for apps targeting Android 17+
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Not one byte of that repo changed. **That failure mode is invisible to every
|
|
97
|
+
other tool in the toolchain** โ a linter diffs your code, and your code is fine.
|
|
98
|
+
|
|
99
|
+
(Why does `READ_CONTACTS` escalate to a blocker while geofencing stops at a
|
|
100
|
+
warning? Because the permission string is either in your manifest or it isn't,
|
|
101
|
+
whereas whether *geofencing* is what justifies your location FGS depends on a
|
|
102
|
+
form you filed in Play Console, which isn't in the repo. See
|
|
103
|
+
[On being trustworthy](#on-being-trustworthy).)
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Install
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
uvx store-preflight-mcp # MCP server (stdio)
|
|
111
|
+
pipx install store-preflight-mcp # CLI too
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Claude Code / any MCP client:
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{
|
|
118
|
+
"mcpServers": {
|
|
119
|
+
"store-preflight": {
|
|
120
|
+
"command": "uvx",
|
|
121
|
+
"args": ["store-preflight-mcp"]
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Zero non-`mcp` runtime dependencies. No crawl, no API key, no network. The
|
|
128
|
+
ruleset ships as Python and the whole scan is local.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Tools
|
|
133
|
+
|
|
134
|
+
| Tool | Use |
|
|
135
|
+
|---|---|
|
|
136
|
+
| `preflight_scan` | Scan a repo. What will be rejected, where, and why. |
|
|
137
|
+
| `check_api_declaration` | **Before** writing: "I'm about to use `UserDefaults` โ what must I declare?" |
|
|
138
|
+
| `get_required_declaration` | The exact key + reason code + paste-ready snippet. |
|
|
139
|
+
| `explain_rejection` | Paste `ITMS-91053` from the email โ a concrete fix. |
|
|
140
|
+
| `list_policy_deadlines` | The dated calendar. What's enforced, what lands next. |
|
|
141
|
+
|
|
142
|
+
`check_api_declaration` is the one that changes agent behaviour โ it's cheap
|
|
143
|
+
enough to call before writing the code, which is the only time the answer is free.
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
> check_api_declaration("UserDefaults", store="apple")
|
|
147
|
+
|
|
148
|
+
๐ BLOCKER ยท status enforced โ enforced since 2024-05-01 (806 days ago)
|
|
149
|
+
|
|
150
|
+
You must declare: NSPrivacyAccessedAPICategoryUserDefaults in PrivacyInfo.xcprivacy
|
|
151
|
+
|
|
152
|
+
Valid values:
|
|
153
|
+
ยท CA92.1 โ Read/write info accessible only to the app itself...
|
|
154
|
+
ยท 1C8F.1 โ ...only to apps, extensions and App Clips in the same App Group...
|
|
155
|
+
ยท C56D.1 โ Third-party SDK only: wrapper function around UserDefaults...
|
|
156
|
+
ยท AC6B.1 โ Read com.apple.configuration.managed (MDM managed app config)...
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## CI
|
|
162
|
+
|
|
163
|
+
```yaml
|
|
164
|
+
- run: pipx install store-preflight-mcp
|
|
165
|
+
- run: store-preflight scan . --format sarif -o preflight.sarif
|
|
166
|
+
- uses: github/codeql-action/upload-sarif@v3
|
|
167
|
+
with: { sarif_file: preflight.sarif }
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
SARIF means findings render as inline PR annotations, on the line that caused
|
|
171
|
+
them. `--fail-on blocker` (the default) exits non-zero, so a rejection becomes a
|
|
172
|
+
red check instead of an email.
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Coverage
|
|
177
|
+
|
|
178
|
+
**App Store**
|
|
179
|
+
- All **5** required-reason API categories, all **17** reason codes, each with
|
|
180
|
+
Apple's verbatim meaning โ `UserDefaults`, `FileTimestamp`, `SystemBootTime`,
|
|
181
|
+
`DiskSpace`, `ActiveKeyboards`
|
|
182
|
+
- **17** `Info.plist` purpose strings, incl. the iOS 17 calendar split
|
|
183
|
+
(`NSCalendarsFullAccessUsageDescription`) most projects still miss
|
|
184
|
+
- Privacy manifest validity: ITMS-91054 / 91055 / 91056
|
|
185
|
+
- All **86** privacy-impacting SDKs + npm/pub โ framework aliasing
|
|
186
|
+
- `NSPrivacyTracking` without `NSPrivacyTrackingDomains`
|
|
187
|
+
- The Xcode 26 / iOS 26 SDK upload gate (since 2026-04-28)
|
|
188
|
+
- All **6** ITMS rejection codes decoded
|
|
189
|
+
|
|
190
|
+
**Google Play**
|
|
191
|
+
- Restricted permissions: `QUERY_ALL_PACKAGES`, `MANAGE_EXTERNAL_STORAGE`,
|
|
192
|
+
SMS/Call Log, `ACCESS_BACKGROUND_LOCATION`, `REQUEST_INSTALL_PACKAGES`,
|
|
193
|
+
`USE_EXACT_ALARM`, `USE_FULL_SCREEN_INTENT`, accessibility, photo/video
|
|
194
|
+
- **14** data safety categories with their data types
|
|
195
|
+
- All **14** foreground service types (including `mediaProcessing`, which most
|
|
196
|
+
lists miss), each with its required permission and runtime prerequisite
|
|
197
|
+
- Advertising ID, including the auto-merged-from-`play-services-ads` trap
|
|
198
|
+
- **New for 2026:** the Contacts policy (2026-10-28) and the geofencing
|
|
199
|
+
withdrawal (2026-08-26)
|
|
200
|
+
|
|
201
|
+
**Languages:** Swift, Objective-C, Kotlin, Java, JS/TS, Dart โ plus
|
|
202
|
+
`Info.plist`, `PrivacyInfo.xcprivacy`, `AndroidManifest.xml`, `package.json`,
|
|
203
|
+
`pubspec.yaml`, `Podfile`. React Native, Flutter and native all work; no build
|
|
204
|
+
required.
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## On being trustworthy
|
|
209
|
+
|
|
210
|
+
A preflight tool that cries wolf gets `--ignore`d within a week, and then it may
|
|
211
|
+
as well not exist. Three deliberate concessions:
|
|
212
|
+
|
|
213
|
+
**Findings carry their own epistemics.** Apple's required-reason rules are
|
|
214
|
+
mechanically checkable: the symbol is in your binary or it isn't. Play's data
|
|
215
|
+
safety form is *not* โ it keys on **transmission off-device**, not on holding a
|
|
216
|
+
permission. Google says plainly that on-device processing needs no disclosure. So
|
|
217
|
+
`READ_CONTACTS` does **not** imply a Contacts disclosure, and every such mapping
|
|
218
|
+
is marked `heuristic`: it raises a question, and **a heuristic finding can never
|
|
219
|
+
fail your build** (enforced by a test).
|
|
220
|
+
|
|
221
|
+
**The target API level is not hardcoded.** When this ruleset was built
|
|
222
|
+
(2026-07-15), Google's own timeline page was internally inconsistent โ metadata
|
|
223
|
+
dated May 2026, content still describing the August 2025 / API 35 deadline โ and
|
|
224
|
+
*no API 36 announcement existed on any Google property*, despite third-party
|
|
225
|
+
blogs confidently asserting one. So the **rule** is encoded ("within one year of
|
|
226
|
+
the latest major Android release") along with `last_verified` and a
|
|
227
|
+
`needs-live-check` flag. A tool that states a fake deadline confidently is worse
|
|
228
|
+
than one that says "verify this".
|
|
229
|
+
|
|
230
|
+
**The SDK check is a prompt, not a verdict.** Apple's requirement attaches when
|
|
231
|
+
an app is new or an update *adds* a listed SDK โ not to every app that contains
|
|
232
|
+
Firebase. And "any SDKs that repackage those on the list" are included, so exact
|
|
233
|
+
matching under-detects. Both facts make a confident verdict impossible, so it
|
|
234
|
+
ships as a warning that says why.
|
|
235
|
+
|
|
236
|
+
What we do *not* concede: comments and string literals are stripped before symbol
|
|
237
|
+
matching (with line numbers preserved), `node_modules`/`Pods` are never walked,
|
|
238
|
+
and `tools:node="remove"` is honoured โ so the correct way to drop the
|
|
239
|
+
auto-merged `AD_ID` permission isn't reported as the violation.
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Verify it
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
python smoke_test.py
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
70 assertions, no network. Four fixture repos: one that gets rejected, the same
|
|
250
|
+
app fixed, a manifest that is wrong on its own terms, and one whose code never
|
|
251
|
+
changes while the policy date moves under it. The suite also pins the
|
|
252
|
+
facts that are easy to get wrong:
|
|
253
|
+
|
|
254
|
+
```
|
|
255
|
+
โ exactly 5 required-reason categories
|
|
256
|
+
โ exactly 17 reason codes
|
|
257
|
+
โ 86 SDKs on the commonly-used list
|
|
258
|
+
โ getattrlist maps to BOTH FileTimestamp and DiskSpace (not 1:1)
|
|
259
|
+
โ ITMS-91054 title is 'Invalid API category declaration'
|
|
260
|
+
โ 14 foreground service types incl. mediaProcessing
|
|
261
|
+
โ target SDK is a rule, not a hardcoded literal
|
|
262
|
+
โ no heuristic finding is ever a blocker
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Those aren't padding โ each one is a mistake that was actually made and caught
|
|
266
|
+
during the build. `getattrlist` really does oblige two separate declarations. The
|
|
267
|
+
SDK list really is 86, not 87, because Apple publishes "BoringSSL / openssl_grpc"
|
|
268
|
+
as one entry.
|
|
269
|
+
|
|
270
|
+
See [RULES.md](RULES.md) for provenance and how to re-verify against Apple's
|
|
271
|
+
JSON feed.
|
|
272
|
+
|
|
273
|
+
## Licence
|
|
274
|
+
|
|
275
|
+
MIT
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# store-preflight-mcp
|
|
2
|
+
|
|
3
|
+
**Your agent just wrote code that will get your app rejected. It has no idea.**
|
|
4
|
+
|
|
5
|
+
An MCP server that maps *API usage โ required declaration โ dated policy*, so a
|
|
6
|
+
coding agent finds out at write time instead of from an App Store Connect email
|
|
7
|
+
three days after the PR merged.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
$ store-preflight scan . --date 2026-07-15
|
|
11
|
+
|
|
12
|
+
๐ UserDefaults requires a declared reason in PrivacyInfo.xcprivacy
|
|
13
|
+
ITMS-91053
|
|
14
|
+
No PrivacyInfo.xcprivacy exists anywhere in this project. Code touches
|
|
15
|
+
UserDefaults APIs, so App Store Connect will refuse the upload.
|
|
16
|
+
โ ios/RejectedApp/AnalyticsModule.swift:7 private let defaults = UserDefaults.standard
|
|
17
|
+
fix: PrivacyInfo.xcprivacy โ NSPrivacyAccessedAPICategoryUserDefaults
|
|
18
|
+
|
|
19
|
+
โ failing: 7 finding(s) at or above blocker
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
That's the whole product. An agent adds an analytics module, touches
|
|
23
|
+
`UserDefaults`, and nothing in the world tells it that Apple has required a
|
|
24
|
+
`CA92.1` entry in a file it has never heard of since **1 May 2024**. The binary
|
|
25
|
+
is rejected days later, by which point nobody remembers which PR did it.
|
|
26
|
+
|
|
27
|
+
Doc MCPs describe APIs. None of them map an API to the declaration it obliges, or
|
|
28
|
+
to the date that obligation acquired teeth.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Why dates are the whole design
|
|
33
|
+
|
|
34
|
+
This is the sibling of [mobile-docs-mcp](https://github.com/asif786ka/mobile-docs-mcp),
|
|
35
|
+
and it reuses its core insight. There, a symbol isn't "real" โ it's real *within a
|
|
36
|
+
version range* (`since` / `deprecated` / `removed`). Here, a rule isn't "true" โ
|
|
37
|
+
it's true *within a date window*:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
PolicyWindow(
|
|
41
|
+
announced = "2023-06-05", # WWDC23
|
|
42
|
+
warning_from = "2024-03-13", # email only, upload still succeeds
|
|
43
|
+
enforced_from = "2024-05-01", # hard block
|
|
44
|
+
)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`PolicyWindow.status_on(date)` is the direct analogue of
|
|
48
|
+
`VersionRange.status_at(version)`. Same model, different axis โ because store
|
|
49
|
+
policy ships on deadlines, not releases.
|
|
50
|
+
|
|
51
|
+
This isn't decoration. It's what makes the tool usable:
|
|
52
|
+
|
|
53
|
+
- **Severity follows the calendar.** A blocker that doesn't bite until October is
|
|
54
|
+
reported as a *warning* in July, automatically. Nobody hand-edits severities as
|
|
55
|
+
deadlines pass.
|
|
56
|
+
- **You can scan the future.** `--date 2026-10-28` tells you what breaks when a
|
|
57
|
+
deadline lands, before it lands.
|
|
58
|
+
- **The tests don't rot.** Every assertion is pinned to a fixed date, so the suite
|
|
59
|
+
doesn't silently change verdict when a deadline passes.
|
|
60
|
+
|
|
61
|
+
The same unchanged repo, scanned on two dates:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
$ store-preflight scan . --date 2026-07-15
|
|
65
|
+
โน๏ธ Geofencing is no longer an approved use case for a location foreground service
|
|
66
|
+
โ ๏ธ READ_CONTACTS requires a declaration for apps targeting Android 17+
|
|
67
|
+
|
|
68
|
+
$ store-preflight scan . --date 2026-10-28
|
|
69
|
+
โ ๏ธ Geofencing is no longer an approved use case for a location foreground service
|
|
70
|
+
๐ READ_CONTACTS requires a declaration for apps targeting Android 17+
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Not one byte of that repo changed. **That failure mode is invisible to every
|
|
74
|
+
other tool in the toolchain** โ a linter diffs your code, and your code is fine.
|
|
75
|
+
|
|
76
|
+
(Why does `READ_CONTACTS` escalate to a blocker while geofencing stops at a
|
|
77
|
+
warning? Because the permission string is either in your manifest or it isn't,
|
|
78
|
+
whereas whether *geofencing* is what justifies your location FGS depends on a
|
|
79
|
+
form you filed in Play Console, which isn't in the repo. See
|
|
80
|
+
[On being trustworthy](#on-being-trustworthy).)
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Install
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
uvx store-preflight-mcp # MCP server (stdio)
|
|
88
|
+
pipx install store-preflight-mcp # CLI too
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Claude Code / any MCP client:
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"mcpServers": {
|
|
96
|
+
"store-preflight": {
|
|
97
|
+
"command": "uvx",
|
|
98
|
+
"args": ["store-preflight-mcp"]
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Zero non-`mcp` runtime dependencies. No crawl, no API key, no network. The
|
|
105
|
+
ruleset ships as Python and the whole scan is local.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Tools
|
|
110
|
+
|
|
111
|
+
| Tool | Use |
|
|
112
|
+
|---|---|
|
|
113
|
+
| `preflight_scan` | Scan a repo. What will be rejected, where, and why. |
|
|
114
|
+
| `check_api_declaration` | **Before** writing: "I'm about to use `UserDefaults` โ what must I declare?" |
|
|
115
|
+
| `get_required_declaration` | The exact key + reason code + paste-ready snippet. |
|
|
116
|
+
| `explain_rejection` | Paste `ITMS-91053` from the email โ a concrete fix. |
|
|
117
|
+
| `list_policy_deadlines` | The dated calendar. What's enforced, what lands next. |
|
|
118
|
+
|
|
119
|
+
`check_api_declaration` is the one that changes agent behaviour โ it's cheap
|
|
120
|
+
enough to call before writing the code, which is the only time the answer is free.
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
> check_api_declaration("UserDefaults", store="apple")
|
|
124
|
+
|
|
125
|
+
๐ BLOCKER ยท status enforced โ enforced since 2024-05-01 (806 days ago)
|
|
126
|
+
|
|
127
|
+
You must declare: NSPrivacyAccessedAPICategoryUserDefaults in PrivacyInfo.xcprivacy
|
|
128
|
+
|
|
129
|
+
Valid values:
|
|
130
|
+
ยท CA92.1 โ Read/write info accessible only to the app itself...
|
|
131
|
+
ยท 1C8F.1 โ ...only to apps, extensions and App Clips in the same App Group...
|
|
132
|
+
ยท C56D.1 โ Third-party SDK only: wrapper function around UserDefaults...
|
|
133
|
+
ยท AC6B.1 โ Read com.apple.configuration.managed (MDM managed app config)...
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## CI
|
|
139
|
+
|
|
140
|
+
```yaml
|
|
141
|
+
- run: pipx install store-preflight-mcp
|
|
142
|
+
- run: store-preflight scan . --format sarif -o preflight.sarif
|
|
143
|
+
- uses: github/codeql-action/upload-sarif@v3
|
|
144
|
+
with: { sarif_file: preflight.sarif }
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
SARIF means findings render as inline PR annotations, on the line that caused
|
|
148
|
+
them. `--fail-on blocker` (the default) exits non-zero, so a rejection becomes a
|
|
149
|
+
red check instead of an email.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Coverage
|
|
154
|
+
|
|
155
|
+
**App Store**
|
|
156
|
+
- All **5** required-reason API categories, all **17** reason codes, each with
|
|
157
|
+
Apple's verbatim meaning โ `UserDefaults`, `FileTimestamp`, `SystemBootTime`,
|
|
158
|
+
`DiskSpace`, `ActiveKeyboards`
|
|
159
|
+
- **17** `Info.plist` purpose strings, incl. the iOS 17 calendar split
|
|
160
|
+
(`NSCalendarsFullAccessUsageDescription`) most projects still miss
|
|
161
|
+
- Privacy manifest validity: ITMS-91054 / 91055 / 91056
|
|
162
|
+
- All **86** privacy-impacting SDKs + npm/pub โ framework aliasing
|
|
163
|
+
- `NSPrivacyTracking` without `NSPrivacyTrackingDomains`
|
|
164
|
+
- The Xcode 26 / iOS 26 SDK upload gate (since 2026-04-28)
|
|
165
|
+
- All **6** ITMS rejection codes decoded
|
|
166
|
+
|
|
167
|
+
**Google Play**
|
|
168
|
+
- Restricted permissions: `QUERY_ALL_PACKAGES`, `MANAGE_EXTERNAL_STORAGE`,
|
|
169
|
+
SMS/Call Log, `ACCESS_BACKGROUND_LOCATION`, `REQUEST_INSTALL_PACKAGES`,
|
|
170
|
+
`USE_EXACT_ALARM`, `USE_FULL_SCREEN_INTENT`, accessibility, photo/video
|
|
171
|
+
- **14** data safety categories with their data types
|
|
172
|
+
- All **14** foreground service types (including `mediaProcessing`, which most
|
|
173
|
+
lists miss), each with its required permission and runtime prerequisite
|
|
174
|
+
- Advertising ID, including the auto-merged-from-`play-services-ads` trap
|
|
175
|
+
- **New for 2026:** the Contacts policy (2026-10-28) and the geofencing
|
|
176
|
+
withdrawal (2026-08-26)
|
|
177
|
+
|
|
178
|
+
**Languages:** Swift, Objective-C, Kotlin, Java, JS/TS, Dart โ plus
|
|
179
|
+
`Info.plist`, `PrivacyInfo.xcprivacy`, `AndroidManifest.xml`, `package.json`,
|
|
180
|
+
`pubspec.yaml`, `Podfile`. React Native, Flutter and native all work; no build
|
|
181
|
+
required.
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## On being trustworthy
|
|
186
|
+
|
|
187
|
+
A preflight tool that cries wolf gets `--ignore`d within a week, and then it may
|
|
188
|
+
as well not exist. Three deliberate concessions:
|
|
189
|
+
|
|
190
|
+
**Findings carry their own epistemics.** Apple's required-reason rules are
|
|
191
|
+
mechanically checkable: the symbol is in your binary or it isn't. Play's data
|
|
192
|
+
safety form is *not* โ it keys on **transmission off-device**, not on holding a
|
|
193
|
+
permission. Google says plainly that on-device processing needs no disclosure. So
|
|
194
|
+
`READ_CONTACTS` does **not** imply a Contacts disclosure, and every such mapping
|
|
195
|
+
is marked `heuristic`: it raises a question, and **a heuristic finding can never
|
|
196
|
+
fail your build** (enforced by a test).
|
|
197
|
+
|
|
198
|
+
**The target API level is not hardcoded.** When this ruleset was built
|
|
199
|
+
(2026-07-15), Google's own timeline page was internally inconsistent โ metadata
|
|
200
|
+
dated May 2026, content still describing the August 2025 / API 35 deadline โ and
|
|
201
|
+
*no API 36 announcement existed on any Google property*, despite third-party
|
|
202
|
+
blogs confidently asserting one. So the **rule** is encoded ("within one year of
|
|
203
|
+
the latest major Android release") along with `last_verified` and a
|
|
204
|
+
`needs-live-check` flag. A tool that states a fake deadline confidently is worse
|
|
205
|
+
than one that says "verify this".
|
|
206
|
+
|
|
207
|
+
**The SDK check is a prompt, not a verdict.** Apple's requirement attaches when
|
|
208
|
+
an app is new or an update *adds* a listed SDK โ not to every app that contains
|
|
209
|
+
Firebase. And "any SDKs that repackage those on the list" are included, so exact
|
|
210
|
+
matching under-detects. Both facts make a confident verdict impossible, so it
|
|
211
|
+
ships as a warning that says why.
|
|
212
|
+
|
|
213
|
+
What we do *not* concede: comments and string literals are stripped before symbol
|
|
214
|
+
matching (with line numbers preserved), `node_modules`/`Pods` are never walked,
|
|
215
|
+
and `tools:node="remove"` is honoured โ so the correct way to drop the
|
|
216
|
+
auto-merged `AD_ID` permission isn't reported as the violation.
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## Verify it
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
python smoke_test.py
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
70 assertions, no network. Four fixture repos: one that gets rejected, the same
|
|
227
|
+
app fixed, a manifest that is wrong on its own terms, and one whose code never
|
|
228
|
+
changes while the policy date moves under it. The suite also pins the
|
|
229
|
+
facts that are easy to get wrong:
|
|
230
|
+
|
|
231
|
+
```
|
|
232
|
+
โ exactly 5 required-reason categories
|
|
233
|
+
โ exactly 17 reason codes
|
|
234
|
+
โ 86 SDKs on the commonly-used list
|
|
235
|
+
โ getattrlist maps to BOTH FileTimestamp and DiskSpace (not 1:1)
|
|
236
|
+
โ ITMS-91054 title is 'Invalid API category declaration'
|
|
237
|
+
โ 14 foreground service types incl. mediaProcessing
|
|
238
|
+
โ target SDK is a rule, not a hardcoded literal
|
|
239
|
+
โ no heuristic finding is ever a blocker
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Those aren't padding โ each one is a mistake that was actually made and caught
|
|
243
|
+
during the build. `getattrlist` really does oblige two separate declarations. The
|
|
244
|
+
SDK list really is 86, not 87, because Apple publishes "BoringSSL / openssl_grpc"
|
|
245
|
+
as one entry.
|
|
246
|
+
|
|
247
|
+
See [RULES.md](RULES.md) for provenance and how to re-verify against Apple's
|
|
248
|
+
JSON feed.
|
|
249
|
+
|
|
250
|
+
## Licence
|
|
251
|
+
|
|
252
|
+
MIT
|