@aetherpush/cli 0.4.2 → 0.5.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.
@@ -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"}
package/bin/script/cli.js CHANGED
File without changes
@@ -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.");
@@ -584,8 +606,8 @@ async function login(command) {
584
606
  throw new Error(body.error || body.message || `Login failed (HTTP ${res.status}).`);
585
607
  }
586
608
  if (body.mfaRequired) {
587
- throw new Error("This account has multi-factor authentication enabled, which the CLI cannot complete. " +
588
- "Create an access key in the dashboard (Account > Access Keys) and log in with: aether login --accessKey <key>");
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.");
589
611
  }
590
612
  const accessKey = body.accessKey;
591
613
  if (!accessKey) {
@@ -595,10 +617,66 @@ async function login(command) {
595
617
  serializeConnectionInfo(accessKey, /*preserveAccessKeyOnLogout*/ false, serverUrl);
596
618
  (0, exports.log)(chalk.green(`Successfully logged in as ${email}.`));
597
619
  }
598
- 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
+ }
599
665
  exports.sdk = null;
600
666
  deleteConnectionInfoCache();
601
- 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}`);
602
680
  }
603
681
  function formatDate(unixOffset) {
604
682
  const date = moment(unixOffset);
@@ -1379,18 +1457,16 @@ const runReactNativeBundleCommand = (bundleName, development, entryFile, outputF
1379
1457
  });
1380
1458
  };
1381
1459
  exports.runReactNativeBundleCommand = runReactNativeBundleCommand;
1382
- function serializeConnectionInfo(accessKey, preserveAccessKeyOnLogout, customServerUrl) {
1383
- const connectionInfo = {
1384
- accessKey: accessKey,
1385
- preserveAccessKeyOnLogout: preserveAccessKeyOnLogout,
1386
- };
1387
- if (customServerUrl) {
1388
- connectionInfo.customServerUrl = customServerUrl;
1389
- }
1390
- fs.mkdirSync(path.dirname(configFilePath), { recursive: true });
1391
- const json = JSON.stringify(connectionInfo);
1392
- fs.writeFileSync(configFilePath, json, { encoding: "utf8" });
1393
- (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.`);
1394
1470
  }
1395
1471
  function sessionList(command) {
1396
1472
  throwForInvalidOutputFormat(command.format);
@@ -1448,6 +1524,14 @@ function whoami(command) {
1448
1524
  function isCommandOptionSpecified(option) {
1449
1525
  return option !== undefined && option !== null;
1450
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
+ }
1451
1535
  function getSdk(accessKey, headers, customServerUrl) {
1452
1536
  const sdk = new AccountManager(accessKey, CLI_HEADERS, customServerUrl);
1453
1537
  /*