@better-update/cli 0.12.1 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -28,7 +28,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
28
28
 
29
29
  //#endregion
30
30
  //#region package.json
31
- var version = "0.12.1";
31
+ var version = "0.13.1";
32
32
 
33
33
  //#endregion
34
34
  //#region src/lib/interactive-mode.ts
@@ -1820,6 +1820,80 @@ const promptConfirm = (message, options) => Effect.gen(function* () {
1820
1820
  })));
1821
1821
  });
1822
1822
 
1823
+ //#endregion
1824
+ //#region src/lib/apple-auth.ts
1825
+ const APPLE_PROVIDER_ID_ENV = "APPLE_PROVIDER_ID";
1826
+ const readEnv = (name) => Effect.gen(function* () {
1827
+ return yield* (yield* CliRuntime).getEnv(name);
1828
+ });
1829
+ const parseProviderId = (raw) => {
1830
+ const id = Number(raw);
1831
+ return Number.isInteger(id) ? Effect.succeed(id) : Effect.fail(new AppleAuthError$1({ message: `${APPLE_PROVIDER_ID_ENV} must be a numeric provider ID, got "${raw}".` }));
1832
+ };
1833
+ const readEnvProviderId = Effect.gen(function* () {
1834
+ const raw = yield* readEnv(APPLE_PROVIDER_ID_ENV);
1835
+ if (!raw) return;
1836
+ return yield* parseProviderId(raw);
1837
+ });
1838
+ const switchSessionProvider = (appleUtils, providerId) => Effect.tryPromise({
1839
+ try: async () => appleUtils.Session.setSessionProviderIdAsync(providerId),
1840
+ catch: (error) => new AppleAuthError$1({ message: `Failed to switch App Store Connect provider (${providerId}): ${String(error)}` })
1841
+ }).pipe(Effect.asVoid);
1842
+ /**
1843
+ * Resolve App Store Connect provider for the current session.
1844
+ *
1845
+ * Selection order: APPLE_PROVIDER_ID env → single available provider →
1846
+ * interactive prompt (always, when multi-team + interactive) → fall back to
1847
+ * apple-utils' currentProviderId (non-interactive only).
1848
+ *
1849
+ * Multi-team users are always re-prompted in interactive mode so a wrong pick
1850
+ * from a previous run can be corrected — we do NOT cache the team choice.
1851
+ *
1852
+ * `switched` flags that the apple-utils cookie jar was mutated.
1853
+ *
1854
+ * Non-interactive (CI): env or single-team paths still work; multi-team falls
1855
+ * back to whatever apple-utils auto-resolved from cookies. Fails with
1856
+ * InteractiveProhibitedError when multi-team and no signal at all.
1857
+ */
1858
+ const resolveProvider = (appleUtils, availableProviders, currentProviderId) => Effect.gen(function* () {
1859
+ let switched = false;
1860
+ const applyChoice = (picked) => Effect.gen(function* () {
1861
+ if (currentProviderId !== picked) {
1862
+ yield* switchSessionProvider(appleUtils, picked);
1863
+ switched = true;
1864
+ }
1865
+ return picked;
1866
+ });
1867
+ const envId = yield* readEnvProviderId;
1868
+ if (envId !== void 0) return {
1869
+ providerId: yield* applyChoice(envId),
1870
+ switched
1871
+ };
1872
+ if (availableProviders.length === 0) return {
1873
+ providerId: currentProviderId,
1874
+ switched
1875
+ };
1876
+ const [firstProvider] = availableProviders;
1877
+ if (availableProviders.length === 1 && firstProvider) return {
1878
+ providerId: yield* applyChoice(firstProvider.providerId),
1879
+ switched
1880
+ };
1881
+ if (!(yield* InteractiveMode).allow) {
1882
+ if (currentProviderId !== void 0) return {
1883
+ providerId: currentProviderId,
1884
+ switched
1885
+ };
1886
+ return yield* new InteractiveProhibitedError({ message: "Multiple App Store Connect providers are available but no APPLE_PROVIDER_ID is set; re-run interactively or set the env var." });
1887
+ }
1888
+ return {
1889
+ providerId: yield* applyChoice(yield* promptSelect("Select App Store Connect provider:", availableProviders.map((provider) => ({
1890
+ value: provider.providerId,
1891
+ label: `${provider.name} [${provider.subType}] (${provider.providerId})`
1892
+ })))),
1893
+ switched
1894
+ };
1895
+ });
1896
+
1823
1897
  //#endregion
1824
1898
  //#region ../../packages/safe-json/src/index.ts
1825
1899
  const parseJsonResult = (text) => {
@@ -1852,14 +1926,10 @@ const AppleSessionStoreLive = Layer.effect(AppleSessionStore, Effect.gen(functio
1852
1926
  if (!content) return null;
1853
1927
  const parsed = safeJsonParse(content);
1854
1928
  if (!isRecord(parsed)) return null;
1855
- if (typeof parsed["teamId"] !== "string" || typeof parsed["username"] !== "string" || !parsed["cookies"]) return null;
1856
- const providerIdRaw = parsed["providerId"];
1857
- const hasProviderId = typeof providerIdRaw === "number" && Number.isInteger(providerIdRaw);
1929
+ if (typeof parsed["username"] !== "string" || !parsed["cookies"]) return null;
1858
1930
  return {
1859
1931
  cookies: parsed["cookies"],
1860
- teamId: parsed["teamId"],
1861
- username: parsed["username"],
1862
- ...hasProviderId ? { providerId: providerIdRaw } : {}
1932
+ username: parsed["username"]
1863
1933
  };
1864
1934
  }),
1865
1935
  saveSession: (session) => Effect.gen(function* () {
@@ -1905,17 +1975,30 @@ const sessionFromInfo = (username, info) => ({
1905
1975
  teamName: info.provider.name,
1906
1976
  providerId: info.provider.providerId
1907
1977
  });
1908
- const restoreFromCookies = (appleUtils, cookies, providerId, teamId) => Effect.tryPromise({
1909
- try: async () => {
1910
- const input = {
1911
- cookies,
1912
- ...providerId === void 0 ? {} : { providerId },
1913
- ...teamId === void 0 ? {} : { teamId }
1914
- };
1915
- return appleUtils.Auth.loginWithCookiesAsync(input);
1916
- },
1978
+ const sessionFromProvider = (username, provider) => ({
1979
+ username,
1980
+ teamId: provider.publicProviderId,
1981
+ teamName: provider.name,
1982
+ providerId: provider.providerId
1983
+ });
1984
+ const restoreFromCookies = (appleUtils, cookies) => Effect.tryPromise({
1985
+ try: async () => appleUtils.Auth.loginWithCookiesAsync({ cookies }),
1917
1986
  catch: (cause) => new AppleAuthError$1({ message: `Failed to restore Apple session: ${formatCause(cause)}` })
1918
1987
  });
1988
+ /**
1989
+ * After a cookie restore or fresh credentials login, re-resolve the team via
1990
+ * {@link resolveProvider}. The cookies are accepted as-is (auth state) but the
1991
+ * team is treated as a per-run choice — we never trust a previously-cached team,
1992
+ * so a wrong pick can always be corrected on the next run.
1993
+ */
1994
+ const resolveSessionTeam = (appleUtils, state) => Effect.gen(function* () {
1995
+ const { availableProviders } = state.session;
1996
+ const resolution = yield* resolveProvider(appleUtils, availableProviders, state.context.providerId ?? state.session.provider.providerId);
1997
+ if (!resolution.switched || resolution.providerId === void 0) return sessionFromAuthState(state);
1998
+ const picked = availableProviders.find((provider) => provider.providerId === resolution.providerId);
1999
+ if (picked === void 0) return yield* new AppleAuthError$1({ message: `Selected provider ${String(resolution.providerId)} not in available providers list.` });
2000
+ return sessionFromProvider(state.username, picked);
2001
+ });
1919
2002
  const loginWithCredentials = (appleUtils, credentials) => Effect.tryPromise({
1920
2003
  try: async () => appleUtils.Auth.loginWithUserCredentialsAsync(credentials, { autoResolveProvider: true }),
1921
2004
  catch: (cause) => new AppleAuthError$1({ message: `Apple login failed: ${formatCause(cause)}` })
@@ -1942,12 +2025,10 @@ const interactiveLogin = (appleUtils, options, cachedUsername) => Effect.gen(fun
1942
2025
  password
1943
2026
  });
1944
2027
  if (state === null) return yield* new AppleAuthError$1({ message: "Apple login returned no session (unexpected)." });
1945
- const session = sessionFromAuthState(state);
2028
+ const session = yield* resolveSessionTeam(appleUtils, state);
1946
2029
  yield* store.saveSession({
1947
2030
  cookies: readJarCookies(appleUtils),
1948
- username: session.username,
1949
- teamId: session.teamId,
1950
- ...session.providerId === void 0 ? {} : { providerId: session.providerId }
2031
+ username: session.username
1951
2032
  });
1952
2033
  yield* store.saveLastUsername(session.username);
1953
2034
  return session;
@@ -1955,15 +2036,15 @@ const interactiveLogin = (appleUtils, options, cachedUsername) => Effect.gen(fun
1955
2036
  const tryRestore = (appleUtils, store) => Effect.gen(function* () {
1956
2037
  const stored = yield* store.loadSession;
1957
2038
  if (stored === null) return null;
1958
- const restored = yield* restoreFromCookies(appleUtils, stored.cookies, stored.providerId, stored.teamId);
2039
+ const restored = yield* restoreFromCookies(appleUtils, stored.cookies).pipe(Effect.catchAll(() => Effect.succeed(null)));
1959
2040
  if (restored === null) return null;
1960
- return sessionFromAuthState(restored);
2041
+ return yield* resolveSessionTeam(appleUtils, restored);
1961
2042
  });
1962
2043
  const makeAppleAuthLive = (appleUtils = defaultAppleUtils) => Layer.effect(AppleAuth, Effect.gen(function* () {
1963
2044
  const store = yield* AppleSessionStore;
1964
2045
  return {
1965
2046
  ensureLoggedIn: (options = {}) => Effect.gen(function* () {
1966
- const restored = yield* tryRestore(appleUtils, store).pipe(Effect.catchAll(() => Effect.succeed(null)));
2047
+ const restored = yield* tryRestore(appleUtils, store);
1967
2048
  if (restored !== null) return restored;
1968
2049
  return yield* interactiveLogin(appleUtils, options, yield* store.loadLastUsername).pipe(Effect.provideService(AppleSessionStore, store));
1969
2050
  }),
@@ -1974,15 +2055,10 @@ const makeAppleAuthLive = (appleUtils = defaultAppleUtils) => Layer.effect(Apple
1974
2055
  whoami: Effect.gen(function* () {
1975
2056
  const stored = yield* store.loadSession;
1976
2057
  if (stored === null) return null;
1977
- const restored = yield* restoreFromCookies(appleUtils, stored.cookies, stored.providerId, stored.teamId).pipe(Effect.catchAll(() => Effect.succeed(null)));
2058
+ const restored = yield* restoreFromCookies(appleUtils, stored.cookies).pipe(Effect.catchAll(() => Effect.succeed(null)));
1978
2059
  if (restored !== null) return sessionFromAuthState(restored);
1979
2060
  const info = appleUtils.Session.getAnySessionInfo();
1980
- return info === null ? {
1981
- username: stored.username,
1982
- teamId: stored.teamId,
1983
- teamName: null,
1984
- providerId: stored.providerId
1985
- } : sessionFromInfo(stored.username, info);
2061
+ return info === null ? null : sessionFromInfo(stored.username, info);
1986
2062
  }),
1987
2063
  buildRequestContext: (session) => ({
1988
2064
  teamId: session.teamId,
@@ -5278,17 +5354,30 @@ const DISTRIBUTION_TO_CERTIFICATE_TYPE = {
5278
5354
  DEVELOPMENT: AppleUtils.CertificateType.IOS_DEVELOPMENT
5279
5355
  };
5280
5356
  var AppleIdGenerateFailedError = class extends Data.TaggedError("AppleIdGenerateFailedError") {};
5357
+ const CERT_LIMIT_PATTERN = /already have a current.*certificate|pending certificate request/iu;
5358
+ const messageOf = (cause) => cause instanceof Error ? cause.message : String(cause);
5281
5359
  const wrap = (step, run) => Effect.tryPromise({
5282
5360
  try: run,
5283
5361
  catch: (cause) => new AppleIdGenerateFailedError({
5284
5362
  step,
5285
- message: cause instanceof Error ? cause.message : String(cause)
5363
+ message: messageOf(cause)
5286
5364
  })
5287
5365
  });
5366
+ const wrapCertificateCreate = (run) => Effect.tryPromise({
5367
+ try: run,
5368
+ catch: (cause) => {
5369
+ const message = messageOf(cause);
5370
+ if (CERT_LIMIT_PATTERN.test(message)) return new CertificateLimitError({ message });
5371
+ return new AppleIdGenerateFailedError({
5372
+ step: "apple-create-certificate",
5373
+ message
5374
+ });
5375
+ }
5376
+ });
5288
5377
  const generateAndUploadDistributionCertificateViaAppleId = (api, input) => Effect.gen(function* () {
5289
5378
  const ctx = input.context;
5290
5379
  const certificateType = input.certificateType === "IOS_DEVELOPMENT" ? AppleUtils.CertificateType.IOS_DEVELOPMENT : AppleUtils.CertificateType.IOS_DISTRIBUTION;
5291
- const result = yield* wrap("apple-create-certificate", async () => AppleUtils.createCertificateAndP12Async(ctx, { certificateType }));
5380
+ const result = yield* wrapCertificateCreate(async () => AppleUtils.createCertificateAndP12Async(ctx, { certificateType }));
5292
5381
  const metadata = yield* extractMetadataFromP12({
5293
5382
  p12Base64: result.certificateP12,
5294
5383
  password: result.password
@@ -5312,6 +5401,16 @@ const generateAndUploadDistributionCertificateViaAppleId = (api, input) => Effec
5312
5401
  developerPortalIdentifier: result.certificate.id
5313
5402
  };
5314
5403
  });
5404
+ const listDistributionCertsViaAppleId = (ctx, certificateType = "IOS_DISTRIBUTION") => Effect.gen(function* () {
5405
+ const filter = certificateType === "IOS_DEVELOPMENT" ? AppleUtils.CertificateType.IOS_DEVELOPMENT : AppleUtils.CertificateType.IOS_DISTRIBUTION;
5406
+ return (yield* wrap("apple-list-certificates", async () => AppleUtils.Certificate.getAsync(ctx, { query: { filter: { certificateType: filter } } }))).map((entry) => ({
5407
+ developerPortalIdentifier: entry.id,
5408
+ serialNumber: entry.attributes.serialNumber,
5409
+ displayName: entry.attributes.displayName,
5410
+ expirationDate: entry.attributes.expirationDate
5411
+ }));
5412
+ });
5413
+ const revokeDistributionCertViaAppleId = (ctx, developerPortalIdentifier) => wrap("apple-revoke-certificate", async () => AppleUtils.Certificate.deleteAsync(ctx, { id: developerPortalIdentifier }));
5315
5414
  const findOrCreateBundleId = (ctx, bundleIdentifier) => Effect.gen(function* () {
5316
5415
  const existing = yield* wrap("apple-find-bundle-id", async () => AppleUtils.BundleId.findAsync(ctx, { identifier: bundleIdentifier }));
5317
5416
  if (existing !== null) return existing.id;
@@ -5393,13 +5492,66 @@ const chooseIosSetupPath = (api) => Effect.gen(function* () {
5393
5492
  label: "Use an App Store Connect API key"
5394
5493
  }]);
5395
5494
  });
5495
+ const interactiveAppleIdCertLimitRecover = (ctx) => Effect.gen(function* () {
5496
+ yield* Console.log("");
5497
+ yield* Console.log("Apple reports the certificate limit was hit (max 3 distribution certs per team).");
5498
+ const certs = yield* listDistributionCertsViaAppleId(ctx, "IOS_DISTRIBUTION");
5499
+ if (certs.length === 0) return yield* new AppleIdGenerateFailedError({
5500
+ step: "limit-recover",
5501
+ message: "Apple says the certificate limit is hit but no existing certificates were returned."
5502
+ });
5503
+ const toRevoke = yield* promptMultiSelect("Select one or more certificates to revoke before retrying", certs.map((entry) => ({
5504
+ value: entry.developerPortalIdentifier,
5505
+ label: `${entry.serialNumber.slice(0, 12)}… (${entry.displayName}, exp ${entry.expirationDate.slice(0, 10)})`
5506
+ })), { required: true });
5507
+ yield* Effect.forEach(toRevoke, (id) => revokeDistributionCertViaAppleId(ctx, id), { concurrency: "inherit" });
5508
+ yield* Console.log(`Revoked ${toRevoke.length} certificate(s); retrying generation...`);
5509
+ });
5510
+ const generateDistributionCertViaAppleIdInteractive = (api, ctx) => Effect.gen(function* () {
5511
+ yield* Console.log("Generating distribution certificate via Apple ID...");
5512
+ const generate = generateAndUploadDistributionCertificateViaAppleId(api, { context: ctx });
5513
+ return yield* generate.pipe(Effect.catchTag("CertificateLimitError", () => interactiveAppleIdCertLimitRecover(ctx).pipe(Effect.flatMap(() => generate))));
5514
+ });
5515
+ const GENERATE_NEW = "__generate__";
5516
+ const chooseDistributionCertViaAppleId = (api, ctx, appleTeamId) => Effect.gen(function* () {
5517
+ const items = (yield* api.appleDistributionCertificates.list()).items.filter((cert) => cert.appleTeamId === appleTeamId);
5518
+ if (items.length === 0) {
5519
+ const created = yield* generateDistributionCertViaAppleIdInteractive(api, ctx);
5520
+ return {
5521
+ id: created.id,
5522
+ appleTeamId: created.appleTeamId
5523
+ };
5524
+ }
5525
+ const choice = yield* promptSelect("Select a distribution certificate (or 'generate' for a fresh one)", [{
5526
+ value: GENERATE_NEW,
5527
+ label: "Generate a new distribution certificate"
5528
+ }, ...items.map((cert) => ({
5529
+ value: cert.id,
5530
+ label: `${cert.serialNumber.slice(0, 12)}… (team ${cert.appleTeamId})`
5531
+ }))]);
5532
+ if (choice === GENERATE_NEW) {
5533
+ const created = yield* generateDistributionCertViaAppleIdInteractive(api, ctx);
5534
+ return {
5535
+ id: created.id,
5536
+ appleTeamId: created.appleTeamId
5537
+ };
5538
+ }
5539
+ const cert = items.find((entry) => entry.id === choice);
5540
+ if (cert === void 0) return yield* new AppleIdGenerateFailedError({
5541
+ step: "pick-certificate",
5542
+ message: `Selected certificate ${choice} not found after listing`
5543
+ });
5544
+ return {
5545
+ id: cert.id,
5546
+ appleTeamId: cert.appleTeamId
5547
+ };
5548
+ });
5396
5549
  const setupIosViaAppleId = (api, input) => Effect.gen(function* () {
5397
5550
  const auth = yield* AppleAuth;
5398
5551
  const session = yield* auth.ensureLoggedIn();
5399
5552
  const ctx = auth.buildRequestContext(session);
5400
5553
  yield* Console.log(`Logged in as ${session.username}. Team: ${session.teamName ?? session.teamId} (${session.teamId}).`);
5401
- yield* Console.log("Generating distribution certificate via Apple ID...");
5402
- const cert = yield* generateAndUploadDistributionCertificateViaAppleId(api, { context: ctx });
5554
+ const cert = yield* chooseDistributionCertViaAppleId(api, ctx, session.teamId);
5403
5555
  const distributionType = IOS_DISTRIBUTION_TO_TYPE[input.distribution];
5404
5556
  yield* Console.log("Generating provisioning profile via Apple ID...");
5405
5557
  const profile = yield* generateAndUploadProvisioningProfileViaAppleId(api, {