@expo/build-tools 24.0.0 → 24.2.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.
@@ -4,6 +4,7 @@ export declare function shallowCloneRepositoryAsync({ logger, archiveSource, des
4
4
  logger: bunyan;
5
5
  archiveSource: ArchiveSource & {
6
6
  type: ArchiveSourceType.GIT;
7
+ repositoryUrl: string;
7
8
  };
8
9
  destinationDirectory: string;
9
10
  }): Promise<void>;
@@ -19,26 +19,21 @@ const zod_1 = require("zod");
19
19
  const git_1 = require("./git");
20
20
  const turtleFetch_1 = require("../utils/turtleFetch");
21
21
  async function prepareProjectSourcesAsync(ctx, destinationDirectory) {
22
- let projectArchive = ctx.job.projectArchive;
23
22
  if (ctx.isLocal) {
24
- console.warn('Local build, skipping project archive refresh');
25
- }
26
- else {
27
- const projectArchiveResult = await (0, results_1.asyncResult)(fetchProjectArchiveSourceAsync(ctx));
28
- if (!projectArchiveResult.ok) {
29
- ctx.logger.error({ err: projectArchiveResult.reason }, 'Failed to refresh project archive, falling back to the original one');
23
+ if (ctx.job.projectArchive.type !== eas_build_job_1.ArchiveSourceType.PATH) {
24
+ throw new eas_build_job_1.SystemError(`Expected a PATH project source for a local build, received ${ctx.job.projectArchive.type}.`, { trackingCode: 'INVALID_LOCAL_PROJECT_SOURCE' });
30
25
  }
31
- projectArchive = projectArchiveResult.value ?? ctx.job.projectArchive;
26
+ await prepareProjectSourcesLocallyAsync(ctx, ctx.job.projectArchive.path, destinationDirectory);
27
+ return { handled: true };
28
+ }
29
+ const projectArchiveResult = await (0, results_1.asyncResult)(fetchProjectArchiveSourceAsync(ctx));
30
+ if (!projectArchiveResult.ok) {
31
+ throw new eas_build_job_1.SystemError('Failed to fetch project sources. Re-run the job.', {
32
+ cause: projectArchiveResult.reason,
33
+ });
32
34
  }
35
+ const projectArchive = projectArchiveResult.value;
33
36
  switch (projectArchive.type) {
34
- case eas_build_job_1.ArchiveSourceType.R2:
35
- case eas_build_job_1.ArchiveSourceType.GCS: {
36
- throw new Error('Remote project sources should be resolved earlier to URL');
37
- }
38
- case eas_build_job_1.ArchiveSourceType.PATH: {
39
- await prepareProjectSourcesLocallyAsync(ctx, projectArchive.path, destinationDirectory); // used in eas build --local
40
- return { handled: true };
41
- }
42
37
  case eas_build_job_1.ArchiveSourceType.NONE: {
43
38
  // May be used in no-sources jobs like submission jobs.
44
39
  return { handled: true };
@@ -205,7 +200,20 @@ async function fetchProjectArchiveSourceAsync(ctx) {
205
200
  if (!jsonResult.ok) {
206
201
  throw new Error(`Expected JSON response from server (${response.status}): ${jsonResult.reason}`);
207
202
  }
208
- const dataResult = zod_1.z.object({ data: eas_build_job_1.ArchiveSourceSchemaZ }).safeParse(jsonResult.value);
203
+ const dataResult = zod_1.z
204
+ .object({
205
+ data: zod_1.z.discriminatedUnion('type', [
206
+ zod_1.z.object({
207
+ type: zod_1.z.literal(eas_build_job_1.ArchiveSourceType.GIT),
208
+ repositoryUrl: zod_1.z.string().url(),
209
+ gitRef: zod_1.z.string().nullable(),
210
+ gitCommitHash: zod_1.z.string(),
211
+ }),
212
+ zod_1.z.object({ type: zod_1.z.literal(eas_build_job_1.ArchiveSourceType.URL), url: zod_1.z.string().url() }),
213
+ zod_1.z.object({ type: zod_1.z.literal(eas_build_job_1.ArchiveSourceType.NONE) }),
214
+ ]),
215
+ })
216
+ .safeParse(jsonResult.value);
209
217
  if (!dataResult.success) {
210
218
  throw new Error(`Unexpected data from server (${response.status}): ${zod_1.z.prettifyError(dataResult.error)}`);
211
219
  }
package/dist/generic.js CHANGED
@@ -13,6 +13,7 @@ const projectSources_1 = require("./common/projectSources");
13
13
  const customBuildContext_1 = require("./customBuildContext");
14
14
  const easFunctionGroups_1 = require("./steps/easFunctionGroups");
15
15
  const easFunctions_1 = require("./steps/easFunctions");
16
+ const localEgress_1 = require("./steps/utils/localEgress");
16
17
  const outputs_1 = require("./utils/outputs");
17
18
  const retry_1 = require("./utils/retry");
18
19
  async function runGenericJobAsync(ctx) {
@@ -63,7 +64,16 @@ async function runGenericJobAsync(ctx) {
63
64
  throw parseError;
64
65
  }
65
66
  });
66
- const runResult = await (0, results_1.asyncResult)(workflow.executeAsync());
67
+ const runResult = await (0, results_1.asyncResult)((async () => {
68
+ try {
69
+ await workflow.executeAsync();
70
+ }
71
+ finally {
72
+ // The session-owning step may never run if simulator boot/setup fails,
73
+ // or may time out while resource acquisition is still in flight.
74
+ await (0, localEgress_1.stopLocalEgressResourcesAsync)(ctx.logger);
75
+ }
76
+ })());
67
77
  await ctx.runBuildPhase(eas_build_job_1.BuildPhase.COMPLETE_JOB, async () => {
68
78
  const results = await Promise.allSettled([
69
79
  (0, outputs_1.uploadJobOutputsToWwwAsync)(globalContext, {
@@ -49,6 +49,7 @@ const startAppiumRemoteSession_1 = require("./functions/startAppiumRemoteSession
49
49
  const startCuttlefishDevice_1 = require("./functions/startCuttlefishDevice");
50
50
  const startIosSimulator_1 = require("./functions/startIosSimulator");
51
51
  const startIosSimulatorRecordings_1 = require("./functions/startIosSimulatorRecordings");
52
+ const startLocalEgress_1 = require("./functions/startLocalEgress");
52
53
  const startWebPreviewRemoteSession_1 = require("./functions/startWebPreviewRemoteSession");
53
54
  const startServeSimMetrics_1 = require("./functions/startServeSimMetrics");
54
55
  const collectServeSimMetrics_1 = require("./functions/collectServeSimMetrics");
@@ -99,6 +100,7 @@ function getEasFunctions(ctx) {
99
100
  (0, startAndroidEmulator_1.createStartAndroidEmulatorBuildFunction)(),
100
101
  (0, startCuttlefishDevice_1.createStartCuttlefishDeviceBuildFunction)(),
101
102
  (0, startIosSimulator_1.createStartIosSimulatorBuildFunction)(),
103
+ (0, startLocalEgress_1.createStartLocalEgressBuildFunction)(),
102
104
  (0, startIosSimulatorRecordings_1.createStartIosSimulatorRecordingsBuildFunction)(),
103
105
  (0, finishIosSimulatorRecordings_1.createFinishIosSimulatorRecordingsBuildFunction)(),
104
106
  (0, uploadDeviceRunSessionScreenRecordings_1.createUploadDeviceRunSessionScreenRecordingsBuildFunction)(ctx),
@@ -11,6 +11,7 @@ const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
11
11
  const node_fs_1 = __importDefault(require("node:fs"));
12
12
  const node_os_1 = __importDefault(require("node:os"));
13
13
  const node_path_1 = __importDefault(require("node:path"));
14
+ const localEgressSession_1 = require("../utils/localEgressSession");
14
15
  const sentry_1 = require("../../sentry");
15
16
  const agentDeviceArtifacts_1 = require("../utils/agentDeviceArtifacts");
16
17
  const agentDeviceEvents_1 = require("../utils/agentDeviceEvents");
@@ -48,7 +49,7 @@ function createStartAgentDeviceRemoteSessionBuildFunction(ctx) {
48
49
  allowedValueTypeName: steps_1.BuildStepInputValueTypeName.NUMBER,
49
50
  }),
50
51
  ],
51
- fn: async ({ logger, global }, { inputs, env, signal }) => {
52
+ fn: (0, localEgressSession_1.withLocalEgressSession)(async ({ logger, global }, { inputs, env, signal }) => {
52
53
  // Fail fast before any expensive setup if the injected env
53
54
  // vars are missing: DEVICE_RUN_SESSION_ID (to report the remote config
54
55
  // back to the API server), EAS_SIMULATOR_NGROK_TUNNEL_DOMAIN (base domain
@@ -92,13 +93,16 @@ function createStartAgentDeviceRemoteSessionBuildFunction(ctx) {
92
93
  timeoutMs: STARTUP_TIMEOUT_MS,
93
94
  });
94
95
  logger.info(`Web preview URL: ${webPreview.previewUrl}`);
95
- await (0, remoteDeviceRunSession_1.uploadRemoteSessionConfigAsync)({
96
+ await (0, localEgressSession_1.uploadRemoteSessionConfigWithLocalEgressAsync)({
97
+ env,
98
+ signal,
96
99
  ctx,
97
100
  deviceRunSessionId,
98
101
  remoteConfig: {
99
102
  agentDeviceRemoteSessionUrl,
100
103
  agentDeviceRemoteSessionToken: daemonToken,
101
104
  webPreviewUrl: webPreview.previewUrl,
105
+ ...(webPreview.previewToken ? { webPreviewToken: webPreview.previewToken } : {}),
102
106
  },
103
107
  logger,
104
108
  });
@@ -142,7 +146,7 @@ function createStartAgentDeviceRemoteSessionBuildFunction(ctx) {
142
146
  }
143
147
  await daemonProcess.stopAsync();
144
148
  }
145
- },
149
+ }),
146
150
  });
147
151
  }
148
152
  async function stopAgentDeviceEventCollectionSafelyAsync({ eventCollection, deviceRunSessionId, logger, }) {
@@ -14,6 +14,7 @@ const node_os_1 = __importDefault(require("node:os"));
14
14
  const node_path_1 = __importDefault(require("node:path"));
15
15
  const semver_1 = __importDefault(require("semver"));
16
16
  const zod_1 = require("zod");
17
+ const localEgressSession_1 = require("../utils/localEgressSession");
17
18
  const AndroidEmulatorUtils_1 = require("../../utils/AndroidEmulatorUtils");
18
19
  const IosSimulatorUtils_1 = require("../../utils/IosSimulatorUtils");
19
20
  const retry_1 = require("../../utils/retry");
@@ -43,7 +44,7 @@ function createStartAppiumRemoteSessionBuildFunction(ctx) {
43
44
  allowedValueTypeName: steps_1.BuildStepInputValueTypeName.NUMBER,
44
45
  }),
45
46
  ],
46
- fn: async ({ logger, global }, { inputs, env, signal }) => {
47
+ fn: (0, localEgressSession_1.withLocalEgressSession)(async ({ logger, global }, { inputs, env, signal }) => {
47
48
  const deviceRunSessionId = (0, remoteDeviceRunSession_1.getDeviceRunSessionIdOrThrow)(env);
48
49
  const ngrokTunnelDomain = (0, remoteDeviceRunSession_1.getNgrokTunnelDomainOrThrow)(env);
49
50
  const ngrokAuthtoken = (0, remoteDeviceRunSession_1.getNgrokAuthtokenOrThrow)(env);
@@ -113,7 +114,9 @@ function createStartAppiumRemoteSessionBuildFunction(ctx) {
113
114
  logger,
114
115
  timeoutMs: APPIUM_STARTUP_TIMEOUT_MS,
115
116
  });
116
- await (0, remoteDeviceRunSession_1.uploadRemoteSessionConfigAsync)({
117
+ await (0, localEgressSession_1.uploadRemoteSessionConfigWithLocalEgressAsync)({
118
+ env,
119
+ signal,
117
120
  ctx,
118
121
  deviceRunSessionId,
119
122
  remoteConfig: {
@@ -124,6 +127,7 @@ function createStartAppiumRemoteSessionBuildFunction(ctx) {
124
127
  'appium:udid': device.udid,
125
128
  },
126
129
  webPreviewUrl: webPreview.previewUrl,
130
+ ...(webPreview.previewToken ? { webPreviewToken: webPreview.previewToken } : {}),
127
131
  },
128
132
  logger,
129
133
  });
@@ -151,7 +155,7 @@ function createStartAppiumRemoteSessionBuildFunction(ctx) {
151
155
  await appiumProcess.stopAsync();
152
156
  await node_fs_1.default.promises.rm(appiumHome, { recursive: true, force: true });
153
157
  }
154
- },
158
+ }),
155
159
  });
156
160
  }
157
161
  function resolveAppium3VersionSpec(packageVersion) {
@@ -16,6 +16,7 @@ const node_os_1 = __importDefault(require("node:os"));
16
16
  const node_path_1 = __importDefault(require("node:path"));
17
17
  const semver_1 = __importDefault(require("semver"));
18
18
  const zod_1 = require("zod");
19
+ const localEgressSession_1 = require("../utils/localEgressSession");
19
20
  const sentry_1 = require("../../sentry");
20
21
  const processes_1 = require("../../utils/processes");
21
22
  const retry_1 = require("../../utils/retry");
@@ -63,7 +64,7 @@ function createStartArgentRemoteSessionBuildFunction(ctx) {
63
64
  allowedValueTypeName: steps_1.BuildStepInputValueTypeName.NUMBER,
64
65
  }),
65
66
  ],
66
- fn: async ({ logger, global }, { inputs, env, signal }) => {
67
+ fn: (0, localEgressSession_1.withLocalEgressSession)(async ({ logger, global }, { inputs, env, signal }) => {
67
68
  // Fail fast before any expensive setup if the injected env
68
69
  // vars are missing: DEVICE_RUN_SESSION_ID (to report the remote config
69
70
  // back to the API server), EAS_SIMULATOR_NGROK_TUNNEL_DOMAIN (base domain
@@ -173,13 +174,16 @@ function createStartArgentRemoteSessionBuildFunction(ctx) {
173
174
  timeoutMs: STARTUP_TIMEOUT_MS,
174
175
  });
175
176
  logger.info(`Web preview URL: ${webPreview.previewUrl}`);
176
- await (0, remoteDeviceRunSession_1.uploadRemoteSessionConfigAsync)({
177
+ await (0, localEgressSession_1.uploadRemoteSessionConfigWithLocalEgressAsync)({
178
+ env,
179
+ signal,
177
180
  ctx,
178
181
  deviceRunSessionId,
179
182
  remoteConfig: {
180
183
  toolsUrl: publicToolsUrl,
181
184
  ...(toolServerToken ? { toolsAuthToken: toolServerToken } : {}),
182
185
  webPreviewUrl: webPreview.previewUrl,
186
+ ...(webPreview.previewToken ? { webPreviewToken: webPreview.previewToken } : {}),
183
187
  },
184
188
  logger,
185
189
  });
@@ -216,7 +220,7 @@ function createStartArgentRemoteSessionBuildFunction(ctx) {
216
220
  }
217
221
  await argentServer.stopAsync();
218
222
  }
219
- },
223
+ }),
220
224
  });
221
225
  }
222
226
  async function stopArgentEventCollectionSafelyAsync({ eventCollection, deviceRunSessionId, logger, }) {
@@ -0,0 +1,10 @@
1
+ import { BuildFunction } from '@expo/steps';
2
+ /**
3
+ * Points the device host's system HTTP(S) proxy at the EAS CLI's egress client.
4
+ * Must run before `eas/start_ios_simulator`: the simulator reads the system proxy
5
+ * at boot. Nothing else on the host changes, so requests from libraries that
6
+ * bypass the system proxy are not covered; the shared session monitor reports
7
+ * them. The shared session cleanup releases the resources started here when
8
+ * the session ends, with a job finalizer as a fallback.
9
+ */
10
+ export declare function createStartLocalEgressBuildFunction(): BuildFunction;
@@ -0,0 +1,156 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createStartLocalEgressBuildFunction = createStartLocalEgressBuildFunction;
7
+ const steps_1 = require("@expo/steps");
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_os_1 = __importDefault(require("node:os"));
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ const localEgress_1 = require("../utils/localEgress");
12
+ const remoteDeviceRunSession_1 = require("../utils/remoteDeviceRunSession");
13
+ /**
14
+ * The downloader and ngrok SDK do not accept an AbortSignal. Stop awaiting them
15
+ * on cancellation, while retaining handlers that dispose any eventual result.
16
+ */
17
+ function awaitLocalEgressAcquisitionAsync(pending, signal, disposeAbandonedAsync, logger) {
18
+ return new Promise((resolve, reject) => {
19
+ let abandoned = false;
20
+ const abort = () => {
21
+ signal.removeEventListener('abort', abort);
22
+ abandoned = true;
23
+ reject(signal.reason);
24
+ };
25
+ signal.addEventListener('abort', abort, { once: true });
26
+ if (signal.aborted) {
27
+ abort();
28
+ }
29
+ void pending
30
+ .then(async (result) => {
31
+ signal.removeEventListener('abort', abort);
32
+ if (abandoned) {
33
+ await disposeAbandonedAsync(result);
34
+ }
35
+ else {
36
+ resolve(result);
37
+ }
38
+ }, async (error) => {
39
+ signal.removeEventListener('abort', abort);
40
+ if (abandoned) {
41
+ await disposeAbandonedAsync();
42
+ }
43
+ else {
44
+ reject(error);
45
+ }
46
+ })
47
+ .catch(err => logger.warn({ err }, 'Could not release a cancelled local egress acquisition.'));
48
+ });
49
+ }
50
+ /**
51
+ * Points the device host's system HTTP(S) proxy at the EAS CLI's egress client.
52
+ * Must run before `eas/start_ios_simulator`: the simulator reads the system proxy
53
+ * at boot. Nothing else on the host changes, so requests from libraries that
54
+ * bypass the system proxy are not covered; the shared session monitor reports
55
+ * them. The shared session cleanup releases the resources started here when
56
+ * the session ends, with a job finalizer as a fallback.
57
+ */
58
+ function createStartLocalEgressBuildFunction() {
59
+ return new steps_1.BuildFunction({
60
+ namespace: 'eas',
61
+ id: 'start_local_egress',
62
+ name: 'Start local egress',
63
+ __metricsId: 'eas/start_local_egress',
64
+ supportedRuntimePlatforms: [steps_1.BuildRuntimePlatform.DARWIN],
65
+ fn: async ({ logger }, { env, signal }) => {
66
+ const ngrokTunnelDomain = (0, remoteDeviceRunSession_1.getNgrokTunnelDomainOrThrow)(env);
67
+ const ngrokAuthtoken = (0, remoteDeviceRunSession_1.getNgrokAuthtokenOrThrow)(env);
68
+ let workDir;
69
+ let server;
70
+ let tunnel;
71
+ let finishSetup;
72
+ const setupFinished = new Promise(resolve => {
73
+ finishSetup = resolve;
74
+ });
75
+ // Register before acquisition. Cancellation ends setup promptly; pending
76
+ // SDK calls retain their own late-result disposal handlers.
77
+ const lifetimeSignal = (0, localEgress_1.registerLocalEgressResources)(async () => {
78
+ await setupFinished;
79
+ const results = await Promise.allSettled([
80
+ Promise.resolve().then(() => tunnel?.stopAsync()),
81
+ Promise.resolve().then(() => server?.stopAsync()),
82
+ ]);
83
+ for (const result of results) {
84
+ if (result.status === 'rejected') {
85
+ logger.warn({ err: result.reason }, 'Could not stop a local egress resource.');
86
+ }
87
+ }
88
+ await Promise.all([
89
+ node_fs_1.default.promises.rm(localEgress_1.LOCAL_EGRESS_HANDOFF_PATH, { force: true }),
90
+ workDir ? node_fs_1.default.promises.rm(workDir, { recursive: true, force: true }) : undefined,
91
+ ]);
92
+ });
93
+ const startupSignal = signal ? AbortSignal.any([signal, lifetimeSignal]) : lifetimeSignal;
94
+ try {
95
+ startupSignal.throwIfAborted();
96
+ workDir = await node_fs_1.default.promises.mkdtemp(node_path_1.default.join(node_os_1.default.tmpdir(), 'eas-local-egress-'));
97
+ startupSignal.throwIfAborted();
98
+ logger.info(`Downloading the reverse tunnel server (chisel ${localEgress_1.CHISEL_VERSION}).`);
99
+ const downloadDir = workDir;
100
+ const chiselPath = await awaitLocalEgressAcquisitionAsync((0, localEgress_1.downloadChiselAsync)({ destinationDir: downloadDir, logger }), startupSignal, async () => await node_fs_1.default.promises.rm(downloadDir, { recursive: true, force: true }), logger);
101
+ startupSignal.throwIfAborted();
102
+ const credentials = (0, localEgress_1.generateEgressCredentials)();
103
+ const authfilePath = node_path_1.default.join(workDir, 'authfile.json');
104
+ await node_fs_1.default.promises.writeFile(authfilePath, (0, localEgress_1.createChiselAuthfileContents)({ ...credentials, port: localEgress_1.LOCAL_EGRESS_PROXY_PORT }), { encoding: 'utf8', mode: 0o600 });
105
+ startupSignal.throwIfAborted();
106
+ const controlPort = await (0, remoteDeviceRunSession_1.findAvailablePortAsync)();
107
+ startupSignal.throwIfAborted();
108
+ logger.info(`Starting the reverse tunnel server on 127.0.0.1:${controlPort}.`);
109
+ const started = await (0, localEgress_1.startChiselServerAsync)({
110
+ chiselPath,
111
+ controlPort,
112
+ authfilePath,
113
+ env,
114
+ signal: startupSignal,
115
+ });
116
+ server = started.process;
117
+ startupSignal.throwIfAborted();
118
+ tunnel = await awaitLocalEgressAcquisitionAsync((0, remoteDeviceRunSession_1.startNgrokTunnelAsync)({
119
+ port: controlPort,
120
+ subdomainPrefix: 'egress',
121
+ baseDomain: ngrokTunnelDomain,
122
+ authtoken: ngrokAuthtoken,
123
+ logger,
124
+ }), startupSignal, async (listener) => await listener?.stopAsync(), logger);
125
+ startupSignal.throwIfAborted();
126
+ const { service } = await (0, localEgress_1.configureSystemProxyAsync)({
127
+ env,
128
+ logger,
129
+ port: localEgress_1.LOCAL_EGRESS_PROXY_PORT,
130
+ signal: startupSignal,
131
+ });
132
+ startupSignal.throwIfAborted();
133
+ await (0, localEgress_1.writeLocalEgressHandoffAsync)({
134
+ url: tunnel.url,
135
+ token: credentials.password,
136
+ fingerprint: started.fingerprint,
137
+ port: localEgress_1.LOCAL_EGRESS_PROXY_PORT,
138
+ });
139
+ startupSignal.throwIfAborted();
140
+ logger.info(`Local egress is configured on network service "${service}". HTTP(S) and WebSocket ` +
141
+ 'requests that honor the system proxy (WebKit, URLSession and other CFNetwork clients) ' +
142
+ 'fail until the EAS CLI egress client connects, then exit from that machine. Requests ' +
143
+ 'from libraries that bypass the system proxy are not covered and exit from this worker. ' +
144
+ 'Connection sampling may report these bypasses, but can miss short connections and unconnected UDP traffic.');
145
+ }
146
+ catch (error) {
147
+ finishSetup();
148
+ await (0, localEgress_1.stopLocalEgressResourcesAsync)(logger);
149
+ throw error;
150
+ }
151
+ finally {
152
+ finishSetup();
153
+ }
154
+ },
155
+ });
156
+ }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createStartWebPreviewRemoteSessionBuildFunction = createStartWebPreviewRemoteSessionBuildFunction;
4
4
  const steps_1 = require("@expo/steps");
5
+ const localEgressSession_1 = require("../utils/localEgressSession");
5
6
  const remoteDeviceRunSession_1 = require("../utils/remoteDeviceRunSession");
6
7
  const STARTUP_TIMEOUT_MS = 60_000;
7
8
  function createStartWebPreviewRemoteSessionBuildFunction(ctx) {
@@ -22,7 +23,7 @@ function createStartWebPreviewRemoteSessionBuildFunction(ctx) {
22
23
  allowedValueTypeName: steps_1.BuildStepInputValueTypeName.NUMBER,
23
24
  }),
24
25
  ],
25
- fn: async ({ logger, global }, { inputs, env, signal }) => {
26
+ fn: (0, localEgressSession_1.withLocalEgressSession)(async ({ logger, global }, { inputs, env, signal }) => {
26
27
  const deviceRunSessionId = (0, remoteDeviceRunSession_1.getDeviceRunSessionIdOrThrow)(env);
27
28
  const ngrokTunnelDomain = (0, remoteDeviceRunSession_1.getNgrokTunnelDomainOrThrow)(env);
28
29
  const maxDurationSeconds = inputs.max_duration_seconds?.value;
@@ -42,10 +43,15 @@ function createStartWebPreviewRemoteSessionBuildFunction(ctx) {
42
43
  });
43
44
  logger.info(`Preview URL: ${webPreview.previewUrl}`);
44
45
  try {
45
- await (0, remoteDeviceRunSession_1.uploadRemoteSessionConfigAsync)({
46
+ await (0, localEgressSession_1.uploadRemoteSessionConfigWithLocalEgressAsync)({
47
+ env,
48
+ signal,
46
49
  ctx,
47
50
  deviceRunSessionId,
48
- remoteConfig: { previewUrl: webPreview.previewUrl },
51
+ remoteConfig: {
52
+ previewUrl: webPreview.previewUrl,
53
+ ...(webPreview.previewToken ? { previewToken: webPreview.previewToken } : {}),
54
+ },
49
55
  logger,
50
56
  });
51
57
  await (0, remoteDeviceRunSession_1.waitForDeviceRunSessionStoppedAsync)({
@@ -59,6 +65,6 @@ function createStartWebPreviewRemoteSessionBuildFunction(ctx) {
59
65
  finally {
60
66
  await webPreview.stopAsync();
61
67
  }
62
- },
68
+ }),
63
69
  });
64
70
  }
@@ -0,0 +1,152 @@
1
+ import { bunyan } from '@expo/logger';
2
+ import { BuildStepEnv } from '@expo/steps';
3
+ import { type DetachedProcessHandle } from './remoteDeviceRunSession';
4
+ /**
5
+ * Local egress provides an HTTP(S) proxy through the machine running the EAS CLI.
6
+ * Requests that use this proxy exit through that machine's network.
7
+ *
8
+ * Worker side: a chisel server in reverse mode listens on loopback and is exposed
9
+ * through the session's ngrok domain. When the CLI's egress client connects, chisel
10
+ * opens LOCAL_EGRESS_PROXY_PORT on this host and forwards every connection to the
11
+ * HTTP proxy the CLI runs. The macOS system proxy points at that port. Until the
12
+ * client connects, nothing listens on the port and proxied requests fail.
13
+ *
14
+ * Contract: HTTP(S) and WebSocket requests that honor the system proxy (WebKit,
15
+ * URLSession and other CFNetwork clients) exit from the egress client's network
16
+ * and fail while the client is disconnected. Requests from libraries that bypass
17
+ * the system proxy are not covered and exit from this host. Nothing here enforces
18
+ * routing; the session monitor reports such direct connections instead.
19
+ */
20
+ export declare const LOCAL_EGRESS_PROXY_HOST = "127.0.0.1";
21
+ export declare const LOCAL_EGRESS_PROXY_PORT = 8899;
22
+ export declare const LOCAL_EGRESS_USERNAME = "eas";
23
+ export declare const LOCAL_EGRESS_HANDOFF_PATH: string;
24
+ export declare const CHISEL_VERSION = "1.12.0";
25
+ export type LocalEgressHandoff = {
26
+ /** Public URL of the reverse tunnel server, reachable through ngrok. */
27
+ url: string;
28
+ /** Secret the client presents to the tunnel server, paired with LOCAL_EGRESS_USERNAME. */
29
+ token: string;
30
+ /** Fingerprint of the tunnel server key, for the client to pin. */
31
+ fingerprint: string;
32
+ /** Loopback port on this host that the client must serve. */
33
+ port: number;
34
+ };
35
+ export declare function getChiselAssetName({ platform, arch, }: {
36
+ platform: NodeJS.Platform;
37
+ arch: string;
38
+ }): string;
39
+ export declare function getChiselDownloadUrl(assetName: string): string;
40
+ export declare function downloadChiselAsync({ destinationDir, logger, platform, arch, }: {
41
+ destinationDir: string;
42
+ logger: bunyan;
43
+ platform?: NodeJS.Platform;
44
+ arch?: string;
45
+ }): Promise<string>;
46
+ export declare function generateEgressCredentials(): {
47
+ user: string;
48
+ password: string;
49
+ };
50
+ /**
51
+ * chisel authfile: one user, allowed to open reverse remotes on loopback only.
52
+ * The proxy port carries proxied requests. Any unprivileged loopback port may be
53
+ * forwarded too: the EAS CLI opens one per `--egress-allow localhost:<port>`
54
+ * entry so `127.0.0.1:<port>` inside the simulator reaches that port on the
55
+ * developer's machine, like `adb reverse`. iOS never sends loopback-literal
56
+ * requests to the system proxy, so the proxy alone cannot serve them.
57
+ * Reverse remotes are matched as `R:<interface>:<port>`.
58
+ */
59
+ export declare function createChiselAuthfileContents({ user, password, port, }: {
60
+ user: string;
61
+ password: string;
62
+ port: number;
63
+ }): string;
64
+ export declare function parseChiselFingerprint(output: string): string | null;
65
+ export declare function startChiselServerAsync({ chiselPath, controlPort, authfilePath, env, signal, }: {
66
+ chiselPath: string;
67
+ controlPort: number;
68
+ authfilePath: string;
69
+ env: BuildStepEnv;
70
+ signal?: AbortSignal;
71
+ }): Promise<{
72
+ process: DetachedProcessHandle;
73
+ fingerprint: string;
74
+ }>;
75
+ export declare function parseDefaultRouteInterface(routeOutput: string): string | null;
76
+ /**
77
+ * `networksetup -listnetworkserviceorder` prints each service as two lines:
78
+ * "(1) Wi-Fi" followed by "(Hardware Port: Wi-Fi, Device: en0)". Return the
79
+ * service name whose device matches.
80
+ */
81
+ export declare function parseNetworkServiceNameForDevice(listOutput: string, device: string): string | null;
82
+ export declare function resolveActiveNetworkServiceNameAsync({ env, }: {
83
+ env: BuildStepEnv;
84
+ }): Promise<string>;
85
+ export declare function buildNetworksetupProxyArgs({ service, host, port, }: {
86
+ service: string;
87
+ host: string;
88
+ port: number;
89
+ }): string[][];
90
+ /**
91
+ * Point the macOS system HTTP and HTTPS proxy at the loopback egress port. The
92
+ * simulator reads these settings when it boots, so this must run before
93
+ * `start_ios_simulator`. Existing bypass and automatic proxy settings are left
94
+ * unchanged. Requests that bypass this proxy can leave from this host's address.
95
+ */
96
+ export declare function configureSystemProxyAsync({ env, logger, port, signal, }: {
97
+ env: BuildStepEnv;
98
+ logger: bunyan;
99
+ port: number;
100
+ signal?: AbortSignal;
101
+ }): Promise<{
102
+ service: string;
103
+ }>;
104
+ export declare function writeLocalEgressHandoffAsync(handoff: LocalEgressHandoff, handoffPath?: string): Promise<void>;
105
+ export declare function readLocalEgressHandoffAsync(handoffPath?: string): Promise<LocalEgressHandoff | null>;
106
+ /** remoteConfig fields the CLI needs to start the egress client. */
107
+ export declare function buildEgressRemoteConfigFields(handoff: LocalEgressHandoff | null): Record<string, string | number>;
108
+ export declare function registerLocalEgressResources(stopAsync: () => Promise<void>): AbortSignal;
109
+ export declare function stopLocalEgressResourcesAsync(logger: bunyan): Promise<void>;
110
+ export declare function isPortListeningAsync({ host, port, timeoutMs, }: {
111
+ host: string;
112
+ port: number;
113
+ timeoutMs?: number;
114
+ }): Promise<boolean>;
115
+ export declare function parseExitIpResponse(body: string): string;
116
+ export declare function fetchExitIpThroughProxyAsync({ port, env, }: {
117
+ port: number;
118
+ env: BuildStepEnv;
119
+ }): Promise<string>;
120
+ /**
121
+ * Processes inside the simulator run on the host as descendants of launchd_sim,
122
+ * so `ps -axo pid=,ppid=,comm=` ancestry identifies them. Returns every
123
+ * descendant pid; launchd_sim itself opens no network connections.
124
+ */
125
+ export declare function collectSimulatorProcessIds(psOutput: string): number[];
126
+ export type DirectSimulatorConnection = {
127
+ pid: number;
128
+ command: string;
129
+ protocol: string;
130
+ /** Remote peer as lsof prints it, e.g. `93.184.216.34:443` or `[2606::1]:443`. */
131
+ remote: string;
132
+ };
133
+ /**
134
+ * Parse `lsof -nP -i -F pcPnT` output. Returns the TCP and UDP connections that
135
+ * simulator processes hold to peers outside loopback. Those did not go through
136
+ * the proxy on 127.0.0.1, so they exit from this host instead of the egress
137
+ * client. Listening sockets, unconnected UDP sockets and connections to
138
+ * loopback (the proxy itself) are ignored.
139
+ */
140
+ export declare function parseDirectSimulatorConnections(lsofOutput: string, simulatorPids: ReadonlySet<number>): DirectSimulatorConnection[];
141
+ /**
142
+ * Log proxy listener availability, the exit IP observed by a worker request
143
+ * through it, and simulator connections that bypassed the proxy. Neither check
144
+ * verifies that proxied simulator requests reach the egress client. Never
145
+ * rejects: it runs in the background for the whole session.
146
+ */
147
+ export declare function monitorLocalEgressAsync({ port, env, logger, signal, }: {
148
+ port: number;
149
+ env: BuildStepEnv;
150
+ logger: bunyan;
151
+ signal: AbortSignal;
152
+ }): Promise<void>;