diff --git a/src/core/credentials/accounts.ts b/src/core/credentials/accounts.ts index 5d96ac8..d6c45f6 100644 --- a/src/core/credentials/accounts.ts +++ b/src/core/credentials/accounts.ts @@ -11,6 +11,7 @@ import { deleteSecret, getSecret, setSecret } from './keychain.js'; import { migrateLegacySigningIndex, p12PasswordAccount } from './appleSigning.js'; import { AppStoreIdentityService } from '../services/appStoreIdentity.js'; import type { LaunchSecretStoreService } from '../services/secretStore.js'; +import type { MutableDeep } from '../types/mutable.js'; /** Secret-store account holding one Apple account's `.p8` PEM, namespaced by Key ID. */ const p8Account = (keyId: string): string => { return `asc-p8:${keyId}`; @@ -25,23 +26,19 @@ export type AccountFailure = Readonly<{ readonly message: string; }>; export const makeAccountFailure = Data.tagged('AccountFailure'); -const AccountRecordSchema: Schema.Schema = Schema.mutable( - Schema.Struct({ - keyId: Schema.String, - issuerId: Schema.String, - label: Schema.String, - teamId: Schema.optionalWith(Schema.String, { exact: true }), - apps: Schema.optionalWith(Schema.mutable(Schema.Array(Schema.String)), { exact: true }), - addedAt: Schema.String, - resolvedAt: Schema.optionalWith(Schema.String, { exact: true }), - }), -); -const AccountsFileSchema: Schema.Schema = Schema.mutable( - Schema.Struct({ - active: Schema.NullOr(Schema.String), - accounts: Schema.mutable(Schema.Array(AccountRecordSchema)), - }), -); +const AccountRecordSchema: Schema.Schema = Schema.Struct({ + keyId: Schema.String, + issuerId: Schema.String, + label: Schema.String, + teamId: Schema.optionalWith(Schema.String, { exact: true }), + apps: Schema.optionalWith(Schema.Array(Schema.String), { exact: true }), + addedAt: Schema.String, + resolvedAt: Schema.optionalWith(Schema.String, { exact: true }), +}); +const AccountsFileSchema: Schema.Schema = Schema.Struct({ + active: Schema.NullOr(Schema.String), + accounts: Schema.Array(AccountRecordSchema), +}); type AccountStorageRequirements = FileSystem.FileSystem | LaunchPathsService | Path.Path; const emptyAccountsFile = (): AccountsFile => ({ active: null, accounts: [] }); /** ISO-8601 stamp for `addedAt`/`resolvedAt`. */ @@ -139,7 +136,7 @@ export const findAccount = ( ); /** Match an account by its label or Key ID, case-insensitively - the selector form users type. */ export const matchAccount = ( - accounts: AccountRecord[], + accounts: readonly AccountRecord[], selector: string, ): AccountRecord | undefined => { const needle = selector.trim().toLowerCase(); @@ -209,14 +206,14 @@ export const addAccount = ( if (!hasIdentity && input.apps !== undefined) hasIdentity = input.apps.length > 0; let addedAt = existing?.addedAt; if (addedAt === undefined) addedAt = timestamp; - const record: AccountRecord = { + const record: MutableDeep = { keyId: input.keyId, issuerId: input.issuerId, label: input.label, addedAt, }; if (input.teamId !== null && input.teamId !== undefined) record.teamId = input.teamId; - if (input.apps !== undefined && input.apps.length > 0) record.apps = input.apps; + if (input.apps !== undefined && input.apps.length > 0) record.apps = [...input.apps]; if (hasIdentity) record.resolvedAt = timestamp; let accounts = [...file.accounts, record]; if (existing) { @@ -232,16 +229,16 @@ export const addAccount = ( export const updateAccountIdentity = ( keyId: string, teamId: string | null, - apps: string[], + apps: readonly string[], ): Effect.Effect => Effect.gen(function* () { const file = yield* readAccounts(); const timestamp = yield* currentTimestamp(); const accounts = file.accounts.map((account) => { if (account.keyId !== keyId) return account; - const next: AccountRecord = { ...account, resolvedAt: timestamp }; - if (teamId != null) next.teamId = teamId; - if (apps.length > 0) next.apps = apps; + let next: AccountRecord = { ...account, resolvedAt: timestamp }; + if (teamId != null) next = { ...next, teamId }; + if (apps.length > 0) next = { ...next, apps: [...apps] }; return next; }); yield* writeAccounts({ active: file.active, accounts }); @@ -364,7 +361,7 @@ export const decideBuildAccount = (file: AccountsFile, selector?: string): Build export type ResolveBuildAccountOptions = { selector?: string | undefined; interactive: boolean; - pick: (accounts: AccountRecord[]) => Effect.Effect; + pick: (accounts: readonly AccountRecord[]) => Effect.Effect; }; /** * Resolve the account a build should use, applying {@link decideBuildAccount} and then either using diff --git a/src/core/credentials/appleSigning.test.ts b/src/core/credentials/appleSigning.test.ts index 26c0926..e2064ef 100644 --- a/src/core/credentials/appleSigning.test.ts +++ b/src/core/credentials/appleSigning.test.ts @@ -96,7 +96,7 @@ const seedCredentials = ( uuid: string; } >, - installedUuids: string[], + installedUuids: readonly string[], ): void => { const dir = join(home.dir, '.launch', 'credentials', KEY_ID); mkdirSync(dir, { recursive: true }); @@ -182,7 +182,7 @@ describe('profileStaleAgainstCapabilities - regenerate-vs-reuse decision (#261)' profileContent: 'base64-bytes', }; /** A client stub exposing only the one read this decision makes - no network. */ - function clientWithCapabilities(types: string[]) { + function clientWithCapabilities(types: readonly string[]) { return { listBundleIdCapabilities: vi.fn(() => Effect.succeed( diff --git a/src/core/credentials/appleSigning.ts b/src/core/credentials/appleSigning.ts index 3f15d73..ac70133 100644 --- a/src/core/credentials/appleSigning.ts +++ b/src/core/credentials/appleSigning.ts @@ -2,6 +2,7 @@ import { FileSystem, Path } from '@effect/platform'; import { Effect, Schema } from 'effect'; import type { Platform } from '../types/app.js'; import type { AscKey, SigningAssets } from '../types/credentials.js'; +import type { MutableDeep } from '../types/mutable.js'; import type { Logger } from '../services/logger.js'; import { adHocProfileType, appStoreProfileType, platformLabel } from '../services/platform.js'; import { @@ -84,7 +85,7 @@ export const describeStoredCredentials = ( export const loadCachedSigningAssets = ( keyId: string, bundleId: string, - extensions: string[] = [], + extensions: readonly string[] = [], ): Effect.Effect< SigningAssets | null, never, @@ -121,7 +122,7 @@ export const loadCachedSigningAssets = ( return null; extensionProfiles[ext] = extProfile.name; } - const signingAssets: SigningAssets = { + const signingAssets: MutableDeep = { bundleId, teamId: profile.teamId, certName: DISTRIBUTION_CERT_NAME, diff --git a/src/core/credentials/capabilities.ts b/src/core/credentials/capabilities.ts index cb077f6..468913d 100644 --- a/src/core/credentials/capabilities.ts +++ b/src/core/credentials/capabilities.ts @@ -203,7 +203,7 @@ export const APP_GROUP_PORTAL_URL = * to-do: which groups, where to create them, and which bundle ids must join. Pure - for the build path to * warn before archiving and for unit tests. */ -export const appGroupPortalNotice = (containers: string[]): string | null => { +export const appGroupPortalNotice = (containers: readonly string[]): string | null => { if (containers.length === 0) return null; const groups = containers.map((id) => `"${id}"`).join(', '); let plural = 'App Groups'; diff --git a/src/core/credentials/pushKeyStore.ts b/src/core/credentials/pushKeyStore.ts index f5cf191..10f0a4b 100644 --- a/src/core/credentials/pushKeyStore.ts +++ b/src/core/credentials/pushKeyStore.ts @@ -10,6 +10,7 @@ import { import { getSecret, setSecret } from './keychain.js'; import { decodeP8, encodeP8 } from './accounts.js'; import type { LaunchSecretStoreService } from '../services/secretStore.js'; +import type { MutableDeep } from '../types/mutable.js'; /** Secret-store account holding one APNs key's `.p8` PEM, namespaced by Key ID. */ const apnsAccount = (keyId: string): string => { return `apns-p8:${keyId}`; @@ -93,7 +94,7 @@ export const importPushKey = ( const timestamp = yield* currentTimestamp(); let importedAt = existing?.importedAt; if (importedAt === undefined) importedAt = timestamp; - const record: ApnsKeyRecord = { + const record: MutableDeep = { keyId: input.keyId, importedAt, }; diff --git a/src/core/credentials/signingPreflight.ts b/src/core/credentials/signingPreflight.ts index 0f69083..90160c8 100644 --- a/src/core/credentials/signingPreflight.ts +++ b/src/core/credentials/signingPreflight.ts @@ -37,7 +37,7 @@ export const resolveExtensionBundleIdsForApp = ( ): Effect.Effect => Effect.gen(function* () { const pathService = yield* Path.Path; - let configured: string[] = []; + let configured: readonly string[] = []; if (app.iosExtensions !== undefined) configured = app.iosExtensions; const nativeDirectory = pathService.join(app.dir, 'ios'); const discovered = yield* discoverExtensionBundleIds(nativeDirectory, app.bundleId); @@ -59,7 +59,7 @@ export const appGroupPreflightNotice = ( export const gatherTargetSigningReadiness = ( asc: SigningPreflightAscApi, bundleId: string, - extensions: string[], + extensions: readonly string[], entitlements: Record | undefined, ): Effect.Effect => { const required = mapEntitlementsToCapabilities(entitlements).enable; @@ -85,7 +85,9 @@ export const gatherTargetSigningReadiness = ( ); }; /** Turn readiness facts into build-time warning strings (best-effort - never throws). */ -export const signingPreflightWarnings = (readiness: TargetSigningReadiness[]): string[] => { +export const signingPreflightWarnings = ( + readiness: readonly TargetSigningReadiness[], +): string[] => { return multiTargetSigningWarnings(readiness); }; /** Turn readiness facts into doctor checks - unregistered/missing-capability targets fail the run. */