@allocator-one/harness-cli 1.8.0 → 1.9.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/dist/cli.js CHANGED
@@ -93,31 +93,15 @@ import { accessSync, constants as fsConstants } from "fs";
93
93
  import { createServer } from "http";
94
94
  import { delimiter, join } from "path";
95
95
  import { createInterface } from "readline";
96
-
97
- // src/core/login-page.ts
98
- var LOGO = '<svg width="210" height="34" viewBox="0 0 210 34" fill="none" role="img" aria-label="Infra One" xmlns="http://www.w3.org/2000/svg">\n <style>\n path {\n fill: #353535\n }\n\n @media (prefers-color-scheme: dark) {\n path {\n fill: white\n }\n }\n </style>\n <g id="I1_logo_full">\n <g id="Infra One Logo">\n <path fill-rule="evenodd" clip-rule="evenodd" d="M44.2202 29.5139V4.04761H49.617V29.5139H44.2202Z" />\n <path fill-rule="evenodd" clip-rule="evenodd" d="M121.159 11.873V13.998C119.877 12.4464 117.853 11.4345 115.29 11.4345C110.23 11.4345 106.284 15.3809 106.284 20.7103C106.284 26.0397 110.23 29.9861 115.29 29.9861C118.157 29.9861 119.877 28.9742 121.159 27.3214V29.4464H125.948V11.9067H121.159V11.873ZM116.099 25.8373C113.063 25.8373 110.938 23.7123 110.938 20.6091C110.938 17.5059 113.063 15.4484 116.099 15.4484C119.135 15.4484 121.327 17.5734 121.327 20.6091C121.327 23.6448 119.034 25.8373 116.099 25.8373Z" />\n <path d="M15.6508 0H27.8611V34H15.6508V0ZM0 11.131C0 7.92659 2.66468 5.2619 5.86905 5.2619H6.34127C9.54564 5.2619 12.2103 7.92659 12.2103 11.131C12.2103 14.3353 9.54564 17 6.34127 17H5.86905C2.66468 17 0 14.3353 0 11.131Z" />\n <path fill-rule="evenodd" clip-rule="evenodd" d="M88.4405 10.4901C87.6984 10.2203 87.3611 10.0179 86.3155 10.0179C84.8651 10.0179 84.123 10.7599 84.123 12.2103V13.627H89.4524V17.6746H84.123V29.4802H79.3333V17.6746H76.2976V13.627H79.3333V12.2103C79.3333 8.36511 81.1548 5.86908 85.4048 5.86908C87.631 5.86908 88.1706 6.13892 89.621 6.71233L88.4405 10.4901Z" />\n <path fill-rule="evenodd" clip-rule="evenodd" d="M98.0199 14.4365C99.133 12.4127 101.224 11.2321 104.091 11.6032V15.9206C100.246 15.381 98.0199 17.0337 98.0199 21.7897V29.4127H93.2302V11.873H98.0199V14.4365Z" />\n <path fill-rule="evenodd" clip-rule="evenodd" d="M65.5715 11.4008C70.3611 11.4008 72.756 14.5377 72.756 19.2262V29.5139H67.9663V20.0357C67.9663 17.371 66.6845 15.6171 64.1211 15.6171C61.5576 15.6171 59.7024 17.6409 59.7024 20.7778V29.5139H54.9127V11.873H59.7024V14.0655C60.9842 12.5139 62.9068 11.4008 65.5715 11.4008Z" />\n <path fill-rule="evenodd" clip-rule="evenodd" d="M151.01 4.621C143.387 4.621 137.687 10.0516 137.687 17.3035C137.687 24.5555 143.286 29.9861 151.01 29.9861C158.734 29.9861 164.232 24.4881 164.232 17.3035C164.232 10.119 158.633 4.621 151.01 4.621ZM151.01 25.5674C145.95 25.5674 142.746 21.992 142.746 17.3035C142.746 12.6151 145.95 9.10712 151.01 9.10712C156.069 9.10712 159.274 12.7837 159.274 17.3035C159.274 21.8234 155.968 25.5674 151.01 25.5674Z" />\n <path fill-rule="evenodd" clip-rule="evenodd" d="M179.309 11.4008C184.099 11.4008 186.494 14.5377 186.494 19.2262V29.5139H181.704V20.0357C181.704 17.371 180.423 15.6171 177.859 15.6171C175.296 15.6171 173.44 17.6409 173.44 20.7778V29.5139H168.651V11.873H173.44V14.0655C174.722 12.5139 176.645 11.4008 179.309 11.4008Z" />\n <path fill-rule="evenodd" clip-rule="evenodd" d="M209.565 20.2381C209.565 15.1786 206.26 11.3333 200.188 11.3333C194.117 11.3333 190.643 15.3809 190.643 20.6091C190.643 25.8373 194.69 29.8849 200.559 29.8849C206.429 29.8849 208.284 27.119 209.397 24.8254L205.079 23.6448C204.337 24.9266 202.954 25.9385 200.661 25.9385C197.355 25.9385 195.702 24.1845 195.331 21.8909H209.498C209.599 21.3512 209.599 20.7778 209.599 20.2381H209.565ZM195.298 19.125C195.567 17.1012 197.321 15.2797 200.155 15.2797C202.988 15.2797 204.742 17.0337 204.944 19.125H195.298Z" />\n </g>\n </g>\n</svg>';
99
- function escapeHtml(text) {
100
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
101
- }
102
- function loginPage(message) {
103
- return `<!doctype html>
104
- <html lang="en"><head><meta charset="utf-8">
105
- <meta name="viewport" content="width=device-width, initial-scale=1">
106
- <!-- The logo paints itself white under a dark preference; without this the
107
- canvas stays white and the logo disappears. -->
108
- <meta name="color-scheme" content="light dark">
109
- <title>harness login</title></head>
110
- <body style="margin: 0;">
111
- <main style="min-height: 100vh; padding: 1.5rem; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2rem; text-align: center; font-family: system-ui, sans-serif;">
112
- ${LOGO}
113
- <p style="margin: 0;">${escapeHtml(message)}</p>
114
- </main>
115
- </body></html>
116
- `;
117
- }
118
-
119
- // src/core/auth.ts
120
96
  var DEFAULT_TIMEOUT_MS = 18e4;
97
+ var RESPONSE_DRAIN_GRACE_MS = 100;
98
+ var RESPONSE_FLUSH_TIMEOUT_MS = 2e3;
99
+ var MAX_PAGE_RESPONSES = 4;
100
+ var loginPageImport;
101
+ function loadLoginPage() {
102
+ loginPageImport ??= import("./login-page-UVUKFFTT.js");
103
+ return loginPageImport;
104
+ }
121
105
  function generateState() {
122
106
  return randomBytes(24).toString("base64url");
123
107
  }
@@ -183,72 +167,313 @@ async function loopbackLogin(baseUrl, options) {
183
167
  });
184
168
  });
185
169
  if (port === null) {
186
- return { ok: false, message: "could not open a loopback port for the browser handoff" };
170
+ return {
171
+ ok: false,
172
+ message: "could not open a loopback port for the browser handoff"
173
+ };
187
174
  }
188
175
  try {
189
176
  const query = new URLSearchParams({ port: String(port), state: state2, challenge });
190
177
  const url = `${baseUrl}/cli/login?${query.toString()}`;
178
+ const callbackPromise = awaitCallback(server, state2, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
179
+ void loadLoginPage().catch(() => {
180
+ loginPageImport = void 0;
181
+ });
191
182
  const openBrowser = options.openBrowser ?? defaultOpenBrowser;
192
- openBrowser(url);
183
+ try {
184
+ openBrowser(url);
185
+ } catch (error) {
186
+ const detail = error instanceof Error ? error.message : String(error);
187
+ return { ok: false, message: `could not open a browser: ${detail}` };
188
+ }
193
189
  notify(`Opening ${url}`);
194
190
  notify("Waiting for the browser authorization (Ctrl-C to abort)\u2026");
195
- const code = await awaitCallback(server, state2, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
196
- if (!code.ok) return code;
197
- const token = await exchangeCode(baseUrl, code.code, verifier, options);
198
- if (!token.ok) return token;
199
- return await verifyAndStore(baseUrl, token.token, options);
191
+ const callback = await callbackPromise;
192
+ if (!callback.ok) return callback;
193
+ try {
194
+ const token = await exchangeCode(baseUrl, callback.code, verifier, options);
195
+ if (!token.ok) {
196
+ await callback.respond(
197
+ 400,
198
+ "Harness could not exchange the browser authorization. Return to the terminal for details.",
199
+ "error"
200
+ );
201
+ return token;
202
+ }
203
+ const result = await verifyAndStore(baseUrl, token.token, options);
204
+ if (result.ok) {
205
+ await callback.respond(200, "You can close this browser and return to the terminal.", "success");
206
+ } else {
207
+ await callback.respond(
208
+ 400,
209
+ "Harness could not verify and store this login. Return to the terminal for details.",
210
+ "error"
211
+ );
212
+ }
213
+ return result;
214
+ } catch (error) {
215
+ await callback.respond(500, "Harness could not finish the login. Return to the terminal for details.", "error");
216
+ const detail = error instanceof Error ? error.message : String(error);
217
+ return { ok: false, message: `could not finish the browser login: ${detail}` };
218
+ }
200
219
  } finally {
201
- server.closeAllConnections();
202
220
  server.close();
221
+ const forceClose = setTimeout(
222
+ () => server.closeAllConnections(),
223
+ RESPONSE_FLUSH_TIMEOUT_MS + RESPONSE_DRAIN_GRACE_MS
224
+ );
225
+ forceClose.unref();
203
226
  }
204
227
  }
205
228
  function awaitCallback(server, state2, timeoutMs) {
206
229
  return new Promise((resolve) => {
230
+ let phase = "waiting";
231
+ let finalResponse = null;
232
+ let callbackPageResponses = 0;
233
+ const rootPageResponses = /* @__PURE__ */ new Map();
234
+ const acceptedResponders = [];
235
+ const inFlightResponses = /* @__PURE__ */ new Set();
236
+ const trackResponse = (responsePromise) => {
237
+ inFlightResponses.add(responsePromise);
238
+ void responsePromise.finally(() => inFlightResponses.delete(responsePromise));
239
+ return responsePromise;
240
+ };
241
+ const drainTrackedResponses = async () => {
242
+ await new Promise((settle) => setTimeout(settle, RESPONSE_DRAIN_GRACE_MS));
243
+ const deadline = Date.now() + RESPONSE_FLUSH_TIMEOUT_MS;
244
+ while (inFlightResponses.size > 0 && Date.now() < deadline) {
245
+ const remaining = deadline - Date.now();
246
+ await Promise.race([
247
+ Promise.allSettled([...inFlightResponses]),
248
+ new Promise((settle) => setTimeout(settle, remaining))
249
+ ]);
250
+ }
251
+ };
207
252
  const timer = setTimeout(() => {
208
- resolve({ ok: false, message: "timed out waiting for the browser authorization" });
253
+ if (phase !== "waiting") return;
254
+ const timeoutResponse = {
255
+ status: 410,
256
+ message: "This login attempt has ended \u2014 run harness login again if needed.",
257
+ pageState: "error"
258
+ };
259
+ phase = "terminal";
260
+ finalResponse = timeoutResponse;
261
+ resolve({
262
+ ok: false,
263
+ message: "timed out waiting for the browser authorization"
264
+ });
265
+ void drainTrackedResponses();
209
266
  }, timeoutMs);
267
+ server.once("close", () => {
268
+ clearTimeout(timer);
269
+ if (phase !== "waiting") return;
270
+ phase = "terminal";
271
+ resolve({ ok: false, message: "the browser login listener closed before authorization completed" });
272
+ });
210
273
  server.on("request", (request, response) => {
211
- const respond = (status, message) => {
212
- response.writeHead(status, { "content-type": "text/html; charset=utf-8" });
213
- response.end(loginPage(message));
274
+ const respond = callbackResponder(response);
275
+ const respondWithPage = (status, message, pageState) => {
276
+ if (callbackPageResponses >= MAX_PAGE_RESPONSES) {
277
+ respondEmpty(response, 429);
278
+ return Promise.resolve();
279
+ }
280
+ callbackPageResponses += 1;
281
+ return respond(status, message, pageState);
282
+ };
283
+ const respondAtRoot = (status, message, pageState) => {
284
+ const rootPhase = phase === "terminal" ? "terminal" : "waiting";
285
+ const responses = rootPageResponses.get(rootPhase) ?? 0;
286
+ if (responses >= MAX_PAGE_RESPONSES) {
287
+ respondEmpty(response, 429);
288
+ return Promise.resolve();
289
+ }
290
+ rootPageResponses.set(rootPhase, responses + 1);
291
+ return respond(status, message, pageState);
214
292
  };
215
293
  let url;
216
294
  try {
217
295
  url = new URL(request.url ?? "/", "http://127.0.0.1");
218
296
  } catch {
219
- respond(404, "Waiting for the harness login callback.");
297
+ respondEmpty(response, 404);
298
+ return;
299
+ }
300
+ const path = url.pathname.replace(/\/+$/, "") || "/";
301
+ if (path !== "/callback") {
302
+ if (path === "/") {
303
+ const rootResponse = phase === "terminal" && finalResponse ? finalResponse : {
304
+ status: 200,
305
+ message: "Waiting for the Harness browser authorization to finish.",
306
+ pageState: "waiting"
307
+ };
308
+ void trackResponse(
309
+ respondAtRoot(rootResponse.status, rootResponse.message, rootResponse.pageState)
310
+ );
311
+ } else {
312
+ respondEmpty(response, path === "/favicon.ico" ? 204 : 404);
313
+ }
220
314
  return;
221
315
  }
222
- if (url.pathname !== "/callback") {
223
- respond(404, "Waiting for the harness login callback.");
316
+ const matchingState = url.searchParams.get("state") === state2;
317
+ if (!matchingState) {
318
+ respondEmpty(response, 400);
319
+ return;
320
+ }
321
+ if (phase === "terminal") {
322
+ if (!url.searchParams.has("code") && !url.searchParams.has("error")) {
323
+ respondEmpty(response, 400);
324
+ return;
325
+ }
326
+ const result = finalResponse ?? {
327
+ status: 410,
328
+ message: "This login attempt has ended \u2014 run harness login again if needed.",
329
+ pageState: "error"
330
+ };
331
+ void trackResponse(respondWithPage(result.status, result.message, result.pageState));
332
+ return;
333
+ }
334
+ if (phase === "processing") {
335
+ if (url.searchParams.has("error")) {
336
+ respondEmpty(response, 409);
337
+ return;
338
+ }
339
+ const duplicateCode = url.searchParams.get("code");
340
+ if (duplicateCode === null || duplicateCode === "") {
341
+ respondEmpty(response, 400);
342
+ return;
343
+ }
344
+ if (acceptedResponders.length >= MAX_PAGE_RESPONSES) {
345
+ respondEmpty(response, 429);
346
+ return;
347
+ }
348
+ acceptedResponders.push(respondWithPage);
349
+ return;
350
+ }
351
+ if (url.searchParams.has("error")) {
352
+ const denialResponse = {
353
+ status: 400,
354
+ message: "Browser authorization was denied. Return to the terminal to try again.",
355
+ pageState: "error"
356
+ };
357
+ phase = "terminal";
358
+ finalResponse = denialResponse;
359
+ clearTimeout(timer);
360
+ void (async () => {
361
+ await trackResponse(respondWithPage(denialResponse.status, denialResponse.message, denialResponse.pageState));
362
+ await drainTrackedResponses();
363
+ resolve({ ok: false, message: "browser authorization was denied" });
364
+ })();
224
365
  return;
225
366
  }
226
367
  const code = url.searchParams.get("code");
227
- if (url.searchParams.get("state") === state2 && code !== null && code !== "") {
228
- respond(200, "Signed in \u2014 you can close this tab and return to the terminal.");
368
+ if (code !== null && code !== "") {
369
+ phase = "processing";
370
+ acceptedResponders.push(respondWithPage);
229
371
  clearTimeout(timer);
230
- resolve({ ok: true, code });
372
+ resolve({
373
+ ok: true,
374
+ code,
375
+ respond: async (status, message, pageState) => {
376
+ finalResponse = { status, message, pageState };
377
+ phase = "terminal";
378
+ const responders = acceptedResponders.splice(0);
379
+ await Promise.all(
380
+ responders.map((acceptedRespond) => trackResponse(acceptedRespond(status, message, pageState)))
381
+ );
382
+ await drainTrackedResponses();
383
+ }
384
+ });
231
385
  } else {
232
- respond(400, "This is not the callback this login is waiting for \u2014 run harness login again if needed.");
386
+ respondEmpty(response, 400);
233
387
  }
234
388
  });
235
389
  });
236
390
  }
391
+ function callbackResponder(response) {
392
+ return async (status, message, state2) => {
393
+ if (response.writableEnded || response.destroyed) return;
394
+ return new Promise((resolve) => {
395
+ let flushTimer;
396
+ const armFlushTimeout = () => {
397
+ if (flushTimer !== void 0) clearTimeout(flushTimer);
398
+ flushTimer = setTimeout(() => {
399
+ response.destroy();
400
+ settle();
401
+ }, RESPONSE_FLUSH_TIMEOUT_MS);
402
+ flushTimer.unref();
403
+ };
404
+ const settle = () => {
405
+ if (flushTimer !== void 0) clearTimeout(flushTimer);
406
+ response.off("finish", settle);
407
+ response.off("close", settle);
408
+ response.off("error", settle);
409
+ response.off("drain", armFlushTimeout);
410
+ resolve();
411
+ };
412
+ response.once("finish", settle);
413
+ response.once("close", settle);
414
+ response.once("error", settle);
415
+ response.on("drain", armFlushTimeout);
416
+ void loadLoginPage().then(({ loginPage }) => {
417
+ if (response.writableEnded || response.destroyed) {
418
+ settle();
419
+ return;
420
+ }
421
+ try {
422
+ const html = loginPage(message, state2);
423
+ response.writeHead(status, {
424
+ "cache-control": "no-store",
425
+ connection: "close",
426
+ "content-length": String(Buffer.byteLength(html)),
427
+ "content-type": "text/html; charset=utf-8",
428
+ "x-content-type-options": "nosniff"
429
+ });
430
+ armFlushTimeout();
431
+ response.end(html);
432
+ } catch {
433
+ response.destroy();
434
+ settle();
435
+ }
436
+ }).catch(() => {
437
+ response.destroy();
438
+ settle();
439
+ });
440
+ });
441
+ };
442
+ }
443
+ function respondEmpty(response, status) {
444
+ response.once("error", () => void 0);
445
+ try {
446
+ response.writeHead(status, {
447
+ "cache-control": "no-store",
448
+ connection: "close",
449
+ "x-content-type-options": "nosniff"
450
+ });
451
+ response.end();
452
+ } catch {
453
+ response.destroy();
454
+ }
455
+ }
237
456
  async function exchangeCode(baseUrl, code, verifier, options) {
238
457
  try {
239
- const body = await bareClient(baseUrl, "", options).post(
240
- "/api/v1/cli/token-exchange",
241
- { code, code_verifier: verifier }
242
- );
458
+ const body = await bareClient(baseUrl, "", options).post("/api/v1/cli/token-exchange", { code, code_verifier: verifier });
243
459
  const token = body.data?.token;
244
460
  if (typeof token === "string" && token !== "") return { ok: true, token };
245
- return { ok: false, message: "the login-code exchange returned no token \u2014 aborting" };
461
+ return {
462
+ ok: false,
463
+ message: "the login-code exchange returned no token \u2014 aborting"
464
+ };
246
465
  } catch (error) {
247
466
  if (error instanceof HttpError) {
248
- return { ok: false, message: `the login-code exchange failed: ${error.message}` };
467
+ return {
468
+ ok: false,
469
+ message: `the login-code exchange failed: ${error.message}`
470
+ };
249
471
  }
250
472
  if (error instanceof TransportError) {
251
- return { ok: false, message: `the login-code exchange failed: cannot reach the API (${error.message})` };
473
+ return {
474
+ ok: false,
475
+ message: `the login-code exchange failed: cannot reach the API (${error.message})`
476
+ };
252
477
  }
253
478
  throw error;
254
479
  }
@@ -288,7 +513,10 @@ async function verifyAndStore(baseUrl, token, options) {
288
513
  } catch (error) {
289
514
  await client.delete("/api/v1/session").catch(() => void 0);
290
515
  const detail = error instanceof Error ? error.message : String(error);
291
- return { ok: false, message: `could not store credentials at ${path}: ${detail}` };
516
+ return {
517
+ ok: false,
518
+ message: `could not store credentials at ${path}: ${detail}`
519
+ };
292
520
  }
293
521
  if (superseded !== null) {
294
522
  await bareClient(superseded.url, superseded.token, options).delete("/api/v1/session").catch(() => void 0);
@@ -378,7 +606,10 @@ function defaultNotify(line) {
378
606
  process.stderr.write(line + "\n");
379
607
  }
380
608
  function defaultPrompt() {
381
- const readline = createInterface({ input: process.stdin, output: process.stderr });
609
+ const readline = createInterface({
610
+ input: process.stdin,
611
+ output: process.stderr
612
+ });
382
613
  return new Promise((resolve) => {
383
614
  readline.question("Token: ", (answer) => {
384
615
  readline.close();