@capgo/cli
Version:
A CLI to upload to capgo servers
87 lines (86 loc) • 4.22 kB
TypeScript
/**
* Pure decision logic for the iOS "verify App Store app" onboarding step.
*
* No filesystem, network, or React access — every input (the local Release
* build ID, the remote App Store Connect apps, and the registered bundle IDs)
* is passed in so the module stays synchronous and unit-testable, mirroring
* `decideBuilderCtaSurface` / `shouldBlockIncompatibleUpload` in
* `cli/src/bundle/builder-cta.ts`.
*
* The single invariant the step enforces (always `app_store` mode): an App
* Store Connect app must exist whose `bundleId` equals the Release build ID.
*/
/**
* Classification of why (and whether) the verification invariant is met.
*
* - `exact-match` — an ASC app's bundle ID == the Release build ID. Pass.
* - `wrong-build-id` — apps exist but none match → likely a wrong build ID (Path A).
* - `no-app-identifier-exists` — no apps at all, but the identifier is already
* registered in the Developer portal (Path B; the
* ASC new-app form can select the existing id).
* - `no-app-unregistered` — no apps at all and the identifier is not yet
* registered (Path B; register first, then create).
*
* `no-apps-in-account` is the umbrella for the `apps.length === 0` cases. We
* keep it in the union because the analytics/step layer surfaces it as a
* coarse result, but `classifyAppVerification` deliberately returns the finer
* registered/unregistered split because that distinction is what changes the
* actionable Path B wording ("identifier already exists" vs "will be
* registered"). The umbrella value is therefore never returned by the
* classifier itself.
*/
export type AppVerifyResult = 'exact-match' | 'wrong-build-id' | 'no-app-identifier-exists' | 'no-app-unregistered' | 'no-apps-in-account';
/** Minimal shape of an App Store Connect app needed for verification. */
export interface AscAppLike {
bundleId: string;
name: string;
}
export interface ClassifyAppVerificationInput {
/** The authoritative Release `PRODUCT_BUNDLE_IDENTIFIER` from the project. */
releaseBundleId: string;
/** Apps that actually exist in the user's App Store Connect account. */
apps: AscAppLike[];
/** Bundle IDs registered in the Apple Developer portal (diagnostic only). */
registeredBundleIds: string[];
}
export interface ClassifyAppVerificationResult {
result: AppVerifyResult;
/** The matched ASC app when `result === 'exact-match'`, else `null`. */
matchedApp: AscAppLike | null;
}
/**
* Pure classification of the verification invariant.
*
* 1. An app whose `bundleId === releaseBundleId` → `exact-match` (+ that app).
* 2. Otherwise, if any apps exist → `wrong-build-id` (the build signs an id that
* matches none of the account's apps).
* 3. Otherwise (no apps), if `releaseBundleId` is already registered →
* `no-app-identifier-exists`.
* 4. Otherwise (no apps, not registered) → `no-app-unregistered`.
*/
export declare function classifyAppVerification(input: ClassifyAppVerificationInput): ClassifyAppVerificationResult;
/** Which resolution path the verification gate is enforcing. */
export type GatePath = 'fix-build-id' | 'create-app';
export interface EvaluateGateInput {
/** Whether the invariant now holds (re-checked live on each Continue). */
satisfied: boolean;
/** 1-based count of blocked Continue attempts so far. */
attempt: number;
}
export interface EvaluateGateResult {
/** Whether the user may proceed past the step. */
proceed: boolean;
/**
* How loud the (still-blocked) warning box should be. `0` when satisfied;
* otherwise the attempt count clamped to `3` so the escalation tops out
* instead of growing unbounded.
*/
escalationLevel: number;
}
/**
* Pure gate decision. When the invariant is satisfied the user proceeds with no
* escalation; otherwise they are blocked and the escalation level is the attempt
* count capped at {@link MAX_ESCALATION_LEVEL} so the warning box can ramp its
* treatment without overflowing.
*/
export declare function evaluateGate(input: EvaluateGateInput): EvaluateGateResult;