scan, privacy answers and credentials from a second game's first release (lessons 35-37): review-prompt needs rating context, not a bare "rate"; account signals skip import lines and module paths, config-only accounts say "from config"; accounts.inApp without deletion code stays a medium 5.1.1(v) finding; privacy-manifest --propose builds NSPrivacyCollectedDataTypes from appPrivacy.types and check warns on an empty list; appPrivacy.types is authoritative in the App Privacy sheet (unlisted = not collected), ad / support / AR types default to not collected without tracking or ad SDKs; Play advertising ID from SDKs, the AD_ID permission, app-ads.txt or the flag, never from a Device ID alone; scan.ignore[].evidence and warnings for entries that match nothing; empty credentials template, placeholder values count as missing (check, doctor)

This commit is contained in:
CRTkafa 2026-10-13 05:39:09 +08:00
parent 86cd79dd45
commit 1366e7e231
34 changed files with 881 additions and 71 deletions

View file

@ -641,7 +641,17 @@ export interface ShipConfig {
* them in App Store Connect). `storebound scan` proposes them from the detected SDKs and code. */
appPrivacy?: AppPrivacyConfig;
/** `storebound scan`: findings the owner has looked at and accepts, by rule id (with the reason, shown in the report). */
scan?: { ignore?: { rule: string; store?: 'apple' | 'play'; /** Only findings whose title contains this text (one check of a rule that has several). */ match?: string; reason: string }[] };
scan?: {
ignore?: {
rule: string; store?: 'apple' | 'play';
/** Only findings whose TITLE contains this text (one check of a rule that has several), case-insensitive. */
match?: string;
/** Only findings with an evidence line ("file:line text", as the report prints it) containing this text, e.g.
* "src/main.ts:12" or a code fragment. Combine with match to narrow further. */
evidence?: string;
reason: string;
}[];
};
/** Ads: `validate` checks that <app.website>/app-ads.txt is reachable and contains every line listed here. */
ads?: { appAdsTxt?: string[] };
/** Rendered store graphics dir (<renders>/<languageId>/<size>/NN.png|jpg). Defaults to shots.out. */
@ -1069,8 +1079,8 @@ export function validateConfig(c: unknown): string[] {
e.push(...validateAppPrivacyTypes(cfg.appPrivacy.types));
}
for (const [i, x] of (cfg.scan?.ignore ?? []).entries())
if (!x || typeof x.rule !== 'string' || !x.rule || typeof x.reason !== 'string' || !x.reason.trim() || (x.store !== undefined && x.store !== 'apple' && x.store !== 'play') || (x.match !== undefined && (typeof x.match !== 'string' || !x.match.trim())))
e.push(`scan.ignore[${i}]: { rule: '<rule id from storebound scan>', reason: 'why this is fine', store?: 'apple' | 'play', match?: '<text in the finding title>' }`);
if (!x || typeof x.rule !== 'string' || !x.rule || typeof x.reason !== 'string' || !x.reason.trim() || (x.store !== undefined && x.store !== 'apple' && x.store !== 'play') || (x.match !== undefined && (typeof x.match !== 'string' || !x.match.trim())) || (x.evidence !== undefined && (typeof x.evidence !== 'string' || !x.evidence.trim())))
e.push(`scan.ignore[${i}]: { rule: '<rule id from storebound scan>', reason: 'why this is fine', store?: 'apple' | 'play', match?: '<text in the finding title>', evidence?: '<text in an evidence line (file:line text)>' }`);
if (cfg.ads?.appAdsTxt && !cfg.ads.appAdsTxt.every(l => typeof l === 'string' && l.split(',').length >= 3))
e.push('ads.appAdsTxt: lines like "example-network.com, pub-123, DIRECT, abc123"');
const seen = new Set<string>();