84 lines
8.6 KiB
TypeScript
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'],
|
|
};
|
|
}
|