eas-cli 18.3.0 → 18.5.0

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.
Files changed (58) hide show
  1. package/README.md +98 -95
  2. package/build/build/android/prepareJob.js +2 -2
  3. package/build/build/build.js +27 -6
  4. package/build/build/ios/prepareJob.js +2 -2
  5. package/build/build/metadata.js +2 -1
  6. package/build/commandUtils/EasCommand.js +23 -2
  7. package/build/commandUtils/context/contextUtils/getProjectIdAsync.js +2 -0
  8. package/build/commandUtils/flags.d.ts +1 -0
  9. package/build/commandUtils/flags.js +12 -0
  10. package/build/commandUtils/workflow/fetchLogs.js +11 -2
  11. package/build/commandUtils/workflow/types.d.ts +5 -1
  12. package/build/commandUtils/workflow/utils.js +22 -16
  13. package/build/commands/build/dev.d.ts +1 -0
  14. package/build/commands/build/dev.js +9 -1
  15. package/build/commands/metadata/pull.d.ts +1 -0
  16. package/build/commands/metadata/pull.js +11 -4
  17. package/build/commands/metadata/push.d.ts +1 -0
  18. package/build/commands/metadata/push.js +11 -4
  19. package/build/commands/project/onboarding.js +3 -0
  20. package/build/commands/workflow/logs.js +12 -12
  21. package/build/graphql/generated.d.ts +673 -46
  22. package/build/graphql/generated.js +58 -20
  23. package/build/graphql/queries/UserQuery.js +3 -0
  24. package/build/graphql/types/Update.js +3 -0
  25. package/build/metadata/apple/config/reader.d.ts +5 -1
  26. package/build/metadata/apple/config/reader.js +8 -0
  27. package/build/metadata/apple/config/writer.d.ts +5 -1
  28. package/build/metadata/apple/config/writer.js +13 -0
  29. package/build/metadata/apple/data.d.ts +6 -2
  30. package/build/metadata/apple/rules/infoRestrictedWords.js +6 -1
  31. package/build/metadata/apple/tasks/age-rating.d.ts +1 -1
  32. package/build/metadata/apple/tasks/age-rating.js +19 -3
  33. package/build/metadata/apple/tasks/app-review-detail.js +7 -2
  34. package/build/metadata/apple/tasks/index.js +4 -0
  35. package/build/metadata/apple/tasks/previews.d.ts +18 -0
  36. package/build/metadata/apple/tasks/previews.js +208 -0
  37. package/build/metadata/apple/tasks/screenshots.d.ts +18 -0
  38. package/build/metadata/apple/tasks/screenshots.js +224 -0
  39. package/build/metadata/apple/types.d.ts +34 -1
  40. package/build/metadata/auth.d.ts +11 -1
  41. package/build/metadata/auth.js +96 -2
  42. package/build/metadata/download.d.ts +5 -1
  43. package/build/metadata/download.js +16 -8
  44. package/build/metadata/upload.d.ts +5 -1
  45. package/build/metadata/upload.js +11 -4
  46. package/build/project/projectUtils.d.ts +0 -2
  47. package/build/project/projectUtils.js +0 -12
  48. package/build/project/workflow.js +1 -1
  49. package/build/sentry.d.ts +2 -0
  50. package/build/sentry.js +22 -0
  51. package/build/update/utils.d.ts +2 -2
  52. package/build/update/utils.js +1 -0
  53. package/build/user/User.d.ts +2 -2
  54. package/build/user/User.js +3 -0
  55. package/build/user/expoBrowserAuthFlowLauncher.js +70 -13
  56. package/oclif.manifest.json +1995 -1918
  57. package/package.json +12 -11
  58. package/schema/metadata-0.json +36 -0
@@ -1,15 +1,19 @@
1
1
  import { ExpoConfig } from '@expo/config';
2
2
  import { SubmitProfile } from '@expo/eas-json';
3
3
  import { Analytics } from '../analytics/AnalyticsManager';
4
+ import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
4
5
  import { CredentialsContext } from '../credentials/context';
5
6
  /**
6
7
  * Generate a local store configuration from the stores.
7
8
  * Note, only App Store is supported at this time.
8
9
  */
9
- export declare function downloadMetadataAsync({ projectDir, profile, exp, analytics, credentialsCtx, }: {
10
+ export declare function downloadMetadataAsync({ projectDir, profile, exp, analytics, credentialsCtx, nonInteractive, graphqlClient, projectId, }: {
10
11
  projectDir: string;
11
12
  profile: SubmitProfile;
12
13
  exp: ExpoConfig;
13
14
  analytics: Analytics;
14
15
  credentialsCtx: CredentialsContext;
16
+ nonInteractive: boolean;
17
+ graphqlClient: ExpoGraphqlClient;
18
+ projectId: string;
15
19
  }): Promise<string>;
@@ -16,16 +16,21 @@ const prompts_1 = require("../prompts");
16
16
  * Generate a local store configuration from the stores.
17
17
  * Note, only App Store is supported at this time.
18
18
  */
19
- async function downloadMetadataAsync({ projectDir, profile, exp, analytics, credentialsCtx, }) {
19
+ async function downloadMetadataAsync({ projectDir, profile, exp, analytics, credentialsCtx, nonInteractive, graphqlClient, projectId, }) {
20
20
  const filePath = (0, resolve_1.getStaticConfigFilePath)({ projectDir, profile });
21
21
  const fileExists = await fs_extra_1.default.pathExists(filePath);
22
22
  if (fileExists) {
23
- const filePathRelative = path_1.default.relative(projectDir, filePath);
24
- const overwrite = await (0, prompts_1.confirmAsync)({
25
- message: `Do you want to overwrite the existing "${filePathRelative}"?`,
26
- });
27
- if (!overwrite) {
28
- throw new errors_1.MetadataValidationError(`Store config already exists at "${filePath}"`);
23
+ if (nonInteractive) {
24
+ log_1.default.log(`Overwriting existing store config at "${path_1.default.relative(projectDir, filePath)}".`);
25
+ }
26
+ else {
27
+ const filePathRelative = path_1.default.relative(projectDir, filePath);
28
+ const overwrite = await (0, prompts_1.confirmAsync)({
29
+ message: `Do you want to overwrite the existing "${filePathRelative}"?`,
30
+ });
31
+ if (!overwrite) {
32
+ throw new errors_1.MetadataValidationError(`Store config already exists at "${filePath}"`);
33
+ }
29
34
  }
30
35
  }
31
36
  const { app, auth } = await (0, auth_1.getAppStoreAuthAsync)({
@@ -33,6 +38,9 @@ async function downloadMetadataAsync({ projectDir, profile, exp, analytics, cred
33
38
  credentialsCtx,
34
39
  projectDir,
35
40
  profile,
41
+ nonInteractive,
42
+ graphqlClient,
43
+ projectId,
36
44
  });
37
45
  const { unsubscribeTelemetry, executionId } = await (0, telemetry_1.subscribeTelemetryAsync)(analytics, AnalyticsManager_1.MetadataEvent.APPLE_METADATA_DOWNLOAD, { app, auth });
38
46
  log_1.default.addNewLineIfNone();
@@ -40,7 +48,7 @@ async function downloadMetadataAsync({ projectDir, profile, exp, analytics, cred
40
48
  const errors = [];
41
49
  const config = (0, resolve_1.createAppleWriter)();
42
50
  const tasks = (0, tasks_1.createAppleTasks)();
43
- const taskCtx = { app };
51
+ const taskCtx = { app, projectDir };
44
52
  for (const task of tasks) {
45
53
  try {
46
54
  await task.prepareAsync({ context: taskCtx });
@@ -1,17 +1,21 @@
1
1
  import { ExpoConfig } from '@expo/config';
2
2
  import { SubmitProfile } from '@expo/eas-json';
3
3
  import { Analytics } from '../analytics/AnalyticsManager';
4
+ import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
4
5
  import { CredentialsContext } from '../credentials/context';
5
6
  /**
6
7
  * Sync a local store configuration with the stores.
7
8
  * Note, only App Store is supported at this time.
8
9
  */
9
- export declare function uploadMetadataAsync({ projectDir, profile, exp, analytics, credentialsCtx, }: {
10
+ export declare function uploadMetadataAsync({ projectDir, profile, exp, analytics, credentialsCtx, nonInteractive, graphqlClient, projectId, }: {
10
11
  projectDir: string;
11
12
  profile: SubmitProfile;
12
13
  exp: ExpoConfig;
13
14
  analytics: Analytics;
14
15
  credentialsCtx: CredentialsContext;
16
+ nonInteractive: boolean;
17
+ graphqlClient: ExpoGraphqlClient;
18
+ projectId: string;
15
19
  }): Promise<{
16
20
  appleLink: string;
17
21
  }>;
@@ -14,13 +14,16 @@ const prompts_1 = require("../prompts");
14
14
  * Sync a local store configuration with the stores.
15
15
  * Note, only App Store is supported at this time.
16
16
  */
17
- async function uploadMetadataAsync({ projectDir, profile, exp, analytics, credentialsCtx, }) {
18
- const storeConfig = await loadConfigWithValidationPromptAsync(projectDir, profile);
17
+ async function uploadMetadataAsync({ projectDir, profile, exp, analytics, credentialsCtx, nonInteractive, graphqlClient, projectId, }) {
18
+ const storeConfig = await loadConfigWithValidationPromptAsync(projectDir, profile, nonInteractive);
19
19
  const { app, auth } = await (0, auth_1.getAppStoreAuthAsync)({
20
20
  exp,
21
21
  credentialsCtx,
22
22
  projectDir,
23
23
  profile,
24
+ nonInteractive,
25
+ graphqlClient,
26
+ projectId,
24
27
  });
25
28
  const { unsubscribeTelemetry, executionId } = await (0, telemetry_1.subscribeTelemetryAsync)(analytics, AnalyticsManager_1.MetadataEvent.APPLE_METADATA_UPLOAD, { app, auth });
26
29
  log_1.default.addNewLineIfNone();
@@ -32,7 +35,7 @@ async function uploadMetadataAsync({ projectDir, profile, exp, analytics, creden
32
35
  // This version is the parent model of all changes we are going to push.
33
36
  version: config.getVersion()?.versionString,
34
37
  });
35
- const taskCtx = { app };
38
+ const taskCtx = { app, projectDir };
36
39
  for (const task of tasks) {
37
40
  try {
38
41
  await task.prepareAsync({ context: taskCtx });
@@ -55,12 +58,16 @@ async function uploadMetadataAsync({ projectDir, profile, exp, analytics, creden
55
58
  }
56
59
  return { appleLink: `https://appstoreconnect.apple.com/apps/${app.id}/appstore` };
57
60
  }
58
- async function loadConfigWithValidationPromptAsync(projectDir, profile) {
61
+ async function loadConfigWithValidationPromptAsync(projectDir, profile, nonInteractive) {
59
62
  try {
60
63
  return await (0, resolve_1.loadConfigAsync)({ projectDir, profile });
61
64
  }
62
65
  catch (error) {
63
66
  if (error instanceof errors_1.MetadataValidationError) {
67
+ if (nonInteractive) {
68
+ (0, errors_1.logMetadataValidationError)(error);
69
+ throw error;
70
+ }
64
71
  (0, errors_1.logMetadataValidationError)(error);
65
72
  log_1.default.newLine();
66
73
  log_1.default.warn('Without further updates, the current store configuration can fail to be synchronized with the App Store or pass App Store review.');
@@ -1,8 +1,6 @@
1
1
  import { ExpoConfig } from '@expo/config';
2
2
  import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
3
3
  import { AccountFragment } from '../graphql/generated';
4
- import { Actor } from '../user/User';
5
- export declare function getUsernameForBuildMetadataAndBuildJob(user: Actor): string | undefined;
6
4
  /**
7
5
  * Return a useful name describing the project config.
8
6
  * - dynamic: app.config.js
@@ -1,6 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getUsernameForBuildMetadataAndBuildJob = getUsernameForBuildMetadataAndBuildJob;
4
3
  exports.getProjectConfigDescription = getProjectConfigDescription;
5
4
  exports.isExpoUpdatesInstalled = isExpoUpdatesInstalled;
6
5
  exports.isExpoNotificationsInstalled = isExpoNotificationsInstalled;
@@ -26,17 +25,6 @@ const api_1 = require("../api");
26
25
  const AppQuery_1 = require("../graphql/queries/AppQuery");
27
26
  const log_1 = tslib_1.__importStar(require("../log"));
28
27
  const expoCli_1 = require("../utils/expoCli");
29
- function getUsernameForBuildMetadataAndBuildJob(user) {
30
- switch (user.__typename) {
31
- case 'User':
32
- return user.username;
33
- case 'SSOUser':
34
- return user.username;
35
- case 'Robot':
36
- // robot users don't have usernames
37
- return undefined;
38
- }
39
- }
40
28
  /**
41
29
  * Return a useful name describing the project config.
42
30
  * - dynamic: app.config.js
@@ -44,7 +44,7 @@ async function hasIgnoredIosProjectAsync(projectDir, vcsClient) {
44
44
  const pbxProjectPath = config_plugins_1.IOSConfig.Paths.getPBXProjectPath(projectDir);
45
45
  return await vcsClient.isFileIgnoredAsync(path_1.default.relative(vcsRootPath, pbxProjectPath));
46
46
  }
47
- finally {
47
+ catch {
48
48
  return false;
49
49
  }
50
50
  }
@@ -0,0 +1,2 @@
1
+ import * as Sentry from '@sentry/node';
2
+ export default Sentry;
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ const Sentry = tslib_1.__importStar(require("@sentry/node"));
5
+ const easCli_1 = require("./utils/easCli");
6
+ Sentry.init({
7
+ dsn: process.env.EAS_CLI_SENTRY_DSN,
8
+ enabled: !!process.env.EAS_CLI_SENTRY_DSN,
9
+ environment: getSentryEnvironment(),
10
+ release: easCli_1.easCliVersion ? `eas-cli@${easCli_1.easCliVersion}` : undefined,
11
+ });
12
+ Sentry.setTag('source', 'eas-cli');
13
+ function getSentryEnvironment() {
14
+ if (process.env.EXPO_LOCAL) {
15
+ return 'local';
16
+ }
17
+ else if (process.env.EXPO_STAGING) {
18
+ return 'staging';
19
+ }
20
+ return 'production';
21
+ }
22
+ exports.default = Sentry;
@@ -1,9 +1,9 @@
1
1
  import { ExpoConfig } from '@expo/config';
2
2
  import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
3
- import { AppPlatform, Robot, SsoUser, Update, UpdateBranchFragment, UpdateFragment, UpdatePublishMutation, User } from '../graphql/generated';
3
+ import { AppPlatform, PartnerActor, Robot, SsoUser, Update, UpdateBranchFragment, UpdateFragment, UpdatePublishMutation, User } from '../graphql/generated';
4
4
  import { RequestedPlatform } from '../platform';
5
5
  export type FormatUpdateParameter = Pick<Update, 'id' | 'createdAt' | 'message'> & {
6
- actor?: Pick<Robot, '__typename' | 'firstName'> | Pick<User, '__typename' | 'username'> | Pick<SsoUser, '__typename' | 'username'> | null;
6
+ actor?: Pick<Robot, '__typename' | 'firstName'> | Pick<User, '__typename' | 'username'> | Pick<SsoUser, '__typename' | 'username'> | Pick<PartnerActor, '__typename' | 'username'> | null;
7
7
  };
8
8
  export type UpdateJsonInfo = {
9
9
  branch: string;
@@ -110,6 +110,7 @@ function formatUpdateTitle(update) {
110
110
  let actorName;
111
111
  switch (actor?.__typename) {
112
112
  case 'User':
113
+ case 'PartnerActor':
113
114
  case 'SSOUser': {
114
115
  actorName = actor.username;
115
116
  break;
@@ -1,9 +1,9 @@
1
- import { CurrentUserQuery, Robot, SsoUser, User } from '../graphql/generated';
1
+ import { CurrentUserQuery, PartnerActor, Robot, SsoUser, User } from '../graphql/generated';
2
2
  export type Actor = NonNullable<CurrentUserQuery['meActor']>;
3
3
  /**
4
4
  * Resolve the name of the actor, either normal user, sso user or robot user.
5
5
  * This should be used whenever the "current user" needs to be displayed.
6
6
  * The display name CANNOT be used as project owner.
7
7
  */
8
- export declare function getActorDisplayName(actor?: Pick<Robot, '__typename' | 'firstName'> | Pick<User, '__typename' | 'username'> | Pick<SsoUser, '__typename' | 'username'> | null): string;
8
+ export declare function getActorDisplayName(actor?: Pick<Robot, '__typename' | 'firstName'> | Pick<User, '__typename' | 'username'> | Pick<SsoUser, '__typename' | 'username'> | Pick<PartnerActor, '__typename' | 'username'> | null): string;
9
9
  export declare function getActorUsername(actor?: Actor): string | null;
@@ -15,6 +15,8 @@ function getActorDisplayName(actor) {
15
15
  return actor.firstName ? `${actor.firstName} (robot)` : 'robot';
16
16
  case 'SSOUser':
17
17
  return actor.username;
18
+ case 'PartnerActor':
19
+ return actor.username;
18
20
  case undefined:
19
21
  return 'unknown';
20
22
  }
@@ -23,6 +25,7 @@ function getActorUsername(actor) {
23
25
  switch (actor?.__typename) {
24
26
  case 'User':
25
27
  case 'SSOUser':
28
+ case 'PartnerActor':
26
29
  return actor.username;
27
30
  case 'Robot':
28
31
  case undefined:
@@ -4,20 +4,65 @@ exports.getSessionUsingBrowserAuthFlowAsync = getSessionUsingBrowserAuthFlowAsyn
4
4
  const tslib_1 = require("tslib");
5
5
  const assert_1 = tslib_1.__importDefault(require("assert"));
6
6
  const better_opn_1 = tslib_1.__importDefault(require("better-opn"));
7
+ const crypto_1 = tslib_1.__importDefault(require("crypto"));
7
8
  const http_1 = tslib_1.__importDefault(require("http"));
8
- const querystring_1 = tslib_1.__importDefault(require("querystring"));
9
9
  const api_1 = require("../api");
10
+ const fetch_1 = tslib_1.__importDefault(require("../fetch"));
10
11
  const log_1 = tslib_1.__importDefault(require("../log"));
12
+ const CLIENT_ID = 'eas-cli';
13
+ function generateCodeVerifier() {
14
+ return crypto_1.default.randomBytes(32).toString('base64url');
15
+ }
16
+ function generateCodeChallenge(codeVerifier) {
17
+ return crypto_1.default.createHash('sha256').update(codeVerifier).digest('base64url');
18
+ }
19
+ function generateState() {
20
+ return crypto_1.default.randomBytes(32).toString('base64url');
21
+ }
22
+ async function exchangeCodeForSessionSecretAsync({ code, codeVerifier, redirectUri, }) {
23
+ const tokenUrl = `${(0, api_1.getExpoApiBaseUrl)()}/v2/auth/token`;
24
+ const response = await (0, fetch_1.default)(tokenUrl, {
25
+ method: 'POST',
26
+ headers: {
27
+ 'Content-Type': 'application/json',
28
+ },
29
+ body: JSON.stringify({
30
+ grant_type: 'authorization_code',
31
+ code,
32
+ redirect_uri: redirectUri,
33
+ code_verifier: codeVerifier,
34
+ client_id: CLIENT_ID,
35
+ }),
36
+ });
37
+ const result = await response.json();
38
+ const sessionSecret = result?.data?.session_secret;
39
+ if (!sessionSecret) {
40
+ throw new Error('Failed to obtain session secret from token exchange.');
41
+ }
42
+ return sessionSecret;
43
+ }
11
44
  async function getSessionUsingBrowserAuthFlowAsync({ sso = false }) {
12
45
  const scheme = 'http';
13
46
  const hostname = 'localhost';
14
- const path = '/auth/callback';
47
+ const callbackPath = '/auth/callback';
15
48
  const expoWebsiteUrl = (0, api_1.getExpoWebsiteBaseUrl)();
49
+ const codeVerifier = generateCodeVerifier();
50
+ const codeChallenge = generateCodeChallenge(codeVerifier);
51
+ const state = generateState();
52
+ const buildRedirectUri = (port) => `${scheme}://${hostname}:${port}${callbackPath}`;
16
53
  const buildExpoLoginUrl = (port, sso) => {
17
- const params = querystring_1.default.stringify({
18
- confirm_account: true,
19
- app_redirect_uri: `${scheme}://${hostname}:${port}${path}`,
20
- });
54
+ // Note: we avoid URLSearchParams here because better-opn calls encodeURI()
55
+ // on the URL before passing it to AppleScript, which would double-encode
56
+ // the percent-encoded values from URLSearchParams.toString().
57
+ const params = [
58
+ `client_id=${CLIENT_ID}`,
59
+ `redirect_uri=${buildRedirectUri(port)}`,
60
+ `response_type=code`,
61
+ `code_challenge=${codeChallenge}`,
62
+ `code_challenge_method=S256`,
63
+ `state=${state}`,
64
+ `confirm_account=true`,
65
+ ].join('&');
21
66
  return `${expoWebsiteUrl}${sso ? '/sso-login' : '/login'}?${params}`;
22
67
  };
23
68
  // Start server and begin auth flow
@@ -34,22 +79,34 @@ async function getSessionUsingBrowserAuthFlowAsync({ sso = false }) {
34
79
  connection.destroy();
35
80
  }
36
81
  };
37
- try {
82
+ const handleRequestAsync = async () => {
38
83
  if (!(request.method === 'GET' && request.url?.includes('/auth/callback'))) {
39
84
  throw new Error('Unexpected login response.');
40
85
  }
41
86
  const url = new URL(request.url, `http:${request.headers.host}`);
42
- const sessionSecret = url.searchParams.get('session_secret');
43
- if (!sessionSecret) {
44
- throw new Error('Request missing session_secret search parameter.');
87
+ const code = url.searchParams.get('code');
88
+ const returnedState = url.searchParams.get('state');
89
+ if (!code) {
90
+ throw new Error('Request missing code search parameter.');
91
+ }
92
+ if (returnedState !== state) {
93
+ throw new Error('State mismatch. Possible CSRF attack.');
45
94
  }
95
+ const address = server.address();
96
+ (0, assert_1.default)(address !== null && typeof address === 'object');
97
+ const redirectUri = buildRedirectUri(address.port);
98
+ const sessionSecret = await exchangeCodeForSessionSecretAsync({
99
+ code,
100
+ codeVerifier,
101
+ redirectUri,
102
+ });
46
103
  resolve(sessionSecret);
47
104
  redirectAndCleanup('success');
48
- }
49
- catch (error) {
105
+ };
106
+ handleRequestAsync().catch(error => {
50
107
  redirectAndCleanup('error');
51
108
  reject(error);
52
- }
109
+ });
53
110
  });
54
111
  server.listen(0, hostname, () => {
55
112
  log_1.default.log('Waiting for browser login...');