storebound/cli/apple/encryption.ts

53 lines
3.6 KiB
TypeScript

// Export compliance for App Store builds, from apple.usesNonExemptEncryption in storebound.config.ts.
// storebound apple encryption [--build 42] [--dry]
// false (only OS-provided encryption such as HTTPS through the system): sets usesNonExemptEncryption = false on every
// build that has no answer yet (or on --build). Better: ITSAppUsesNonExemptEncryption = NO in Info.plist, then builds
// arrive answered.
// true: needs an approved encryption declaration (App Store Connect > App Information > App Encryption Documentation,
// with documents where required). This command lists the declarations and links the approved one to the builds; it
// never creates a declaration or claims an exemption for you (export-compliance skill).
// Not set: refuses. storebound never guesses this answer.
import { consoleLink } from '../lib/console-links';
import type { Ctx } from '../lib/ctx';
import { need } from '../lib/ctx';
import { done, log, warn } from '../lib/log';
import { ascClient, type J } from './asc';
export async function run(ctx: Ctx): Promise<void> {
const { config, root } = ctx.loaded;
const answer = config.apple?.usesNonExemptEncryption;
if (answer === undefined) throw new Error('apple.usesNonExemptEncryption is not set in storebound.config.ts. Decide it with the export-compliance skill (false only if the app uses no encryption beyond the OS); storebound never guesses it.');
const dry = ctx.args.has('dry');
const c = await ascClient(root, need(config.apple?.bundleId, 'apple.bundleId'), config.apple?.appId);
const appId = await c.app();
const only = ctx.args.get('build');
const builds = (await c.all(`/v1/builds?filter[app]=${appId}&sort=-uploadedDate&limit=50`))
.filter(b => (only ? String(b.attributes.version) === only : b.attributes.usesNonExemptEncryption == null) && !b.attributes.expired);
if (only && !builds.length) throw new Error(`build ${only} not found (or expired)`);
log('apple', `${builds.length} build(s) to answer: ${builds.map(b => b.attributes.version).join(', ') || 'none'}`);
if (answer === false) {
for (const b of builds) {
if (b.attributes.usesNonExemptEncryption === false) { log('apple', `build ${b.attributes.version}: already exempt`); continue; }
if (!dry) await c.asc('PATCH', `/v1/builds/${b.id}`, { data: { type: 'builds', id: b.id, attributes: { usesNonExemptEncryption: false } } });
log('apple', `build ${b.attributes.version}: usesNonExemptEncryption = false${dry ? ' (dry)' : ''}`);
}
done('apple', 'encryption answered (add ITSAppUsesNonExemptEncryption = NO to Info.plist so new builds come answered)');
return;
}
const decls: J[] = await c.all(`/v1/appEncryptionDeclarations?filter[app]=${appId}&limit=50`);
for (const d of decls) log('apple', `declaration ${d.id}: ${d.attributes.appEncryptionDeclarationState} exempt=${d.attributes.exempt} code=${d.attributes.codeValue ?? '-'}`);
const approved = decls.find(d => d.attributes.appEncryptionDeclarationState === 'APPROVED');
if (!approved) {
warn('apple', `no APPROVED encryption declaration: create it in ${consoleLink('asc.encryption')} (export-compliance skill), then re-run`);
process.exitCode = 1;
return;
}
for (const b of builds) {
if (!dry) await c.asc('PATCH', `/v1/builds/${b.id}`, { data: { type: 'builds', id: b.id, attributes: { usesNonExemptEncryption: true },
relationships: { appEncryptionDeclaration: { data: { type: 'appEncryptionDeclarations', id: approved.id } } } } });
log('apple', `build ${b.attributes.version}: linked to declaration ${approved.id}${dry ? ' (dry)' : ''}`);
}
done('apple', 'encryption done');
}