heroku 10.15.1-beta.0 → 10.15.2-beta.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.
@@ -182,6 +182,14 @@ class Dyno extends stream_1.Duplex {
182
182
  const r = https.request(options);
183
183
  r.end();
184
184
  r.on('error', this.reject);
185
+ r.on('response', response => {
186
+ var _a;
187
+ const statusCode = response.statusCode;
188
+ if (statusCode === 403) {
189
+ r.destroy();
190
+ (_a = this.reject) === null || _a === void 0 ? void 0 : _a.call(this, new Error("You can't access this space from your IP address. Contact your team admin."));
191
+ }
192
+ });
185
193
  r.on('upgrade', (_, remote) => {
186
194
  const s = net.createServer(client => {
187
195
  client.on('end', () => {
@@ -241,7 +249,7 @@ class Dyno extends stream_1.Duplex {
241
249
  lastErr = data.toString();
242
250
  // suppress host key and permission denied messages
243
251
  const messages = [
244
- "Warning: Permanently added '[127.0.0.1]"
252
+ "Warning: Permanently added '[127.0.0.1]",
245
253
  ];
246
254
  const killMessages = [
247
255
  'too many authentication failures',
@@ -11,3 +11,18 @@ export declare function shouldPrependLauncher(heroku: APIClient, appName: string
11
11
  * Builds the command string, automatically deciding whether to prepend `launcher`.
12
12
  */
13
13
  export declare function buildCommandWithLauncher(heroku: APIClient, appName: string, args: string[], disableLauncher: boolean): Promise<string>;
14
+ /**
15
+ * Fetches the response body from an HTTP request when the response body isn't available
16
+ * from the error object (e.g., EventSource doesn't expose response bodies).
17
+ *
18
+ * Uses native fetch API with a custom https.Agent to handle staging SSL certificates
19
+ * (rejectUnauthorized: false).
20
+ *
21
+ * Note: Node.js native fetch doesn't support custom agents directly, so we use
22
+ * https.request when rejectUnauthorized is needed, but structure the code with
23
+ *
24
+ * @param url - The URL to fetch the response body from
25
+ * @param expectedStatusCode - Only return body if status code matches (default: 403)
26
+ * @returns The response body as a string, or empty string if unavailable or status doesn't match
27
+ */
28
+ export declare function fetchHttpResponseBody(url: string, expectedStatusCode?: number): Promise<string>;
@@ -1,8 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.buildCommandWithLauncher = exports.shouldPrependLauncher = exports.buildEnvFromFlag = exports.buildCommand = exports.revertSortedArgs = void 0;
3
+ exports.fetchHttpResponseBody = exports.buildCommandWithLauncher = exports.shouldPrependLauncher = exports.buildEnvFromFlag = exports.buildCommand = exports.revertSortedArgs = void 0;
4
4
  /* eslint-disable @typescript-eslint/ban-ts-comment */
5
5
  const core_1 = require("@oclif/core");
6
+ const https = require("https");
7
+ const url_1 = require("url");
6
8
  // this function exists because oclif sorts argv
7
9
  // and to capture all non-flag command inputs
8
10
  function revertSortedArgs(processArgs, argv) {
@@ -77,3 +79,100 @@ async function buildCommandWithLauncher(heroku, appName, args, disableLauncher)
77
79
  return buildCommand(args, prependLauncher);
78
80
  }
79
81
  exports.buildCommandWithLauncher = buildCommandWithLauncher;
82
+ /**
83
+ * Fetches the response body from an HTTP request when the response body isn't available
84
+ * from the error object (e.g., EventSource doesn't expose response bodies).
85
+ *
86
+ * Uses native fetch API with a custom https.Agent to handle staging SSL certificates
87
+ * (rejectUnauthorized: false).
88
+ *
89
+ * Note: Node.js native fetch doesn't support custom agents directly, so we use
90
+ * https.request when rejectUnauthorized is needed, but structure the code with
91
+ *
92
+ * @param url - The URL to fetch the response body from
93
+ * @param expectedStatusCode - Only return body if status code matches (default: 403)
94
+ * @returns The response body as a string, or empty string if unavailable or status doesn't match
95
+ */
96
+ async function fetchHttpResponseBody(url, expectedStatusCode = 403) {
97
+ const controller = new AbortController();
98
+ const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout
99
+ try {
100
+ const parsedUrl = new url_1.URL(url);
101
+ const userAgent = process.env.HEROKU_DEBUG_USER_AGENT || 'heroku-run';
102
+ // Note: Native fetch in Node.js doesn't support custom https.Agent for SSL certificate handling.
103
+ // We use https.request when rejectUnauthorized: false is needed (staging environments).
104
+ // This maintains compatibility while using modern async/await and AbortController patterns.
105
+ if (parsedUrl.protocol !== 'https:') {
106
+ // For non-HTTPS URLs, use native fetch
107
+ const response = await fetch(url, {
108
+ method: 'GET',
109
+ headers: {
110
+ 'User-Agent': userAgent,
111
+ Accept: 'text/plain',
112
+ },
113
+ signal: controller.signal,
114
+ });
115
+ clearTimeout(timeoutId);
116
+ if (response.status === expectedStatusCode) {
117
+ return await response.text();
118
+ }
119
+ return '';
120
+ }
121
+ // For HTTPS with rejectUnauthorized: false (staging), use https.request
122
+ // This is the same pattern as dyno.ts - necessary for staging SSL certs
123
+ return await new Promise(resolve => {
124
+ const cleanup = () => {
125
+ clearTimeout(timeoutId);
126
+ controller.abort();
127
+ };
128
+ const options = {
129
+ hostname: parsedUrl.hostname,
130
+ port: parsedUrl.port || 443,
131
+ path: parsedUrl.pathname + parsedUrl.search,
132
+ method: 'GET',
133
+ headers: {
134
+ 'User-Agent': userAgent,
135
+ Accept: 'text/plain',
136
+ },
137
+ rejectUnauthorized: false, // Allow staging self-signed certificates
138
+ };
139
+ const req = https.request(options, res => {
140
+ let body = '';
141
+ res.setEncoding('utf8');
142
+ res.on('data', chunk => {
143
+ body += chunk;
144
+ });
145
+ res.on('end', () => {
146
+ cleanup();
147
+ if (res.statusCode === expectedStatusCode) {
148
+ resolve(body);
149
+ }
150
+ else {
151
+ resolve('');
152
+ }
153
+ });
154
+ });
155
+ req.on('error', () => {
156
+ cleanup();
157
+ resolve('');
158
+ });
159
+ // Abort on timeout
160
+ controller.signal.addEventListener('abort', () => {
161
+ req.destroy();
162
+ resolve('');
163
+ }, { once: true });
164
+ req.end();
165
+ });
166
+ }
167
+ catch (error) {
168
+ clearTimeout(timeoutId);
169
+ // AbortError is expected on timeout - return empty string
170
+ if (error instanceof Error && error.name === 'AbortError') {
171
+ return '';
172
+ }
173
+ // For other errors, return empty string for graceful degradation
174
+ // This matches the previous behavior where errors returned empty string
175
+ return '';
176
+ }
177
+ }
178
+ exports.fetchHttpResponseBody = fetchHttpResponseBody;
@@ -15,25 +15,49 @@ function readLogs(logplexURL, isTail, recreateSessionTimeout) {
15
15
  'User-Agent': userAgent,
16
16
  },
17
17
  });
18
- es.addEventListener('error', function (err) {
19
- if (err && (err.status || err.message)) {
20
- const msg = (isTail && (err.status === 404 || err.status === 403)) ?
21
- 'Log stream timed out. Please try again.' :
22
- `Logs eventsource failed with: ${err.status}${err.message ? ` ${err.message}` : ''}`;
23
- reject(new Error(msg));
18
+ let isResolved = false;
19
+ let hasReceivedMessages = false;
20
+ const safeReject = (error) => {
21
+ if (!isResolved) {
22
+ isResolved = true;
24
23
  es.close();
24
+ reject(error);
25
25
  }
26
- if (!isTail) {
27
- resolve();
26
+ };
27
+ const safeResolve = () => {
28
+ if (!isResolved) {
29
+ isResolved = true;
28
30
  es.close();
31
+ resolve();
29
32
  }
30
- // should only land here if --tail and no error status or message
31
- });
33
+ };
32
34
  es.addEventListener('message', function (e) {
35
+ hasReceivedMessages = true;
33
36
  e.data.trim().split(/\n+/).forEach(line => {
34
37
  core_1.ux.log((0, colorize_1.default)(line));
35
38
  });
36
39
  });
40
+ es.addEventListener('error', function (err) {
41
+ if (err && (err.status || err.message)) {
42
+ let msg;
43
+ if (err.status === 403) {
44
+ msg = hasReceivedMessages ?
45
+ 'Log stream access expired. Please try again.' :
46
+ "You can't access this space from your IP address. Contact your team admin.";
47
+ }
48
+ else if (err.status === 404 && isTail) {
49
+ msg = 'Log stream access expired. Please try again.';
50
+ }
51
+ else {
52
+ msg = `Logs eventsource failed with: ${err.status}${err.message ? ` ${err.message}` : ''}`;
53
+ }
54
+ safeReject(new Error(msg));
55
+ }
56
+ else if (!isTail) {
57
+ safeResolve();
58
+ }
59
+ // should only land here if --tail and no error status or message
60
+ });
37
61
  if (isTail && recreateSessionTimeout) {
38
62
  setTimeout(() => {
39
63
  reject(new Error('Fir log stream timeout'));
@@ -15015,5 +15015,5 @@
15015
15015
  ]
15016
15016
  }
15017
15017
  },
15018
- "version": "10.15.1-beta.0"
15018
+ "version": "10.15.2-beta.0"
15019
15019
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "heroku",
3
3
  "description": "CLI to interact with Heroku",
4
- "version": "10.15.1-beta.0",
4
+ "version": "10.15.2-beta.0",
5
5
  "author": "Heroku",
6
6
  "bin": "./bin/run",
7
7
  "bugs": "https://github.com/heroku/cli/issues",
@@ -397,5 +397,5 @@
397
397
  "version": "oclif readme --multi && git add README.md ../../docs"
398
398
  },
399
399
  "types": "lib/index.d.ts",
400
- "gitHead": "88abd3605aca5bf74fa0a8400230ad52c3951476"
400
+ "gitHead": "ed1c1a07c328d6b18d1feee24c3ff6ebf0153187"
401
401
  }