@aetherpush/cli 0.4.1 → 0.5.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.
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.startLoopbackListener = startLoopbackListener;
4
+ const http = require("http");
5
+ const CALLBACK_PATH = "/callback";
6
+ /**
7
+ * A one-shot HTTP listener on the loopback interface that receives the
8
+ * authorization code the browser is redirected to.
9
+ *
10
+ * Bound to 127.0.0.1 rather than every interface, and on an ephemeral port the
11
+ * server pins to this ceremony, so nothing off this machine can reach it and a
12
+ * second `aether login` cannot collide with the first.
13
+ */
14
+ function startLoopbackListener() {
15
+ return new Promise((resolve, reject) => {
16
+ let settle = null;
17
+ let fail = null;
18
+ let received = null;
19
+ const server = http.createServer((req, res) => {
20
+ const requestUrl = new URL(req.url || "/", "http://127.0.0.1");
21
+ if (requestUrl.pathname !== CALLBACK_PATH) {
22
+ res.statusCode = 404;
23
+ res.end("Not found");
24
+ return;
25
+ }
26
+ const result = {
27
+ code: requestUrl.searchParams.get("code") || "",
28
+ state: requestUrl.searchParams.get("state"),
29
+ error: requestUrl.searchParams.get("error"),
30
+ };
31
+ res.statusCode = 200;
32
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
33
+ res.end(renderPage(result));
34
+ received = result;
35
+ if (settle) {
36
+ settle(result);
37
+ }
38
+ });
39
+ server.on("error", (err) => {
40
+ if (fail) {
41
+ fail(err);
42
+ return;
43
+ }
44
+ reject(err);
45
+ });
46
+ server.listen(0, "127.0.0.1", () => {
47
+ const address = server.address();
48
+ if (!address || typeof address === "string") {
49
+ server.close();
50
+ reject(new Error("The local callback listener did not report a port."));
51
+ return;
52
+ }
53
+ resolve({
54
+ redirectUri: `http://127.0.0.1:${address.port}${CALLBACK_PATH}`,
55
+ waitForCallback(timeoutMs) {
56
+ if (received) {
57
+ return Promise.resolve(received);
58
+ }
59
+ return new Promise((resolveWait, rejectWait) => {
60
+ const timer = setTimeout(() => {
61
+ settle = null;
62
+ fail = null;
63
+ rejectWait(new Error("Timed out waiting for the browser to complete authorization. Run 'aether login' again."));
64
+ }, timeoutMs);
65
+ settle = (result) => {
66
+ clearTimeout(timer);
67
+ settle = null;
68
+ fail = null;
69
+ resolveWait(result);
70
+ };
71
+ fail = (error) => {
72
+ clearTimeout(timer);
73
+ settle = null;
74
+ fail = null;
75
+ rejectWait(error);
76
+ };
77
+ });
78
+ },
79
+ close() {
80
+ server.close();
81
+ },
82
+ });
83
+ });
84
+ });
85
+ }
86
+ function renderPage(result) {
87
+ const heading = result.error ? "Authorization failed" : "You're signed in";
88
+ const detail = result.error
89
+ ? "Nothing was authorized. Return to your terminal for the details."
90
+ : "You can close this tab and return to your terminal.";
91
+ return `<!doctype html>
92
+ <html lang="en">
93
+ <head>
94
+ <meta charset="utf-8" />
95
+ <title>${heading}</title>
96
+ <style>
97
+ body { font-family: system-ui, -apple-system, sans-serif; margin: 0; display: grid; place-items: center; min-height: 100vh; background: #0b0b10; color: #e7e7ee; }
98
+ main { text-align: center; padding: 2rem; }
99
+ h1 { font-size: 1.25rem; margin: 0 0 0.5rem; }
100
+ p { margin: 0; color: #9a9aab; }
101
+ </style>
102
+ </head>
103
+ <body>
104
+ <main>
105
+ <h1>${heading}</h1>
106
+ <p>${detail}</p>
107
+ </main>
108
+ </body>
109
+ </html>`;
110
+ }
111
+ //# sourceMappingURL=loopback-listener.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loopback-listener.js","sourceRoot":"","sources":["../../../script/auth/loopback-listener.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * RFC 7636 helpers. The verifier never leaves this machine; only its SHA-256
3
+ * challenge is sent when the ceremony starts, so an authorization code
4
+ * intercepted on the way back is useless without this process.
5
+ */
6
+ export interface PkcePair {
7
+ verifier: string;
8
+ challenge: string;
9
+ }
10
+ export declare function generatePkcePair(): PkcePair;
11
+ export declare function generateState(): string;
12
+ export declare function generateDeviceId(): string;
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generatePkcePair = generatePkcePair;
4
+ exports.generateState = generateState;
5
+ exports.generateDeviceId = generateDeviceId;
6
+ const crypto = require("crypto");
7
+ function generatePkcePair() {
8
+ const verifier = crypto.randomBytes(32).toString("base64url");
9
+ const challenge = crypto.createHash("sha256").update(verifier, "ascii").digest("base64url");
10
+ return { verifier, challenge };
11
+ }
12
+ function generateState() {
13
+ return crypto.randomBytes(16).toString("base64url");
14
+ }
15
+ function generateDeviceId() {
16
+ return crypto.randomUUID();
17
+ }
18
+ //# sourceMappingURL=pkce.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pkce.js","sourceRoot":"","sources":["../../../script/auth/pkce.ts"],"names":[],"mappings":";;AAaA,4CAIC;AAED,sCAEC;AAED,4CAEC;AAzBD,iCAAiC;AAajC,SAAgB,gBAAgB;IAC9B,MAAM,QAAQ,GAAG,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;IAC9D,MAAM,SAAS,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IAC5F,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC;AACjC,CAAC;AAED,SAAgB,aAAa;IAC3B,OAAO,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AACtD,CAAC;AAED,SAAgB,gBAAgB;IAC9B,OAAO,MAAM,CAAC,UAAU,EAAE,CAAC;AAC7B,CAAC"}
@@ -26,7 +26,9 @@ const react_native_utils_1 = require("./react-native-utils");
26
26
  const file_utils_1 = require("./utils/file-utils");
27
27
  const ci_metadata_1 = require("./utils/ci-metadata");
28
28
  const release_json_1 = require("./utils/release-json");
29
- const configFilePath = path.join(process.env.LOCALAPPDATA || process.env.HOME, ".aether", "config.json");
29
+ const keyStore = require("./auth/key-store");
30
+ const pkce_1 = require("./auth/pkce");
31
+ const browser_login_1 = require("./auth/browser-login");
30
32
  const DEFAULT_AETHER_SERVER_URL = "https://api.aetherpush.com";
31
33
  const emailValidator = require("email-validator");
32
34
  const packageJson = require("../../package.json");
@@ -291,12 +293,16 @@ function removeCollaborator(command) {
291
293
  }
292
294
  function deleteConnectionInfoCache(printMessage = true) {
293
295
  try {
294
- fs.unlinkSync(configFilePath);
296
+ keyStore.clearCredential();
295
297
  if (printMessage) {
296
- (0, exports.log)(`Logged out. The session file at ${chalk.cyan(configFilePath)} has been deleted.`);
298
+ (0, exports.log)(`Logged out. The credentials at ${chalk.cyan(keyStore.getCredentialPath())} have been deleted.`);
299
+ }
300
+ }
301
+ catch (ex) {
302
+ if (printMessage) {
303
+ (0, exports.log)(chalk.yellow(ex?.message || `Could not delete ${keyStore.getCredentialPath()}.`));
297
304
  }
298
305
  }
299
- catch (ex) { }
300
306
  }
301
307
  function deleteFolder(folderPath) {
302
308
  return new Promise((resolve, reject) => {
@@ -414,23 +420,25 @@ function deploymentHistory(command) {
414
420
  });
415
421
  }
416
422
  function deserializeConnectionInfo() {
423
+ let stored;
417
424
  try {
418
- const savedConnection = fs.readFileSync(configFilePath, {
419
- encoding: "utf8",
420
- });
421
- let connectionInfo = JSON.parse(savedConnection);
422
- // If the connection info is in the legacy format, convert it to the modern format
423
- if (connectionInfo.accessKeyName) {
424
- connectionInfo = {
425
- accessKey: connectionInfo.accessKeyName,
426
- };
427
- }
428
- const connInfo = connectionInfo;
429
- return connInfo;
425
+ stored = keyStore.readCredential();
430
426
  }
431
- catch (ex) {
427
+ catch {
428
+ // A home directory we cannot read is "not logged in", not a crash on every
429
+ // command.
430
+ return;
431
+ }
432
+ if (!stored) {
432
433
  return;
433
434
  }
435
+ return {
436
+ accessKey: stored.accessKey,
437
+ customServerUrl: stored.serverUrl,
438
+ preserveAccessKeyOnLogout: stored.preserveAccessKeyOnLogout,
439
+ credentialId: stored.credentialId,
440
+ accountEmail: stored.accountEmail,
441
+ };
434
442
  }
435
443
  function execute(command) {
436
444
  connectionInfo = deserializeConnectionInfo();
@@ -445,9 +453,6 @@ function execute(command) {
445
453
  switch (command.type) {
446
454
  // Must not be logged in
447
455
  case cli.CommandType.login:
448
- if (connectionInfo && !command.nonInteractive) {
449
- throw new Error("You are already logged in from this machine.");
450
- }
451
456
  break;
452
457
  case cli.CommandType.register:
453
458
  if (connectionInfo) {
@@ -461,7 +466,7 @@ function execute(command) {
461
466
  if (!connectionInfo) {
462
467
  throw new Error("You are not currently logged in. Run 'aether login' to authenticate with Aether.");
463
468
  }
464
- exports.sdk = getSdk(connectionInfo.accessKey, CLI_HEADERS, connectionInfo.customServerUrl);
469
+ exports.sdk = getSdk(connectionInfo.accessKey, CLI_HEADERS, resolveServerUrl(connectionInfo.customServerUrl));
465
470
  break;
466
471
  }
467
472
  switch (command.type) {
@@ -547,7 +552,7 @@ function getTotalActiveFromDeploymentMetrics(metrics) {
547
552
  return totalActive;
548
553
  }
549
554
  async function login(command) {
550
- const serverUrl = command.serverUrl || DEFAULT_AETHER_SERVER_URL;
555
+ const serverUrl = command.serverUrl || connectionInfo?.customServerUrl || DEFAULT_AETHER_SERVER_URL;
551
556
  if (command.accessKey) {
552
557
  exports.sdk = getSdk(command.accessKey, CLI_HEADERS, serverUrl);
553
558
  const authenticated = await exports.sdk.isAuthenticated();
@@ -557,9 +562,26 @@ async function login(command) {
557
562
  serializeConnectionInfo(command.accessKey, /*preserveAccessKeyOnLogout*/ true, serverUrl);
558
563
  return;
559
564
  }
565
+ if (connectionInfo && !command.nonInteractive) {
566
+ // A credential the server no longer accepts must not block signing in
567
+ // again: every other command already clears it on a 401.
568
+ const stillValid = await isStoredCredentialUsable(serverUrl);
569
+ if (stillValid) {
570
+ throw new Error("You are already logged in from this machine.");
571
+ }
572
+ (0, exports.log)(chalk.yellow("The stored session is no longer valid. Signing in again."));
573
+ deleteConnectionInfoCache(/*printMessage*/ false);
574
+ connectionInfo = null;
575
+ exports.sdk = null;
576
+ }
560
577
  if (command.nonInteractive) {
561
578
  throw new Error("Interactive login is unavailable in non-interactive mode. Re-run with --accessKey <key>.");
562
579
  }
580
+ if (!command.password) {
581
+ await browserLogin(command, serverUrl);
582
+ return;
583
+ }
584
+ (0, exports.log)(chalk.yellow("Password sign-in is deprecated and does not work on accounts with multi-factor authentication. Run 'aether login' to sign in through your browser."));
563
585
  const { email, password } = await promptForLoginCredentials();
564
586
  if (!email) {
565
587
  throw new Error("Email is required.");
@@ -583,6 +605,10 @@ async function login(command) {
583
605
  if (!res.ok) {
584
606
  throw new Error(body.error || body.message || `Login failed (HTTP ${res.status}).`);
585
607
  }
608
+ if (body.mfaRequired) {
609
+ throw new Error("This account has multi-factor authentication enabled, which password sign-in cannot complete. " +
610
+ "Run 'aether login' without --password to authorize this machine through your browser.");
611
+ }
586
612
  const accessKey = body.accessKey;
587
613
  if (!accessKey) {
588
614
  throw new Error("Server returned an empty access key.");
@@ -591,10 +617,66 @@ async function login(command) {
591
617
  serializeConnectionInfo(accessKey, /*preserveAccessKeyOnLogout*/ false, serverUrl);
592
618
  (0, exports.log)(chalk.green(`Successfully logged in as ${email}.`));
593
619
  }
594
- function logout(command) {
620
+ async function browserLogin(command, serverUrl) {
621
+ const deviceId = keyStore.readOrCreateDeviceId(pkce_1.generateDeviceId);
622
+ const result = await (0, browser_login_1.runBrowserLogin)({
623
+ serverUrl,
624
+ deviceId,
625
+ deviceName: os.hostname(),
626
+ clientVersion: packageJson.version,
627
+ clientPlatform: `${process.platform}-${process.arch}`,
628
+ headers: CLI_HEADERS,
629
+ forceDeviceFlow: !!command.device,
630
+ log: (message) => (0, exports.log)(message),
631
+ });
632
+ exports.sdk = getSdk(result.accessKey, CLI_HEADERS, serverUrl);
633
+ serializeConnectionInfo(result.accessKey,
634
+ /*preserveAccessKeyOnLogout*/ false, command.serverUrl || connectionInfo?.customServerUrl, {
635
+ credentialId: result.credentialId,
636
+ deviceId,
637
+ accountEmail: result.email,
638
+ });
639
+ (0, exports.log)(chalk.green(`Signed in as ${result.email || "your Aether account"}.`));
640
+ (0, exports.log)("This device is now authorized.");
641
+ }
642
+ async function isStoredCredentialUsable(serverUrl) {
643
+ try {
644
+ const probe = getSdk(connectionInfo.accessKey, CLI_HEADERS, resolveServerUrl(connectionInfo.customServerUrl) || serverUrl);
645
+ return await probe.isAuthenticated();
646
+ }
647
+ catch {
648
+ // Offline or unreachable: assume the stored session is still good rather
649
+ // than discarding a working credential because the network is down.
650
+ return true;
651
+ }
652
+ }
653
+ async function logout(command) {
654
+ const current = connectionInfo;
655
+ if (current && current.credentialId) {
656
+ try {
657
+ await revokeCurrentDevice(current);
658
+ (0, exports.log)("This device is no longer authorized on the server.");
659
+ }
660
+ catch (err) {
661
+ (0, exports.log)(chalk.yellow(`Could not revoke this device on the server (${err?.message || "unknown error"}). ` +
662
+ "It can still be revoked from the dashboard under Account > CLI & Devices."));
663
+ }
664
+ }
595
665
  exports.sdk = null;
596
666
  deleteConnectionInfoCache();
597
- return Promise.resolve();
667
+ }
668
+ async function revokeCurrentDevice(current) {
669
+ const serverUrl = resolveServerUrl(current.customServerUrl).replace(/\/$/, "");
670
+ const response = await fetch(`${serverUrl}/v1/cli/devices/current`, {
671
+ method: "DELETE",
672
+ headers: { Accept: "application/json", Authorization: `Bearer ${current.accessKey}`, ...CLI_HEADERS },
673
+ });
674
+ // 401 means the credential is already dead; 404 means this server has no such
675
+ // endpoint. Neither is worth alarming the user about on the way out.
676
+ if (response.status === 204 || response.status === 404 || response.status === 401) {
677
+ return;
678
+ }
679
+ throw new Error(`HTTP ${response.status}`);
598
680
  }
599
681
  function formatDate(unixOffset) {
600
682
  const date = moment(unixOffset);
@@ -1375,18 +1457,16 @@ const runReactNativeBundleCommand = (bundleName, development, entryFile, outputF
1375
1457
  });
1376
1458
  };
1377
1459
  exports.runReactNativeBundleCommand = runReactNativeBundleCommand;
1378
- function serializeConnectionInfo(accessKey, preserveAccessKeyOnLogout, customServerUrl) {
1379
- const connectionInfo = {
1380
- accessKey: accessKey,
1381
- preserveAccessKeyOnLogout: preserveAccessKeyOnLogout,
1382
- };
1383
- if (customServerUrl) {
1384
- connectionInfo.customServerUrl = customServerUrl;
1385
- }
1386
- fs.mkdirSync(path.dirname(configFilePath), { recursive: true });
1387
- const json = JSON.stringify(connectionInfo);
1388
- fs.writeFileSync(configFilePath, json, { encoding: "utf8" });
1389
- (0, exports.log)(`Session file written to ${chalk.cyan(configFilePath)}. Run ${chalk.cyan("aether logout")} to terminate the session.`);
1460
+ function serializeConnectionInfo(accessKey, preserveAccessKeyOnLogout, customServerUrl, extra) {
1461
+ keyStore.writeCredential({
1462
+ accessKey,
1463
+ preserveAccessKeyOnLogout,
1464
+ serverUrl: customServerUrl,
1465
+ credentialId: extra?.credentialId,
1466
+ deviceId: extra?.deviceId,
1467
+ accountEmail: extra?.accountEmail,
1468
+ });
1469
+ (0, exports.log)(`Credentials written to ${chalk.cyan(keyStore.getCredentialPath())}. Run ${chalk.cyan("aether logout")} to terminate the session.`);
1390
1470
  }
1391
1471
  function sessionList(command) {
1392
1472
  throwForInvalidOutputFormat(command.format);
@@ -1444,6 +1524,14 @@ function whoami(command) {
1444
1524
  function isCommandOptionSpecified(option) {
1445
1525
  return option !== undefined && option !== null;
1446
1526
  }
1527
+ /**
1528
+ * Only an explicit --serverUrl is persisted, so an absent one means the default
1529
+ * rather than nothing. Falling through to the SDK's own default would point the
1530
+ * CLI at localhost.
1531
+ */
1532
+ function resolveServerUrl(storedServerUrl) {
1533
+ return storedServerUrl || DEFAULT_AETHER_SERVER_URL;
1534
+ }
1447
1535
  function getSdk(accessKey, headers, customServerUrl) {
1448
1536
  const sdk = new AccountManager(accessKey, CLI_HEADERS, customServerUrl);
1449
1537
  /*