Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@
"test:android-version": "bun test/test-android-version.mjs",
"test:platform-flow-contract": "bun test/test-platform-flow-contract.mjs",
"test:tail-engine-shared": "bun test/test-tail-engine-shared.mjs",
"test": "bun run build && bun run test:helper-dce && bun run test:version-detection:setup && bun run test:bundle && bun run test:bundle-validation && bun run test:functional && bun run test:semver && bun run test:auto-bump-version && bun run test:auto-bump-ai-diff && bun run test:version-edge-cases && bun run test:regex && bun run test:upload && bun run test:fail-on-incompatible && bun run test:native-dependencies && bun run test:package-json-guard && bun run test:credentials && bun run test:credentials-export && bun run test:credentials-validation && bun run test:android-service-account-validation && bun run test:build-zip-filter && bun run test:checksum && bun run test:build-needed && bun run test:build-cancellation && bun run test:ci-prompts && bun run test:ci-secrets && bun run test:android-onboarding-progress && bun run test:onboarding-telemetry && bun run test:v2-event-migration && bun run test:analytics && bun run test:cli-headers && bun run test:min-cli-version && bun run test:authenticated-command-invocation && bun run test:analytics-error-category && bun run test:analytics-org-resolver && bun run test:supabase-perf && bun run test:preview-qr && bun run test:app-set-options && bun run test:mcp-analytics && bun run test:mcp-instructions && bun run test:mcp-live-update-onboarding && bun run test:mcp-stdout-guard && bun run test:mcp-platform-select && bun run test:mcp-explain-scopes && bun run test:mcp-oauth-reopen && bun run test:mcp-broker-oauth && bun run test:mcp-broker-session && bun run test:mcp-credentials-manage && bun run test:mcp-resume-prompt && bun run test:mcp-build-job && bun run test:mcp-build-tools && bun run test:app-created-source && bun run test:app-list-output-text && bun run test:doctor-analytics && bun run test:posthog-exception && bun run test:cli-recovery && bun run test:create-supabase-client && bun run test:build-platform-selection && bun run test:builder-project-discovery && bun run test:onboarding-recovery && bun run test:onboarding-progress && bun run test:onboarding-run-targets && bun run test:run-device-command && bun run test:init-monorepo-targeting && bun run test:init-app-conflict && bun run test:channel-add-exists && bun run test:wait-log && bun run test:init-guardrails && bun run test:init-replay && bun run test:init-telemetry && bun run test:prompt-preferences && bun run test:esm-sdk && bun run test:mcp && bun run test:mcp-no-key-handshake && bun run test:auth-session && bun run test:version-detection && bun run test:platform-paths && bun run test:project-type-detection && bun run test:payload-split && bun run test:manifest-path-encoding && bun run test:macos-signing && bun run test:asc-key-protocol && bun run test:apple-api-import-helpers && bun run test:apple-api-verify-key && bun run test:bundle-id-detector && bun run test:apple-api-app-list && bun run test:app-verification && bun run test:pbxproj-parser && bun run test:ai-log-capture && bun run test:ai-analyze-flow && bun run test:cicd-failure-help && bun run test:ai-sse-parser && bun run test:ai-render-markdown && bun run test:ai-stream-markdown && bun run test:ai-onboarding-mode && bun run test:ai-fit && bun run test:platform-layout && bun run test:frame-fit && bun run test:onboarding-min-size && bun run test:min-size-gate && bun run test:shell-size-gate && bun run test:build-log-sanitize && bun run test:build-output-viewport && bun run test:diff-viewer-viewport && bun run test:build-complete-exit && bun run test:ai-analyze-stream && bun run test:support-mailto && bun run test:support-redact && bun run test:support-internal-log && bun run test:support-help-menu && bun run test:support-contact && bun run test:support-upload-prompt && bun run test:support-bundle-files && bun run test:self-update && bun run test:update-prompt && bun run test:apple-api-cert-create && bun run test:android-tail-engine && bun run test:android-tail-render && bun run test:android-tail-routing && bun run test:dev-gate-stripped && bun run test:frame-fit-ios-shared && bun run test:ios-confirm-app-id && bun run test:ios-create-new && bun run test:ios-e2e && bun run test:ios-flow-contract && bun run test:ios-import-discovery && bun run test:ios-import-export && bun run test:ios-import-pickers && bun run test:ios-import-recovery && bun run test:ios-recovery && bun run test:ios-resume && bun run test:ios-tail-handoff && bun run test:ios-tui-render && bun run test:p8-error && bun run test:ios-tui-routing && bun run test:ios-updater-sync-validation && bun run test:ios-verify-app && bun run test:ios-marketing-version && bun run test:android-version && bun run test:platform-flow-contract && bun run test:tail-engine-shared && bun run test:prescan && bun run test:android-reporting-api && bun run test:android-app-verification && bun run test:android-rename && bun run test:appflow-auth && bun run test:appflow-api-map && bun run test:appflow-validate && bun run test:appflow-flow && bun run test:appflow-gapfill && bun run test:appflow-engine && bun run test:appflow-tail && bun run test:appflow-fetch && bun run test:appflow-sa-decode && bun run test:app-permission-helper && bun run test:2fa-compliance-network && bun run test:organization-set-api-host && bun run test:trial-warning && bun run test:plan-validation",
"test": "bun run build && bun run test:helper-dce && bun run test:version-detection:setup && bun run test:bundle && bun run test:bundle-validation && bun run test:functional && bun run test:semver && bun run test:auto-bump-version && bun run test:auto-bump-ai-diff && bun run test:version-edge-cases && bun run test:regex && bun run test:upload && bun run test:fail-on-incompatible && bun run test:native-dependencies && bun run test:package-json-guard && bun run test:credentials && bun run test:credentials-export && bun run test:ios-provisioning-map && bun run test:ios-provisioning-command && bun run test:credentials-validation && bun run test:android-service-account-validation && bun run test:build-zip-filter && bun run test:checksum && bun run test:build-needed && bun run test:build-cancellation && bun run test:ci-prompts && bun run test:ci-secrets && bun run test:android-onboarding-progress && bun run test:onboarding-telemetry && bun run test:v2-event-migration && bun run test:analytics && bun run test:cli-headers && bun run test:min-cli-version && bun run test:authenticated-command-invocation && bun run test:analytics-error-category && bun run test:analytics-org-resolver && bun run test:supabase-perf && bun run test:preview-qr && bun run test:app-set-options && bun run test:mcp-analytics && bun run test:mcp-instructions && bun run test:mcp-live-update-onboarding && bun run test:mcp-stdout-guard && bun run test:mcp-platform-select && bun run test:mcp-explain-scopes && bun run test:mcp-oauth-reopen && bun run test:mcp-broker-oauth && bun run test:mcp-broker-session && bun run test:mcp-credentials-manage && bun run test:mcp-resume-prompt && bun run test:mcp-build-job && bun run test:mcp-build-tools && bun run test:app-created-source && bun run test:app-list-output-text && bun run test:doctor-analytics && bun run test:posthog-exception && bun run test:cli-recovery && bun run test:create-supabase-client && bun run test:build-platform-selection && bun run test:builder-project-discovery && bun run test:onboarding-recovery && bun run test:onboarding-progress && bun run test:onboarding-run-targets && bun run test:run-device-command && bun run test:init-monorepo-targeting && bun run test:init-app-conflict && bun run test:channel-add-exists && bun run test:wait-log && bun run test:init-guardrails && bun run test:init-replay && bun run test:init-telemetry && bun run test:prompt-preferences && bun run test:esm-sdk && bun run test:mcp && bun run test:mcp-no-key-handshake && bun run test:auth-session && bun run test:version-detection && bun run test:platform-paths && bun run test:project-type-detection && bun run test:payload-split && bun run test:manifest-path-encoding && bun run test:macos-signing && bun run test:asc-key-protocol && bun run test:apple-api-import-helpers && bun run test:apple-api-verify-key && bun run test:bundle-id-detector && bun run test:apple-api-app-list && bun run test:app-verification && bun run test:pbxproj-parser && bun run test:ai-log-capture && bun run test:ai-analyze-flow && bun run test:cicd-failure-help && bun run test:ai-sse-parser && bun run test:ai-render-markdown && bun run test:ai-stream-markdown && bun run test:ai-onboarding-mode && bun run test:ai-fit && bun run test:platform-layout && bun run test:frame-fit && bun run test:onboarding-min-size && bun run test:min-size-gate && bun run test:shell-size-gate && bun run test:build-log-sanitize && bun run test:build-output-viewport && bun run test:diff-viewer-viewport && bun run test:build-complete-exit && bun run test:ai-analyze-stream && bun run test:support-mailto && bun run test:support-redact && bun run test:support-internal-log && bun run test:support-help-menu && bun run test:support-contact && bun run test:support-upload-prompt && bun run test:support-bundle-files && bun run test:self-update && bun run test:update-prompt && bun run test:apple-api-cert-create && bun run test:android-tail-engine && bun run test:android-tail-render && bun run test:android-tail-routing && bun run test:dev-gate-stripped && bun run test:frame-fit-ios-shared && bun run test:ios-confirm-app-id && bun run test:ios-create-new && bun run test:ios-e2e && bun run test:ios-flow-contract && bun run test:ios-import-discovery && bun run test:ios-import-export && bun run test:ios-import-pickers && bun run test:ios-import-recovery && bun run test:ios-recovery && bun run test:ios-resume && bun run test:ios-tail-handoff && bun run test:ios-tui-render && bun run test:p8-error && bun run test:ios-tui-routing && bun run test:ios-updater-sync-validation && bun run test:ios-verify-app && bun run test:ios-marketing-version && bun run test:android-version && bun run test:platform-flow-contract && bun run test:tail-engine-shared && bun run test:prescan && bun run test:android-reporting-api && bun run test:android-app-verification && bun run test:android-rename && bun run test:appflow-auth && bun run test:appflow-api-map && bun run test:appflow-validate && bun run test:appflow-flow && bun run test:appflow-gapfill && bun run test:appflow-engine && bun run test:appflow-tail && bun run test:appflow-fetch && bun run test:appflow-sa-decode && bun run test:app-permission-helper && bun run test:2fa-compliance-network && bun run test:organization-set-api-host && bun run test:trial-warning && bun run test:plan-validation",
"test:build-platform-selection": "bun test/test-build-platform-selection.mjs",
"test:builder-project-discovery": "bun test/test-builder-project-discovery.mjs",
"test:ai-log-capture": "bun test/test-ai-log-capture.mjs",
Expand Down Expand Up @@ -223,7 +223,9 @@
"test:trial-warning": "bun test/test-trial-warning.mjs",
"test:plan-validation": "bun test/test-plan-validation.mjs",
"test:auto-bump-version": "bun test/test-auto-bump-version.mjs",
"test:auto-bump-ai-diff": "bun test/test-auto-bump-ai-diff.mjs"
"test:auto-bump-ai-diff": "bun test/test-auto-bump-ai-diff.mjs",
"test:ios-provisioning-map": "bun test/test-ios-provisioning-map.mjs",
"test:ios-provisioning-command": "bun test/test-ios-provisioning-command.mjs"
},
"dependencies": {
"@inkjs/ui": "^2.0.0",
Expand Down
53 changes: 13 additions & 40 deletions cli/src/build/credentials-export-command.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { SavedCredentials } from '../schemas/build'
import type { CredentialsPlatform, CredentialsStoreName, CredentialsStores } from './credentials-store-selection'
import type { FileHandle } from 'node:fs/promises'
import { link, mkdtemp, open, rmdir, unlink } from 'node:fs/promises'
import { dirname, join, resolve } from 'node:path'
Expand All @@ -8,18 +8,15 @@ import { canPromptInteractively, formatError } from '../utils'
import { getGlobalCredentialsPath, getLocalCredentialsPath, loadSavedCredentials } from './credentials'
import { canDecodeCredentialBase64, decodeCredentialBase64 } from './credentials-base64'
import { quoteCredentialsExportTerminalValue, writeCredentialsExportStderr } from './credentials-export-terminal'
import { credentialsPlatformFields, hasConfiguredCredentials, resolveCredentialsStore } from './credentials-store-selection'

type Platform = 'ios' | 'android'
type Store = 'local' | 'global'
type CredentialsExportOptions = { appId?: string, platform?: string, local?: boolean, global?: boolean, file?: string, raw?: boolean, decodeBase64?: boolean }
type CredentialsExportStores = Record<Store, SavedCredentials | null>
type ResolvedCredentialsExport = { value: string, source: Store, platforms: Platform[] }
type ResolvedCredentialsExport = { value: string, source: CredentialsStoreName, platforms: CredentialsPlatform[] }
type FileValue = { data: string | Buffer, decoded: boolean, warnLiteral: boolean }
type FileHandleForExport = Pick<FileHandle, 'writeFile' | 'chmod' | 'close'>
type FileWriterDependencies = { mkdtemp?: typeof mkdtemp, open?: typeof open, link?: typeof link, unlink?: typeof unlink, rmdir?: typeof rmdir }

const platforms: Platform[] = ['ios', 'android']
const stores: Store[] = ['local', 'global']
const platforms: CredentialsPlatform[] = ['ios', 'android']

export function isCredentialsExportInvocation(argv: readonly string[]): boolean {
for (let commandIndex = 2; commandIndex < argv.length - 2; commandIndex++) {
Expand Down Expand Up @@ -47,57 +44,33 @@ export function isCredentialsExportInvocation(argv: readonly string[]): boolean
return false
}

function record(value: unknown): Record<string, unknown> | undefined {
return value !== null && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : undefined
}

function platformFields(saved: SavedCredentials | null, platform: Platform): Record<string, unknown> | undefined {
const app = record(saved)
return app && Object.hasOwn(app, platform) ? record(app[platform]) : undefined
}

function configured(saved: SavedCredentials | null, platform?: Platform): boolean {
const fields = platform === undefined ? platforms.map(item => platformFields(saved, item)) : [platformFields(saved, platform)]
return fields.some(field => Object.values(field ?? {}).some(value => typeof value === 'string'))
}

function storedValue(saved: SavedCredentials, platform: Platform, variable: string): string | undefined {
const fields = platformFields(saved, platform)
function storedValue(saved: CredentialsStores[CredentialsStoreName], platform: CredentialsPlatform, variable: string): string | undefined {
const fields = credentialsPlatformFields(saved, platform)
const value = fields && Object.hasOwn(fields, variable) ? fields[variable] : undefined
return typeof value === 'string' ? value : undefined
}

const quoted = (value: string | undefined) => quoteCredentialsExportTerminalValue(value)

function resolvedFor(saved: SavedCredentials, source: Store, platform: Platform, variable: string): ResolvedCredentialsExport {
if (!configured(saved, platform))
function resolvedFor(saved: NonNullable<CredentialsStores[CredentialsStoreName]>, source: CredentialsStoreName, platform: CredentialsPlatform, variable: string): ResolvedCredentialsExport {
if (!hasConfiguredCredentials(saved, platform))
throw new Error(`${platform} is not configured in the ${source} store`)
const value = storedValue(saved, platform, variable)
if (value === undefined)
throw new Error(`${quoted(variable)} is not stored for ${platform} in the ${source} store`)
return { value, source, platforms: [platform] }
}

export function resolveCredentialsExport(variable: string, options: CredentialsExportOptions, savedStores: CredentialsExportStores): ResolvedCredentialsExport {
if (options.local && options.global)
throw new Error('Cannot use --local and --global together')
if (options.platform !== undefined && !platforms.includes(options.platform as Platform))
export function resolveCredentialsExport(variable: string, options: CredentialsExportOptions, savedStores: CredentialsStores): ResolvedCredentialsExport {
if (options.platform !== undefined && !platforms.includes(options.platform as CredentialsPlatform))
throw new Error('--platform must be ios or android')

const available = stores.filter(source => configured(savedStores[source]))
const source = options.local || options.global ? options.local ? 'local' : 'global' : available[0]
if (source === undefined)
throw new Error(`No saved Builder credentials for ${quoted(options.appId)}`)
if (!configured(savedStores[source]))
throw new Error(`No saved Builder credentials for ${quoted(options.appId)} in the ${source} store`)
if (!options.local && !options.global && available.length > 1)
throw new Error('Saved Builder credentials exist in both stores; pass --local or --global')
const saved = savedStores[source]!
const selected = options.platform as Platform | undefined
const { source, saved } = resolveCredentialsStore(options, savedStores)
const selected = options.platform as CredentialsPlatform | undefined
if (selected)
return resolvedFor(saved, source, selected, variable)

const configuredPlatforms = platforms.filter(platform => configured(saved, platform))
const configuredPlatforms = platforms.filter(platform => hasConfiguredCredentials(saved, platform))
if (configuredPlatforms.length === 1)
return resolvedFor(saved, source, configuredPlatforms[0]!, variable)
const [first, second] = configuredPlatforms
Expand Down
60 changes: 60 additions & 0 deletions cli/src/build/credentials-store-selection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type { SavedCredentials } from '../schemas/build'
import { quoteCredentialsExportTerminalValue } from './credentials-export-terminal'

export type CredentialsStoreName = 'local' | 'global'
export type CredentialsPlatform = 'ios' | 'android'

export interface CredentialsStoreOptions {
appId?: string
local?: boolean
global?: boolean
}

export type CredentialsStores = Record<CredentialsStoreName, SavedCredentials | null>

export interface ResolvedCredentialsStore {
source: CredentialsStoreName
saved: SavedCredentials
}

const platforms: CredentialsPlatform[] = ['ios', 'android']
const stores: CredentialsStoreName[] = ['local', 'global']

function record(value: unknown): Record<string, unknown> | undefined {
return value !== null && typeof value === 'object' && !Array.isArray(value)
? value as Record<string, unknown>
: undefined
}

export function credentialsPlatformFields(saved: SavedCredentials | null, platform: CredentialsPlatform): Record<string, unknown> | undefined {
const app = record(saved)
return app && Object.hasOwn(app, platform) ? record(app[platform]) : undefined
}

export function hasConfiguredCredentials(saved: SavedCredentials | null, platform?: CredentialsPlatform): boolean {
const fields = platform === undefined
? platforms.map(item => credentialsPlatformFields(saved, item))
: [credentialsPlatformFields(saved, platform)]
return fields.some(field => Object.values(field ?? {}).some(value => typeof value === 'string'))
}

export function resolveCredentialsStore(options: CredentialsStoreOptions, savedStores: CredentialsStores): ResolvedCredentialsStore {
if (options.local && options.global)
throw new Error('Cannot use --local and --global together')

const available = stores.filter(source => hasConfiguredCredentials(savedStores[source]))
const source = options.local || options.global
? options.local ? 'local' : 'global'
: available[0]
const appId = quoteCredentialsExportTerminalValue(options.appId)

if (source === undefined)
throw new Error(`No saved Builder credentials for ${appId}`)
const saved = savedStores[source]
if (!hasConfiguredCredentials(saved))
throw new Error(`No saved Builder credentials for ${appId} in the ${source} store`)
if (!options.local && !options.global && available.length > 1)
throw new Error('Saved Builder credentials exist in both stores; pass --local or --global')

return { source, saved: saved! }
}
Loading
Loading