apple signing: every signable target (extensions, App Group check), build keychain unlock over SSH, api confirm docs
This commit is contained in:
parent
2e1c7cdc3c
commit
779cbdfcfd
17 changed files with 829 additions and 42 deletions
|
|
@ -43,6 +43,20 @@ export interface AppleSigning {
|
|||
capabilities?: (CapabilityType | CapabilitySpec)[];
|
||||
/** Bundle ids of app extensions / widgets / watch apps in the same archive: each gets its own profile. */
|
||||
extraBundleIds?: string[];
|
||||
/** Override for the app extensions / widgets / watch app / App Clip that `signing ensure` finds in the Xcode project
|
||||
* (every target's PRODUCT_BUNDLE_IDENTIFIER and entitlements): when set, these replace the detected ones. Each is
|
||||
* registered as a bundle id, gets the capabilities its entitlements need (or `capabilities` here) and a profile. */
|
||||
extensions?: ExtensionSpec[];
|
||||
}
|
||||
|
||||
export interface ExtensionSpec {
|
||||
bundleId: string;
|
||||
/** A label for messages and the bundle id's name on the developer portal. */
|
||||
name?: string;
|
||||
/** Default: the capabilities of the target's entitlements file (none when there is none). */
|
||||
capabilities?: (CapabilityType | CapabilitySpec)[];
|
||||
/** App group ids the target's entitlements list (com.apple.security.application-groups); checked against the profile. */
|
||||
appGroups?: string[];
|
||||
}
|
||||
|
||||
export function validateSigning(s: AppleSigning | undefined): string[] {
|
||||
|
|
@ -56,6 +70,14 @@ export function validateSigning(s: AppleSigning | undefined): string[] {
|
|||
const t = typeof c === 'string' ? c : c?.type;
|
||||
if (!CAPABILITY_TYPES.includes(t as CapabilityType)) e.push(`apple.signing.capabilities: unknown capability "${String(t)}" (${CAPABILITY_TYPES.slice(0, 6).join(', ')}, ...)`);
|
||||
}
|
||||
for (const x of s.extensions ?? []) {
|
||||
if (!x || typeof x.bundleId !== 'string' || !/^[A-Za-z0-9.-]+$/.test(x.bundleId)) { e.push(`apple.signing.extensions: "${String(x?.bundleId)}" is not a bundle id`); continue; }
|
||||
for (const c of x.capabilities ?? []) {
|
||||
const t = typeof c === 'string' ? c : c?.type;
|
||||
if (!CAPABILITY_TYPES.includes(t as CapabilityType)) e.push(`apple.signing.extensions ${x.bundleId}: unknown capability "${String(t)}"`);
|
||||
}
|
||||
if (x.appGroups && (!Array.isArray(x.appGroups) || x.appGroups.some(g => typeof g !== 'string' || !/^group\.[A-Za-z0-9.-]+$/.test(g)))) e.push(`apple.signing.extensions ${x.bundleId}: appGroups are ids like group.com.example.app`);
|
||||
}
|
||||
for (const b of s.extraBundleIds ?? []) if (!/^[A-Za-z0-9.-]+$/.test(b)) e.push(`apple.signing.extraBundleIds: "${b}" is not a bundle id`);
|
||||
return e;
|
||||
}
|
||||
|
|
@ -158,19 +180,20 @@ export const profileName = (bundleId: string, type: ProfileType, date = new Date
|
|||
export interface ProfilePlan { reuse: ApiProfile | null; create: Req | null; why: string }
|
||||
|
||||
/** Reuse an ACTIVE profile of the type that holds the certificate and is valid for 14 more days; else create one. */
|
||||
export function planProfile(existing: ApiProfile[], o: { bundleId: string; bundleIdId: string; certId: string; type: ProfileType; now?: number }): ProfilePlan {
|
||||
export function planProfile(existing: ApiProfile[], o: { bundleId: string; bundleIdId: string; certId: string; type: ProfileType; now?: number; /** make a new profile even when one is reusable (after an App Group was ticked in the portal) */ refresh?: boolean }): ProfilePlan {
|
||||
const now = o.now ?? Date.now();
|
||||
const ok = existing.filter(p => p.attributes.profileType === o.type && p.attributes.profileState === 'ACTIVE'
|
||||
&& Date.parse(p.attributes.expirationDate ?? '') - now > 14 * 86_400_000 && (p.certIds ?? []).includes(o.certId))
|
||||
.sort((a, b) => Date.parse(b.attributes.expirationDate!) - Date.parse(a.attributes.expirationDate!));
|
||||
if (ok[0]) return { reuse: ok[0], create: null, why: `profile "${ok[0].attributes.name}" is active (expires ${ok[0].attributes.expirationDate?.slice(0, 10)})` };
|
||||
if (ok[0] && !o.refresh) return { reuse: ok[0], create: null, why: `profile "${ok[0].attributes.name}" is active (expires ${ok[0].attributes.expirationDate?.slice(0, 10)})` };
|
||||
const stale = existing.filter(p => p.attributes.profileType === o.type && !p.managed);
|
||||
const managed = existing.filter(p => p.attributes.profileType === o.type && p.managed);
|
||||
const why = [
|
||||
stale.length ? `${stale.length} ${o.type} profile(s), none active with this certificate (states: ${[...new Set(stale.map(p => p.attributes.profileState))].join(', ')})` : '',
|
||||
managed.length ? `${managed.length} Xcode-managed (automatic signing; not readable through the API, not reused)` : '',
|
||||
].filter(Boolean).join('; ') || `no ${o.type} profile`;
|
||||
return { reuse: null, why, create: {
|
||||
const why2 = ok[0] ? `--refresh-profile: a new profile instead of "${ok[0].attributes.name}"` : why;
|
||||
return { reuse: null, why: why2, create: {
|
||||
method: 'POST', path: '/v1/profiles', note: `create ${o.type} profile for ${o.bundleId}`,
|
||||
body: { data: { type: 'profiles', attributes: { name: profileName(o.bundleId, o.type, new Date(now)), profileType: o.type },
|
||||
relationships: { bundleId: one('bundleIds', o.bundleIdId), certificates: many('certificates', [o.certId]) } } },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue