run402 2.41.0 → 2.41.1

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/operator.mjs +28 -6
  2. package/package.json +1 -1
package/lib/operator.mjs CHANGED
@@ -147,6 +147,10 @@ function startLoopbackServer({ expectedState, timeoutMs }) {
147
147
  rejectCode = rej;
148
148
  });
149
149
  let timer;
150
+ // Track live sockets: server.close() alone stops NEW connections but leaves
151
+ // the browser's keep-alive socket open, which keeps Node's event loop alive
152
+ // and hangs the CLI after a successful login. close() must destroy them.
153
+ const sockets = new Set();
150
154
  const server = createServer((req, res) => {
151
155
  let u;
152
156
  try {
@@ -162,19 +166,32 @@ function startLoopbackServer({ expectedState, timeoutMs }) {
162
166
  const code = u.searchParams.get("code");
163
167
  const gotState = u.searchParams.get("state");
164
168
  const errParam = u.searchParams.get("error");
165
- res.writeHead(200, { "content-type": "text/html" });
169
+ // `connection: close` so the browser does not keep the socket alive.
170
+ res.writeHead(200, { "content-type": "text/html", connection: "close" });
166
171
  res.end(
167
172
  "<!doctype html><html><body style=\"font-family:system-ui;padding:3rem\">" +
168
173
  "<h2>run402 - you're signed in.</h2><p>You can close this window and return to your terminal.</p></body></html>",
169
174
  );
170
- cleanup();
175
+ // Do NOT tear down here — let the response flush. The caller calls close()
176
+ // once it has the code (or on any failure path).
171
177
  if (errParam) rejectCode(new Error(`authorization error: ${errParam}`));
172
178
  else if (!code) rejectCode(new Error("no authorization code on the loopback redirect"));
173
179
  else if (gotState !== expectedState) rejectCode(new Error("state mismatch on the loopback redirect (possible CSRF) - aborted"));
174
180
  else resolveCode(code);
175
181
  });
176
- function cleanup() {
182
+ server.on("connection", (s) => {
183
+ sockets.add(s);
184
+ s.once("close", () => sockets.delete(s));
185
+ });
186
+ function close() {
177
187
  clearTimeout(timer);
188
+ for (const s of sockets) {
189
+ try {
190
+ s.destroy();
191
+ } catch {
192
+ // already gone
193
+ }
194
+ }
178
195
  try {
179
196
  server.close();
180
197
  } catch {
@@ -182,18 +199,18 @@ function startLoopbackServer({ expectedState, timeoutMs }) {
182
199
  }
183
200
  }
184
201
  timer = setTimeout(() => {
185
- cleanup();
202
+ close();
186
203
  rejectCode(new Error("timed out waiting for browser approval"));
187
204
  }, timeoutMs);
188
205
  server.on("error", (e) => {
189
- cleanup();
206
+ close();
190
207
  rejectCode(e);
191
208
  });
192
209
  const ready = new Promise((res, rej) => {
193
210
  server.once("error", rej);
194
211
  server.listen(0, "127.0.0.1", () => res(server.address().port));
195
212
  });
196
- return { ready, codePromise, close: cleanup };
213
+ return { ready, codePromise, close };
197
214
  }
198
215
 
199
216
  /**
@@ -242,8 +259,13 @@ async function loopbackLogin(args, { stepUp }) {
242
259
  try {
243
260
  session = await sdk.operator.exchangeCliToken({ code, codeVerifier, redirectUri, state });
244
261
  } catch (err) {
262
+ close();
245
263
  return reportSdkError(err);
246
264
  }
265
+ // The loopback server has done its job. Tear it down (destroying any
266
+ // keep-alive socket) so Node's event loop drains and the CLI exits instead
267
+ // of hanging until Ctrl+C.
268
+ close();
247
269
 
248
270
  const cached = controlPlaneSessionFromTokenResponse(session);
249
271
  saveControlPlaneSession(cached);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run402",
3
- "version": "2.41.0",
3
+ "version": "2.41.1",
4
4
  "description": "CLI for Run402 — provision Postgres databases, deploy static sites, generate images, and manage wallets via x402 and MPP micropayments.",
5
5
  "type": "module",
6
6
  "bin": {