storebound/cli/lib/console-apple-group-plan.ts

84 lines
8.6 KiB
TypeScript

// `storebound console plan apple-app-group`: the App Store Connect API can switch the APP_GROUPS capability on for a
// bundle id but cannot assign a group to it, so an app extension (a packet-tunnel extension, a share extension, a
// widget) whose entitlements list `com.apple.security.application-groups` gets a profile with an empty group list until
// the group is ticked in the developer portal. Agent-first like the other plans: the agent drives the signed-in
// developer.apple.com page with its own browser tool, presses Save itself (it is a capability edit on the owner's own
// App ID, not a store submission: finalByAgent), then `storebound apple signing ensure --refresh-profile` and
// `storebound console verify apple-app-group` (read-only, API). Pure.
// Modeled on Apple's help ("Configure App Groups", "Register an App Group"), not yet recorded on a real portal page: the
// labels are where to start, the agent adapts.
import { bundleIdPortalUrl, PORTAL_IDENTIFIERS_URL } from './targets';
import { NEVER_PRESS } from './console-forms';
import type { ConsolePlan, PlanAnswer, PlanIds, PlanStep } from './console-plan';
export const PORTAL_REMOVE = ['Remove', 'Remove App ID', 'Delete', 'Delete identifier', 'Revoke', 'Revoke certificate', 'Download'];
const CHECK = 'document.querySelector("main")?.innerText ?? document.body.innerText';
export function appleAppGroupPlan(p: PlanIds): ConsolePlan {
const targets = p.appGroupTargets ?? [];
const one = targets.length === 1 ? targets[0]! : null;
const answers: PlanAnswer[] = [];
const steps: PlanStep[] = [];
const add = (s: Omit<PlanStep, 'n'>): void => { steps.push({ n: steps.length + 1, ...s }); };
const a = (x: PlanAnswer): void => { answers.push(x); };
if (!targets.length) {
a({ id: 'targets', question: 'Which bundle id needs which App Group(s)?', value: null, text: '(none found)', status: 'ask-owner', source: 'entitlements of the Xcode targets',
note: 'No target of the project lists com.apple.security.application-groups. Pass --bundle-id <id> --groups group.a,group.b, or list the extension in apple.signing.extensions with appGroups.' });
}
for (const t of targets) {
a({ id: `${t.bundleId}/capability`, question: `${t.bundleId}: App Groups capability`, value: true, text: 'on', status: 'certain', source: t.source ?? 'entitlements', control: 'checkbox', label: 'App Groups', action: 'tick', where: 'Capabilities list of the App ID page' });
for (const g of t.groups) a({ id: `${t.bundleId}/${g}`, question: `${t.bundleId}: App Group`, value: g, text: g, status: 'certain', source: t.source ?? 'entitlements', control: 'checkbox', label: g, action: 'tick', where: 'the App Group assignment dialog (App Groups > Configure)' });
}
const start = one?.resourceId
? { url: bundleIdPortalUrl(one.resourceId), deepLink: true, reach: [] as string[] }
: { url: PORTAL_IDENTIFIERS_URL, deepLink: false, reach: ['Certificates, Identifiers & Profiles > Identifiers', targets.length ? `search the bundle id (${targets.map(t => t.bundleId).join(', ')}) and open it` : 'search the bundle id and open it', 'with --lookup the plan gets the deep link (https://developer.apple.com/account/resources/identifiers/bundleId/edit/<resource id>)'] };
add({ text: `Open ${start.url} in the signed-in browser (never sign in, never type the account password: if the page asks to sign in, tell the owner).${start.deepLink ? '' : ' Identifiers > search the bundle id > open it.'}` });
for (const t of targets) {
const url = t.resourceId ? bundleIdPortalUrl(t.resourceId) : null;
const gs = t.groups.join(', ');
add({ text: `${t.bundleId}${url ? ` (${url})` : ''}: on the "Edit Your App ID Configuration" page, find App Groups in the Capabilities list and tick its checkbox if it is not ticked.`, answers: [`${t.bundleId}/capability`] });
add({ text: `${t.bundleId}: press "Configure" next to App Groups. In the dialog that lists the registered App Groups tick ${gs} (exact identifiers; tick nothing else), then press "Continue". A group that is not listed must be registered first: Identifiers > the blue + > App Groups > Continue > Description and Identifier (the group id of the sheet) > Continue > Register; then reopen this page.`, click: ['Configure', 'Continue'], answers: t.groups.map(g => `${t.bundleId}/${g}`) });
add({ text: `${t.bundleId}: press "Save" at the top right. If a dialog says the provisioning profiles of this App ID become invalid, press "Confirm" (a new profile is made by \`storebound apple signing ensure --refresh-profile\`). Read the page back: App Groups shows the configured count.`, click: ['Save', 'Confirm'] });
}
add({ text: `Then run \`storebound apple signing ensure --refresh-profile\` (a new profile that carries the group), and \`storebound console verify apple-app-group${one ? ` --bundle-id ${one.bundleId}` : ''}\` (read-only: reads the App ID and the newest profile through the API and checks every group).` });
return {
form: 'apple-app-group', store: 'apple', title: 'Apple Developer portal: assign App Groups to an App ID (the API cannot)',
ownerBatch: [], start, steps, answers,
askOwner: answers.filter(x => x.status !== 'certain').map(x => `${x.question}: ask the owner${x.note ? ` (${x.note})` : ''}`),
buttons: {
neverPress: [...new Set([...PORTAL_REMOVE, 'Revoke', 'Account', 'Certificates', 'Keys', ...NEVER_PRESS])],
final: 'Save', finalByAgent: true,
allowed: [
{ button: 'Configure', class: 'navigation', where: 'App Groups row: opens the group assignment dialog' },
{ button: 'Continue', class: 'navigation', where: 'the group assignment dialog and the App Group registration form' },
{ button: 'Register', class: 'portal-register', where: 'App Groups registration form only: registers the group id of the answer sheet (a new group identifier, nothing public)' },
{ button: 'Save', class: 'portal-save', where: 'top right of the App ID page: saves the capability edit (the agent presses it; not a store submission)' },
{ button: 'Confirm', class: 'portal-save', where: 'the dialog that warns the profiles become invalid after Save' },
],
rule: 'The agent presses everything it needs here, including Save (a capability edit on the owner\'s own App ID; no store submission, nothing public). Never press Remove / Delete / Revoke, never untick or edit another capability, never touch Certificates, Keys or Account pages; stop and tell the owner if the page asks to sign in.',
},
uiFacts: [
'Modeled on Apple\'s help pages (not recorded on a real portal page yet): Certificates, Identifiers & Profiles > Identifiers > the App ID > Capabilities list > App Groups (checkbox) > Configure > the group list > Continue > Save (top right) > Confirm.',
'The App Store Connect API can enable the APP_GROUPS capability (POST /v1/bundleIdCapabilities) but has no field that assigns a group: an extension\'s profile has an empty application-groups list until the group is ticked here, and the archive then fails at codesign.',
'Saving the App ID invalidates its provisioning profiles; manual signing needs `storebound apple signing ensure --refresh-profile`, automatic signing lets Xcode renew its managed profile at archive time.',
'The deep link uses the bundle id\'s App Store Connect API resource id: https://developer.apple.com/account/resources/identifiers/bundleId/edit/<resource id> (`console plan apple-app-group --bundle-id <id> --lookup` reads it).',
'Group identifiers start with group. and are registered once per team under Identifiers > App Groups.',
],
driving: [
'Use your own browser tool (Claude in Chrome with the owner\'s signed-in session). Read the page as text or an accessibility tree first; click by visible text; adapt when the page differs from these steps, but never press anything in buttons.neverPress.',
'Tick boxes by clicking the checkbox, then read its checked state back. After Save read the App ID page again.',
'Press Save yourself (buttons.finalByAgent): it is not a store submission. Never sign in, never type passwords or 2FA codes.',
],
inPageLoop: '',
verify: {
command: `storebound console verify apple-app-group${one ? ` --bundle-id ${one.bundleId}` : ''}`, source: 'api', capture: CHECK,
expect: ['Every App Group of the answer sheet is on the App ID\'s newest readable profile (re-download after `storebound apple signing ensure --refresh-profile`) or named by the App Groups capability settings.', 'Exit code 1 while a group is missing or cannot be proven.'],
},
fillable: true,
guidance: ['Open: developer.apple.com > Account > Certificates, Identifiers & Profiles > Identifiers > the App ID'],
};
}