apple signing: App Group association fails early under automatic signing too (API capability + newest profile, steps and deep link, exit before the archive); console plan/verify apple-app-group

This commit is contained in:
CRTkafa 2026-10-25 22:34:14 +07:00
parent d978bb7a8b
commit 304407e523
20 changed files with 590 additions and 60 deletions

View file

@ -110,17 +110,20 @@ export function appGroupGaps(want: string[], profileEnt: Record<string, unknown>
/** The developer portal page of a bundle id (the id is the App Store Connect API resource id of the bundle id). */
export const bundleIdPortalUrl = (resourceId: string): string => `https://developer.apple.com/account/resources/identifiers/bundleId/edit/${encodeURIComponent(resourceId)}`;
export interface AppGroupGap { bundleId: string; resourceId: string | null; groups: string[]; url: string | null }
export interface AppGroupGap { bundleId: string; resourceId: string | null; groups: string[]; url: string | null; /** why it is a gap / not proven (one line) */ why?: string }
/** The agent-first steps to tick the groups (the API cannot do it); the never-press list is part of the text. */
export const PORTAL_IDENTIFIERS_URL = 'https://developer.apple.com/account/resources/identifiers/list';
/** The agent-first steps to tick the groups (the API cannot do it); the never-press list is part of the text.
* The first line is the headline, the rest are the steps. */
export function appGroupSteps(g: AppGroupGap): string[] {
const url = g.url ?? 'https://developer.apple.com/account/resources/identifiers/list';
const url = g.url ?? PORTAL_IDENTIFIERS_URL;
return [
`${g.bundleId}: the profile lacks App Group(s) ${g.groups.join(', ')} (the API can enable the App Groups capability but cannot assign a group to a bundle id).`,
`Open ${url} in the signed-in browser (agent-first: you drive it).`,
'Capabilities list > App Groups: tick it if it is not ticked, press Configure, tick each group above (Continue if the group is new: register it first under Identifiers > App Groups), Continue, then Save at the top right (and Confirm in the dialog that says the profiles get invalidated).',
'Never press: Remove (the bundle id), Delete, Revoke, or any Certificates / Keys / Account button.',
`Then run: storebound apple signing ensure --refresh-profile (makes a new profile that carries the group), and storebound apple signing status to read it back.`,
`${g.bundleId}: ${g.why ?? `the profile lacks App Group(s) ${g.groups.join(', ')}`} (the App Store Connect API can enable the App Groups capability but cannot assign a group to a bundle id).`,
`Open ${url} in the signed-in browser (agent-first: you drive it; plan: storebound console plan apple-app-group --bundle-id ${g.bundleId}${g.url ? '' : ' --lookup'}).`,
`Certificates, Identifiers & Profiles > Identifiers > ${g.bundleId} > App Groups (tick it if it is not ticked) > Configure > tick ${g.groups.join(', ')} (register a new group first under Identifiers > App Groups) > Continue > Save (and Confirm in the dialog that says the profiles get invalidated).`,
'Never press: Remove (the bundle id), Delete, Revoke, or any Certificates / Keys / Account button; change no other capability.',
'Then run: storebound apple signing ensure --refresh-profile (makes a new profile that carries the group), and storebound apple signing status to read it back.',
];
}
@ -136,3 +139,46 @@ export function appGroupGapsFor(targets: { bundleId: string; appGroups: string[]
}
return out;
}
/** What the API (or an installed profile) shows about one App ID's App Group association. */
export interface GroupEvidence {
/** App Store Connect API resource id of the bundle id (the portal deep link); null: not registered / unknown */
resourceId: string | null;
/** the APP_GROUPS capability is on the App ID (null: unknown) */
capabilityOn: boolean | null;
/** the raw settings of the APP_GROUPS capability (any field that names a group proves it) */
capabilitySettings?: unknown;
/** Entitlements of the newest readable profile of the bundle id (a managed one too); null: none readable */
profileEnt: Record<string, unknown> | null;
}
/** Groups of `want` that the capability's settings name (the API has no documented App Group field; if a field ever
* lists them, this proves the association without a profile). */
export const capabilityGroups = (settings: unknown, want: string[]): string[] => {
if (settings === null || settings === undefined) return [];
const text = JSON.stringify(settings);
return want.filter(g => text.includes(g));
};
export interface GroupVerdicts { gaps: AppGroupGap[]; unproven: AppGroupGap[]; ok: string[] }
/**
* Every target whose entitlements list App Groups: proven (a readable profile carries each group, or the capability
* settings name it), a gap (the evidence shows the group missing: a readable profile without it, or the capability
* off) or unproven (nothing readable: the API cannot show the association). Pure.
*/
export function appGroupVerdicts(targets: { bundleId: string; appGroups: string[] }[], evidence: (bundleId: string) => GroupEvidence | null): GroupVerdicts {
const out: GroupVerdicts = { gaps: [], unproven: [], ok: [] };
for (const t of targets) {
if (!t.appGroups.length) continue;
const ev = evidence(t.bundleId);
const rid = ev?.resourceId ?? null, url = rid ? bundleIdPortalUrl(rid) : null;
const viaCap = new Set(capabilityGroups(ev?.capabilitySettings, t.appGroups));
const missing = (ev?.profileEnt ? appGroupGaps(t.appGroups, ev.profileEnt) : t.appGroups).filter(g => !viaCap.has(g));
if (!missing.length) { out.ok.push(t.bundleId); continue; }
if (ev?.capabilityOn === false) out.gaps.push({ bundleId: t.bundleId, resourceId: rid, groups: missing, url, why: `the App Groups capability is not enabled on the App ID, so ${missing.join(', ')} is not associated` });
else if (ev?.profileEnt) out.gaps.push({ bundleId: t.bundleId, resourceId: rid, groups: missing, url, why: `the newest profile lacks App Group(s) ${missing.join(', ')} (an empty application-groups list means the group is not ticked on the App ID)` });
else out.unproven.push({ bundleId: t.bundleId, resourceId: rid, groups: missing, url, why: ev ? `App Group(s) ${missing.join(', ')} cannot be proven: the API shows no group on the App ID's capability and no readable profile` : `App Group(s) ${missing.join(', ')} cannot be proven: the App ID is not registered or could not be read` });
}
return out;
}