// Code signing keychain for `storebound apple build` outside the Mac's logged-in GUI session. codesign in an SSH // session (or any session without the login keychain unlocked) fails late with errSecInternalComponent. The fixes: // - run from the GUI session (a .command file opened with `open -a Terminal`) // - a CI runner / Jenkins agent started in that session, or with an unlocked keychain // - a build keychain: --keychain , unlocked with --keychain-unlock (password from STOREBOUND_KEYCHAIN_PASSWORD, // never printed) and its key partition list set so codesign may use the keys without a prompt. // Pure planning here; cli/apple/build.ts runs it. export const KEYCHAIN_PASSWORD_ENV = 'STOREBOUND_KEYCHAIN_PASSWORD'; /** The refusal for a build in an SSH / non-GUI session, naming every fix. */ export function sshBuildRefusal(reason: string, o: { unlockFlag?: boolean } = {}): string { const unlock = o.unlockFlag !== false; return `apple build: ${reason}; codesign there fails with errSecInternalComponent (locked keychain). Fixes, pick one:\n` + ` 1. Run it in the Mac's logged-in GUI session: write the command into a .command file and \`open -a Terminal \` (unset SSH_CONNECTION SSH_CLIENT SSH_TTY inside).\n` + ` 2. A CI runner / Jenkins agent started in the GUI session, or one whose keychain is unlocked first (\`security unlock-keychain -p "$${KEYCHAIN_PASSWORD_ENV}" \`, then \`security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$${KEYCHAIN_PASSWORD_ENV}" \`).\n` + (unlock ? ` 3. Let storebound do step 2: export ${KEYCHAIN_PASSWORD_ENV}=... (the keychain password; never printed) and add --keychain-unlock (the login keychain), or --keychain --keychain-unlock for a build keychain that holds the Apple Distribution key + certificate (storebound apple signing ensure --keychain imports them).\n` + ` --allow-ssh skips this check when the keychain is already unlocked in this session.` : ' --allow-ssh skips this check when the keychain is already unlocked in this session.'); } export interface KeychainPlan { keychain: string; steps: string[][] } /** The `security` calls --keychain-unlock runs, with PASSWORD as a placeholder (substituted by the caller, never logged). */ export function unlockSteps(keychain: string, searchList: string[]): string[][] { const steps: string[][] = [['security', 'unlock-keychain', '-p', 'PASSWORD', keychain], ['security', 'set-key-partition-list', '-S', 'apple-tool:,apple:,codesign:', '-s', '-k', 'PASSWORD', keychain]]; if (!searchList.includes(keychain)) steps.push(['security', 'list-keychains', '-d', 'user', '-s', keychain, ...searchList]); return steps; } /** `security list-keychains` output -> paths (quotes stripped). */ export const parseKeychainList = (out: string): string[] => out.split('\n').map(l => l.trim().replace(/^"|"$/g, '')).filter(Boolean); /** The password from the environment, or an error that names the variable (the value is never in a message). */ export function keychainPassword(env: Record): string { const p = env[KEYCHAIN_PASSWORD_ENV]; if (!p) throw new Error(`--keychain-unlock needs the keychain password in the environment variable ${KEYCHAIN_PASSWORD_ENV} (not an argument: arguments end up in shell history)`); return p; } /** A command line for logs with the password replaced. */ export const showSecurity = (argv: string[]): string => argv.map(a => (a === 'PASSWORD' ? '$' + KEYCHAIN_PASSWORD_ENV : a)).join(' '); /** Settings that make xcodebuild sign with a given keychain. */ export const keychainSigningArgs = (keychain: string): string[] => [`OTHER_CODE_SIGN_FLAGS=--keychain ${keychain}`];