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.
Files changed (2) hide show
  1. package/lib/auth.js +76 -14
  2. 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
- const codeResp = await post("https://github.com/login/device/code", {
86
- client_id: clientId,
87
- scope,
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("Failed to start GitHub device flow");
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
- const tokenResp = await post("https://github.com/login/oauth/access_token", {
121
- client_id: clientId,
122
- device_code,
123
- grant_type: "urn:ietf:params:oauth:grant-type:device_code",
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(tokenResp.error_description || tokenResp.error || "Auth failed");
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("Timed out waiting for authorization (5 minutes)");
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-egregore",
3
- "version": "0.16.2",
3
+ "version": "0.16.3",
4
4
  "description": "Set up Egregore for your team in one command",
5
5
  "license": "MIT",
6
6
  "bin": {