@mmerterden/dev-toolkit-mcp 2.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +708 -0
  2. package/LICENSE +21 -0
  3. package/README.md +387 -0
  4. package/index.js +1277 -0
  5. package/package.json +80 -0
  6. package/tools/design-check/content-cardinality.js +204 -0
  7. package/tools/design-check/geometry.js +140 -0
  8. package/tools/design-check/index.js +213 -0
  9. package/tools/design-check/mock-detect.js +213 -0
  10. package/tools/design-check/report.js +590 -0
  11. package/tools/design-check/scan.js +91 -0
  12. package/tools/design-check/scenario-inventory.js +598 -0
  13. package/tools/design-check/visual-compare.js +961 -0
  14. package/tools/ios-app-store-audit/context.js +180 -0
  15. package/tools/ios-app-store-audit/data/apple-required-sdks.json +32 -0
  16. package/tools/ios-app-store-audit/data/debug-tools-blocklist.json +133 -0
  17. package/tools/ios-app-store-audit/index.js +164 -0
  18. package/tools/ios-app-store-audit/models.js +47 -0
  19. package/tools/ios-app-store-audit/rules/asset-validation.js +72 -0
  20. package/tools/ios-app-store-audit/rules/binary-size.js +70 -0
  21. package/tools/ios-app-store-audit/rules/code-signing.js +95 -0
  22. package/tools/ios-app-store-audit/rules/dead-reference.js +131 -0
  23. package/tools/ios-app-store-audit/rules/debug-tool-leak.js +185 -0
  24. package/tools/ios-app-store-audit/rules/duplicate-resource.js +130 -0
  25. package/tools/ios-app-store-audit/rules/embedded-sdk.js +126 -0
  26. package/tools/ios-app-store-audit/rules/entitlement.js +105 -0
  27. package/tools/ios-app-store-audit/rules/extension-signing.js +105 -0
  28. package/tools/ios-app-store-audit/rules/info-plist.js +158 -0
  29. package/tools/ios-app-store-audit/rules/ipv6-compliance.js +101 -0
  30. package/tools/ios-app-store-audit/rules/privacy-manifest.js +121 -0
  31. package/tools/ios-app-store-audit/rules/production-hygiene.js +237 -0
  32. package/tools/ios-app-store-audit/rules/provisioning-profile.js +127 -0
  33. package/tools/ios-app-store-audit/rules/required-reason-api.js +123 -0
  34. package/tools/ios-app-store-audit/rules/sdk-floor.js +104 -0
  35. package/tools/ios-app-store-audit/rules/swift-abi.js +64 -0
  36. package/tools/ios-app-store-audit/rules/team-id.js +62 -0
  37. package/tools/ios-testflight/index.js +479 -0
  38. package/ui-tree-dumper.swift +122 -0
@@ -0,0 +1,180 @@
1
+ /**
2
+ * context.js - parses an .xcarchive into an ArchiveContext object that the
3
+ * 18 rules consume. Direct port of XCArchiveParser.swift +
4
+ * InfoPlistParser.swift + PrivacyManifestParser.swift + EntitlementParser.swift.
5
+ *
6
+ * Plist parsing is delegated to `plutil -convert json` (system binary on
7
+ * macOS, ships with the OS) - more reliable than any Node `plist` package
8
+ * across the long tail of Apple plist quirks (binary plists, XML-with-DTD,
9
+ * mixed array/dict roots).
10
+ */
11
+
12
+ import { execSync } from "child_process";
13
+ import { existsSync, readdirSync, statSync } from "fs";
14
+ import { join, basename } from "path";
15
+
16
+ /** Run a shell command, return trimmed stdout or empty string on failure. */
17
+ function run(cmd) {
18
+ try {
19
+ return execSync(cmd, { encoding: "utf-8", maxBuffer: 64 * 1024 * 1024 }).trim();
20
+ } catch {
21
+ return "";
22
+ }
23
+ }
24
+
25
+ /** Parse a binary or XML plist file into a JS object via `plutil`. */
26
+ export function parsePlist(plistPath) {
27
+ if (!existsSync(plistPath)) return null;
28
+ const json = run(`plutil -convert json -o - "${plistPath}" 2>/dev/null`);
29
+ if (!json) return null;
30
+ try {
31
+ return JSON.parse(json);
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ /** Recursively measure total size of a directory in bytes. */
38
+ export function directorySize(path) {
39
+ if (!existsSync(path)) return 0;
40
+ let total = 0;
41
+ const stack = [path];
42
+ while (stack.length > 0) {
43
+ const cur = stack.pop();
44
+ let st;
45
+ try {
46
+ st = statSync(cur);
47
+ } catch {
48
+ continue;
49
+ }
50
+ if (st.isDirectory()) {
51
+ let entries = [];
52
+ try {
53
+ entries = readdirSync(cur);
54
+ } catch {
55
+ continue;
56
+ }
57
+ for (const e of entries) stack.push(join(cur, e));
58
+ } else if (st.isFile()) {
59
+ total += st.size;
60
+ }
61
+ }
62
+ return total;
63
+ }
64
+
65
+ /**
66
+ * Extract entitlements from a Mach-O binary using `codesign -d --entitlements`.
67
+ * Returns parsed plist as object, or null.
68
+ */
69
+ export function extractEntitlements(executablePath) {
70
+ if (!existsSync(executablePath)) return null;
71
+ // codesign emits the entitlements XML to stderr in older toolchains, stdout in newer.
72
+ // `--xml -` forces XML output to stdout. We then convert to JSON via plutil.
73
+ const xml = run(`codesign -d --entitlements :- --xml "${executablePath}" 2>/dev/null`);
74
+ if (!xml || !xml.includes("<plist")) {
75
+ // Fall back to old non-xml form (pre-Xcode 14).
76
+ const raw = run(`codesign -d --entitlements - "${executablePath}" 2>/dev/null`);
77
+ if (!raw || !raw.includes("<plist")) return null;
78
+ return run(`echo '${raw.replace(/'/g, "'\\''")}' | plutil -convert json -o - - 2>/dev/null`)
79
+ ? safeJsonParse(run(`echo '${raw.replace(/'/g, "'\\''")}' | plutil -convert json -o - - 2>/dev/null`))
80
+ : null;
81
+ }
82
+ // Pipe XML into plutil for JSON conversion.
83
+ try {
84
+ const json = execSync(`plutil -convert json -o - -`, {
85
+ input: xml,
86
+ encoding: "utf-8",
87
+ }).trim();
88
+ return JSON.parse(json);
89
+ } catch {
90
+ return null;
91
+ }
92
+ }
93
+
94
+ function safeJsonParse(s) {
95
+ try { return JSON.parse(s); } catch { return null; }
96
+ }
97
+
98
+ /**
99
+ * Parse an .xcarchive into a Context object.
100
+ *
101
+ * @param {string} archivePath - Absolute path to the .xcarchive bundle.
102
+ * @returns {object} - { archivePath, appPath, infoPlist, privacyManifest, entitlements, embeddedFrameworks, embeddedPlugins, executablePath, archiveSizeBytes, assetCatalogPath }
103
+ * @throws if the path doesn't exist, isn't an .xcarchive, or has no .app inside.
104
+ */
105
+ export function parseArchive(archivePath) {
106
+ if (!existsSync(archivePath)) {
107
+ throw new Error(`Archive not found at ${archivePath}`);
108
+ }
109
+ if (!archivePath.endsWith(".xcarchive")) {
110
+ throw new Error(`Not a valid .xcarchive at ${archivePath}`);
111
+ }
112
+
113
+ // Locate the .app bundle inside Products/Applications/
114
+ const productsPath = join(archivePath, "Products/Applications");
115
+ let appBundles = [];
116
+ try {
117
+ appBundles = readdirSync(productsPath).filter((n) => n.endsWith(".app"));
118
+ } catch {
119
+ throw new Error(`Products/Applications/ missing in ${archivePath}`);
120
+ }
121
+ if (appBundles.length === 0) {
122
+ throw new Error(`No .app bundle found in archive ${archivePath}`);
123
+ }
124
+ const appName = appBundles[0];
125
+ const appPath = join(productsPath, appName);
126
+
127
+ // Parse Info.plist
128
+ const infoPlist = parsePlist(join(appPath, "Info.plist")) || {};
129
+
130
+ // Parse PrivacyInfo.xcprivacy if present (root-level, not inside frameworks)
131
+ const privacyManifestPath = join(appPath, "PrivacyInfo.xcprivacy");
132
+ const privacyManifest = parsePlist(privacyManifestPath);
133
+
134
+ // Resolve executable path
135
+ const executableName = infoPlist.CFBundleExecutable || appName.replace(/\.app$/, "");
136
+ const executablePath = join(appPath, executableName);
137
+ const executableExists = existsSync(executablePath);
138
+
139
+ // Entitlements via codesign
140
+ const entitlements = executableExists ? extractEntitlements(executablePath) : null;
141
+
142
+ // Embedded frameworks
143
+ const frameworksDir = join(appPath, "Frameworks");
144
+ let embeddedFrameworks = [];
145
+ if (existsSync(frameworksDir)) {
146
+ embeddedFrameworks = readdirSync(frameworksDir)
147
+ .filter((n) => n.endsWith(".framework") || n.endsWith(".xcframework"))
148
+ .map((n) => join(frameworksDir, n));
149
+ }
150
+
151
+ // Embedded plugins / extensions
152
+ const plugInsDir = join(appPath, "PlugIns");
153
+ let embeddedPlugins = [];
154
+ if (existsSync(plugInsDir)) {
155
+ embeddedPlugins = readdirSync(plugInsDir)
156
+ .filter((n) => n.endsWith(".appex"))
157
+ .map((n) => join(plugInsDir, n));
158
+ }
159
+
160
+ // Archive size (entire .xcarchive)
161
+ const archiveSizeBytes = directorySize(archivePath);
162
+
163
+ // Asset catalog (Assets.car)
164
+ const assetsPath = join(appPath, "Assets.car");
165
+ const assetCatalogPath = existsSync(assetsPath) ? assetsPath : null;
166
+
167
+ return {
168
+ archivePath,
169
+ appPath,
170
+ appName,
171
+ infoPlist,
172
+ privacyManifest,
173
+ entitlements,
174
+ embeddedFrameworks,
175
+ embeddedPlugins,
176
+ executablePath: executableExists ? executablePath : null,
177
+ archiveSizeBytes,
178
+ assetCatalogPath,
179
+ };
180
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "_source": "https://developer.apple.com/support/third-party-SDK-requirements/",
3
+ "_note": "Commonly used third-party SDKs that REQUIRE both a privacy manifest AND a signature per Apple's Spring 2024 policy.",
4
+ "sdks": [
5
+ "Abseil", "AFNetworking", "Alamofire", "AppAuth",
6
+ "BoringSSL", "openssl_grpc",
7
+ "Capacitor", "Charts", "connectivity_plus", "Cordova",
8
+ "device_info_plus", "DKImagePickerController", "DKPhotoGallery",
9
+ "FBAEMKit", "FBLPromises", "FBSDKCoreKit", "FBSDKCoreKit_Basics", "FBSDKLoginKit", "FBSDKShareKit",
10
+ "file_picker",
11
+ "FirebaseABTesting", "FirebaseAuth", "FirebaseCore", "FirebaseCoreDiagnostics",
12
+ "FirebaseCoreExtension", "FirebaseCoreInternal", "FirebaseCrashlytics", "FirebaseDynamicLinks",
13
+ "FirebaseFirestore", "FirebaseInstallations", "FirebaseMessaging", "FirebaseRemoteConfig",
14
+ "Flutter", "flutter_inappwebview", "flutter_local_notifications", "fluttertoast",
15
+ "FMDB", "geolocator_apple",
16
+ "GoogleDataTransport", "GoogleSignIn", "GoogleToolboxForMac", "GoogleUtilities",
17
+ "grpcpp", "GTMAppAuth", "GTMSessionFetcher",
18
+ "hermes",
19
+ "image_picker_ios", "IQKeyboardManager", "IQKeyboardManagerSwift",
20
+ "Kingfisher", "leveldb", "Lottie", "MBProgressHUD", "nanopb",
21
+ "OneSignal", "OneSignalCore", "OneSignalExtension", "OneSignalOutcomes",
22
+ "OpenSSL", "OrderedSet",
23
+ "package_info", "package_info_plus", "path_provider", "path_provider_ios",
24
+ "Promises", "Protobuf", "Reachability", "RealmSwift",
25
+ "RxCocoa", "RxRelay", "RxSwift",
26
+ "SDWebImage", "share_plus", "shared_preferences_ios", "SnapKit",
27
+ "sqflite", "Starscream", "SVProgressHUD", "SwiftyGif", "SwiftyJSON",
28
+ "Toast",
29
+ "UnityFramework", "url_launcher", "url_launcher_ios",
30
+ "video_player_avfoundation", "wakelock", "webview_flutter_wkwebview"
31
+ ]
32
+ }
@@ -0,0 +1,133 @@
1
+ {
2
+ "_purpose": "Debug / development tools that must NOT ship to App Store. Apple Guidelines 2.5.1, 2.5.2.",
3
+ "frameworks": [
4
+ { "name": "netfox", "tool": "Netfox", "category": "Network Debugger" },
5
+ { "name": "NFX", "tool": "Netfox", "category": "Network Debugger" },
6
+ { "name": "CocoaDebug", "tool": "CocoaDebug", "category": "Network/Log Debugger" },
7
+ { "name": "Helios", "tool": "Helios", "category": "Network Debugger" },
8
+ { "name": "HeliosTrace", "tool": "HeliosTrace", "category": "Network Debugger" },
9
+ { "name": "Wormholy", "tool": "Wormholy", "category": "Network Debugger" },
10
+ { "name": "Bagel", "tool": "Bagel", "category": "Network Debugger" },
11
+ { "name": "Atlantis", "tool": "Atlantis/Proxyman", "category": "Network Debugger" },
12
+ { "name": "PulseUI", "tool": "Pulse", "category": "Network Logger UI" },
13
+ { "name": "PulseCore", "tool": "Pulse", "category": "Network Logger" },
14
+ { "name": "ResponseDetective", "tool": "ResponseDetective", "category": "Network Debugger" },
15
+ { "name": "OHHTTPStubs", "tool": "OHHTTPStubs", "category": "Network Stubber" },
16
+ { "name": "Mocker", "tool": "Mocker", "category": "Network Mocker" },
17
+ { "name": "Swifter", "tool": "Swifter (Local Server)", "category": "Debug HTTP Server" },
18
+ { "name": "FLEX", "tool": "FLEX", "category": "Runtime Debugger" },
19
+ { "name": "FLEXTool", "tool": "FLEX", "category": "Runtime Debugger" },
20
+ { "name": "Reveal", "tool": "Reveal", "category": "View Debugger" },
21
+ { "name": "LookinServer", "tool": "Lookin", "category": "View Debugger" },
22
+ { "name": "InAppViewDebugger", "tool": "InAppViewDebugger", "category": "View Debugger" },
23
+ { "name": "Sherlock", "tool": "Sherlock", "category": "View Debugger" },
24
+ { "name": "LayoutInspector", "tool": "LayoutInspector", "category": "View Debugger" },
25
+ { "name": "Sleipnir", "tool": "Sleipnir", "category": "View Debugger" },
26
+ { "name": "DoraemonKit", "tool": "DoraemonKit/DoKit", "category": "Debug Toolkit" },
27
+ { "name": "DoKit", "tool": "DoKit", "category": "Debug Toolkit" },
28
+ { "name": "GodEye", "tool": "GodEye", "category": "Debug Toolkit" },
29
+ { "name": "Woodpecker", "tool": "Woodpecker", "category": "Debug Toolkit" },
30
+ { "name": "LifetimeTracker", "tool": "LifetimeTracker", "category": "Memory Leak Debugger" },
31
+ { "name": "Chisel", "tool": "Chisel", "category": "LLDB Debugger" },
32
+ { "name": "Hyperion", "tool": "Hyperion", "category": "Debug Toolkit" },
33
+ { "name": "InAppDebugger", "tool": "InAppDebugger", "category": "Debug Toolkit" },
34
+ { "name": "Diagnostics", "tool": "Diagnostics", "category": "Debug Toolkit" },
35
+ { "name": "DebugSwift", "tool": "DebugSwift", "category": "Debug Toolkit" },
36
+ { "name": "FPSCounter", "tool": "FPSCounter", "category": "Performance Debugger" },
37
+ { "name": "MLeaksFinder", "tool": "MLeaksFinder", "category": "Memory Leak Debugger" },
38
+ { "name": "FBRetainCycleDetector","tool": "FBRetainCycleDetector", "category": "Memory Leak Debugger" },
39
+ { "name": "FBMemoryProfiler", "tool": "FBMemoryProfiler", "category": "Memory Profiler" },
40
+ { "name": "SwiftyBeaver", "tool": "SwiftyBeaver", "category": "Logger (review if needed in prod)" },
41
+ { "name": "XCGLogger", "tool": "XCGLogger", "category": "Logger (review if needed in prod)" }
42
+ ],
43
+ "symbols": [
44
+ { "symbol": "NFXListController", "tool": "Netfox" },
45
+ { "symbol": "NFXHTTPModel", "tool": "Netfox" },
46
+ { "symbol": "nfx_started", "tool": "Netfox" },
47
+ { "symbol": "CocoaDebugSettings", "tool": "CocoaDebug" },
48
+ { "symbol": "_TtC10CocoaDebug", "tool": "CocoaDebug" },
49
+ { "symbol": "FLEXManager", "tool": "FLEX" },
50
+ { "symbol": "FLEXExplorerToolbar", "tool": "FLEX" },
51
+ { "symbol": "FLEXObjectExplorer", "tool": "FLEX" },
52
+ { "symbol": "WormholyMethodSwizzling", "tool": "Wormholy" },
53
+ { "symbol": "RequestModelBeWormholy", "tool": "Wormholy" },
54
+ { "symbol": "HeliosLogger", "tool": "Helios" },
55
+ { "symbol": "HeliosNetworkInterceptor", "tool": "Helios" },
56
+ { "symbol": "HeliosTrace.enable", "tool": "HeliosTrace" },
57
+ { "symbol": "HeliosTrace.showBubble", "tool": "HeliosTrace" },
58
+ { "symbol": "HeliosTraceProtocol", "tool": "HeliosTrace" },
59
+ { "symbol": "DoraemonManager", "tool": "DoraemonKit" },
60
+ { "symbol": "DoraemonHomeViewController", "tool": "DoraemonKit" },
61
+ { "symbol": "GodEyeController", "tool": "GodEye" },
62
+ { "symbol": "AtlantisHelper", "tool": "Atlantis/Proxyman" },
63
+ { "symbol": "ProxymanHelper", "tool": "Proxyman" },
64
+ { "symbol": "LookinServer", "tool": "Lookin" },
65
+ { "symbol": "Lookin_iOS", "tool": "Lookin" },
66
+ { "symbol": "IBARevealServer", "tool": "Reveal" },
67
+ { "symbol": "RevealServer", "tool": "Reveal" },
68
+ { "symbol": "LifetimeTrackerDashboard", "tool": "LifetimeTracker" },
69
+ { "symbol": "BagelRequestInfo", "tool": "Bagel" },
70
+ { "symbol": "PulseUI", "tool": "Pulse" },
71
+ { "symbol": "NetworkLoggerInsights", "tool": "Pulse" },
72
+ { "symbol": "NFXShakeEnabled", "tool": "Netfox Shake Trigger" },
73
+ { "symbol": "CocoaDebugShake", "tool": "CocoaDebug Shake Trigger" },
74
+ { "symbol": "showDebugPanel", "tool": "Custom Debug Panel" },
75
+ { "symbol": "debugMenuPresenter", "tool": "Custom Debug Menu" },
76
+ { "symbol": "FLEXManager.shared.showExplorer", "tool": "FLEX Shake Trigger" },
77
+ { "symbol": "toggleFLEX", "tool": "FLEX Toggle" },
78
+ { "symbol": "HyperionCore", "tool": "Hyperion" },
79
+ { "symbol": "HyperionManager", "tool": "Hyperion" },
80
+ { "symbol": "InjectionClient", "tool": "Injection (Hot Reload)" },
81
+ { "symbol": "_TtC13InjectionNext", "tool": "Injection (Hot Reload)" },
82
+ { "symbol": "URLProtocolSwizzler", "tool": "Network Interceptor" },
83
+ { "symbol": "NSURLSessionSwizzle", "tool": "Network Interceptor" },
84
+ { "symbol": "NetworkInterceptor_swizzle", "tool": "Network Interceptor" }
85
+ ],
86
+ "urlSchemes": [
87
+ { "scheme": "nfx", "tool": "Netfox" },
88
+ { "scheme": "flex", "tool": "FLEX" },
89
+ { "scheme": "cocoadebug", "tool": "CocoaDebug" },
90
+ { "scheme": "wormholy", "tool": "Wormholy" },
91
+ { "scheme": "dokit", "tool": "DoKit" },
92
+ { "scheme": "lookin", "tool": "Lookin" }
93
+ ],
94
+ "genericPatterns": [
95
+ { "pattern": "URLProtocol.registerClass", "category": "Network Interceptor (URLProtocol swizzling)" },
96
+ { "pattern": "URLSessionConfiguration.protocolClasses","category": "Network Interceptor (protocol injection)" },
97
+ { "pattern": "swizzleSessionConfiguration", "category": "Network Interceptor (session swizzling)" },
98
+ { "pattern": "NetworkInterceptor", "category": "Network Interceptor" },
99
+ { "pattern": "HTTPRequestInterceptor", "category": "HTTP Request Interceptor" },
100
+ { "pattern": "RequestLogger", "category": "Network Request Logger" },
101
+ { "pattern": "ResponseLogger", "category": "Network Response Logger" },
102
+ { "pattern": "NetworkLogger", "category": "Network Logger" },
103
+ { "pattern": "TrafficMonitor", "category": "Network Traffic Monitor" },
104
+ { "pattern": "PacketCapture", "category": "Network Packet Capture" },
105
+ { "pattern": "DebugWindow", "category": "Debug Floating Window" },
106
+ { "pattern": "DebugOverlay", "category": "Debug Overlay" },
107
+ { "pattern": "DebugBubble", "category": "Debug Bubble UI" },
108
+ { "pattern": "FloatingDebug", "category": "Floating Debug Panel" },
109
+ { "pattern": "DebugConsole", "category": "In-App Debug Console" },
110
+ { "pattern": "InAppConsole", "category": "In-App Console" },
111
+ { "pattern": "DebugDashboard", "category": "Debug Dashboard" },
112
+ { "pattern": "ViewHierarchyInspector", "category": "View Hierarchy Inspector" },
113
+ { "pattern": "RuntimeBrowser", "category": "Runtime Browser" },
114
+ { "pattern": "ObjectInspector", "category": "Object Inspector" },
115
+ { "pattern": "PropertyInspector", "category": "Property Inspector" },
116
+ { "pattern": "DatabaseBrowser", "category": "Database Browser" },
117
+ { "pattern": "SQLiteBrowser", "category": "SQLite Browser" },
118
+ { "pattern": "CoreDataDebug", "category": "Core Data Debugger" },
119
+ { "pattern": "UserDefaultsExplorer", "category": "UserDefaults Explorer" },
120
+ { "pattern": "KeychainViewer", "category": "Keychain Viewer" },
121
+ { "pattern": "StorageInspector", "category": "Storage Inspector" },
122
+ { "pattern": "CacheBrowser", "category": "Cache Browser" },
123
+ { "pattern": "FileExplorer", "category": "File Explorer (Debug)" },
124
+ { "pattern": "SandboxBrowser", "category": "Sandbox Browser" },
125
+ { "pattern": "LogViewer", "category": "In-App Log Viewer" },
126
+ { "pattern": "ConsoleViewer", "category": "Console Viewer" },
127
+ { "pattern": "LogDashboard", "category": "Log Dashboard" },
128
+ { "pattern": "EnvironmentSwitcher", "category": "Environment Switcher" },
129
+ { "pattern": "ServerSwitcher", "category": "Server Switcher" },
130
+ { "pattern": "FeatureFlagDebug", "category": "Feature Flag Debugger" },
131
+ { "pattern": "DebugSettings", "category": "Debug Settings Panel" }
132
+ ]
133
+ }
@@ -0,0 +1,164 @@
1
+ /**
2
+ * ios_app_store_audit - full 18-rule App Store compliance scanner for
3
+ * .xcarchive bundles, ported from the standalone ArchiveGuard Swift package.
4
+ *
5
+ * Public surface:
6
+ * runAudit({ archivePath, rules })
7
+ * archivePath: absolute path to a .xcarchive bundle
8
+ * rules: "all" (default) | "core" | "deep" | comma-separated list of ruleIDs
9
+ * returns:
10
+ * {
11
+ * archive: <path>,
12
+ * app: <bundle name>,
13
+ * summary: { error, warning, info, total },
14
+ * verdict: "PASS" | "WARN" | "FAIL",
15
+ * violations: [ { ruleID, severity, message, ... } ]
16
+ * }
17
+ *
18
+ * Each rule lives in `rules/<rule-id>.js` and exports
19
+ * id, name, description, check(ctx, opts)
20
+ * The dispatcher imports them lazily so a rule failure can never poison
21
+ * the rest of the scan.
22
+ */
23
+
24
+ import { parseArchive } from "./context.js";
25
+ import { compareSeverity } from "./models.js";
26
+
27
+ // ---------- Rule registry --------------------------------------------------
28
+ // Order is the human-friendly report order (privacy first, fs hygiene last).
29
+ // Each entry is { id, module: dynamic-import path, group: "core"|"deep" }.
30
+ //
31
+ // group "core" → checks the existing ios_archive_audit (lighter) covered.
32
+ // These are safe to run on every CI invocation.
33
+ // group "deep" → the 18-rule full scan. Heavier (binary string scans,
34
+ // embedded SDK enumeration, etc.).
35
+ const RULE_REGISTRY = [
36
+ { id: "code-signing", module: "./rules/code-signing.js", group: "core" },
37
+ { id: "entitlements", module: "./rules/entitlement.js", group: "core" },
38
+ { id: "info-plist", module: "./rules/info-plist.js", group: "core" },
39
+ { id: "privacy-manifest", module: "./rules/privacy-manifest.js", group: "core" },
40
+ { id: "binary-size", module: "./rules/binary-size.js", group: "core" },
41
+ { id: "team-id-consistency", module: "./rules/team-id.js", group: "deep" },
42
+ { id: "provisioning-profile", module: "./rules/provisioning-profile.js", group: "deep" },
43
+ { id: "production-hygiene", module: "./rules/production-hygiene.js", group: "deep" },
44
+ { id: "ipv6-compliance", module: "./rules/ipv6-compliance.js", group: "deep" },
45
+ { id: "duplicate-resource", module: "./rules/duplicate-resource.js", group: "deep" },
46
+ { id: "dead-reference", module: "./rules/dead-reference.js", group: "deep" },
47
+ { id: "embedded-sdk", module: "./rules/embedded-sdk.js", group: "deep" },
48
+ { id: "required-reason-api", module: "./rules/required-reason-api.js", group: "deep" },
49
+ { id: "asset-validation", module: "./rules/asset-validation.js", group: "deep" },
50
+ { id: "swift-abi", module: "./rules/swift-abi.js", group: "deep" },
51
+ { id: "extension-signing", module: "./rules/extension-signing.js", group: "deep" },
52
+ { id: "debug-tool-leak", module: "./rules/debug-tool-leak.js", group: "deep" },
53
+ // ITMS-90725 is a hard upload rejection and costs nothing to check, so it
54
+ // belongs in "core" rather than the heavier deep scan.
55
+ { id: "sdk-floor", module: "./rules/sdk-floor.js", group: "core" },
56
+ ];
57
+ // Keeping the registry the single source of truth lets the dispatcher
58
+ // skip-with-warning when a rule module is missing - easier than touching
59
+ // multiple imports per commit.
60
+
61
+ const RULE_GROUPS = {
62
+ all: () => RULE_REGISTRY.map((r) => r.id),
63
+ deep: () => RULE_REGISTRY.map((r) => r.id),
64
+ core: () => RULE_REGISTRY.filter((r) => r.group === "core").map((r) => r.id),
65
+ };
66
+
67
+ function resolveRuleSelection(rules) {
68
+ if (!rules || rules === "all" || rules === "deep") return RULE_GROUPS.all();
69
+ if (rules === "core") return RULE_GROUPS.core();
70
+ // Comma-separated explicit list.
71
+ return rules.split(",").map((s) => s.trim()).filter(Boolean);
72
+ }
73
+
74
+ // ---------- Public entry ---------------------------------------------------
75
+
76
+ /**
77
+ * Run an App Store compliance audit against an .xcarchive.
78
+ *
79
+ * @param {object} args
80
+ * @param {string} args.archivePath - absolute path to a .xcarchive
81
+ * @param {string} [args.rules] - "all" | "core" | "deep" | csv of ruleIDs
82
+ * @param {object} [args.options] - per-rule options, keyed by ruleID
83
+ * @returns {Promise<object>} structured audit report
84
+ */
85
+ export async function runAudit({ archivePath, rules = "all", options = {} } = {}) {
86
+ const ctx = parseArchive(archivePath);
87
+
88
+ const selectedIDs = new Set(resolveRuleSelection(rules));
89
+ const toRun = RULE_REGISTRY.filter((r) => selectedIDs.has(r.id));
90
+
91
+ const violations = [];
92
+ const ranIDs = [];
93
+ const skippedIDs = [];
94
+
95
+ for (const entry of toRun) {
96
+ let mod;
97
+ try {
98
+ mod = await import(entry.module);
99
+ } catch (err) {
100
+ // Rule module not implemented yet (early-port commits). Skip cleanly.
101
+ skippedIDs.push({ id: entry.id, reason: `module not loadable: ${err.message}` });
102
+ continue;
103
+ }
104
+ if (typeof mod.check !== "function") {
105
+ skippedIDs.push({ id: entry.id, reason: "module has no check()" });
106
+ continue;
107
+ }
108
+ try {
109
+ const ruleOpts = options[entry.id] || {};
110
+ const out = mod.check(ctx, ruleOpts);
111
+ if (Array.isArray(out)) {
112
+ for (const v of out) violations.push(v);
113
+ }
114
+ ranIDs.push(entry.id);
115
+ } catch (err) {
116
+ // A single rule throwing must NEVER abort the whole audit. Record as info.
117
+ violations.push({
118
+ ruleID: entry.id,
119
+ severity: "warning",
120
+ message: `Rule '${entry.id}' threw: ${err.message}`,
121
+ suggestion: "Open an issue with the archive details - this is a tool bug, not your archive's fault.",
122
+ });
123
+ }
124
+ }
125
+
126
+ // Sort: error → warning → info, then by ruleID alphabetically inside each.
127
+ violations.sort((a, b) => {
128
+ const c = compareSeverity(b.severity, a.severity);
129
+ return c !== 0 ? c : a.ruleID.localeCompare(b.ruleID);
130
+ });
131
+
132
+ const summary = {
133
+ error: violations.filter((v) => v.severity === "error").length,
134
+ warning: violations.filter((v) => v.severity === "warning").length,
135
+ info: violations.filter((v) => v.severity === "info").length,
136
+ total: violations.length,
137
+ };
138
+ const verdict = summary.error > 0 ? "FAIL" : summary.warning > 0 ? "WARN" : "PASS";
139
+
140
+ return {
141
+ archive: archivePath,
142
+ app: ctx.appName,
143
+ rulesRun: ranIDs,
144
+ rulesSkipped: skippedIDs,
145
+ summary,
146
+ verdict,
147
+ violations,
148
+ };
149
+ }
150
+
151
+ // ---------- MCP tool descriptor (consumed by main index.js) ---------------
152
+
153
+ export const TOOL_DESCRIPTOR = {
154
+ name: "ios_app_store_audit",
155
+ description: "Deep App Store Review compliance audit for .xcarchive bundles. 18-rule catalog covering privacy manifest, code signing, embedded SDKs, entitlements, asset hygiene, IPv6 compliance, debug-tool leak detection, Swift ABI compatibility, and more. Returns structured JSON with severity-ranked violations and ITMS error code mappings. Replaces the lighter ios_archive_audit (deprecated). Use --rules=core for the 6 baseline checks, --rules=deep|all for the full scan.",
156
+ inputSchema: {
157
+ type: "object",
158
+ properties: {
159
+ archive_path: { type: "string", description: "Absolute path to the .xcarchive bundle" },
160
+ rules: { type: "string", description: "'all' (default) | 'core' | 'deep' | comma-separated ruleIDs" },
161
+ },
162
+ required: ["archive_path"],
163
+ },
164
+ };
@@ -0,0 +1,47 @@
1
+ /**
2
+ * models.js - shared types for the ios_app_store_audit tool.
3
+ *
4
+ * Mirrors the Swift Severity / Violation types from ArchiveGuard so the JSON
5
+ * shape we emit is byte-equivalent to what `archive-guard --format=json`
6
+ * used to produce. Consumers (multi-agent-pipeline apple-archive-compliance
7
+ * skill, manual users) keep working without code changes - only the source
8
+ * of the JSON moves from a Swift CLI to this MCP tool.
9
+ */
10
+
11
+ export const Severity = Object.freeze({
12
+ INFO: "info",
13
+ WARNING: "warning",
14
+ ERROR: "error",
15
+ });
16
+
17
+ const SEV_RANK = { info: 0, warning: 1, error: 2 };
18
+
19
+ /** Compare two severities. Higher rank wins. Returns -1 / 0 / +1. */
20
+ export function compareSeverity(a, b) {
21
+ return Math.sign(SEV_RANK[a] - SEV_RANK[b]);
22
+ }
23
+
24
+ /**
25
+ * Create a Violation object. Mirrors `Violation.swift` field-for-field.
26
+ *
27
+ * @param {object} v
28
+ * @param {string} v.ruleID - e.g. "binary-size"
29
+ * @param {("info"|"warning"|"error")} v.severity
30
+ * @param {string} v.message - human-readable description
31
+ * @param {string} [v.guidelineRef] - Apple guideline / ITMS code
32
+ * @param {string} [v.file] - path relative to the archive
33
+ * @param {string} [v.suggestion] - fix recommendation
34
+ */
35
+ export function makeViolation({ ruleID, severity, message, guidelineRef, file, suggestion }) {
36
+ if (!ruleID || !severity || !message) {
37
+ throw new Error(`makeViolation requires ruleID + severity + message; got ${JSON.stringify({ ruleID, severity, message })}`);
38
+ }
39
+ if (!Object.values(Severity).includes(severity)) {
40
+ throw new Error(`Invalid severity '${severity}' - must be one of ${Object.values(Severity).join(", ")}`);
41
+ }
42
+ const v = { ruleID, severity, message };
43
+ if (guidelineRef) v.guidelineRef = guidelineRef;
44
+ if (file) v.file = file;
45
+ if (suggestion) v.suggestion = suggestion;
46
+ return v;
47
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * asset-validation - Verifies app icon configuration + launch storyboard
3
+ * presence. Direct port of AssetValidationRule.swift.
4
+ *
5
+ * Apple Guidelines: 2.3, 2.1.
6
+ */
7
+
8
+ import { readdirSync } from "fs";
9
+ import { Severity, makeViolation } from "../models.js";
10
+
11
+ export const id = "asset-validation";
12
+ export const name = "Asset Validation";
13
+ export const description = "Checks for required app icons and asset catalog presence";
14
+
15
+ export function check(ctx) {
16
+ const violations = [];
17
+ const plist = ctx.infoPlist || {};
18
+
19
+ // Compiled asset catalog
20
+ if (!ctx.assetCatalogPath) {
21
+ violations.push(makeViolation({
22
+ ruleID: id, guidelineRef: "2.3", severity: Severity.WARNING,
23
+ message: "No compiled asset catalog (Assets.car) found in app bundle",
24
+ file: ctx.appPath,
25
+ suggestion: "Ensure your asset catalog is included in the target",
26
+ }));
27
+ }
28
+
29
+ // Icon configuration
30
+ let iconFiles = [];
31
+ if (plist.CFBundleIcons?.CFBundlePrimaryIcon?.CFBundleIconFiles) {
32
+ const arr = plist.CFBundleIcons.CFBundlePrimaryIcon.CFBundleIconFiles;
33
+ if (Array.isArray(arr)) iconFiles = arr;
34
+ } else if (typeof plist.CFBundleIconFile === "string") {
35
+ iconFiles = [plist.CFBundleIconFile];
36
+ }
37
+
38
+ let appContents = [];
39
+ try {
40
+ appContents = readdirSync(ctx.appPath);
41
+ } catch {
42
+ appContents = [];
43
+ }
44
+ const hasIconFiles = appContents.some((f) => f.startsWith("AppIcon"));
45
+ const hasAssetCatalog = ctx.assetCatalogPath != null;
46
+
47
+ if (iconFiles.length === 0 && !hasIconFiles && !hasAssetCatalog) {
48
+ violations.push(makeViolation({
49
+ ruleID: id, guidelineRef: "2.3", severity: Severity.ERROR,
50
+ message: "No app icon configuration found",
51
+ file: "Info.plist",
52
+ suggestion: "Add an AppIcon set to your asset catalog with all required sizes",
53
+ }));
54
+ }
55
+
56
+ // Launch storyboard
57
+ if (plist.UILaunchStoryboardName == null) {
58
+ const hasLaunchStoryboard = appContents.some((f) =>
59
+ f.includes("LaunchScreen") && (f.endsWith(".storyboardc") || f.endsWith(".nib"))
60
+ );
61
+ if (!hasLaunchStoryboard) {
62
+ violations.push(makeViolation({
63
+ ruleID: id, guidelineRef: "2.1", severity: Severity.WARNING,
64
+ message: "No launch storyboard found - required for full-screen support on all devices",
65
+ file: ctx.appPath,
66
+ suggestion: "Add a LaunchScreen.storyboard to your project",
67
+ }));
68
+ }
69
+ }
70
+
71
+ return violations;
72
+ }