UNPKG

@capgo/cli

Version:
673 lines (672 loc) 37.8 kB
import type { Buffer } from 'node:buffer'; import type { AscApp, AscDistributionCert, AscProfileSummary } from '../apple-api.js'; import type { ApiKeyData, CertificateData, EnrichedIdentityAvailability, OnboardingProgress, OnboardingStep, ProfileData } from '../types.js'; import type { AsyncCommandRunner, CiSecretDiscovery, CiSecretEntry, CiSecretTarget, CommandRunner } from '../ci-secrets.js'; import type { DiscoveredProfile, ExportedP12, IdentityProfileMatch, SigningIdentity } from '../macos-signing.js'; import type { MobileprovisionDetail } from '../../mobileprovision-parser.js'; import type { BuildCredentials } from '../../../schemas/build.js'; import type { BuildLogger, BuildRequestOptions, BuildRequestResult } from '../../request.js'; import type { EnvExportOpts, EnvExportResult } from '../env-export.js'; import type { GeneratedWorkflow, WorkflowGeneratorOpts } from '../workflow-generator.js'; import type { WorkflowWriteOptions, WorkflowWriteResult } from '../workflow-writer.js'; import type { TailTransient } from '../tail/flow.js'; import type { AppVerifyResult, AscAppLike, GatePath } from '../app-verification.js'; import type { DetectedBundleIds } from '../bundle-id-detector.js'; /** * Stable reason an identity has no usable matching profile. Drives the * `import-no-match-recovery` menu variant. (Mirrors the `noMatchReason` enum.) */ export type IosNoMatchReason = 'apple-no-cert-match' | 'apple-no-profiles-linked' | 'apple-bundle-mismatch' | 'apple-distribution-mismatch' | 'apple-other' | 'no-profile-on-disk'; /** * A duplicate Capgo provisioning profile (creating-profile / import-create). * Matches the `{ id, name, profileType }` triple returned by apple-api's * findCapgoProfiles() and carried on DuplicateProfileError.profiles. Derived * from the real AscProfileSummary so it tracks any future field additions. */ export type IosDuplicateProfile = Pick<AscProfileSummary, 'id' | 'name' | 'profileType'>; export type IosStepKind = 'auto' | 'input' | 'choice' | 'done' | 'error'; export interface IosStepOption { value: string; label?: string; note?: string; } export interface IosStepView { step: OnboardingStep; kind: IosStepKind; title?: string; prompt?: string; collect?: string[]; options?: IosStepOption[]; message?: string; } /** * Per-step runtime context the driver supplies to the view builder AND threads * back through `IosEffectResult.transient` between effects. EVERY field is * OPTIONAL so a caller that only passes `{ appId }` still gets a usable view. * * This is the iOS "ephemeral inventory" — driver-held transient state that is * NEVER persisted to progress.json (it carries Apple-side selections + raw * cert/profile/keychain payloads). The total resume function (getIosResumeStep) * NEVER produces a step that depends on these — on resume the driver re-runs the * silent inventory (import-scanning) and re-renders the picker. See the audit's * "Ephemeral inventory" section for the producer/consumer map. */ export interface IosStepCtx { appId?: string; /** Selected signing identity (import-pick-identity). REQUIRED by import-exporting. */ chosenIdentity?: SigningIdentity; /** Selected provisioning profile (import-pick-profile). REQUIRED by import-exporting. */ chosenProfile?: DiscoveredProfile; /** Discovery result list from import-scanning (identities + on-disk profiles). */ importMatches?: IdentityProfileMatch[]; /** Scanned on-disk profiles (paired with importMatches). */ importProfiles?: DiscoveredProfile[]; /** Per-identity Apple-side availability (import-validating-all-certs). */ identityAvailability?: Record<string, EnrichedIdentityAvailability>; /** Per-identity prefetched Apple profiles (parallel prefetch after validation). */ profilePrefetch?: Record<string, DiscoveredProfile[]>; /** Apple cert resource id for the chosen identity (import-checking-apple-cert). */ _appleCertIdForChosen?: string; /** Why the chosen identity has no usable profile — drives the recovery menu. */ noMatchReason?: IosNoMatchReason; /** Duplicate Capgo profiles (creating-profile / import-create-profile-only). */ duplicateProfiles?: IosDuplicateProfile[]; /** Existing Apple certs offered for revocation when the cert limit is hit. */ existingCerts?: AscDistributionCert[]; /** The user's revoke selection (cert-limit-prompt → revoking-certificate). */ certToRevoke?: AscDistributionCert; /** * Whether the host can show a native file picker — gates the * import-no-match-recovery / import-portal-explanation "use a .mobileprovision * from disk" option (the TUI's canUseFilePicker(), app.tsx:3570/3733). The * DRIVER threads the host capability here; the view defaults it to true (the * macOS-first onboarding target) so a caller that only passes { appId } still * gets the file-picker recovery option. */ canUseFilePicker?: boolean; /** Resolved certificate data (creating-profile create-new / import-exporting). */ certData?: CertificateData; /** Resolved profile data (creating-profile create-new / import-exporting). */ profileData?: ProfileData; /** Apple team id resolved alongside certData/profileData. */ teamId?: string; /** Keychain export password (import-exporting). Transient only. */ importedP12Password?: string; /** Buffer of .p8 file content during validation (only the PATH is persisted). */ p8Content?: Buffer; /** * True once the p8-method-select file-picker effect has opened the native * dialog this drive — the engine returns it in transient and the driver * threads it back as `deps.carried.pickerOpened` so a re-render does NOT * re-open the picker. Mirrors the TUI's `pickerOpenedRef` guard. */ pickerOpened?: boolean; /** * True once the import-provide-profile-path .mobileprovision picker has opened * the native dialog this drive — returned in transient and threaded back as * `deps.carried.profilePickerOpened` so a re-render does NOT re-open the * picker. SEPARATE from `pickerOpened` (the .p8 picker). Mirrors the TUI's * `mobileprovisionPickerOpenedRef` guard (app.tsx:1689). */ profilePickerOpened?: boolean; /** Verified key id + issuer id (mirror of completedSteps.apiKeyVerified). */ apiKey?: ApiKeyData; /** ASC apps fetched by the verify-app effect (picker source + Path B re-poll). */ verifyApps?: AscApp[]; /** Registered Developer-portal bundle ids (diagnostic — sharpens Path B wording). */ verifyRegisteredIds?: string[]; /** The authoritative Release build id, re-detected FRESH from disk ('' = unresolved). */ verifyReleaseBundleId?: string; /** The Debug-config bundle id when it differs from Release (else ''). */ verifyDebugBundleId?: string; /** True when Debug + Release literal ids both exist AND differ (awareness note + telemetry). */ verifyDebugReleaseDiffer?: boolean; /** * The verify-app classification (the pure classifyAppVerification result), * widened with the two pass-through outcomes ('fetch-failed' / * 'no-release-config') so the driver's Result telemetry mirrors the TUI's. * Present once the initial fetch has run — its absence is what makes * iosViewForStep render verify-app as an AUTO effect instead of the gate. */ verifyResult?: AppVerifyResult | 'fetch-failed' | 'no-release-config'; /** Which gate path the user is on (null = the picker). */ verifyPath?: GatePath | null; /** The existing app picked in Path A (its bundleId is the target to match). */ verifyChosenApp?: AscAppLike | null; /** 1-based count of blocked Continue attempts (drives the escalating warning). */ verifyAttempt?: number; /** Path B: ask before re-opening the browser after a blocked re-poll. */ verifyAskReopen?: boolean; /** Where verify-app routes on pass/pass-through (set by verifying-key on import). */ pendingVerifyNext?: OnboardingStep; /** Human-readable error message the error view renders (failing step's message). */ error?: string; /** The step to re-run when the user picks "Try again" (absent = no retry offered). */ retryStep?: OnboardingStep; ciSecretEntries?: TailTransient['ciSecretEntries']; savedCredentials?: TailTransient['savedCredentials']; ciSecretTargets?: TailTransient['ciSecretTargets']; ciSecretSetupAdvice?: TailTransient['ciSecretSetupAdvice']; ciSecretRepoLabel?: TailTransient['ciSecretRepoLabel']; ciSecretExistingKeys?: TailTransient['ciSecretExistingKeys']; ciSecretUploadSummary?: TailTransient['ciSecretUploadSummary']; envExportPath?: TailTransient['envExportPath']; workflowFilePath?: TailTransient['workflowFilePath']; buildUrl?: TailTransient['buildUrl']; buildOutput?: TailTransient['buildOutput']; aiJobId?: TailTransient['aiJobId']; availableScripts?: TailTransient['availableScripts']; recommendedScript?: TailTransient['recommendedScript']; envExportError?: TailTransient['envExportError']; ciSecretError?: TailTransient['ciSecretError']; } /** * Async dependencies the iOS effects need (Apple API client, CSR + keychain * export, mobileprovision parsing, persistence, the shared tail helpers, and * status/log callbacks). EVERY helper is OPTIONAL and ADDITIVE so a driver can * inject only what the path it drives needs and the skeleton's stubs keep * type-checking. Data types are the REAL exports from apple-api / macos-signing / * mobileprovision-parser / csr; only the call-shape envelopes are engine-local. */ export interface IosEffectDeps { appId?: string; /** Verify an ASC API key (keyId + issuerId via the .p8). */ verifyApiKey?: (args: { keyId: string; issuerId: string; p8Content: Buffer; }) => Promise<{ teamId?: string; }>; /** * Create a distribution certificate from a CSR. Returns the RAW Apple cert * response (mirrors the real apple-api `createCertificate` helper): the cert * resource id, the base64 DER `certificateContent`, the expiry, and the team * id. The engine pairs `certificateContent` + the CSR private key via * `createP12` to produce the final .p12 — keeping the IO-free engine in charge * of assembling the CertificateData credential. Throws CertificateLimitError * (carrying the existing certs) when Apple's per-team cert limit is hit. */ createCertificate?: (args: { csr: string; accessToken?: string; }) => Promise<{ certificateId: string; certificateContent: string; expirationDate: string; teamId: string; }>; /** Revoke an existing certificate (cert-limit recovery). */ revokeCertificate?: (certificateId: string) => Promise<void>; /** Create a provisioning profile for a bundle id + cert. */ createProfile?: (args: { bundleId: string; certificateId: string; distribution?: string; }) => Promise<ProfileData>; /** Delete a provisioning profile (duplicate-profile recovery). */ deleteProfile?: (profileId: string) => Promise<void>; /** Resolve the Apple cert resource id from a local cert SHA-1. */ findCertIdBySha1?: (sha1: string) => Promise<string | null>; /** Classify a cert's Apple-side availability (import-validating-all-certs). */ classifyCertAvailability?: (identity: SigningIdentity) => Promise<EnrichedIdentityAvailability>; /** List the team's distribution certificates (cert-limit prompt). */ listCertificates?: () => Promise<AscDistributionCert[]>; /** Check for duplicate Capgo profiles for a bundle id. */ checkDuplicateProfiles?: (bundleId: string) => Promise<IosDuplicateProfile[]>; /** Ensure the bundle id exists on Apple (import-create-profile-only). */ ensureBundleId?: (bundleId: string) => Promise<void>; /** * List the Apple profiles linked to a cert (import-checking-apple-cert). * * Returns the RAW Apple shape (AscProfileSummary[]) exactly as the real * apple-api `listProfilesForCert` helper does — id / name / profileType / * profileContent / expirationDate / bundleIdentifier. The engine itself * synthesizes each summary into a DiscoveredProfile (populating profileBase64 * + certificateSha1s=[identity.sha1]) via `synthesizeProfileFromAscSummary`, * byte-for-byte mirroring the TUI's inline mapping at app.tsx:1556 / :1460. * Keeping the dep at the raw Apple shape means the driver pre-binds nothing * more than the real helper. */ listProfilesForCert?: (certificateId: string) => Promise<AscProfileSummary[]>; /** List every ASC app visible to the API key (verify-app fetch + Path B re-poll). */ listApps?: () => Promise<AscApp[]>; /** List every registered bundle-id identifier (verify-app diagnostics). */ listBundleIds?: () => Promise<string[]>; /** * FRESH bundle-id detection from disk (verify-app + the Path-A re-check). The * driver pre-binds the real `detectIosBundleIds({ cwd, iosDir, capacitorAppId })` * — the engine reads `releaseResolved`/`pbxproj` for the authoritative Release * id, `debug`/`debugReleaseDiffer` for the awareness note, and `capacitor` for * the persisted iosBundleIdContextAppId snapshot. Called PER CHECK so an edit * the user made since the wizard started is picked up (the TUI bypasses its * memo the same way, app.tsx:1522/3088). */ detectBundleIds?: () => DetectedBundleIds; /** * Rewrite the Release PRODUCT_BUNDLE_IDENTIFIER assignments equal to `fromId` * to `toId` in the Xcode project (the Path-A auto-fix). The driver pre-binds * the real `writeReleaseBundleId(cwd, iosDir, …)`; returns the number of * replaced assignments (0 = nothing matched). Throws only on an FS error. */ writeReleaseBundleId?: (fromId: string, toId: string) => { changed: number; }; /** Generate a CSR + private key PEM. */ generateCsr?: (args?: { commonName?: string; }) => { csr: string; privateKeyPem: string; }; /** Build a .p12 from a cert + private key. Returns base64. */ createP12?: (args: { certificatePem: string; privateKeyPem: string; password: string; }) => string; /** List the Mac's code-signing identities (import-scanning). */ listSigningIdentities?: () => Promise<SigningIdentity[]>; /** Scan the Mac's on-disk provisioning profiles (import-scanning). */ scanProvisioningProfiles?: () => Promise<DiscoveredProfile[]>; /** * Export a .p12 (cert + key) from the Keychain for the chosen identity * (import-exporting). Signature mirrors the REAL macos-signing helper VERBATIM: * takes the identity's SHA-1 and resolves to { base64, passphrase } (the * auto-generated wrap passphrase becomes the transient importedP12Password the * saving-credentials handoff reads — NEVER persisted, risk #2 / D-iOS-3). */ exportP12FromKeychain?: (targetSha1: string) => Promise<ExportedP12>; /** Parse a `.mobileprovision` file in detail (import-provide-profile-path). */ parseMobileprovisionDetailed?: (bytes: Buffer) => MobileprovisionDetail; loadProgress?: (appId: string) => Promise<OnboardingProgress | null>; saveProgress?: (appId: string, progress: OnboardingProgress) => Promise<void>; deleteProgress?: (appId: string) => Promise<void>; /** Persist the saved build-credential map (saving-credentials). */ updateSavedCredentials?: (appId: string, platform: 'ios' | 'android', credentials: Record<string, string>) => Promise<void>; loadSavedCredentials?: (appId: string) => Promise<unknown>; readFile?: (path: string) => Promise<Buffer>; copyFile?: (src: string, dest: string) => Promise<void>; /** * Open the native .p8 file picker (p8-method-select). Resolves to the chosen * absolute path, or null when the user cancels. The driver pre-binds the real * `openFilePicker` here; tests inject a canned path/null. Mirrors the TUI's * `openFilePicker()` call inside the p8-method-select effect. */ openP8FilePicker?: () => Promise<string | null>; /** * Open the native .mobileprovision file picker (import-provide-profile-path). * Resolves to the chosen absolute path, or null when the user cancels. The * driver pre-binds the real `openMobileprovisionPicker` here; tests inject a * canned path/null. Mirrors the TUI's `openMobileprovisionPicker()` call * inside the import-provide-profile-path effect (app.tsx:1696). The bytes are * then read via `deps.readFile` and parsed via `deps.parseMobileprovisionDetailed`. */ openProfilePicker?: () => Promise<string | null>; /** * Whether the host can show a native file picker. Gates the * import-no-match-recovery / import-portal-explanation "use a .mobileprovision * from disk" option exactly as the TUI's `canUseFilePicker()` does * (app.tsx:3570/3733). Defaults to true when omitted (the macOS-first target). */ canUseFilePicker?: () => boolean; /** * Open a URL in the host's default browser (import-portal-explanation's * "open the portal anyway" branch). Best-effort — the driver pre-binds the * real `open` helper; tests inject a recorder/no-op. Mirrors the TUI's * `open(...)` call at app.tsx:3749. A failure must NOT abort recovery. */ openExternal?: (url: string) => Promise<void> | void; /** * Whether the host is macOS. Gates the post-backup fork: on macOS the user is * offered import-vs-create at `setup-method-select`; off-macOS the import * sub-flow is unavailable so backing-up routes straight to the create-new * `api-key-instructions`. Mirrors the TUI's `isMacOS()` branch. Defaults to * true when omitted (the macOS-first onboarding target). */ isMacOS?: () => boolean; createCiSecretEntries?: (credentials: Partial<BuildCredentials>, apiKey?: string) => CiSecretEntry[]; detectCiSecretTargets?: (runner?: CommandRunner) => CiSecretDiscovery; getCiSecretRepoLabelAsync?: (target: CiSecretTarget, runner?: AsyncCommandRunner) => Promise<string | null>; listExistingCiSecretKeysAsync?: (target: CiSecretTarget, keys: string[], runner?: AsyncCommandRunner) => Promise<string[]>; uploadCiSecretsAsync?: (target: CiSecretTarget, entries: CiSecretEntry[], existingKeys?: string[], runner?: AsyncCommandRunner, onProgress?: (current: number, total: number, keyName: string) => void) => Promise<void>; exportCredentialsToEnv?: (opts: EnvExportOpts) => EnvExportResult; defaultExportPath?: (appId: string, platform: 'ios' | 'android') => string; /** Lockfile-based package-manager detection (the pick-package-manager 'recommended' note). */ detectPackageManager?: () => string; generateWorkflow?: (opts: WorkflowGeneratorOpts) => GeneratedWorkflow; writeWorkflowFile?: (opts: WorkflowGeneratorOpts, writeOptions?: WorkflowWriteOptions) => WorkflowWriteResult; requestBuildInternal?: (appId: string, options: BuildRequestOptions, silent?: boolean, logger?: BuildLogger) => Promise<BuildRequestResult>; /** The streaming BuildLogger threaded into requestBuildInternal (4th arg). */ logger?: BuildLogger; /** The build VIEWER sink (FullscreenBuildOutput), distinct from onLog. */ onBuildOutput?: (line: string) => void; /** Resolves the Capgo API key for the build request (CLI-flag-over-saved). */ resolveApikey?: () => string | undefined; /** Per-key CI-secret upload progress (uploadCiSecretsAsync 5th arg). */ onCiSecretUploadProgress?: (current: number, total: number, keyName: string) => void; /** The 2-phase checking-ci-secrets status text. */ onCiSecretCheckPhase?: (phase: string) => void; /** The ci-secrets-failed reason. */ onCiSecretError?: (message: string) => void; /** Reads the project's package.json scripts map (with-workflow preload). */ getPackageScripts?: () => Record<string, string>; /** Detects the web-framework project type (best-effort; may resolve null). */ findProjectType?: (options?: { quiet?: boolean; }) => Promise<string | null>; /** Maps a detected project type to its recommended build script name. */ findBuildCommandForProjectType?: (projectType: string) => Promise<string | null>; /** Workflow-file telemetry hook (e.g. 'workflow-file-written'). */ trackWorkflowEvent?: (event: string, options?: { decision?: string; }) => void; /** * DRIVER-HELD transient tail state threaded back into each post-save effect. * The TUI resolves these ONCE (at saving-credentials) and keeps them in React * state; a headless driver mirrors that by capturing the matching * IosEffectResult.transient and passing it back here on the NEXT effect. * NEVER persisted to progress.json. When absent (crash-recovery resume) the * effect falls back to a single lossy re-derivation from progress. */ carried?: { savedCredentials?: Record<string, string>; ciSecretEntries?: CiSecretEntry[]; ciSecretExistingKeys?: string[]; /** * Whether the workflow file did NOT exist when previewed (the TUI's * `previewIsNew`, resolved at preview-workflow-file via existsSync). The * writing-workflow-file effect logs '✔ Wrote' vs '✔ Overwrote' from it. * Absent defaults to NEW ('Wrote'). EPHEMERAL — never persisted. */ workflowIsNew?: boolean; /** The chosen signing identity (lossy re-scan source on resume). */ chosenIdentity?: SigningIdentity; /** The chosen provisioning profile (lossy re-scan source on resume). */ chosenProfile?: DiscoveredProfile; /** * The import-scanning discovery inventory (identity↔on-disk-profile matches + * the raw scanned profiles), threaded forward so the NEXT import effect can * read it without a re-scan. Produced by import-scanning into transient; the * driver mirrors it back here for import-validating-all-certs (which batches * classifyCertAvailability over importMatches) and the pickers. EPHEMERAL — * never persisted; on a crash-recovery resume the engine re-lands on * import-scanning and re-populates it. */ importMatches?: IdentityProfileMatch[]; importProfiles?: DiscoveredProfile[]; /** Resolved cert/profile/team export payloads carried into saving-credentials. */ certData?: CertificateData; profileData?: ProfileData; teamId?: string; /** * The validated .p8 file content (ASC private key) the driver carries * between the .p8 input chain and `verifying-key`. ONLY the p8Path is * persisted to progress.json — the raw key bytes ride this transient * channel, mirroring the TUI's `p8ContentRef`. The verifying-key effect * reads it from here; when absent (crash-recovery resume) it falls back to * re-reading the file at `progress.p8Path` via `deps.readFile`. */ p8Content?: Buffer; /** * Tracks that the p8-method-select file-picker effect already ran, so a * re-render does NOT re-open the native picker. Mirrors the TUI's * `pickerOpenedRef`. The driver threads the returned `pickerOpened: true` * transient back here on the next call. */ pickerOpened?: boolean; /** * Tracks that the import-provide-profile-path .mobileprovision file-picker * effect already ran this attempt, so a re-render / re-drive does NOT re-open * the native picker. SEPARATE from `pickerOpened` (the .p8 picker guard) so * the two file pickers never cross-suppress each other — mirrors the TUI's * distinct `mobileprovisionPickerOpenedRef` (app.tsx:1689). The driver threads * the returned `profilePickerOpened: true` transient back here; it RESETS the * flag (to false) before routing into import-provide-profile-path from the * recovery menu, exactly as the TUI clears the ref at app.tsx:3593. */ profilePickerOpened?: boolean; /** * Keychain export passphrase for the IMPORT path's .p12 (import-exporting). * Transient only — the import-exporting effect never persists it, so the * saving-credentials handoff reads it from carried. Absent on the create-new * path (which uses the well-known DEFAULT_P12_PASSWORD) and on a crash-recovery * resume that lost the in-memory state. */ importedP12Password?: string; /** * The existing Apple Distribution certs surfaced when the per-team cert * limit was hit (cert-limit recovery). Produced by `creating-certificate` * into transient.existingCerts; the driver threads the list back here so a * parked `cert-limit-prompt` re-renders (and resolves the user's pick by * cert id) WITHOUT re-hitting Apple. EPHEMERAL — never persisted (the TUI's * `existingCerts` React state); cleared together with `certToRevoke` after * a successful revoke. A restart that loses it re-enters via a fresh * creating-certificate attempt, which re-derives the list. */ existingCerts?: AscDistributionCert[]; /** * The cert the user picked at `cert-limit-prompt` (cert-limit recovery). The * choice is EPHEMERAL — `applyIosInput` persists nothing; the driver records * the picked AscDistributionCert here and re-drives the prompt as a resolver * effect, exactly as the TUI stashes `certToRevoke` in React state before * advancing to `revoking-certificate` (app.tsx:3923). The resolver returns * `revoking-certificate` when present, `error` when absent (the user exited). * Mirrors the BATCH 2 ephemeral-branching mechanism (`pickerOpened` / * `chosenIdentity`): the selection lives in carried, never in progress.json. */ certToRevoke?: AscDistributionCert; /** * The duplicate Capgo profiles surfaced at `duplicate-profile-prompt` * (duplicate-profile recovery). Produced by `creating-profile` / * `import-create-profile-only` into transient; the driver threads the list * back here so `deleting-duplicate-profiles` knows which profiles to delete. * NEVER persisted (only `duplicateProfileOrigin` is — see types.ts). */ duplicateProfiles?: IosDuplicateProfile[]; /** * The user's confirm/exit decision at `duplicate-profile-prompt`. EPHEMERAL — * `applyIosInput` persists nothing (per the audit's sequencing model); the * driver records the choice here and re-drives the prompt as a resolver * effect. `true` → `deleting-duplicate-profiles`; falsy (the user exited) → * `error` (mirroring app.tsx:3942's delete-vs-exitOnboarding branch). */ confirmDeleteDuplicates?: boolean; /** * The user's pick at `import-no-match-recovery` (the 5-way HUB). EPHEMERAL — * `applyIosInput` persists nothing; the driver records the choice here and * re-drives the prompt as a resolver effect. 'create' → * import-create-profile-only (with an ASC key) or api-key-instructions * (without); 'provide-profile-path' → import-provide-profile-path; 'browser' * → import-portal-explanation; 'back' → import-pick-identity. Mirrors the * TUI's recovery-menu onChange (app.tsx:3579). */ recoveryAction?: 'create' | 'provide-profile-path' | 'browser' | 'back'; /** * The user's pick at `import-portal-explanation` (the manual-portal * walkthrough). EPHEMERAL — the driver records the choice here and re-drives * the step as a resolver. 'use-create' → import-create-profile-only; * 'use-file' → import-provide-profile-path; 'open-anyway' / 'back' → * import-no-match-recovery. Mirrors app.tsx:3738. */ portalAction?: 'use-create' | 'open-anyway' | 'use-file' | 'back'; /** * The user's pick at `import-export-warning` (the heads-up before the one * Keychain dialog). EPHEMERAL — `applyIosInput` persists nothing; the driver * records the choice here and re-drives the step as a resolver. 'go' → * import-exporting (the precompiled signed helper is resolved + verified in * the export step itself — PR #2458 removed the swiftc compile step); * 'back' → import-pick-profile; 'exit'/absent → exit onboarding. Mirrors * app.tsx:3769 onChange. */ exportWarningAction?: 'go' | 'back' | 'exit'; /** * The STICKY no-match reason set by the step that ROUTED into recovery * (import-pick-identity / import-checking-apple-cert). The recovery resolver * + the import-provide-profile-path cancel branch thread it back so a * re-entry from a file-picker cancel / portal "open anyway" does NOT * recompute or overwrite it (risk #8) — the menu keeps showing the SAME * variant. Mirrors the TUI leaving `noMatchReason` untouched on back-nav. */ noMatchReason?: IosNoMatchReason; /** * The failing step's human error message — the error screen's content * (BATCH 8). EPHEMERAL — set by the failing effect into transient.error and * threaded back here by the driver so a parked 'error' view re-renders the * message (iosViewForStep('error') reads ctx.error). NEVER persisted — * mirrors the TUI's setError React state; a crash-recovery resume re-enters * the failing phase fresh. */ error?: string; /** * The step to re-run when the user picks "Try again" on the error screen * (BATCH 8). EPHEMERAL — set by the failing effect into transient.retryStep, * threaded back here by the driver, and read by the error RESOLVER * (runIosEffect('error')) to route a retry. NEVER persisted: an error is * transient runtime state, so a crash-recovery resume re-enters the failing * phase fresh (getIosResumeStep never returns 'error'). Mirrors the TUI's * setRetryStep + the ErrorStep 'retry' branch (app.tsx:1116 / 4468). */ retryStep?: OnboardingStep; /** * The user's pick on the error screen (BATCH 8). EPHEMERAL — `applyIosInput` * persists nothing; the driver records the choice here and re-drives the step * as a resolver. 'retry' → re-run carried.retryStep (the failing step); * 'restart' → welcome (a fresh reset); 'exit'/absent → stay on 'error' (the * terminal exit sink — the driver leaves onboarding, mirroring the TUI's * exitOnboarding at app.tsx:4482). NEVER persisted. */ errorAction?: 'retry' | 'restart' | 'exit'; /** * Where verify-app routes once the invariant holds (or on a pass-through * exit): the import continuation (import-validating-all-certs / * import-pick-identity) on the import app_store path, absent on create-new * (verify-app falls back to 'creating-certificate'). Produced by * verifying-key into transient.pendingVerifyNext; the driver threads it back * here. NEVER persisted — a fresh mount has none, so a resume re-entering * verify-app always falls back to creating-certificate (matching the TUI's * pendingVerifyNext React state + getResumeStep's verify-app comment). */ pendingVerifyNext?: OnboardingStep; /** * The user's pick on the PARKED verify-app step (the picker or one of the * two gates). EPHEMERAL — `applyIosInput` persists nothing; the driver * records the pick here and re-drives verify-app as a resolver effect: * 'pick' (+ verifyChosenApp) / 'create-new' route the picker; 'autofix' / * 'continue' drive the Path-A fix-build-id gate; 'recheck' / 'open' / * 'reopen' drive the Path-B create-app gate; 'back' resets to the picker; * 'cancel' exits via the error sink. Mirrors the TUI Select onChange values * (app.tsx:3246/3283/3323/3360). The driver MUST clear it after each * resolver run so a later re-entry runs the initial fetch. */ verifyAction?: 'pick' | 'create-new' | 'autofix' | 'continue' | 'recheck' | 'open' | 'reopen' | 'back' | 'cancel'; /** The existing ASC app picked in the verify-app picker (Path A target). */ verifyChosenApp?: AscAppLike | null; /** The ASC apps fetched by the initial verify-app effect (picker source + re-poll). */ verifyApps?: AscApp[]; /** Registered Developer-portal bundle ids (Path B wording sharpener). */ verifyRegisteredIds?: string[]; /** The authoritative Release build id resolved by the verify-app fresh detect. */ verifyReleaseBundleId?: string; /** The Debug-config bundle id when it differs from Release (else ''). */ verifyDebugBundleId?: string; /** Which gate path the user is on (null = the picker). */ verifyPath?: GatePath | null; /** 1-based count of blocked Continue attempts (the escalation driver). */ verifyAttempt?: number; /** Path B: ask before re-opening the browser after a blocked re-poll. */ verifyAskReopen?: boolean; }; onStatus?: (message: string) => void; onLog?: (message: string, color?: string) => void; /** Internal-only diagnostic line → the support internal log (main PR #2406). Optional; no-op when absent. */ onInternalLog?: (line: string) => void; signal?: AbortSignal; } export interface IosEffectResult { /** Updated progress after the effect ran (matches what was persisted). */ progress: OnboardingProgress; /** Explicit next step when not derivable from progress alone (★ transitions). */ next?: OnboardingStep; /** Transient runtime data that lives in the driver but is NOT persisted. */ transient?: Partial<IosStepCtx>; } /** * The create-new choice/input vocabulary. Mirrors android's `AndroidInput`: * one variant per choice/input step that records (or routes) state. The iOS * `applyIosInput` signature still accepts `unknown`, so callers cast to this. * Navigation-only choices (api-key-instructions) are included for completeness * but return progress unchanged. */ export type IosInput = { step: 'setup-method-select'; value: 'create' | 'import'; } | { step: 'api-key-instructions'; value: 'picker' | 'manual'; } | { step: 'input-p8-path'; value: string; } | { step: 'input-key-id'; value: string; } | { step: 'input-issuer-id'; value: string; } | { step: 'cert-limit-prompt'; value: string; } | { step: 'duplicate-profile-prompt'; value: 'delete' | 'exit'; } | { step: 'verify-app'; value: string; } | { step: 'import-distribution-mode'; value: 'app_store' | 'ad_hoc' | '__cancel__'; } | { step: 'import-pick-identity'; value: string; } | { step: 'import-pick-profile'; value: string; } | { step: 'import-no-match-recovery'; value: 'create' | 'provide-profile-path' | 'browser' | 'back'; } | { step: 'import-portal-explanation'; value: 'use-create' | 'open-anyway' | 'use-file' | 'back'; } | { step: 'import-export-warning'; value: 'go' | 'back' | 'exit'; }; /** * Build the view-model for a given step. Post-save tail steps delegate to the * shared neutral view (adapted back to IosStepView). The create-new choice/input * steps (setup-method fork + .p8 chain) return real per-step views mirroring the * TUI prompts/options (ui/steps/ios-credentials.tsx). All other steps return a * minimal placeholder 'auto' view echoing the step (real per-step views land in * later batches). */ export declare function iosViewForStep(step: OnboardingStep, progress: OnboardingProgress, ctx?: IosStepCtx): IosStepView; /** * Apply a user input to progress. Post-save tail choice/input steps delegate * the reducer to the shared neutral module. The create-new choice/input steps * persist their field(s) exactly as the TUI's onSubmit/onChange handlers do * (ui/app.tsx). All other steps return progress unchanged (real per-step * mutations land in later batches). * * PURE — no IO. The .p8 file read + keyId extraction + Apple verification are * effect-boundary concerns (p8-method-select / verifying-key); the reducers here * only record the raw user input into progress. */ export declare function applyIosInput(step: OnboardingStep, progress: OnboardingProgress, input: unknown): OnboardingProgress; /** * Run the async side-effect for a step. Post-save tail steps (incl. * saving-credentials) delegate to the shared neutral module via toTailDeps; the * neutral result maps 1:1 onto IosEffectResult (next is a wider OnboardingStep; * transient is a subset of IosStepCtx). All other steps are not implemented yet * — the real Apple-API / keychain / build effects land in later batches. */ export declare function runIosEffect(step: OnboardingStep, progress: OnboardingProgress, deps: IosEffectDeps): Promise<IosEffectResult>;