apple submit --build <n> (first submission, the resubmit steps; plan table with --json under serve); desktop owner actions to send for review: owner.apple_submit, owner.apple_resubmit, owner.apple_iap_submit, owner.play_production, and the price gate owner actions (owner.prices_play_products / _play_subscriptions / _apple_subscriptions; owner.prices_import passes --allow-price-changes after the dialog); OWNER_ALLOWED_ARGS ties each forbidden flag to its one action, dry runs never carry owner flags, MCP still has no submit or production release; submit dialog text in the host; system.tools androidSdk, project.inspect playRelease, declarations dataDeletion
This commit is contained in:
parent
2e7dd44ae6
commit
33eb24a3dc
17 changed files with 335 additions and 49 deletions
|
|
@ -6,7 +6,7 @@
|
|||
// config does not prove is an "ask the owner" item, never a guess (AGENTS.md rule 5). Pure.
|
||||
import { parseCsv } from './csv';
|
||||
import { type AccountSignUp, type DataSafety, type PlayPurpose, profileOf, type ShipConfig } from './config';
|
||||
import { CATEGORIES, dataSafetyCsv, dataSafetyIssues, dataSafetyRows } from './data-safety';
|
||||
import { CATEGORIES, DATA_DELETION_RESPONSE, dataDeletionResponse, dataSafetyCsv, dataSafetyIssues, dataSafetyRows } from './data-safety';
|
||||
import type { DeclarationsResult, DeclAsk } from './desktop-protocol';
|
||||
import { NOTES_MAX_BYTES, reviewNotes } from './review-details';
|
||||
|
||||
|
|
@ -112,6 +112,16 @@ export interface DeclarationInputs {
|
|||
uploaded: { csv: string; at: string } | null;
|
||||
}
|
||||
|
||||
/** The general data deletion question (PSL_SUPPORT_DATA_DELETION_BY_USER) as the CSV answers it, where the answer
|
||||
* comes from and the "Delete data URL" it sends. Pure. */
|
||||
export function deletionAnswer(ds: DataSafety, deletionUrl?: string): { dataDeletion: 'yes' | 'no' | 'auto90' | null; dataDeletionSource: string | null; dataDeletionUrl: string | null } {
|
||||
const r = dataDeletionResponse(ds, deletionUrl ? { deletionUrl } : undefined);
|
||||
const dataDeletion = (Object.entries(DATA_DELETION_RESPONSE).find(([, v]) => v === r)?.[0] ?? null) as 'yes' | 'no' | 'auto90' | null;
|
||||
const dataDeletionSource = ds.dataDeletion ? 'dataSafety.dataDeletion' : deletionUrl ? 'accounts.deletionUrl' : ds.deletionRequest !== undefined ? 'dataSafety.deletionRequest'
|
||||
: ds.partialDeletion === true || ds.partialDeletion === 'auto90' ? 'dataSafety.partialDeletion' : null;
|
||||
return { dataDeletion, dataDeletionSource, dataDeletionUrl: dataDeletion === 'yes' ? ds.partialDeletionUrl ?? deletionUrl ?? null : null };
|
||||
}
|
||||
|
||||
export function declarations(config: ShipConfig, inp: DeclarationInputs): DeclarationsResult {
|
||||
const ask: DeclAsk[] = [];
|
||||
const play = !!config.play, apple = !!config.apple;
|
||||
|
|
@ -128,6 +138,7 @@ export function declarations(config: ShipConfig, inp: DeclarationInputs): Declar
|
|||
const types = Object.entries(ds.data).map(([id, u]) => ({ id, category: categoryOf(id), shared: !!u.shared, ephemeral: !!u.ephemeral, optional: !!u.optional, collect: [...u.collect], share: [...(u.shared ? u.share ?? [] : [])] }));
|
||||
dataSafety = {
|
||||
collectsData: ds.collectsData, encryptedInTransit: ds.encryptedInTransit, partialDeletion: ds.partialDeletion ?? null, accountCreation: ds.accountCreation ?? ['PSL_ACM_NONE'],
|
||||
...deletionAnswer(ds, config.accounts?.deletionUrl),
|
||||
types, extra: ds.extra?.length ?? 0, csv, rows: csv ? csv.trim().split('\n').length - 1 : 0,
|
||||
uploaded: inp.uploaded && csv ? { at: inp.uploaded.at, changes: dataSafetyChanges(inp.uploaded.csv, csv) } : null,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -100,6 +100,9 @@ export interface ProjectSummary {
|
|||
variants?: string[];
|
||||
/** play.reportsBucket is set (Play statistics for analytics snapshots). */
|
||||
playReports?: boolean;
|
||||
/** Play production settings from the config: play.managedPublishing (null: not set, Play's default off), the number
|
||||
* of play.countries (null: all countries / regions) and the App category (play.category or the App Store match). */
|
||||
playRelease?: { managedPublishing: boolean | null; countries: number | null; category: string | null } | null;
|
||||
}
|
||||
|
||||
// ---- App-centric model (one app = one product on up to two platforms; a workspace holds many apps) ----
|
||||
|
|
@ -271,6 +274,12 @@ export interface DeclarationsResult {
|
|||
collectsData: boolean; encryptedInTransit: boolean; accountCreation: string[];
|
||||
/** dataSafety.partialDeletion (PSL_SUPPORT_DATA_DELETION_BY_USER; asked once there are accounts); null: not answered. */
|
||||
partialDeletion: boolean | 'auto90' | null;
|
||||
/** PSL_SUPPORT_DATA_DELETION_BY_USER ("can users request that their data is deleted?") as the CSV answers it
|
||||
* (null: not answered), where the answer comes from (dataSafety.dataDeletion, accounts.deletionUrl, ...) and the
|
||||
* Delete data URL it sends. Optional: older sidecars do not send them. */
|
||||
dataDeletion?: 'yes' | 'no' | 'auto90' | null;
|
||||
dataDeletionSource?: string | null;
|
||||
dataDeletionUrl?: string | null;
|
||||
types: { id: string; category: string; shared: boolean; ephemeral: boolean; optional: boolean; collect: string[]; share: string[] }[];
|
||||
/** Extra CSV rows of questions storebound does not model. */
|
||||
extra: number;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue