// 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 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 { 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 App Store Connect > App Information > App Encryption Documentation (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'); }