create-egregore 0.16.2 → 0.16.3
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/auth.js +76 -14
- package/package.json +1 -1
package/lib/auth.js
CHANGED
|
@@ -7,6 +7,7 @@ const https = require("node:https");
|
|
|
7
7
|
|
|
8
8
|
const GITHUB_APP_CLIENT_ID = "Iv23li2obNsAjakoK2RE"; // GitHub App — founder flow (clean install screen)
|
|
9
9
|
const OAUTH_APP_CLIENT_ID = "Ov23liYZjJbbbNgnJ9vH"; // OAuth App — join flow (repo:invite only)
|
|
10
|
+
const SUPPORT_URL = "https://github.com/egregore-labs/egregore";
|
|
10
11
|
|
|
11
12
|
function post(url, body) {
|
|
12
13
|
return new Promise((resolve, reject) => {
|
|
@@ -45,6 +46,26 @@ function sleep(ms) {
|
|
|
45
46
|
return new Promise((r) => setTimeout(r, ms));
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
/**
|
|
50
|
+
* POST with retry + exponential backoff. Handles transient network errors
|
|
51
|
+
* (ECONNREFUSED, ECONNRESET, ETIMEDOUT) by retrying up to `retries` times
|
|
52
|
+
* with a 1s / 2s / 4s backoff. Throws the last error if all retries fail.
|
|
53
|
+
*/
|
|
54
|
+
async function postWithRetry(url, body, retries = 3) {
|
|
55
|
+
let lastErr;
|
|
56
|
+
for (let attempt = 0; attempt < retries; attempt++) {
|
|
57
|
+
try {
|
|
58
|
+
return await post(url, body);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
lastErr = err;
|
|
61
|
+
if (attempt < retries - 1) {
|
|
62
|
+
await sleep(1000 * Math.pow(2, attempt));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
throw lastErr;
|
|
67
|
+
}
|
|
68
|
+
|
|
48
69
|
function openBrowser(url) {
|
|
49
70
|
const { execFile } = require("node:child_process");
|
|
50
71
|
if (process.platform === "darwin") {
|
|
@@ -81,14 +102,27 @@ async function deviceFlow(ui, opts = {}) {
|
|
|
81
102
|
const clientId = isJoin ? OAUTH_APP_CLIENT_ID : GITHUB_APP_CLIENT_ID;
|
|
82
103
|
const scope = isJoin ? "repo:invite" : "";
|
|
83
104
|
|
|
84
|
-
// 1. Request device code
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
105
|
+
// 1. Request device code (with retry — handles transient network hiccups
|
|
106
|
+
// and GitHub edge server blips that caused ECONNREFUSED in the field)
|
|
107
|
+
let codeResp;
|
|
108
|
+
try {
|
|
109
|
+
codeResp = await postWithRetry("https://github.com/login/device/code", {
|
|
110
|
+
client_id: clientId,
|
|
111
|
+
scope,
|
|
112
|
+
});
|
|
113
|
+
} catch (err) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`Could not reach GitHub after 3 attempts: ${err.message}\n` +
|
|
116
|
+
`Check your internet connection and try again.\n` +
|
|
117
|
+
`If this keeps happening, report it at ${SUPPORT_URL}/issues`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
89
120
|
|
|
90
121
|
if (!codeResp.device_code) {
|
|
91
|
-
throw new Error(
|
|
122
|
+
throw new Error(
|
|
123
|
+
`GitHub device flow did not return a code.\n` +
|
|
124
|
+
`This is unusual — please report at ${SUPPORT_URL}/issues`
|
|
125
|
+
);
|
|
92
126
|
}
|
|
93
127
|
|
|
94
128
|
const { device_code, user_code, verification_uri, interval: pollInterval } = codeResp;
|
|
@@ -109,19 +143,40 @@ async function deviceFlow(ui, opts = {}) {
|
|
|
109
143
|
ui.info("");
|
|
110
144
|
openBrowser(verifyUrl);
|
|
111
145
|
|
|
112
|
-
// 3. Poll for token
|
|
146
|
+
// 3. Poll for token. Transient network errors (one bad packet, WiFi blip,
|
|
147
|
+
// GitHub edge restart) must NOT kill the whole flow — the user might
|
|
148
|
+
// be mid-authorize on the browser side. Catch per-poll errors and
|
|
149
|
+
// continue; the outer 5-minute timeout handles genuinely broken networks.
|
|
113
150
|
let interval = (pollInterval || 5) * 1000;
|
|
114
151
|
const timeout = 300000; // 5 minutes
|
|
115
152
|
const start = Date.now();
|
|
153
|
+
let consecutiveNetworkErrors = 0;
|
|
154
|
+
let networkWarningShown = false;
|
|
116
155
|
|
|
117
156
|
while (Date.now() - start < timeout) {
|
|
118
157
|
await sleep(interval);
|
|
119
158
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
159
|
+
let tokenResp;
|
|
160
|
+
try {
|
|
161
|
+
tokenResp = await post("https://github.com/login/oauth/access_token", {
|
|
162
|
+
client_id: clientId,
|
|
163
|
+
device_code,
|
|
164
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
165
|
+
});
|
|
166
|
+
consecutiveNetworkErrors = 0;
|
|
167
|
+
} catch (err) {
|
|
168
|
+
consecutiveNetworkErrors++;
|
|
169
|
+
// After 3 consecutive failures, surface a note so the user knows
|
|
170
|
+
// something is off — but keep polling until the outer timeout.
|
|
171
|
+
if (consecutiveNetworkErrors >= 3 && !networkWarningShown) {
|
|
172
|
+
ui.warn(
|
|
173
|
+
`Network trouble reaching GitHub (${err.message}). Still trying.\n` +
|
|
174
|
+
`If this persists, report at ${SUPPORT_URL}/issues`
|
|
175
|
+
);
|
|
176
|
+
networkWarningShown = true;
|
|
177
|
+
}
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
125
180
|
|
|
126
181
|
if (tokenResp.access_token) {
|
|
127
182
|
return tokenResp.access_token;
|
|
@@ -135,10 +190,17 @@ async function deviceFlow(ui, opts = {}) {
|
|
|
135
190
|
continue;
|
|
136
191
|
}
|
|
137
192
|
|
|
138
|
-
throw new Error(
|
|
193
|
+
throw new Error(
|
|
194
|
+
`${tokenResp.error_description || tokenResp.error || "Auth failed"}\n` +
|
|
195
|
+
`If you need help, see ${SUPPORT_URL}`
|
|
196
|
+
);
|
|
139
197
|
}
|
|
140
198
|
|
|
141
|
-
throw new Error(
|
|
199
|
+
throw new Error(
|
|
200
|
+
`Timed out waiting for authorization (5 minutes).\n` +
|
|
201
|
+
`If the browser tab didn't open, visit the verification URL manually and enter your code.\n` +
|
|
202
|
+
`If this keeps happening, report at ${SUPPORT_URL}/issues`
|
|
203
|
+
);
|
|
142
204
|
}
|
|
143
205
|
|
|
144
206
|
module.exports = { deviceFlow, openBrowser };
|