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

@ -0,0 +1,7 @@
# capacitor-game fixture
A made-up Capacitor 8 farm game (first release) with the patterns a real-use report found: a module named
`buildings/register` imported for its side effects, production rates next to a `bonus` object, accounts and in-app
deletion claimed only by the config (no auth or deletion code), five App Privacy types in `appPrivacy.types` while the
app's PrivacyInfo.xcprivacy lists none, a random install id declared as Device ID in Data safety, and no ad SDK or
AD_ID permission. Not a buildable project.

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<application android:label="Example Farm" />
</manifest>

View file

@ -0,0 +1,5 @@
{
"appId": "org.example.farmgame",
"appName": "Example Farm",
"webDir": "dist"
}

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDisplayName</key>
<string>Example Farm</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
</dict>
</plist>

View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</array>
</dict>
</array>
</dict>
</plist>

View file

@ -0,0 +1,10 @@
{
"name": "farm-game",
"private": true,
"dependencies": {
"@capacitor/android": "8.0.0",
"@capacitor/core": "8.0.0",
"@capacitor/ios": "8.0.0",
"@capacitor/preferences": "8.0.0"
}
}

View file

@ -0,0 +1,7 @@
import { registry } from './registry';
export class Farm { level = 1 }
export class Mill { level = 1 }
registry.register('farm', Farm);
registry.register('mill', Mill);

View file

@ -0,0 +1,11 @@
export const bonus = { gen: (r: number) => 1 + r * 0.1 };
export const F = {
genRate: (lv: number, mult: number) => lv * 2 * mult,
};
export function tick(f: typeof F, lv = 1, r = 0): number {
const rate = F.genRate(lv, bonus.gen(r));
const frameRate = 60; // bonus coins drop every second
const exchangeRate = 10; // coins per gem
return rate * frameRate / exchangeRate;
}

View file

@ -0,0 +1,10 @@
import './buildings/register';
import { Preferences } from '@capacitor/preferences';
import { F, tick } from './economy';
export async function boot(): Promise<void> {
const saved = await Preferences.get({ key: 'farm' });
const installId = saved.value ?? crypto.randomUUID();
await Preferences.set({ key: 'farm', value: installId });
setInterval(() => tick(F), 1000);
}

View file

@ -0,0 +1,41 @@
// Fixture config (made up, example.com placeholders only).
export default {
app: {
name: 'Example Farm',
profile: 'game',
privacyPolicyUrl: 'https://example.com/privacy',
supportUrl: 'https://example.com/support',
website: 'https://example.com',
contactEmail: 'support@example.com',
},
accounts: { enabled: true, deletionUrl: 'https://example.com/delete-account', inApp: 'Settings > Delete account' },
reviewAccess: { signInRequired: false, instructions: 'No sign-in needed.' },
languages: [{ id: 'en', play: 'en-US', apple: 'en-US' }],
play: { packageName: 'org.example.farmgame', accountType: 'organization', targetAges: ['13-15', '16-17', '18+'] },
apple: { bundleId: 'org.example.farmgame', usesNonExemptEncryption: false },
appPrivacy: {
tracking: false,
types: {
USER_ID: { collected: true, linked: true, tracking: false, purposes: ['APP_FUNCTIONALITY'] },
DEVICE_ID: { collected: true, linked: false, tracking: false, purposes: ['APP_FUNCTIONALITY', 'ANALYTICS'] },
GAMEPLAY_CONTENT: { collected: true, linked: true, tracking: false, purposes: ['APP_FUNCTIONALITY'] },
PRODUCT_INTERACTION: { collected: true, linked: false, tracking: false, purposes: ['ANALYTICS'] },
CRASH_DATA: { collected: true, linked: false, tracking: false, purposes: ['APP_FUNCTIONALITY'] },
},
},
dataSafety: {
collectsData: true,
encryptedInTransit: true,
data: {
PSL_USER_ACCOUNT: { shared: false, collect: ['account', 'functionality'] },
PSL_DEVICE_ID: { shared: false, collect: ['functionality', 'analytics'] },
PSL_OTHER_APP_ACTIVITY: { shared: false, collect: ['analytics'] },
PSL_CRASH_LOGS: { shared: false, collect: ['analytics'] },
},
},
scan: {
ignore: [
{ rule: 'privacy-labels', evidence: 'src/no-such-file.ts', reason: 'stale entry: matches nothing' },
],
},
};