eoas 2.3.22 → 3.0.0-beta.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/README.md CHANGED
@@ -5,7 +5,7 @@ EOAS ((Expo Open Application Services) is a powerful helper package designed to
5
5
  ## Quick Start
6
6
 
7
7
  To get started with EOAS, check out the official documentation:
8
- [EOAS Official Documentation](https://axelmarciano.github.io/expo-open-ota/)
8
+ [EOAS Official Documentation](https://mercure-technologies.gitbook.io/expo-open-ota/eoas/overview)
9
9
 
10
10
  ## Learn More
11
11
  For detailed information and to explore the core functionalities of expo-open-ota, visit the main repository:
@@ -0,0 +1,12 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Doctor extends Command {
3
+ static args: {};
4
+ static description: string;
5
+ static examples: string[];
6
+ static flags: {
7
+ channel: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
8
+ url: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
9
+ appId: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
10
+ };
11
+ run(): Promise<void>;
12
+ }
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ const core_1 = require("@oclif/core");
5
+ const expoConfig_1 = require("../lib/expoConfig");
6
+ const fetch_1 = require("../lib/fetch");
7
+ const log_1 = tslib_1.__importDefault(require("../lib/log"));
8
+ const ora_1 = require("../lib/ora");
9
+ const package_1 = require("../lib/package");
10
+ // probeManifest asks the server for a manifest the way a client would, and
11
+ // reports what came back. `appId` is omitted to impersonate a v1 client — the
12
+ // point of this command is that the header may legitimately be absent.
13
+ //
14
+ // A runtime version nothing was ever published against is deliberate: the
15
+ // server answers "no update available" instead of streaming a bundle, which
16
+ // exercises app resolution without downloading anything. Resolution happens
17
+ // before any update lookup, so a 200 proves the app was resolved either way.
18
+ async function probeManifest({ baseUrl, channel, appId, }) {
19
+ const headers = {
20
+ 'expo-platform': 'ios',
21
+ 'expo-runtime-version': 'eoas-doctor-probe',
22
+ 'expo-protocol-version': '1',
23
+ 'expo-channel-name': channel,
24
+ };
25
+ if (appId) {
26
+ headers['expo-app-id'] = appId;
27
+ }
28
+ const response = await (0, fetch_1.fetchWithRetries)(`${baseUrl}/manifest`, { method: 'GET', headers });
29
+ return {
30
+ ok: response.ok,
31
+ status: response.status,
32
+ body: (await response.text()).trim(),
33
+ };
34
+ }
35
+ class Doctor extends core_1.Command {
36
+ static args = {};
37
+ static description = "Check that this project's clients can reach the update server — including builds shipped before expo-app-id existed";
38
+ static examples = ['<%= config.bin %> <%= command.id %> --channel=production'];
39
+ static flags = {
40
+ channel: core_1.Flags.string({
41
+ description: 'Channel to probe with (must exist on the server)',
42
+ required: true,
43
+ }),
44
+ url: core_1.Flags.string({
45
+ description: 'Update server URL. Defaults to updates.url from your Expo config',
46
+ required: false,
47
+ }),
48
+ appId: core_1.Flags.string({
49
+ description: 'App id to probe with. Defaults to updates.requestHeaders["expo-app-id"] from your Expo config',
50
+ required: false,
51
+ }),
52
+ };
53
+ async run() {
54
+ const { flags } = await this.parse(Doctor);
55
+ const projectDir = process.cwd();
56
+ if (!(0, package_1.isExpoInstalled)(projectDir)) {
57
+ log_1.default.error('Expo is not installed in this project. Please install Expo first.');
58
+ process.exit(1);
59
+ }
60
+ const privateConfig = await (0, expoConfig_1.getPrivateExpoConfigAsync)(projectDir, {
61
+ env: process.env,
62
+ });
63
+ const updateUrl = flags.url ?? (0, expoConfig_1.getExpoConfigUpdateUrl)(privateConfig);
64
+ if (!updateUrl) {
65
+ log_1.default.error("Update url is not setup in your config. Please run 'eoas init' to setup the update url, or pass --url");
66
+ process.exit(1);
67
+ }
68
+ let baseUrl;
69
+ try {
70
+ baseUrl = new URL(updateUrl).origin;
71
+ }
72
+ catch (e) {
73
+ log_1.default.error('Invalid URL', e);
74
+ process.exit(1);
75
+ }
76
+ const appId = flags.appId ?? (0, expoConfig_1.getExpoAppId)(privateConfig);
77
+ log_1.default.log(`🩺 Probing ${baseUrl} on channel '${flags.channel}'`);
78
+ log_1.default.newLine();
79
+ // Probe 1 — the fleet already in users' hands. Its binary predates
80
+ // expo-app-id and cannot be made to send it without a store release, so
81
+ // this is the probe that decides whether upgrading the server strands
82
+ // those installs.
83
+ const legacySpinner = (0, ora_1.ora)('Probing as a v1 client (no expo-app-id header)...').start();
84
+ const legacy = await probeManifest({ baseUrl, channel: flags.channel });
85
+ if (legacy.ok) {
86
+ legacySpinner.succeed('✅ v1 clients are served — the server falls back to its EXPO_APP_ID');
87
+ }
88
+ else {
89
+ legacySpinner.fail(`❌ v1 clients are rejected — HTTP ${legacy.status}: ${legacy.body}`);
90
+ }
91
+ // Probe 2 — what a rebuilt binary sends. Skipped when the project has no
92
+ // app id configured, which is itself the v1 shape and not an error.
93
+ let modern;
94
+ if (appId) {
95
+ const modernSpinner = (0, ora_1.ora)(`Probing as a v2 client (expo-app-id: ${appId})...`).start();
96
+ modern = await probeManifest({ baseUrl, channel: flags.channel, appId });
97
+ if (modern.ok) {
98
+ modernSpinner.succeed('✅ v2 clients are served');
99
+ }
100
+ else {
101
+ modernSpinner.fail(`❌ v2 clients are rejected — HTTP ${modern.status}: ${modern.body}`);
102
+ }
103
+ }
104
+ else {
105
+ log_1.default.warn("⚠️ Skipping the v2 probe: no 'expo-app-id' in updates.requestHeaders. That is the v1 config shape — run 'npx eoas init' to add it.");
106
+ }
107
+ log_1.default.newLine();
108
+ if (!legacy.ok && !appId) {
109
+ log_1.default.error('Your v1 clients are being rejected and this project sends no app id at all.');
110
+ log_1.default.error('Every install in the wild has lost OTA coverage. On the server, unset SKIP_LEGACY_APP_ID_FALLBACK to restore it immediately.');
111
+ process.exit(1);
112
+ }
113
+ if (!legacy.ok) {
114
+ log_1.default.warn('Clients built before expo-app-id existed are rejected by this server.');
115
+ log_1.default.warn('That is correct only if every build your users run already ships the header. If any predate it, they have silently stopped updating — unset SKIP_LEGACY_APP_ID_FALLBACK on the server.');
116
+ }
117
+ if (modern && !modern.ok) {
118
+ log_1.default.error(`The app id '${appId}' is not served by this server.`);
119
+ log_1.default.error('Check it matches EXPO_APP_ID (single-app) or an app in the dashboard (control plane).');
120
+ process.exit(1);
121
+ }
122
+ if (legacy.ok && modern?.ok) {
123
+ log_1.default.succeed('Both v1 and v2 clients are served. This server is safe to cut over to.');
124
+ }
125
+ }
126
+ }
127
+ exports.default = Doctor;
@@ -27,6 +27,16 @@ class Init extends core_1.Command {
27
27
  log_1.default.error('Could not find Expo config in this project. Please make sure you have an Expo config.');
28
28
  return;
29
29
  }
30
+ const detectedAppId = config.extra?.eas
31
+ ?.projectId;
32
+ const { appId } = await (0, prompts_1.promptAsync)({
33
+ message: 'Enter the project id for this project (sent as the expo-app-id header).\n' +
34
+ ' See https://mercure-technologies.gitbook.io/expo-open-ota/stateless-mode/getting-started for details.',
35
+ name: 'appId',
36
+ type: 'text',
37
+ initial: detectedAppId,
38
+ validate: v => !!v,
39
+ });
30
40
  const { updateUrl: promptedUrl } = await (0, prompts_1.promptAsync)({
31
41
  message: 'Enter the URL of your update server (ex: https://customota.com)',
32
42
  name: 'updateUrl',
@@ -95,6 +105,7 @@ class Init extends core_1.Command {
95
105
  enabled: true,
96
106
  requestHeaders: {
97
107
  'expo-channel-name': 'process.env.RELEASE_CHANNEL',
108
+ 'expo-app-id': appId,
98
109
  },
99
110
  };
100
111
  const updateConfigSpinner = (0, ora_1.ora)('Updating Expo config').start();
@@ -84,9 +84,9 @@ class Publish extends core_1.Command {
84
84
  };
85
85
  }
86
86
  async run() {
87
- const credentials = (0, auth_1.retrieveExpoCredentials)();
88
- if (!credentials.token && !credentials.sessionSecret) {
89
- log_1.default.error('You are not logged to eas, please run `eas login`');
87
+ const credentials = (0, auth_1.retrieveCredentials)();
88
+ if (!(0, auth_1.validateCredentials)(credentials)) {
89
+ log_1.default.error('Invalid credentials. Please run `eas login or set EXPO_ACCESS_TOKEN or EOO_TOKEN environment variable`');
90
90
  process.exit(1);
91
91
  }
92
92
  const { flags } = await this.parse(Publish);
@@ -116,6 +116,7 @@ class Publish extends core_1.Command {
116
116
  log_1.default.error(e.message);
117
117
  process.exit(1);
118
118
  });
119
+ const appId = (0, expoConfig_1.requireExpoAppId)(config);
119
120
  if (!nonInteractive) {
120
121
  const confirmed = await (0, prompts_1.confirmAsync)({
121
122
  message: `Is this the correct URL of your self-hosted update server? ${serverUrl}`,
@@ -194,7 +195,15 @@ class Publish extends core_1.Command {
194
195
  const specifiedPlatform = platform === expoConfig_1.RequestedPlatform.All ? [] : ['--platform', platform];
195
196
  const sourcemapArgs = dumpSourcemap ? ['--dump-sourcemap'] : [];
196
197
  const [runnerCommand, runnerArgs] = (0, packageRunner_1.splitPackageRunner)(packageRunner);
197
- const { stdout } = await (0, spawn_async_1.default)(runnerCommand, [...runnerArgs, 'expo', 'export', '--output-dir', outputDir, ...sourcemapArgs, ...specifiedPlatform], {
198
+ const { stdout } = await (0, spawn_async_1.default)(runnerCommand, [
199
+ ...runnerArgs,
200
+ 'expo',
201
+ 'export',
202
+ '--output-dir',
203
+ outputDir,
204
+ ...sourcemapArgs,
205
+ ...specifiedPlatform,
206
+ ], {
198
207
  cwd: projectDir,
199
208
  env: {
200
209
  ...process.env,
@@ -238,7 +247,7 @@ class Publish extends core_1.Command {
238
247
  body: {
239
248
  fileNames: files.map(file => file.path),
240
249
  },
241
- requestUploadUrl: `${serverUrl}/requestUploadUrl/${branch}`,
250
+ requestUploadUrl: `${serverUrl}/${appId}/requestUploadUrl/${branch}`,
242
251
  auth: credentials,
243
252
  runtimeVersion,
244
253
  platform,
@@ -251,7 +260,7 @@ class Publish extends core_1.Command {
251
260
  }));
252
261
  const allItems = uploadUrls.flatMap(({ uploadRequests }) => uploadRequests);
253
262
  await Promise.all(allItems.map(async (itm) => {
254
- const isLocalBucketFileUpload = itm.requestUploadUrl.startsWith(`${serverUrl}/uploadLocalFile`);
263
+ const isLocalBucketFileUpload = itm.requestUploadUrl.startsWith(`${serverUrl}/${appId}/uploadLocalFile`);
255
264
  const formData = new form_data_1.default();
256
265
  let file;
257
266
  try {
@@ -266,7 +275,7 @@ class Publish extends core_1.Command {
266
275
  method: 'PUT',
267
276
  headers: {
268
277
  ...formData.getHeaders(),
269
- ...(0, auth_1.getAuthExpoHeaders)(credentials),
278
+ ...(0, auth_1.getAuthHeaders)(credentials),
270
279
  },
271
280
  body: formData,
272
281
  });
@@ -310,14 +319,14 @@ class Publish extends core_1.Command {
310
319
  }
311
320
  const markAsFinishedSpinner = (0, ora_1.ora)('🔗 Marking the updates as finished...').start();
312
321
  const results = await Promise.all(uploadUrls.map(async ({ updateId, platform, runtimeVersion }) => {
313
- const markAsUploadedUrl = new URL(`${serverUrl}/markUpdateAsUploaded/${branch}`);
322
+ const markAsUploadedUrl = new URL(`${serverUrl}/${appId}/markUpdateAsUploaded/${branch}`);
314
323
  markAsUploadedUrl.searchParams.set('platform', platform);
315
324
  markAsUploadedUrl.searchParams.set('updateId', updateId);
316
325
  markAsUploadedUrl.searchParams.set('runtimeVersion', runtimeVersion);
317
326
  const response = await (0, fetch_1.fetchWithRetries)(markAsUploadedUrl.toString(), {
318
327
  method: 'POST',
319
328
  headers: {
320
- ...(0, auth_1.getAuthExpoHeaders)(credentials),
329
+ ...(0, auth_1.getAuthHeaders)(credentials),
321
330
  'Content-Type': 'application/json',
322
331
  },
323
332
  });
@@ -33,9 +33,9 @@ class Publish extends core_1.Command {
33
33
  };
34
34
  }
35
35
  async run() {
36
- const credentials = (0, auth_1.retrieveExpoCredentials)();
37
- if (!credentials.token && !credentials.sessionSecret) {
38
- log_1.default.error('You are not logged to eas, please run `eas login`');
36
+ const credentials = (0, auth_1.retrieveCredentials)();
37
+ if (!(0, auth_1.validateCredentials)(credentials)) {
38
+ log_1.default.error('Invalid credentials. Please run `eas login or set EXPO_ACCESS_TOKEN or EOO_TOKEN environment variable`');
39
39
  process.exit(1);
40
40
  }
41
41
  const { flags } = await this.parse(Publish);
@@ -65,6 +65,7 @@ class Publish extends core_1.Command {
65
65
  log_1.default.error("Update url is not setup in your config. Please run 'eoas init' to setup the update url");
66
66
  process.exit(1);
67
67
  }
68
+ const appId = (0, expoConfig_1.requireExpoAppId)(privateConfig);
68
69
  let baseUrl;
69
70
  try {
70
71
  const parsedUrl = new URL(updateUrl);
@@ -74,9 +75,12 @@ class Publish extends core_1.Command {
74
75
  log_1.default.error('Invalid URL', e);
75
76
  process.exit(1);
76
77
  }
77
- const runtimeVersionsEndpoint = `${baseUrl}/api/branch/${branch}/runtimeVersions`;
78
+ const runtimeVersionsEndpoint = `${baseUrl}/api/apps/${appId}/branch/${branch}/runtimeVersions`;
78
79
  const response = await (0, fetch_1.fetchWithRetries)(runtimeVersionsEndpoint, {
79
- headers: { ...(0, auth_1.getAuthExpoHeaders)(credentials), 'use-expo-auth': 'true' },
80
+ headers: {
81
+ ...(0, auth_1.getAuthHeaders)(credentials),
82
+ 'use-cli-auth': 'true',
83
+ },
80
84
  });
81
85
  if (!response.ok) {
82
86
  log_1.default.error(`Failed to fetch runtime versions: ${await response.text()}`);
@@ -99,9 +103,12 @@ class Publish extends core_1.Command {
99
103
  })),
100
104
  });
101
105
  log_1.default.log(`Selected runtime version: ${selectedRuntimeVersion.runtimeVersion}`);
102
- const updatesEndpoint = `${baseUrl}/api/branch/${branch}/runtimeVersion/${selectedRuntimeVersion.runtimeVersion}/updates`;
106
+ const updatesEndpoint = `${baseUrl}/api/apps/${appId}/branch/${branch}/runtimeVersion/${selectedRuntimeVersion.runtimeVersion}/updates`;
103
107
  const updatesResponse = await (0, fetch_1.fetchWithRetries)(updatesEndpoint, {
104
- headers: { ...(0, auth_1.getAuthExpoHeaders)(credentials), 'use-expo-auth': 'true' },
108
+ headers: {
109
+ ...(0, auth_1.getAuthHeaders)(credentials),
110
+ 'use-cli-auth': 'true',
111
+ },
105
112
  });
106
113
  if (!updatesResponse.ok) {
107
114
  log_1.default.error(`Failed to fetch updates: ${await updatesResponse.text()}`);
@@ -125,7 +132,7 @@ class Publish extends core_1.Command {
125
132
  })),
126
133
  });
127
134
  log_1.default.log(`Re-publishing update: ${selectedUpdated.update.updateUUID}`);
128
- const republishUrl = new URL(`${baseUrl}/republish/${branch}`);
135
+ const republishUrl = new URL(`${baseUrl}/${appId}/republish/${branch}`);
129
136
  republishUrl.searchParams.set('platform', selectedUpdated.update.platform);
130
137
  republishUrl.searchParams.set('runtimeVersion', selectedRuntimeVersion.runtimeVersion);
131
138
  republishUrl.searchParams.set('updateId', selectedUpdated.update.updateId);
@@ -134,7 +141,8 @@ class Publish extends core_1.Command {
134
141
  const republishResponse = await (0, fetch_1.fetchWithRetries)(republishUrl.toString(), {
135
142
  method: 'POST',
136
143
  headers: {
137
- ...(0, auth_1.getAuthExpoHeaders)(credentials),
144
+ ...(0, auth_1.getAuthHeaders)(credentials),
145
+ 'use-cli-auth': 'true',
138
146
  'Content-Type': 'application/json',
139
147
  },
140
148
  });
@@ -6,6 +6,7 @@ export default class Publish extends Command {
6
6
  static flags: {
7
7
  platform: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
8
8
  branch: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
9
+ nonInteractive: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
9
10
  };
10
11
  private sanitizeFlags;
11
12
  run(): Promise<void>;
@@ -28,21 +28,26 @@ class Publish extends core_1.Command {
28
28
  description: 'Name of the branch to point to',
29
29
  required: true,
30
30
  }),
31
+ nonInteractive: core_1.Flags.boolean({
32
+ description: 'Run command in non-interactive mode',
33
+ default: false,
34
+ }),
31
35
  };
32
36
  sanitizeFlags(flags) {
33
37
  return {
34
38
  platform: flags.platform,
35
39
  branch: flags.branch,
40
+ nonInteractive: flags.nonInteractive,
36
41
  };
37
42
  }
38
43
  async run() {
39
- const credentials = (0, auth_1.retrieveExpoCredentials)();
40
- if (!credentials.token && !credentials.sessionSecret) {
41
- log_1.default.error('You are not logged to eas, please run `eas login`');
44
+ const credentials = (0, auth_1.retrieveCredentials)();
45
+ if (!(0, auth_1.validateCredentials)(credentials)) {
46
+ log_1.default.error('Invalid credentials. Please run `eas login or set EXPO_ACCESS_TOKEN or EOO_TOKEN environment variable`');
42
47
  process.exit(1);
43
48
  }
44
49
  const { flags } = await this.parse(Publish);
45
- const { platform, branch } = this.sanitizeFlags(flags);
50
+ const { platform, branch, nonInteractive } = this.sanitizeFlags(flags);
46
51
  if (!branch) {
47
52
  log_1.default.error('Branch name is required');
48
53
  process.exit(1);
@@ -56,14 +61,16 @@ class Publish extends core_1.Command {
56
61
  log_1.default.error('Expo is not installed in this project. Please install Expo first.');
57
62
  process.exit(1);
58
63
  }
59
- const confirmed = await (0, prompts_1.confirmAsync)({
60
- message: `Are you sure you want to publish a rollback to the branch ${branch} ?`,
61
- name: 'export',
62
- type: 'confirm',
63
- });
64
- if (!confirmed) {
65
- log_1.default.error('Operation cancelled');
66
- process.exit(1);
64
+ if (!nonInteractive) {
65
+ const confirmed = await (0, prompts_1.confirmAsync)({
66
+ message: `Are you sure you want to publish a rollback to the branch ${branch} ?`,
67
+ name: 'export',
68
+ type: 'confirm',
69
+ });
70
+ if (!confirmed) {
71
+ log_1.default.error('Operation cancelled');
72
+ process.exit(1);
73
+ }
67
74
  }
68
75
  const privateConfig = await (0, expoConfig_1.getPrivateExpoConfigAsync)(projectDir, {
69
76
  env: process.env,
@@ -77,6 +84,7 @@ class Publish extends core_1.Command {
77
84
  log_1.default.error("Update url is not setup in your config. Please run 'eoas init' to setup the update url");
78
85
  process.exit(1);
79
86
  }
87
+ const appId = (0, expoConfig_1.requireExpoAppId)(privateConfig);
80
88
  let baseUrl;
81
89
  try {
82
90
  const parsedUrl = new URL(updateUrl);
@@ -126,14 +134,14 @@ class Publish extends core_1.Command {
126
134
  const rollbackSpinner = (0, ora_1.ora)('📦 Uploading rollback...').start();
127
135
  const erroredPlatforms = [];
128
136
  await Promise.all(runtimeVersions.map(async ({ runtimeVersion, platform }) => {
129
- const rollbackUrl = new URL(`${baseUrl}/rollback/${branch}`);
137
+ const rollbackUrl = new URL(`${baseUrl}/${appId}/rollback/${branch}`);
130
138
  rollbackUrl.searchParams.set('commitHash', commitHash ?? '');
131
139
  rollbackUrl.searchParams.set('platform', platform);
132
140
  rollbackUrl.searchParams.set('runtimeVersion', runtimeVersion ?? '');
133
141
  const response = await (0, fetch_1.fetchWithRetries)(rollbackUrl.toString(), {
134
142
  method: 'POST',
135
143
  headers: {
136
- ...(0, auth_1.getAuthExpoHeaders)(credentials),
144
+ ...(0, auth_1.getAuthHeaders)(credentials),
137
145
  },
138
146
  });
139
147
  if (!response.ok) {
@@ -1,5 +1,5 @@
1
1
  import Joi from 'joi';
2
- import { ExpoCredentials } from './auth';
2
+ import { Credentials } from './auth';
3
3
  import { RequestedPlatform } from './expoConfig';
4
4
  export declare const MetadataJoi: Joi.ObjectSchema<any>;
5
5
  interface AssetToUpload {
@@ -18,7 +18,7 @@ export declare function requestUploadUrls({ body, requestUploadUrl, auth, runtim
18
18
  fileNames: string[];
19
19
  };
20
20
  requestUploadUrl: string;
21
- auth: ExpoCredentials;
21
+ auth: Credentials;
22
22
  runtimeVersion: string;
23
23
  platform: string;
24
24
  commitHash?: string;
@@ -85,7 +85,7 @@ async function requestUploadUrls({ body, requestUploadUrl, auth, runtimeVersion,
85
85
  const response = await (0, fetch_1.fetchWithRetries)(uploadUrl.toString(), {
86
86
  method: 'POST',
87
87
  headers: {
88
- ...(0, auth_1.getAuthExpoHeaders)(auth),
88
+ ...(0, auth_1.getAuthHeaders)(auth),
89
89
  'Content-Type': 'application/json',
90
90
  },
91
91
  body: JSON.stringify(requestBody),
@@ -1,6 +1,11 @@
1
- export interface ExpoCredentials {
1
+ type ServerImplementation = 'expo' | 'eoo';
2
+ export interface Credentials {
2
3
  token?: string;
3
4
  sessionSecret?: string;
4
5
  }
5
- export declare function retrieveExpoCredentials(): ExpoCredentials;
6
- export declare function getAuthExpoHeaders(credentials: ExpoCredentials): Record<string, string>;
6
+ export declare function detectServerImplementation(): ServerImplementation;
7
+ export declare function retrieveCredentials(): Credentials;
8
+ export declare function validateCredentials(credentials: Credentials): boolean;
9
+ export declare function retrieveExpoCredentials(): Credentials;
10
+ export declare function getAuthHeaders(credentials: Credentials): Record<string, string>;
11
+ export {};
package/dist/lib/auth.js CHANGED
@@ -1,9 +1,29 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getAuthExpoHeaders = exports.retrieveExpoCredentials = void 0;
3
+ exports.getAuthHeaders = exports.retrieveExpoCredentials = exports.validateCredentials = exports.retrieveCredentials = exports.detectServerImplementation = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const os_1 = require("os");
6
6
  const path_1 = tslib_1.__importDefault(require("path"));
7
+ function detectServerImplementation() {
8
+ return process.env.EOO_TOKEN ? 'eoo' : 'expo';
9
+ }
10
+ exports.detectServerImplementation = detectServerImplementation;
11
+ function retrieveCredentials() {
12
+ const serverImplementation = detectServerImplementation();
13
+ if (serverImplementation === 'eoo') {
14
+ return {
15
+ token: process.env.EOO_TOKEN,
16
+ };
17
+ }
18
+ return retrieveExpoCredentials();
19
+ }
20
+ exports.retrieveCredentials = retrieveCredentials;
21
+ function validateCredentials(credentials) {
22
+ if (!credentials)
23
+ return false;
24
+ return !!(credentials.token || credentials.sessionSecret);
25
+ }
26
+ exports.validateCredentials = validateCredentials;
7
27
  function dotExpoHomeDirectory() {
8
28
  const home = (0, os_1.homedir)();
9
29
  if (!home) {
@@ -41,17 +61,17 @@ function retrieveExpoCredentials() {
41
61
  return { token, sessionSecret };
42
62
  }
43
63
  exports.retrieveExpoCredentials = retrieveExpoCredentials;
44
- function getAuthExpoHeaders(credentials) {
64
+ function getAuthHeaders(credentials) {
45
65
  if (credentials.token) {
46
66
  return {
47
67
  Authorization: `Bearer ${credentials.token}`,
48
68
  };
49
69
  }
50
- if (credentials.sessionSecret) {
70
+ if (credentials?.sessionSecret) {
51
71
  return {
52
- 'expo-session': credentials.sessionSecret,
72
+ 'expo-session': credentials?.sessionSecret,
53
73
  };
54
74
  }
55
75
  return {};
56
76
  }
57
- exports.getAuthExpoHeaders = getAuthExpoHeaders;
77
+ exports.getAuthHeaders = getAuthHeaders;
@@ -21,5 +21,7 @@ export declare function ensureExpoConfigExists(projectDir: string): void;
21
21
  export declare function isUsingStaticExpoConfig(projectDir: string): boolean;
22
22
  export declare function getPublicExpoConfigAsync(projectDir: string, opts?: ExpoConfigOptions): Promise<PublicExpoConfig>;
23
23
  export declare function getExpoConfigUpdateUrl(config: ExpoConfig): string | undefined;
24
+ export declare function getExpoAppId(config: ExpoConfig): string | undefined;
25
+ export declare function requireExpoAppId(config: ExpoConfig): string;
24
26
  export declare function createOrModifyExpoConfigAsync(projectDir: string, exp: Partial<ExpoConfig>): Promise<void>;
25
27
  export declare function resolveServerUrl(config: ExpoConfig): Promise<string>;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.resolveServerUrl = exports.createOrModifyExpoConfigAsync = exports.getExpoConfigUpdateUrl = exports.getPublicExpoConfigAsync = exports.isUsingStaticExpoConfig = exports.ensureExpoConfigExists = exports.getPrivateExpoConfigAsync = exports.RequestedPlatform = void 0;
3
+ exports.resolveServerUrl = exports.createOrModifyExpoConfigAsync = exports.requireExpoAppId = exports.getExpoAppId = exports.getExpoConfigUpdateUrl = exports.getPublicExpoConfigAsync = exports.isUsingStaticExpoConfig = exports.ensureExpoConfigExists = exports.getPrivateExpoConfigAsync = exports.RequestedPlatform = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  // This file is copied from eas-cli[https://github.com/expo/eas-cli] to ensure consistent user experience across the CLI.
6
6
  const config_1 = require("@expo/config");
@@ -112,6 +112,26 @@ function getExpoConfigUpdateUrl(config) {
112
112
  return config.updates?.url;
113
113
  }
114
114
  exports.getExpoConfigUpdateUrl = getExpoConfigUpdateUrl;
115
+ // getExpoAppId reads the app id without treating its absence as fatal. A config
116
+ // with no 'expo-app-id' is the shape every v1 project has, so a caller that
117
+ // diagnoses or migrates such a project needs to see the absence rather than be
118
+ // exited on. Commands that cannot proceed without an id use requireExpoAppId.
119
+ function getExpoAppId(config) {
120
+ return config.updates
121
+ ?.requestHeaders?.['expo-app-id'];
122
+ }
123
+ exports.getExpoAppId = getExpoAppId;
124
+ function requireExpoAppId(config) {
125
+ const appId = getExpoAppId(config);
126
+ if (!appId) {
127
+ log_1.default.error("Your Expo config is missing the 'expo-app-id' entry in updates.requestHeaders.");
128
+ log_1.default.error("This usually means you're running eoas v2+ against a v1-style single-app config or your config is missing the 'expo-app-id' entry.");
129
+ log_1.default.error("Fix: run 'npx eoas init' to migrate, or pin to the previous CLI via 'npx eoas@1 ...'.");
130
+ process.exit(1);
131
+ }
132
+ return appId;
133
+ }
134
+ exports.requireExpoAppId = requireExpoAppId;
115
135
  async function createOrModifyExpoConfigAsync(projectDir, exp) {
116
136
  try {
117
137
  ensureExpoConfigExists(projectDir);
@@ -209,7 +229,7 @@ async function resolveServerUrl(config) {
209
229
  const parsedUrl = new URL(updateUrl);
210
230
  baseUrl = parsedUrl.origin;
211
231
  }
212
- catch (e) {
232
+ catch {
213
233
  throw new Error('Invalid update URL.');
214
234
  }
215
235
  return baseUrl;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eoas",
3
- "version": "2.3.22",
3
+ "version": "3.0.0-beta.1",
4
4
  "main": "index.js",
5
5
  "scripts": {
6
6
  "build": "tsc --project tsconfig.json",
@@ -1,2 +0,0 @@
1
- import { ExpoCredentials } from './auth';
2
- export declare function resolveReleaseChannelDynamicallyFromBranch(baseUrl: string, branch: string, credentials: ExpoCredentials): Promise<string>;
@@ -1,24 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.resolveReleaseChannelDynamicallyFromBranch = void 0;
4
- const auth_1 = require("./auth");
5
- const fetch_1 = require("./fetch");
6
- async function resolveReleaseChannelDynamicallyFromBranch(baseUrl, branch, credentials) {
7
- const branchesEndpoint = `${baseUrl}/api/branches`;
8
- const response = await (0, fetch_1.fetchWithRetries)(branchesEndpoint, {
9
- headers: { ...(0, auth_1.getAuthExpoHeaders)(credentials), 'use-expo-auth': 'true' },
10
- });
11
- if (!response.ok) {
12
- throw new Error(`Failed to retrieve branches from server: ${await response.text()}`);
13
- }
14
- const branches = (await response.json());
15
- const branchInfo = branches.find(b => b.branchName === branch);
16
- if (!branchInfo) {
17
- throw new Error(`Branch ${branch} not found`);
18
- }
19
- if (!branchInfo.releaseChannel) {
20
- throw new Error(`Branch ${branch} does not have a release channel linked`);
21
- }
22
- return branchInfo.releaseChannel;
23
- }
24
- exports.resolveReleaseChannelDynamicallyFromBranch = resolveReleaseChannelDynamicallyFromBranch;