-
Notifications
You must be signed in to change notification settings - Fork 3
refactor(credentials): readonly-domain-types (stack 7/12, re-split #307) #380
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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>('AccountFailure'); | ||
| const AccountRecordSchema: Schema.Schema<AccountRecord> = 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<AccountsFile> = Schema.mutable( | ||
| Schema.Struct({ | ||
| active: Schema.NullOr(Schema.String), | ||
| accounts: Schema.mutable(Schema.Array(AccountRecordSchema)), | ||
| }), | ||
| ); | ||
| const AccountRecordSchema: Schema.Schema<AccountRecord> = 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<AccountsFile> = 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<AccountRecord> = { | ||
| 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<void, AccountFailure, AccountStorageRequirements> => | ||
| 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] }; | ||
|
Comment on lines
+239
to
+241
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion: An identity refresh with Severity Level: Major
|
||
| 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<AccountRecord, unknown>; | ||
| pick: (accounts: readonly AccountRecord[]) => Effect.Effect<AccountRecord, unknown>; | ||
| }; | ||
| /** | ||
| * Resolve the account a build should use, applying {@link decideBuildAccount} and then either using | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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[] = [], | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion: The default empty extension list allows callers that omit Severity Level: Major
|
||
| ): Effect.Effect< | ||
| SigningAssets | null, | ||
| never, | ||
|
|
@@ -121,7 +122,7 @@ export const loadCachedSigningAssets = ( | |
| return null; | ||
| extensionProfiles[ext] = extProfile.name; | ||
| } | ||
| const signingAssets: SigningAssets = { | ||
| const signingAssets: MutableDeep<SigningAssets> = { | ||
| bundleId, | ||
| teamId: profile.teamId, | ||
| certName: DISTRIBUTION_CERT_NAME, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -37,7 +37,7 @@ export const resolveExtensionBundleIdsForApp = ( | |
| ): Effect.Effect<string[], unknown, FileSystem.FileSystem | Path.Path> => | ||
| 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<string, unknown> | undefined, | ||
| ): Effect.Effect<TargetSigningReadiness[], unknown> => { | ||
| const required = mapEntitlementsToCapabilities(entitlements).enable; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion: Extensions are always assigned an empty required-capability set, so preflight only detects whether an extension App ID is registered and never reports capabilities missing from that extension. An extension that requires, for example, push notifications or another entitlement not present on the main app will pass preflight and fail later during provisioning or export. Gather each target's entitlements and map its own required capabilities. [incomplete implementation] Severity Level: Major
|
||
|
|
@@ -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. */ | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: When an existing account is re-imported after identity resolution returns an empty app list—for example, a transient Apple lookup failure—the rebuilt record omits
appsinstead of preserving the previously cached list. This silently discards usable account metadata and can make a previously resolved account appear unresolved; preserve the existing list when the new identity result is unavailable, or distinguish a successful empty result from a failed lookup. [stale reference]Severity Level: Major⚠️
(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖