- `storebound console plan <form> [--json] [--lookup]`: read-only plan for an AI agent that drives the browser itself: start URL (deep links), ordered steps, answer sheet with the console's exact labels (certain / ask owner, curly quotes noted), never-press list, verified UI facts, driving guidance (in-page async loop + window.__log), verify instructions. All six forms. - `storebound console verify <form> [--from-text <file|->]`: App Privacy per-type summaries from page text, Play Create app from input states, content rating / target audience summaries, App Store record and age rating from the API; exit 1 on a mismatch. - MCP: console_plan, console_verify (read-only); console fill stays off MCP. - App Privacy (verified 2026-10-12): two information steps, unlabeled purpose checkboxes (nearest parent text), "Definitions and Examples" never pressed; fixture and engine follow. - Play Create app (verified 2026-10-12): /developers/<id>/create-new-app, App package name + read-only Check availability, material-radio inner [role=radio] click, app dashboard URL and play.appId, service account "Add app" as an owner step. - console-autofill skill rewritten agent-first; agents-core rule 10; README; pitfalls 12, 18 (confirmed sign-out), 19; CHANGELOG; play-first-release.
114 lines
9.2 KiB
HTML
114 lines
9.2 KiB
HTML
<!doctype html>
|
|
<!-- App Store Connect > App Privacy after the data types were saved. The PAGE mirrors the STRUCTURE of a real console
|
|
page (sanitized debug dump, October 2026; structure only, no names, ids or data copied): an <h2> "App Privacy" with
|
|
Publish next to it; an <h3> "Privacy Policy" with its Edit and a "?" button; an <h3> "Data Types" in its own box with
|
|
its Edit and a one-line summary; then, in the NEXT box (not under the heading's box), the category names and one
|
|
row per data type as plain <p> texts: the type label, a "?" help button (aria-label "More information") and
|
|
"Set Up <type>", a <p> inside a div with the click handler (no button role, no link). Purchase History's row has
|
|
only its "Set Up" (the console shows the category "Purchases" above it).
|
|
The per-type DIALOG follows the flow verified on a real console (October 2026, structure only): a <dialog> whose h2
|
|
is the type name; step 1 purposes: 6 checkboxes in Apple's order with NO accessible name (the label and its help
|
|
text sit in a sibling <label> without for=, run together), Cancel / Next; step 2 linked: radios "Yes, <type lower>
|
|
data collected from this app is linked to the user’s identity" / "No, ...", Back / Cancel / Next; step 3 an info
|
|
page "Let’s define two important terms..." and step 4 an info page "To help put tracking into context, here are a
|
|
few examples:" (no inputs, Back / Cancel / Next); step 5 tracking: radios "Yes, we use <type lower> data for
|
|
tracking purposes" / "No, we do not use ...", "Definitions and Examples" (help, never clicked) / Back / Cancel /
|
|
Save. Saving closes the dialog; the row then shows an Edit and the summary ("Used for <purposes>", "Linked to the
|
|
user’s identity" or "Not linked to the user’s identity", "Used for tracking purposes" or "Not used for tracking
|
|
purposes"), which `console verify` reads.
|
|
?unknown=<type>: that type's second step is one storebound does not know (switches). ?preset=<type>:<purpose>|...
|
|
(repeatable): that type was set up before with these purposes.
|
|
window.__setups: the "Set Up" texts clicked; window.__answers: what each Save stored; window.__saves. -->
|
|
<html lang="en"><head><meta charset="utf-8"><title>App Store Connect</title><script src="/_common.js"></script></head>
|
|
<body>
|
|
<nav aria-label="Apps"><ul><li><a href="#">App Information</a></li><li><a href="#">App Privacy</a></li><li><a href="#">Pricing and Availability</a></li></ul></nav>
|
|
<main>
|
|
<div><div><h2>App Privacy</h2></div><div><button type="button" id="publish">Publish</button></div></div>
|
|
<hr>
|
|
<div>
|
|
<div><h3><span>Privacy Policy</span></h3><button role="button" type="button" id="pp-edit">Edit</button>
|
|
<div><button role="button" type="button" aria-expanded="false" aria-haspopup="menu">English (U.S.)</button><button type="button" role="button">?</button></div></div>
|
|
<div><div><label for="privacyPolicyUrl">Privacy Policy URL</label><button type="button" role="button" aria-label="More information">?</button></div><p>https://example.com/privacy</p></div>
|
|
</div>
|
|
<hr>
|
|
<div><div><h3><span>Data Types</span></h3><button role="button" type="button" id="dt-edit">Edit</button></div><ul><li id="summary"></li></ul></div>
|
|
<div id="grid"></div>
|
|
</main>
|
|
<div id="dialogs"></div>
|
|
<script>
|
|
// [category, type label in the row (null: the row has only "Set Up"), the type in "Set Up <type>"]
|
|
const ROWS = [
|
|
['Location', 'Coarse Location', 'Coarse Location'],
|
|
['User Content', 'Emails or Text Messages', 'Emails or Text Messages'],
|
|
['Identifiers', 'Device ID', 'Device ID'],
|
|
['Purchases', null, 'Purchase History'],
|
|
['Usage Data', 'Product Interaction', 'Product Interaction'],
|
|
['Diagnostics', 'Crash Data', 'Crash Data'],
|
|
];
|
|
const PURPOSES = ['Third-Party Advertising', 'Developer’s Advertising or Marketing', 'Analytics', 'Product Personalization', 'App Functionality', 'Other Purposes'];
|
|
const q = new URLSearchParams(location.search);
|
|
const unknown = q.get('unknown');
|
|
const preset = Object.fromEntries(q.getAll('preset').map(v => { const [t, ps] = v.split(':'); return [t, ps ? ps.split('|') : []]; }));
|
|
const $ = id => document.getElementById(id);
|
|
window.__setups = []; window.__answers = {}; window.__saves = 0;
|
|
$('summary').textContent = `${ROWS.length} data types collected from this app: ${ROWS.map(r => r[2]).join(', ')}`;
|
|
let cat = null;
|
|
for (const [c, label, type] of ROWS) {
|
|
if (c !== cat) { cat = c; const h = document.createElement('div'); h.innerHTML = `<hr><div><div></div><p>${c}</p></div>`; $('grid').append(h); }
|
|
const row = document.createElement('div');
|
|
row.dataset.type = type;
|
|
row.innerHTML = (label ? `<div><p>${label}</p><div><button tabindex="0" role="button" type="button" aria-expanded="false" aria-label="More information">?</button></div></div>` : '') + '<div class="action"></div>';
|
|
$('grid').append(row);
|
|
if (preset[type]) setUpDone(type, preset[type]); else setUpOpen(type);
|
|
}
|
|
function setUpOpen(type) {
|
|
const a = document.querySelector(`[data-type="${type}"] .action`);
|
|
a.innerHTML = `<p>Set Up ${type}</p>`;
|
|
a.onclick = () => { window.__setups.push(a.textContent.trim()); openType(type); };
|
|
}
|
|
function setUpDone(type, purposes) {
|
|
const a = document.querySelector(`[data-type="${type}"] .action`);
|
|
a.onclick = null;
|
|
const ans = window.__answers[type];
|
|
a.innerHTML = `<p>Used for ${purposes.join(', ')}</p>`
|
|
+ (ans ? `<p>${ans.linked === 'yes' ? 'Linked' : 'Not linked'} to the user’s identity</p><p>${ans.tracking === 'yes' ? 'Used' : 'Not used'} for tracking purposes</p>` : '')
|
|
+ `<button type="button" role="button">Edit</button>`;
|
|
a.querySelector('button').addEventListener('click', () => openType(type));
|
|
}
|
|
// the per-type dialog: purposes > Next > linked > Next > definitions > Next > examples (both text only) > Next > tracking > Save
|
|
function openType(type) {
|
|
const low = type.toLowerCase();
|
|
const d = document.createElement('dialog');
|
|
d.setAttribute('role', 'dialog'); d.setAttribute('aria-modal', 'true'); d.setAttribute('aria-labelledby', 'modal-header');
|
|
const radio = (name, v, html) => `<div><input type="radio" name="${name}" id="${name}_${v}" value="${v}"><div><label for="${name}_${v}"><span>${html}</span></label></div></div>`;
|
|
const step2 = unknown === type
|
|
? `<p>Which kinds of identity does ${low} carry?</p><span role="switch" aria-checked="false" tabindex="0">Account identity</span><span role="switch" aria-checked="false" tabindex="0">Device identity</span>`
|
|
: radio('linked', 'yes', `<strong>Yes</strong>, ${low} data collected from this app is linked to the user’s identity`) + radio('linked', 'no', `<strong>No</strong>, ${low} data collected from this app is not linked to the user’s identity`);
|
|
d.innerHTML = `<div role="document"><h2 id="modal-header"><span>${type}</span></h2>`
|
|
+ `<div data-p="1"><p>Indicate how ${low} collected from this app is being used by you or your third-party partners.</p>`
|
|
+ PURPOSES.map((p, i) => `<div><input type="checkbox" id="purpose_${i}"><div><label><p>${p}</p><p>Description of ${p}</p></label></div></div>`).join('')
|
|
+ `<div><button type="button">Cancel</button><button type="button" data-go="2">Next</button></div></div>`
|
|
+ `<div data-p="2" hidden>${step2}<div><button type="button" data-go="1">Back</button><button type="button">Cancel</button><button type="button" data-go="3">Next</button></div></div>`
|
|
+ `<div data-p="3" hidden><p>Let’s define two important terms before continuing.</p><h5>Tracking</h5><p>Linking data collected from your app with third-party data.</p><h5>Third-Party Data</h5><p>Data about a user collected from other companies.</p><div><button type="button" data-go="2">Back</button><button type="button">Cancel</button><button type="button" data-go="4">Next</button></div></div>`
|
|
+ `<div data-p="4" hidden><p>To help put tracking into context, here are a few examples:</p><ul><li>Displaying targeted advertisements.</li><li>Sharing location data with a data broker.</li></ul><div><button type="button" data-go="3">Back</button><button type="button">Cancel</button><button type="button" data-go="5">Next</button></div></div>`
|
|
+ `<div data-p="5" hidden>`
|
|
+ radio('tracking', 'yes', `<strong>Yes</strong>, we use ${low} data for tracking purposes`) + radio('tracking', 'no', `<strong>No</strong>, we do not use ${low} data for tracking purposes`)
|
|
+ `<div><button type="button" data-help>Definitions and Examples</button><button type="button" data-go="4">Back</button><button type="button">Cancel</button><button type="button" data-save>Save</button></div></div></div>`;
|
|
d.setAttribute('open', '');
|
|
d.addEventListener('click', e => {
|
|
const go = e.target.closest('[data-go]')?.getAttribute('data-go');
|
|
if (go) d.querySelectorAll('[data-p]').forEach(p => { p.hidden = p.getAttribute('data-p') !== go; });
|
|
if (e.target.closest('button')?.textContent === 'Cancel') d.remove();
|
|
if (e.target.closest('[data-save]')) {
|
|
const purposes = PURPOSES.filter((p, i) => d.querySelector(`#purpose_${i}`).checked);
|
|
const val = n => d.querySelector(`input[name="${n}"]:checked`)?.value ?? null;
|
|
window.__answers[type] = { purposes, linked: val('linked'), tracking: val('tracking') };
|
|
window.__saves++;
|
|
d.remove();
|
|
setUpDone(type, purposes);
|
|
}
|
|
});
|
|
$('dialogs').append(d);
|
|
}
|
|
</script>
|
|
</body></html>
|