ITMS-91064: NSPrivacyTracking and NSPrivacyTrackingDomains must agree in every privacy manifest (true needs domains, domains need true); check, scan and privacy-manifest fail on either mismatch with the exact ITMS text; App Privacy tracking no longer demands the app manifest's true: SDK manifests that declare tracking (Pods, frameworks, SwiftPM, Carthage, plugins, the built IPA) are info, known tracking SDKs info, else a warning; privacy-manifest --propose writes true only with appPrivacy.trackingDomains; apple upload checks every manifest in the IPA (XML or binary) before any network call, apple build after the export. Pitfall 29
This commit is contained in:
parent
b7bfe89151
commit
c8213be2b6
19 changed files with 631 additions and 44 deletions
54
cli/check.ts
54
cli/check.ts
|
|
@ -4,9 +4,11 @@
|
|||
// storebound check everything above
|
||||
// storebound check --aso keyword audit of both listing files (App Store keyword field, Play stuffing); exit 1 on errors
|
||||
// --json structured result ({ ok, items, warnings, hints, problems } or { findings, errors }), exit 1 on problems
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { dirname, relative } from 'node:path';
|
||||
import { appPrivacyMismatches } from './lib/app-privacy';
|
||||
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
||||
import { dirname, join, relative } from 'node:path';
|
||||
import { appPrivacyMismatches, type PrivacyMismatch, trackingDeclaredIssues, type TrackingSources } from './lib/app-privacy';
|
||||
import { type BundleManifest, bundleTrackingIssues, ipaManifests, projectSdkManifests, sdkOfProjectPath } from './lib/privacy-files';
|
||||
import { trackingIssue } from './lib/privacy-manifest';
|
||||
import { type AsoFinding, auditAso } from './lib/aso';
|
||||
import { draftLocales } from './lib/aso-intel';
|
||||
import { appleName, playCodes } from './lib/config';
|
||||
|
|
@ -23,6 +25,11 @@ import { allSubscriptions } from './lib/subscriptions';
|
|||
import { type CopyFile, validateCopy } from './shots/lib';
|
||||
import { type CapProject, capacitorProject, prebuildFor, staleMessage, webState } from './lib/web-assets';
|
||||
|
||||
/** The newest .ipa in a folder (apple build's export), or null. */
|
||||
function newestIpa(dir: string): string | null {
|
||||
try { return readdirSync(dir).filter(f => f.endsWith('.ipa')).map(f => join(dir, f)).sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs)[0] ?? null; } catch { return null; }
|
||||
}
|
||||
|
||||
function aso(ctx: Ctx): void {
|
||||
const { config, resolve } = ctx.loaded;
|
||||
const appleMd = config.apple ? readFileSync(resolve(config.apple.listing ?? 'store/appstore.md'), 'utf8') : '';
|
||||
|
|
@ -85,18 +92,39 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
if (todo.length) note(`${relative(process.cwd(), f) || f}: not translated yet (status=translate): ${todo.join(', ')}`);
|
||||
if (drafts.length) note(`${relative(process.cwd(), f) || f}: machine-translated drafts not reviewed: ${drafts.join(', ')} (storebound listing check, then listing reviewed --lang <ids>)`);
|
||||
}
|
||||
// App Privacy answers (appPrivacy.types) against the iOS project: ATT string, PrivacyInfo.xcprivacy tracking and types
|
||||
// Privacy manifests (ITMS-91064: NSPrivacyTracking and NSPrivacyTrackingDomains agree in every manifest, the app's,
|
||||
// the SDKs' and the last built IPA's) and the App Privacy answers against the iOS project: ATT string, a manifest
|
||||
// that declares tracking (the app's or an SDK's), PrivacyInfo.xcprivacy types
|
||||
const apTypes = config.appPrivacy?.types;
|
||||
if (config.apple && apTypes && Object.keys(apTypes).length) {
|
||||
const facts = detectProject(ctx.loaded.root);
|
||||
if (!facts.ios) hints.push('appPrivacy.types: no iOS project found here, so it is not compared with Info.plist / PrivacyInfo.xcprivacy');
|
||||
else {
|
||||
const own = facts.privacyManifests.find(m => !/(^|\/)(Pods|node_modules|SourcePackages|Carthage|\.build)\//.test(m.path)) ?? null;
|
||||
const found = appPrivacyMismatches(apTypes, { attString: facts.plist.has('NSUserTrackingUsageDescription'), manifest: own });
|
||||
for (const f of found) if (f.level === 'error') problems.push(f.msg); else note(f.msg);
|
||||
items.push({ what: 'app privacy', detail: `${Object.keys(apTypes).length} types answered, ${found.length} mismatch(es) with the iOS project` });
|
||||
log('check', `app privacy: ${Object.keys(apTypes).length} types answered, ${found.length} mismatch(es) with the iOS project`);
|
||||
const hasTypes = !!apTypes && Object.keys(apTypes).length > 0;
|
||||
const facts = config.apple ? detectProject(ctx.loaded.root) : null;
|
||||
if (hasTypes && facts && !facts.ios) hints.push('appPrivacy.types: no iOS project found here, so it is not compared with Info.plist / PrivacyInfo.xcprivacy');
|
||||
if (facts?.ios) {
|
||||
const own = facts.privacyManifests.find(m => !/(^|\/)(Pods|node_modules|SourcePackages|Carthage|\.build)\//.test(m.path)) ?? null;
|
||||
const ownPaths = new Set(facts.privacyManifests.map(m => m.path));
|
||||
const sdkMs = projectSdkManifests(ctx.loaded.root).filter(s => !ownPaths.has(s.manifest.path));
|
||||
for (const m of facts.privacyManifests) { const i = trackingIssue(m, sdkOfProjectPath(m.path)); if (i) problems.push(i); }
|
||||
for (const s of sdkMs) { const i = trackingIssue(s.manifest, s.sdk); if (i) problems.push(i); }
|
||||
const ipa = newestIpa(resolve(join(config.apple!.build?.out ?? 'build/ios', 'export')));
|
||||
let built: BundleManifest[] = [];
|
||||
if (ipa) {
|
||||
try { built = ipaManifests(ipa); } catch (e) { note(`${relative(process.cwd(), ipa)}: privacy manifests not readable (${(e as Error).message})`); }
|
||||
for (const i of bundleTrackingIssues(built)) problems.push(`${relative(process.cwd(), ipa)}: ${i} Rebuild after the fix (storebound apple build).`);
|
||||
}
|
||||
const tracking: TrackingSources = {
|
||||
sdkManifests: [...sdkMs.filter(s => s.manifest.tracking === true).map(s => s.sdk), ...built.filter(b => b.sdk && b.manifest.tracking === true).map(b => b.sdk!)],
|
||||
trackerSdks: facts.sdks.filter(s => s.sdk.tracking).map(s => s.sdk.name),
|
||||
};
|
||||
const say = (f: PrivacyMismatch) => { if (f.level === 'error') problems.push(f.msg); else if (f.level === 'warn') note(f.msg); else { hints.push(f.msg); log('check', `hint: ${f.msg}`); } };
|
||||
if (hasTypes) {
|
||||
const found = appPrivacyMismatches(apTypes!, { attString: facts.plist.has('NSUserTrackingUsageDescription'), manifest: own, tracking });
|
||||
found.forEach(say);
|
||||
const n = found.filter(f => f.level !== 'info').length;
|
||||
items.push({ what: 'app privacy', detail: `${Object.keys(apTypes!).length} types answered, ${n} mismatch(es) with the iOS project` });
|
||||
log('check', `app privacy: ${Object.keys(apTypes!).length} types answered, ${n} mismatch(es) with the iOS project`);
|
||||
}
|
||||
const typesTrack = hasTypes && Object.values(apTypes!).some(a => a?.tracking === true);
|
||||
if (config.appPrivacy?.tracking === true && !typesTrack && own) trackingDeclaredIssues('appPrivacy.tracking is true', own, tracking).forEach(say);
|
||||
}
|
||||
if (config.dataSafety) tryDo('data safety', () => {
|
||||
const rows = dataSafetyRows(config.dataSafety!, config.accounts);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue