store-only apps edit their listing on the store (storebound listing live / set: old -> new per field with the checks, a PATCH of only the edited fields; serve listing.set is the one store-only write, its token bound to the store ids; policy.rs STORE_ONLY_WRITES, write_with_confirm for store targets); screenshot captions per language and scene edited as a text edit of the shots copy JSON (JSON files keep their layout); Play content rating, target audience and App content answers in App info; docs/console-parity.md after the editors (73 rows: 55 complete, 18 with the reason) and tests/console-parity.test.ts keeping it honest
This commit is contained in:
parent
3cea2af4e1
commit
7ac028421a
33 changed files with 852 additions and 157 deletions
20
cli/serve.ts
20
cli/serve.ts
|
|
@ -51,12 +51,12 @@ export const FORBIDDEN_METHOD_WORDS = /submit|release|publish|rollout|resume|com
|
|||
/** Read tools whose command prints a structured result with --json. */
|
||||
export const JSON_TOOLS = new Set(['check', 'check_aso', 'aso_audit', 'aso_suggest', 'aso_competitors', 'scan', 'validate', 'health', 'health_issue', 'reviews_list', 'prices_diff', 'analytics_compare', 'apple_status',
|
||||
'apple_next_build', 'play_status', 'play_next_version_code', 'shots_check', 'apple_testflight_status', 'apple_ppo_status', 'catalog_diff',
|
||||
'project_icon', 'project_listings', 'reviews_drafts', 'project_declarations', 'project_listing_text', 'project_renders', 'project_render', 'prices_current', 'project_editable']);
|
||||
'project_icon', 'project_listings', 'reviews_drafts', 'project_declarations', 'project_listing_text', 'project_renders', 'project_render', 'prices_current', 'project_editable', 'listing_live']);
|
||||
|
||||
/** Console forms `storebound console fill` knows (cli/console/fill.ts). */
|
||||
export const CONSOLE_FORMS = ['apple-app-record', 'apple-age-rating', 'apple-app-privacy', 'play-app-record', 'play-content-rating', 'play-target-audience'];
|
||||
/** Write tools that print a structured result with --json in the dry run and the real run (console.fill: the answers). */
|
||||
export const JSON_WRITES = new Set(['console_fill', 'owner_apple_submit', 'owner_apple_resubmit', 'project_edit']);
|
||||
export const JSON_WRITES = new Set(['console_fill', 'owner_apple_submit', 'owner_apple_resubmit', 'project_edit', 'listing_set']);
|
||||
/** Write tools whose command may ask its own question (confirm()): a verified host token answers it, as for owner actions. */
|
||||
const TOKEN_ANSWERS = new Set(['console_fill']);
|
||||
|
||||
|
|
@ -104,6 +104,13 @@ export const DESKTOP_ONLY: ToolDef[] = [
|
|||
description: 'Writes edits of the app folder\'s sources of truth: config values by path (the rest of the file, comments and formatting kept), listing fields per store and language (store/listing.md, store/appstore.md), the App Review contact (.storebound/credentials.json). The dry run shows old -> new, the diff per file and the checks (store limits, required fields, keyword and metadata rules, the whole config validated); an error stops the write. Nothing goes to a store and nothing is committed.',
|
||||
props: { ops: { type: 'string', pattern: '^[\\[{][\\s\\S]{1,400000}$', description: 'The edits as JSON (one edit or a list; see `storebound edit`)' } },
|
||||
required: ['ops'], args: i => ['--ops', String(i.ops)] },
|
||||
{ name: 'listing_live', title: 'Live listing texts', command: ['listing', 'live'], write: false, network: true,
|
||||
description: 'Read-only: the live listing texts per language on both stores (Google Play title, short and full description; App Store name, subtitle, description, keywords, promotional text, what\'s new of the editable version, else the newest).' },
|
||||
{ name: 'listing_set', title: 'Change listing texts on a store', command: ['listing', 'set'], write: true, network: true,
|
||||
description: 'Changes the given text fields of one language straight on one store (no listing files: store-only apps): Google Play one edit with a PATCH of that listing, committed; App Store the localization of the app info being prepared and of the editable version (promotional text also on the live one). The dry run reads the store and shows old -> new per field with the checks (store limits, keyword and metadata rules); an error stops it. Not public until the store reviews it.',
|
||||
props: { store: { type: 'string', enum: ['play', 'apple'] }, lang: { type: 'string', pattern: '^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8}){0,3}$', description: 'Store language code (en-US, tr-TR, zh-Hans)' },
|
||||
fields: { type: 'string', pattern: '^\\{[\\s\\S]{1,60000}$', description: 'The fields as JSON, e.g. {"short":"..."}' } },
|
||||
required: ['store', 'lang', 'fields'], args: i => ['--store', String(i.store), '--lang', String(i.lang), '--fields', String(i.fields)] },
|
||||
{ name: 'console_fill', title: 'Fill in a console form', command: ['console', 'fill'], write: true, network: true,
|
||||
description: 'Fills one store console form that has no API (App Store app record, age rating, App Privacy; Play content rating, target audience) from the config in a visible Chromium the owner signs in to; uncertain fields are highlighted. It moves through the pages itself (Next; App Privacy: Get Started, Edit, each data type and the draft Save in its dialogs; content rating: Save draft) and never submits or publishes: the owner reviews every field and presses the final button (Publish, Submit, Save or Create). The dry run lists the answers; the real run lasts until the owner closes the browser.',
|
||||
props: { form: { type: 'string', enum: CONSOLE_FORMS, description: 'The console form' }, store: { type: 'string', enum: ['apple', 'play'], description: 'Default: from the form' },
|
||||
|
|
@ -239,7 +246,10 @@ const SYSTEM: MethodDescriptor[] = [
|
|||
];
|
||||
|
||||
/** Read methods a store-only app (params.virtual, no app folder) may use: store reads that need only the store ids. */
|
||||
export const VIRTUAL_READS = new Set(['apple_status', 'play_status', 'health', 'health_issue', 'reviews_list', 'apple_testflight_status', 'apple_ppo_status']);
|
||||
export const VIRTUAL_READS = new Set(['apple_status', 'play_status', 'health', 'health_issue', 'reviews_list', 'apple_testflight_status', 'apple_ppo_status', 'listing_live']);
|
||||
/** Writes a store-only app may run (no app folder: they send only what the call carries, never the generated config's
|
||||
* placeholder links). The confirm token binds them to the store ids (`virtual`) instead of a folder. */
|
||||
export const VIRTUAL_WRITES = new Set(['listing_set']);
|
||||
|
||||
const PKG_RE = /^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z0-9_]+)+$/;
|
||||
const BUNDLE_RE = /^[A-Za-z0-9][A-Za-z0-9.-]{0,154}$/;
|
||||
|
|
@ -413,7 +423,7 @@ export class Session {
|
|||
let project: string;
|
||||
if (params.virtual !== undefined) {
|
||||
if (params.project !== undefined) throw new RpcError(ERR.invalidParams, 'project or virtual, not both');
|
||||
if (t.write || !VIRTUAL_READS.has(t.name)) throw new RpcError(ERR.invalidParams, `${method}: needs an app folder (store-only apps have store reads only)`);
|
||||
if (t.write ? !VIRTUAL_WRITES.has(t.name) : !VIRTUAL_READS.has(t.name)) throw new RpcError(ERR.invalidParams, `${method}: needs an app folder (store-only apps have store reads only)`);
|
||||
project = virtualDir(checkVirtual(params.virtual));
|
||||
} else project = NEW_PROJECT.has(t.name) ? checkNewProject(params.project) : checkProject(params.project);
|
||||
const args = (params.args ?? {}) as Json;
|
||||
|
|
@ -428,7 +438,7 @@ export class Session {
|
|||
else {
|
||||
if (params.confirmToken === undefined) throw new RpcError(ERR.confirmRequired, `${method} writes: dryRun false needs a confirmToken from the desktop host`);
|
||||
// Bound to the project string exactly as sent (the host mints over what it sends) and the args object.
|
||||
const v = this.guard!.verify(params.confirmToken, method, { project: params.project, args });
|
||||
const v = this.guard!.verify(params.confirmToken, method, params.virtual !== undefined ? { virtual: params.virtual, args } : { project: params.project, args });
|
||||
if (!v.ok) throw new RpcError(ERR.confirmInvalid, `confirm token rejected: ${v.error}`, { reason: v.error });
|
||||
dryRun = false;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue