template compliance answers never reach a store: init writes unanswered (dataSafety, appPrivacy, reviewNotes, ageRating, accounts, reviewAccess), older configs are recognised by the template values; check / validate / scan block, play data-safety (also --dry), console plan / fill, apple review-details and appinfo refuse. onboard --declarations replaces them on a live app: name and copyright from the stores (a copyright with a private term is left out), the rows the code proves (scan proposal, sign-up methods from auth code and login SDKs), the rest as one owner question list. play data-safety --dry prints the answers that change (old -> new, account and deletion first) against the last uploaded CSV; pitfall 60; tests

This commit is contained in:
CRTkafa 2026-10-14 11:36:30 +08:00
parent 34c2fa22e5
commit 7219ded3e8
27 changed files with 659 additions and 19 deletions

View file

@ -1,5 +1,8 @@
// Play Data safety form through applications.dataSafety (CSV import; replaces every earlier answer).
// storebound play data-safety [--csv out.csv] [--dry] [--force]
// --dry prints the answers that change (question [response]: old -> new, account and deletion answers first) against
// the CSV uploaded last (Play cannot read the form back), then the whole CSV (or --csv writes it). Template answers
// (cli/lib/template-answers.ts) are refused, in the dry run too.
// Answers come from `dataSafety` (and accounts.deletionUrl) in storebound.config.ts. --dry only writes / prints the CSV.
// Both --dry and a real run stop on a missing required row (dataSafetyIssues) before anything is sent.
// Play cannot read the form back, so a run compares with what this project uploaded last: the sha256 of that CSV
@ -9,7 +12,8 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import type { Ctx } from '../lib/ctx';
import { need } from '../lib/ctx';
import { dataSafetyCsv, dataSafetyProblem, dataSafetyRows } from '../lib/data-safety';
import { type DataSafetyChange, dataSafetyCsv, dataSafetyDiff, dataSafetyProblem, dataSafetyRows } from '../lib/data-safety';
import { refuseTemplate } from '../lib/template-answers';
import { DATA_SAFETY_UPLOADED } from '../lib/declarations';
import { done, log } from '../lib/log';
import { playClient } from './client';
@ -36,11 +40,24 @@ function rememberUpload(root: string, pkg: string, sha256: string): void {
writeFileSync(f, JSON.stringify(all, null, 2) + '\n');
}
const ACCOUNT_Q = /ACCOUNT|DELETION/;
/** The changed answers, account and deletion answers first, as `question [response]: old -> new` lines. Pure. */
export function changeLines(changes: DataSafetyChange[]): string[] {
const show = (v: string) => (v ? v : '(empty)');
const line = (c: DataSafetyChange) => ` ${c.question}${c.response ? ` [${c.response}]` : ''}: ${show(c.from)} -> ${show(c.to)}${c.label ? ` (${c.label.slice(0, 70)})` : ''}`;
const acc = changes.filter(c => ACCOUNT_Q.test(c.question)), rest = changes.filter(c => !ACCOUNT_Q.test(c.question));
return [...(acc.length ? [' account and deletion answers:', ...acc.map(line)] : []), ...(rest.length ? [' data types and other answers:', ...rest.map(line)] : [])];
}
export async function run(ctx: Ctx): Promise<void> {
const { config, root, resolve } = ctx.loaded;
const ds = need(config.dataSafety, 'dataSafety');
// template answers are never sent, and the dry run refuses the same way (it shows what a real run would do)
refuseTemplate(config, ['dataSafety', 'accounts'], `play data-safety${ctx.args.has('dry') ? ' --dry' : ''}`);
const csv = dataSafetyCsv(ds, config.accounts);
const problem = dataSafetyProblem(dataSafetyRows(ds, config.accounts));
const rows = dataSafetyRows(ds, config.accounts);
const problem = dataSafetyProblem(rows);
const out = ctx.args.get('csv');
if (out) { writeFileSync(resolve(out), csv); log('play', `wrote ${out}`); }
const types = Object.keys(config.dataSafety!.data);
@ -50,6 +67,11 @@ export async function run(ctx: Ctx): Promise<void> {
const last = lastUpload(root, pkg);
const same = last?.sha256 === hash;
if (ctx.args.has('dry')) {
const upFile = join(root, DATA_SAFETY_UPLOADED);
const before = existsSync(upFile) ? readFileSync(upFile, 'utf8') : null;
const changes = dataSafetyDiff(before, rows);
log('play', `changes against ${before ? `the last uploaded CSV (${DATA_SAFETY_UPLOADED}; Play cannot read the form back)` : 'nothing (no upload recorded: every answer is new)'}: ${changes.length} answer(s)`);
for (const l of changeLines(changes)) console.log(l);
if (!out) process.stdout.write(csv);
if (problem) throw new Error(problem);
log('play', same ? `the same CSV was uploaded ${last!.at.slice(0, 16).replace('T', ' ')} UTC: a real run sends nothing (--force sends it again)` : 'a real run uploads it (it differs from the last upload, or none is recorded)');