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.
- package/lib/lib/run/dyno.js +9 -1
- package/lib/lib/run/helpers.d.ts +15 -0
- package/lib/lib/run/helpers.js +100 -1
- package/lib/lib/run/log-displayer.js +34 -10
- package/oclif.manifest.json +1 -1
- package/package.json +2 -2
package/lib/lib/run/dyno.js
CHANGED
|
@@ -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',
|
package/lib/lib/run/helpers.d.ts
CHANGED
|
@@ -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>;
|
package/lib/lib/run/helpers.js
CHANGED
|
@@ -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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
27
|
-
|
|
26
|
+
};
|
|
27
|
+
const safeResolve = () => {
|
|
28
|
+
if (!isResolved) {
|
|
29
|
+
isResolved = true;
|
|
28
30
|
es.close();
|
|
31
|
+
resolve();
|
|
29
32
|
}
|
|
30
|
-
|
|
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'));
|
package/oclif.manifest.json
CHANGED
|
@@ -15015,5 +15015,5 @@
|
|
|
15015
15015
|
]
|
|
15016
15016
|
}
|
|
15017
15017
|
},
|
|
15018
|
-
"version": "10.15.
|
|
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.
|
|
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": "
|
|
400
|
+
"gitHead": "ed1c1a07c328d6b18d1feee24c3ff6ebf0153187"
|
|
401
401
|
}
|