@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.
- package/README.md +22 -6
- package/bin/script/auth/browser-launcher.d.ts +9 -0
- package/bin/script/auth/browser-launcher.js +58 -0
- package/bin/script/auth/browser-launcher.js.map +1 -0
- package/bin/script/auth/browser-login.d.ts +32 -0
- package/bin/script/auth/browser-login.js +215 -0
- package/bin/script/auth/browser-login.js.map +1 -0
- package/bin/script/auth/key-store.d.ts +24 -0
- package/bin/script/auth/key-store.js +193 -0
- package/bin/script/auth/key-store.js.map +1 -0
- package/bin/script/auth/loopback-listener.d.ts +19 -0
- package/bin/script/auth/loopback-listener.js +111 -0
- package/bin/script/auth/loopback-listener.js.map +1 -0
- package/bin/script/auth/pkce.d.ts +12 -0
- package/bin/script/auth/pkce.js +18 -0
- package/bin/script/auth/pkce.js.map +1 -0
- package/bin/script/cli.js +0 -0
- package/bin/script/command-executor.js +122 -38
- package/bin/script/command-executor.js.map +1 -1
- package/bin/script/command-parser.js +17 -2
- package/bin/script/command-parser.js.map +1 -1
- package/bin/script/types/cli.d.ts +2 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ Requires Node.js 22 or later.
|
|
|
24
24
|
|
|
25
25
|
## Quick start
|
|
26
26
|
|
|
27
|
-
1. Create an account and
|
|
27
|
+
1. Create an account and sign in. `register` asks for your email, name, and password, then sends a verification email; click the link before signing in. `login` opens your browser, where you finish signing in — including any second factor — and authorize this machine.
|
|
28
28
|
|
|
29
29
|
```sh
|
|
30
30
|
aether register
|
|
@@ -86,22 +86,38 @@ If no compiler is found, the command fails and prints the paths it tried.
|
|
|
86
86
|
|
|
87
87
|
Known limitation: on Windows, some versions of the `hermes-compiler` package ship without a `win64-bin` binary. If a Hermes release fails on Windows with that path list, run it from macOS or Linux (for example in CI) instead.
|
|
88
88
|
|
|
89
|
-
##
|
|
89
|
+
## Signing in
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
`aether login` opens your browser, you approve the device there, and the CLI receives its own credential. The browser handles passwords, passkeys, authenticator codes and recovery codes, so the terminal never needs to.
|
|
92
92
|
|
|
93
|
-
|
|
93
|
+
On a machine with no browser — over SSH, in a container, on a locked-down host — ask for a code instead. The CLI prints a short code and a URL to open anywhere else:
|
|
94
94
|
|
|
95
95
|
```sh
|
|
96
|
-
aether login --
|
|
96
|
+
aether login --device
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
The CLI falls back to this automatically when it cannot open a browser or bind a local callback port.
|
|
100
|
+
|
|
101
|
+
For CI and other machines, keep passing a key:
|
|
100
102
|
|
|
101
103
|
```sh
|
|
102
104
|
aether login --accessKey <your-api-key>
|
|
103
105
|
```
|
|
104
106
|
|
|
107
|
+
`aether logout` revokes the device on the server and then clears the local credential. If the server cannot be reached, the local credential is cleared anyway and the device can still be revoked from the dashboard under Account > CLI & Devices.
|
|
108
|
+
|
|
109
|
+
`--password` still signs in with email and password, but it is deprecated and cannot complete sign-in on accounts with multi-factor authentication.
|
|
110
|
+
|
|
111
|
+
## Configuration
|
|
112
|
+
|
|
113
|
+
The credential lives in `~/.aether/credentials.json`, readable only by you. Everything else — server URL, device id, account email — is in `~/.aether/cli.json`. Both are created on `aether login`; the credential file is deleted on `aether logout`. A `~/.aether/config.json` left by an earlier version is migrated automatically the first time you run this one.
|
|
114
|
+
|
|
115
|
+
To target a non-default server (e.g. local development):
|
|
116
|
+
|
|
117
|
+
```sh
|
|
118
|
+
aether login --serverUrl http://localhost:3000
|
|
119
|
+
```
|
|
120
|
+
|
|
105
121
|
## CI/CD
|
|
106
122
|
|
|
107
123
|
The CLI detects CI environments through the `CI` variable and runs non-interactively there; `--ci` forces it. Ready-made pipeline templates for GitLab CI, CircleCI, and Jenkins live in [`examples/ci/`](./examples/ci/), and GitHub Actions has a dedicated action, [`aetherpush-deploy-action`](https://github.com/Monoradioactivo/aetherpush-deploy-action). The [CI/CD guides](https://docs.aetherpush.com/ci-cd/) walk through each platform.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opens a URL in whatever the platform considers the default browser.
|
|
3
|
+
*
|
|
4
|
+
* A zero exit code from the helper does not prove a browser actually appeared:
|
|
5
|
+
* `xdg-open` succeeds in containers with no display, and a proxy can swallow a
|
|
6
|
+
* loopback address afterwards. Callers therefore print the URL as well and keep
|
|
7
|
+
* a way out that does not depend on this returning the truth.
|
|
8
|
+
*/
|
|
9
|
+
export declare function openBrowser(url: string): boolean;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.openBrowser = openBrowser;
|
|
4
|
+
const childProcess = require("child_process");
|
|
5
|
+
/**
|
|
6
|
+
* Opens a URL in whatever the platform considers the default browser.
|
|
7
|
+
*
|
|
8
|
+
* A zero exit code from the helper does not prove a browser actually appeared:
|
|
9
|
+
* `xdg-open` succeeds in containers with no display, and a proxy can swallow a
|
|
10
|
+
* loopback address afterwards. Callers therefore print the URL as well and keep
|
|
11
|
+
* a way out that does not depend on this returning the truth.
|
|
12
|
+
*/
|
|
13
|
+
function openBrowser(url) {
|
|
14
|
+
// The URL came from the server, and `--serverUrl` means the server is not
|
|
15
|
+
// always ours. Anything but an http(s) address is refused before it reaches
|
|
16
|
+
// the shell, and the openers are invoked in forms that cannot read it as a
|
|
17
|
+
// flag or as command text.
|
|
18
|
+
let parsed;
|
|
19
|
+
try {
|
|
20
|
+
parsed = new URL(url);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
const { command, args } = resolveOpenCommand(parsed.toString());
|
|
29
|
+
try {
|
|
30
|
+
const child = childProcess.spawn(command, args, { stdio: "ignore", detached: true });
|
|
31
|
+
if (!child || typeof child.unref !== "function") {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
// spawn reports a missing helper asynchronously. Without a listener that
|
|
35
|
+
// becomes an unhandled 'error' event, which takes the whole process down on
|
|
36
|
+
// exactly the headless machines the device flow exists for.
|
|
37
|
+
if (typeof child.on === "function") {
|
|
38
|
+
child.on("error", () => undefined);
|
|
39
|
+
}
|
|
40
|
+
child.unref();
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function resolveOpenCommand(url) {
|
|
48
|
+
if (process.platform === "darwin") {
|
|
49
|
+
return { command: "open", args: ["--", url] };
|
|
50
|
+
}
|
|
51
|
+
if (process.platform === "win32") {
|
|
52
|
+
// Not `cmd /c start`: cmd re-parses its arguments, and Node's Windows
|
|
53
|
+
// quoting does not escape a double quote the way cmd reads one.
|
|
54
|
+
return { command: "rundll32", args: ["url.dll,FileProtocolHandler", url] };
|
|
55
|
+
}
|
|
56
|
+
return { command: "xdg-open", args: ["--", url] };
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=browser-launcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-launcher.js","sourceRoot":"","sources":["../../../script/auth/browser-launcher.ts"],"names":[],"mappings":";;AAUA,kCAiCC;AA3CD,8CAA8C;AAE9C;;;;;;;GAOG;AACH,SAAgB,WAAW,CAAC,GAAW;IACrC,0EAA0E;IAC1E,4EAA4E;IAC5E,2EAA2E;IAC3E,2BAA2B;IAC3B,IAAI,MAAW,CAAC;IAChB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;IACxB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,MAAM,CAAC,QAAQ,KAAK,OAAO,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAChE,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,kBAAkB,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;IAEhE,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,YAAY,CAAC,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;QACrF,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,CAAC,KAAK,KAAK,UAAU,EAAE,CAAC;YAChD,OAAO,KAAK,CAAC;QACf,CAAC;QACD,yEAAyE;QACzE,4EAA4E;QAC5E,4DAA4D;QAC5D,IAAI,OAAO,KAAK,CAAC,EAAE,KAAK,UAAU,EAAE,CAAC;YACnC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QACrC,CAAC;QACD,KAAK,CAAC,KAAK,EAAE,CAAC;QACd,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,SAAS,kBAAkB,CAAC,GAAW;IACrC,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAClC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC;IAChD,CAAC;IACD,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QACjC,sEAAsE;QACtE,gEAAgE;QAChE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,6BAA6B,EAAE,GAAG,CAAC,EAAE,CAAC;IAC7E,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC;AACpD,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { LoopbackListener } from "./loopback-listener";
|
|
2
|
+
export interface BrowserLoginResult {
|
|
3
|
+
accessKey: string;
|
|
4
|
+
expires: number;
|
|
5
|
+
credentialId?: string;
|
|
6
|
+
email?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface BrowserLoginOptions {
|
|
9
|
+
serverUrl: string;
|
|
10
|
+
deviceId: string;
|
|
11
|
+
deviceName: string;
|
|
12
|
+
clientVersion: string;
|
|
13
|
+
clientPlatform: string;
|
|
14
|
+
headers: Record<string, string>;
|
|
15
|
+
log: (message: string) => void;
|
|
16
|
+
/** Skip the loopback attempt and go straight to the printed code. */
|
|
17
|
+
forceDeviceFlow?: boolean;
|
|
18
|
+
openBrowser?: (url: string) => boolean;
|
|
19
|
+
startLoopbackListener?: () => Promise<LoopbackListener>;
|
|
20
|
+
sleep?: (ms: number) => Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
/** Raised when the server predates browser login, so the CLI can say so plainly. */
|
|
23
|
+
export declare class UnsupportedServerError extends Error {
|
|
24
|
+
constructor(serverUrl: string);
|
|
25
|
+
}
|
|
26
|
+
export declare class AuthorizationDeniedError extends Error {
|
|
27
|
+
constructor();
|
|
28
|
+
}
|
|
29
|
+
export declare class AuthorizationExpiredError extends Error {
|
|
30
|
+
constructor();
|
|
31
|
+
}
|
|
32
|
+
export declare function runBrowserLogin(options: BrowserLoginOptions): Promise<BrowserLoginResult>;
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AuthorizationExpiredError = exports.AuthorizationDeniedError = exports.UnsupportedServerError = void 0;
|
|
4
|
+
exports.runBrowserLogin = runBrowserLogin;
|
|
5
|
+
const pkce_1 = require("./pkce");
|
|
6
|
+
const browser_launcher_1 = require("./browser-launcher");
|
|
7
|
+
const loopback_listener_1 = require("./loopback-listener");
|
|
8
|
+
/** Raised when the server predates browser login, so the CLI can say so plainly. */
|
|
9
|
+
class UnsupportedServerError extends Error {
|
|
10
|
+
constructor(serverUrl) {
|
|
11
|
+
super(`${serverUrl} does not support browser sign-in. Update the Aether server, ` +
|
|
12
|
+
`or sign in with a key: aether login --accessKey <key>`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
exports.UnsupportedServerError = UnsupportedServerError;
|
|
16
|
+
class AuthorizationDeniedError extends Error {
|
|
17
|
+
constructor() {
|
|
18
|
+
super("Authorization was denied in the browser.");
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
exports.AuthorizationDeniedError = AuthorizationDeniedError;
|
|
22
|
+
class AuthorizationExpiredError extends Error {
|
|
23
|
+
constructor() {
|
|
24
|
+
super("The authorization request expired. Run 'aether login' again.");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
exports.AuthorizationExpiredError = AuthorizationExpiredError;
|
|
28
|
+
const LOOPBACK_TIMEOUT_MS = 10 * 60 * 1000;
|
|
29
|
+
const SLOW_DOWN_STEP_SECONDS = 5;
|
|
30
|
+
async function post(serverUrl, path, body, headers) {
|
|
31
|
+
let response;
|
|
32
|
+
try {
|
|
33
|
+
response = await fetch(`${serverUrl.replace(/\/$/, "")}${path}`, {
|
|
34
|
+
method: "POST",
|
|
35
|
+
headers: { "Content-Type": "application/json", Accept: "application/json", ...headers },
|
|
36
|
+
body: JSON.stringify(body),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
throw new Error(`Unable to reach Aether at ${serverUrl}. Are you offline, or behind a firewall or proxy?`);
|
|
41
|
+
}
|
|
42
|
+
const parsed = await response.json().catch(() => ({}));
|
|
43
|
+
return { status: response.status, body: parsed };
|
|
44
|
+
}
|
|
45
|
+
function defaultSleep(ms) {
|
|
46
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
47
|
+
}
|
|
48
|
+
async function runBrowserLogin(options) {
|
|
49
|
+
if (options.forceDeviceFlow) {
|
|
50
|
+
return runDeviceFlow(options);
|
|
51
|
+
}
|
|
52
|
+
const startListener = options.startLoopbackListener || loopback_listener_1.startLoopbackListener;
|
|
53
|
+
let listener;
|
|
54
|
+
try {
|
|
55
|
+
listener = await startListener();
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
options.log("Could not open a local callback port; switching to code entry.");
|
|
59
|
+
return runDeviceFlow(options);
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
return await runLoopbackFlow(options, listener);
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
listener.close();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function runLoopbackFlow(options, listener) {
|
|
69
|
+
const { verifier, challenge } = (0, pkce_1.generatePkcePair)();
|
|
70
|
+
const state = (0, pkce_1.generateState)();
|
|
71
|
+
const started = await post(options.serverUrl, "/v1/auth/cli/authorizations", {
|
|
72
|
+
codeChallenge: challenge,
|
|
73
|
+
codeChallengeMethod: "S256",
|
|
74
|
+
redirectUri: listener.redirectUri,
|
|
75
|
+
state,
|
|
76
|
+
deviceId: options.deviceId,
|
|
77
|
+
deviceName: options.deviceName,
|
|
78
|
+
clientName: "aether-cli",
|
|
79
|
+
clientVersion: options.clientVersion,
|
|
80
|
+
clientPlatform: options.clientPlatform,
|
|
81
|
+
}, options.headers);
|
|
82
|
+
if (started.status === 404) {
|
|
83
|
+
throw new UnsupportedServerError(options.serverUrl);
|
|
84
|
+
}
|
|
85
|
+
if (started.status !== 201) {
|
|
86
|
+
throw new Error(started.body?.error || `Could not start browser sign-in (HTTP ${started.status}).`);
|
|
87
|
+
}
|
|
88
|
+
const authorizeUrl = started.body.authorizeUrl;
|
|
89
|
+
if (typeof authorizeUrl !== "string" || authorizeUrl.length === 0) {
|
|
90
|
+
throw new Error("The server did not return an address to authorize at.");
|
|
91
|
+
}
|
|
92
|
+
options.log("Opening Aether in your browser...");
|
|
93
|
+
if (!(options.openBrowser || browser_launcher_1.openBrowser)(authorizeUrl)) {
|
|
94
|
+
// The docs promise this fallback, and a machine with no opener is exactly
|
|
95
|
+
// the machine that needs it.
|
|
96
|
+
options.log("Could not open a browser on this machine; switching to code entry.");
|
|
97
|
+
return runDeviceFlow(options);
|
|
98
|
+
}
|
|
99
|
+
options.log(`If nothing opens, visit:\n${authorizeUrl}`);
|
|
100
|
+
options.log("Waiting for authorization...");
|
|
101
|
+
const callback = await listener.waitForCallback(LOOPBACK_TIMEOUT_MS);
|
|
102
|
+
// State is checked first, including on the failure path: a response carrying
|
|
103
|
+
// someone else's outcome is not worth acting on either way.
|
|
104
|
+
if (callback.state !== state) {
|
|
105
|
+
throw new Error("The browser returned an authorization response that does not match this sign-in. Nothing was exchanged.");
|
|
106
|
+
}
|
|
107
|
+
if (callback.error === "access_denied") {
|
|
108
|
+
throw new AuthorizationDeniedError();
|
|
109
|
+
}
|
|
110
|
+
if (callback.error) {
|
|
111
|
+
throw new Error(`The browser reported ${callback.error}. Run 'aether login' again.`);
|
|
112
|
+
}
|
|
113
|
+
if (!callback.code) {
|
|
114
|
+
throw new Error("The browser did not return an authorization code.");
|
|
115
|
+
}
|
|
116
|
+
const exchanged = await post(options.serverUrl, "/v1/auth/cli/token", {
|
|
117
|
+
grantType: "authorization_code",
|
|
118
|
+
code: callback.code,
|
|
119
|
+
codeVerifier: verifier,
|
|
120
|
+
redirectUri: listener.redirectUri,
|
|
121
|
+
}, options.headers);
|
|
122
|
+
if (exchanged.status !== 200) {
|
|
123
|
+
throw new Error(describeTokenError(exchanged.body));
|
|
124
|
+
}
|
|
125
|
+
return toResult(exchanged.body);
|
|
126
|
+
}
|
|
127
|
+
async function runDeviceFlow(options) {
|
|
128
|
+
const sleep = options.sleep || defaultSleep;
|
|
129
|
+
const started = await post(options.serverUrl, "/v1/auth/cli/device", {
|
|
130
|
+
deviceId: options.deviceId,
|
|
131
|
+
deviceName: options.deviceName,
|
|
132
|
+
clientName: "aether-cli",
|
|
133
|
+
clientVersion: options.clientVersion,
|
|
134
|
+
clientPlatform: options.clientPlatform,
|
|
135
|
+
}, options.headers);
|
|
136
|
+
if (started.status === 404) {
|
|
137
|
+
throw new UnsupportedServerError(options.serverUrl);
|
|
138
|
+
}
|
|
139
|
+
if (started.status !== 201) {
|
|
140
|
+
throw new Error(started.body?.error || `Could not start device sign-in (HTTP ${started.status}).`);
|
|
141
|
+
}
|
|
142
|
+
const deviceCode = started.body.device_code;
|
|
143
|
+
const userCode = started.body.user_code;
|
|
144
|
+
const verificationUri = started.body.verification_uri;
|
|
145
|
+
if (!deviceCode || !userCode || !verificationUri) {
|
|
146
|
+
throw new Error("The server did not return a usable device authorization.");
|
|
147
|
+
}
|
|
148
|
+
const expiresInSeconds = typeof started.body.expires_in === "number" ? started.body.expires_in : 600;
|
|
149
|
+
let intervalSeconds = typeof started.body.interval === "number" && started.body.interval > 0 ? started.body.interval : 5;
|
|
150
|
+
options.log(`Open:\n${verificationUri}\n\nEnter code:\n${userCode}`);
|
|
151
|
+
const completeUri = started.body.verification_uri_complete;
|
|
152
|
+
(options.openBrowser || browser_launcher_1.openBrowser)(typeof completeUri === "string" && completeUri.length > 0 ? completeUri : verificationUri);
|
|
153
|
+
options.log("Waiting for authorization...");
|
|
154
|
+
const deadline = Date.now() + expiresInSeconds * 1000;
|
|
155
|
+
while (Date.now() < deadline) {
|
|
156
|
+
await sleep(intervalSeconds * 1000);
|
|
157
|
+
let polled;
|
|
158
|
+
try {
|
|
159
|
+
polled = await post(options.serverUrl, "/v1/auth/cli/token", { grantType: "device_code", deviceCode }, options.headers);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
// A dropped connection or a server restart mid-ceremony is not a reason
|
|
163
|
+
// to abandon a request the user may already have approved. Back off and
|
|
164
|
+
// keep asking until the request itself expires.
|
|
165
|
+
intervalSeconds += SLOW_DOWN_STEP_SECONDS;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (polled.status === 200) {
|
|
169
|
+
return toResult(polled.body);
|
|
170
|
+
}
|
|
171
|
+
// Throttling and server faults are transient by definition, and neither
|
|
172
|
+
// carries a device-flow code to branch on.
|
|
173
|
+
if (polled.status === 429 || polled.status >= 500) {
|
|
174
|
+
intervalSeconds += SLOW_DOWN_STEP_SECONDS;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const error = polled.body?.error;
|
|
178
|
+
if (error === "authorization_pending") {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (error === "slow_down") {
|
|
182
|
+
intervalSeconds += SLOW_DOWN_STEP_SECONDS;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (error === "access_denied") {
|
|
186
|
+
throw new AuthorizationDeniedError();
|
|
187
|
+
}
|
|
188
|
+
if (error === "expired_token") {
|
|
189
|
+
throw new AuthorizationExpiredError();
|
|
190
|
+
}
|
|
191
|
+
throw new Error(describeTokenError(polled.body));
|
|
192
|
+
}
|
|
193
|
+
throw new AuthorizationExpiredError();
|
|
194
|
+
}
|
|
195
|
+
function describeTokenError(body) {
|
|
196
|
+
if (body?.error_description) {
|
|
197
|
+
return body.error_description;
|
|
198
|
+
}
|
|
199
|
+
if (body?.error) {
|
|
200
|
+
return `Sign-in failed: ${body.error}`;
|
|
201
|
+
}
|
|
202
|
+
return "Sign-in failed.";
|
|
203
|
+
}
|
|
204
|
+
function toResult(body) {
|
|
205
|
+
if (!body?.accessKey) {
|
|
206
|
+
throw new Error("The server did not return a credential.");
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
accessKey: body.accessKey,
|
|
210
|
+
expires: body.expires,
|
|
211
|
+
credentialId: body.credentialId,
|
|
212
|
+
email: body.account?.email,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
//# sourceMappingURL=browser-login.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-login.js","sourceRoot":"","sources":["../../../script/auth/browser-login.ts"],"names":[],"mappings":";;;AA4EA,0CAoBC;AAhGD,iCAAyD;AACzD,yDAAuE;AACvE,2DAA8G;AAwB9G,oFAAoF;AACpF,MAAa,sBAAuB,SAAQ,KAAK;IAC/C,YAAY,SAAiB;QAC3B,KAAK,CACH,GAAG,SAAS,+DAA+D;YACzE,uDAAuD,CAC1D,CAAC;IACJ,CAAC;CACF;AAPD,wDAOC;AAED,MAAa,wBAAyB,SAAQ,KAAK;IACjD;QACE,KAAK,CAAC,0CAA0C,CAAC,CAAC;IACpD,CAAC;CACF;AAJD,4DAIC;AAED,MAAa,yBAA0B,SAAQ,KAAK;IAClD;QACE,KAAK,CAAC,8DAA8D,CAAC,CAAC;IACxE,CAAC;CACF;AAJD,8DAIC;AAED,MAAM,mBAAmB,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAC3C,MAAM,sBAAsB,GAAG,CAAC,CAAC;AAOjC,KAAK,UAAU,IAAI,CAAC,SAAiB,EAAE,IAAY,EAAE,IAAa,EAAE,OAA+B;IACjG,IAAI,QAAkB,CAAC;IACvB,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,SAAS,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,EAAE;YAC/D,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,EAAE,kBAAkB,EAAE,GAAG,OAAO,EAAE;YACvF,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;SAC3B,CAAC,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,6BAA6B,SAAS,mDAAmD,CAAC,CAAC;IAC7G,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACvD,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AACnD,CAAC;AAED,SAAS,YAAY,CAAC,EAAU;IAC9B,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AAC3D,CAAC;AAEM,KAAK,UAAU,eAAe,CAAC,OAA4B;IAChE,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC;QAC5B,OAAO,aAAa,CAAC,OAAO,CAAC,CAAC;IAChC,CAAC;IAED,MAAM,aAAa,GAAG,OAAO,CAAC,qBAAqB,IAAI,yCAA4B,CAAC;IAEpF,IAAI,QAA0B,CAAC;IAC/B,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,aAAa,EAAE,CAAC;IACnC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,GAAG,CAAC,gEAAgE,CAAC,CAAC;QAC9E,OAAO,aAAa,CAAC,OAAO,CAAC,CAAC;IAChC,CAAC;IAED,IAAI,CAAC;QACH,OAAO,MAAM,eAAe,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IAClD,CAAC;YAAS,CAAC;QACT,QAAQ,CAAC,KAAK,EAAE,CAAC;IACnB,CAAC;AACH,CAAC;AAED,KAAK,UAAU,eAAe,CAAC,OAA4B,EAAE,QAA0B;IACrF,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,IAAA,uBAAgB,GAAE,CAAC;IACnD,MAAM,KAAK,GAAG,IAAA,oBAAa,GAAE,CAAC;IAE9B,MAAM,OAAO,GAAG,MAAM,IAAI,CACxB,OAAO,CAAC,SAAS,EACjB,6BAA6B,EAC7B;QACE,aAAa,EAAE,SAAS;QACxB,mBAAmB,EAAE,MAAM;QAC3B,WAAW,EAAE,QAAQ,CAAC,WAAW;QACjC,KAAK;QACL,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,UAAU,EAAE,OAAO,CAAC,UAAU;QAC9B,UAAU,EAAE,YAAY;QACxB,aAAa,EAAE,OAAO,CAAC,aAAa;QACpC,cAAc,EAAE,OAAO,CAAC,cAAc;KACvC,EACD,OAAO,CAAC,OAAO,CAChB,CAAC;IAEF,IAAI,OAAO,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC3B,MAAM,IAAI,sBAAsB,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IACtD,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,IAAI,yCAAyC,OAAO,CAAC,MAAM,IAAI,CAAC,CAAC;IACtG,CAAC;IAED,MAAM,YAAY,GAAW,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC;IACvD,IAAI,OAAO,YAAY,KAAK,QAAQ,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAClE,MAAM,IAAI,KAAK,CAAC,uDAAuD,CAAC,CAAC;IAC3E,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,mCAAmC,CAAC,CAAC;IACjD,IAAI,CAAC,CAAC,OAAO,CAAC,WAAW,IAAI,8BAAkB,CAAC,CAAC,YAAY,CAAC,EAAE,CAAC;QAC/D,0EAA0E;QAC1E,6BAA6B;QAC7B,OAAO,CAAC,GAAG,CAAC,oEAAoE,CAAC,CAAC;QAClF,OAAO,aAAa,CAAC,OAAO,CAAC,CAAC;IAChC,CAAC;IACD,OAAO,CAAC,GAAG,CAAC,6BAA6B,YAAY,EAAE,CAAC,CAAC;IACzD,OAAO,CAAC,GAAG,CAAC,8BAA8B,CAAC,CAAC;IAE5C,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,eAAe,CAAC,mBAAmB,CAAC,CAAC;IAErE,6EAA6E;IAC7E,4DAA4D;IAC5D,IAAI,QAAQ,CAAC,KAAK,KAAK,KAAK,EAAE,CAAC;QAC7B,MAAM,IAAI,KAAK,CAAC,yGAAyG,CAAC,CAAC;IAC7H,CAAC;IACD,IAAI,QAAQ,CAAC,KAAK,KAAK,eAAe,EAAE,CAAC;QACvC,MAAM,IAAI,wBAAwB,EAAE,CAAC;IACvC,CAAC;IACD,IAAI,QAAQ,CAAC,KAAK,EAAE,CAAC;QACnB,MAAM,IAAI,KAAK,CAAC,wBAAwB,QAAQ,CAAC,KAAK,6BAA6B,CAAC,CAAC;IACvF,CAAC;IACD,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;QACnB,MAAM,IAAI,KAAK,CAAC,mDAAmD,CAAC,CAAC;IACvE,CAAC;IAED,MAAM,SAAS,GAAG,MAAM,IAAI,CAC1B,OAAO,CAAC,SAAS,EACjB,oBAAoB,EACpB;QACE,SAAS,EAAE,oBAAoB;QAC/B,IAAI,EAAE,QAAQ,CAAC,IAAI;QACnB,YAAY,EAAE,QAAQ;QACtB,WAAW,EAAE,QAAQ,CAAC,WAAW;KAClC,EACD,OAAO,CAAC,OAAO,CAChB,CAAC;IAEF,IAAI,SAAS,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC7B,MAAM,IAAI,KAAK,CAAC,kBAAkB,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;IACtD,CAAC;IAED,OAAO,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;AAClC,CAAC;AAED,KAAK,UAAU,aAAa,CAAC,OAA4B;IACvD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,YAAY,CAAC;IAE5C,MAAM,OAAO,GAAG,MAAM,IAAI,CACxB,OAAO,CAAC,SAAS,EACjB,qBAAqB,EACrB;QACE,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,UAAU,EAAE,OAAO,CAAC,UAAU;QAC9B,UAAU,EAAE,YAAY;QACxB,aAAa,EAAE,OAAO,CAAC,aAAa;QACpC,cAAc,EAAE,OAAO,CAAC,cAAc;KACvC,EACD,OAAO,CAAC,OAAO,CAChB,CAAC;IAEF,IAAI,OAAO,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC3B,MAAM,IAAI,sBAAsB,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IACtD,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,IAAI,wCAAwC,OAAO,CAAC,MAAM,IAAI,CAAC,CAAC;IACrG,CAAC;IAED,MAAM,UAAU,GAAW,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC;IACpD,MAAM,QAAQ,GAAW,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC;IAChD,MAAM,eAAe,GAAW,OAAO,CAAC,IAAI,CAAC,gBAAgB,CAAC;IAC9D,IAAI,CAAC,UAAU,IAAI,CAAC,QAAQ,IAAI,CAAC,eAAe,EAAE,CAAC;QACjD,MAAM,IAAI,KAAK,CAAC,0DAA0D,CAAC,CAAC;IAC9E,CAAC;IACD,MAAM,gBAAgB,GAAW,OAAO,OAAO,CAAC,IAAI,CAAC,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC;IAC7G,IAAI,eAAe,GAAW,OAAO,OAAO,CAAC,IAAI,CAAC,QAAQ,KAAK,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;IAEjI,OAAO,CAAC,GAAG,CAAC,UAAU,eAAe,oBAAoB,QAAQ,EAAE,CAAC,CAAC;IACrE,MAAM,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,yBAAyB,CAAC;IAC3D,CAAC,OAAO,CAAC,WAAW,IAAI,8BAAkB,CAAC,CACzC,OAAO,WAAW,KAAK,QAAQ,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,eAAe,CAC1F,CAAC;IACF,OAAO,CAAC,GAAG,CAAC,8BAA8B,CAAC,CAAC;IAE5C,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,gBAAgB,GAAG,IAAI,CAAC;IAEtD,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,EAAE,CAAC;QAC7B,MAAM,KAAK,CAAC,eAAe,GAAG,IAAI,CAAC,CAAC;QAEpC,IAAI,MAAmB,CAAC;QACxB,IAAI,CAAC;YACH,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,oBAAoB,EAAE,EAAE,SAAS,EAAE,aAAa,EAAE,UAAU,EAAE,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;QAC1H,CAAC;QAAC,MAAM,CAAC;YACP,wEAAwE;YACxE,wEAAwE;YACxE,gDAAgD;YAChD,eAAe,IAAI,sBAAsB,CAAC;YAC1C,SAAS;QACX,CAAC;QAED,IAAI,MAAM,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YAC1B,OAAO,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAC/B,CAAC;QAED,wEAAwE;QACxE,2CAA2C;QAC3C,IAAI,MAAM,CAAC,MAAM,KAAK,GAAG,IAAI,MAAM,CAAC,MAAM,IAAI,GAAG,EAAE,CAAC;YAClD,eAAe,IAAI,sBAAsB,CAAC;YAC1C,SAAS;QACX,CAAC;QAED,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC;QACjC,IAAI,KAAK,KAAK,uBAAuB,EAAE,CAAC;YACtC,SAAS;QACX,CAAC;QACD,IAAI,KAAK,KAAK,WAAW,EAAE,CAAC;YAC1B,eAAe,IAAI,sBAAsB,CAAC;YAC1C,SAAS;QACX,CAAC;QACD,IAAI,KAAK,KAAK,eAAe,EAAE,CAAC;YAC9B,MAAM,IAAI,wBAAwB,EAAE,CAAC;QACvC,CAAC;QACD,IAAI,KAAK,KAAK,eAAe,EAAE,CAAC;YAC9B,MAAM,IAAI,yBAAyB,EAAE,CAAC;QACxC,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,kBAAkB,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;IACnD,CAAC;IAED,MAAM,IAAI,yBAAyB,EAAE,CAAC;AACxC,CAAC;AAED,SAAS,kBAAkB,CAAC,IAAS;IACnC,IAAI,IAAI,EAAE,iBAAiB,EAAE,CAAC;QAC5B,OAAO,IAAI,CAAC,iBAAiB,CAAC;IAChC,CAAC;IACD,IAAI,IAAI,EAAE,KAAK,EAAE,CAAC;QAChB,OAAO,mBAAmB,IAAI,CAAC,KAAK,EAAE,CAAC;IACzC,CAAC;IACD,OAAO,iBAAiB,CAAC;AAC3B,CAAC;AAED,SAAS,QAAQ,CAAC,IAAS;IACzB,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,CAAC;QACrB,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;IAC7D,CAAC;IACD,OAAO;QACL,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,YAAY,EAAE,IAAI,CAAC,YAAY;QAC/B,KAAK,EAAE,IAAI,CAAC,OAAO,EAAE,KAAK;KAC3B,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface StoredCredential {
|
|
2
|
+
accessKey: string;
|
|
3
|
+
serverUrl?: string;
|
|
4
|
+
credentialId?: string;
|
|
5
|
+
deviceId?: string;
|
|
6
|
+
accountEmail?: string;
|
|
7
|
+
preserveAccessKeyOnLogout?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function getConfigPath(): string;
|
|
10
|
+
export declare function getCredentialPath(): string;
|
|
11
|
+
export declare function getLegacyConfigPath(): string;
|
|
12
|
+
/**
|
|
13
|
+
* Moves a pre-0.5 `config.json` into the split files. Returns what it found so
|
|
14
|
+
* the caller can keep using it in the same run.
|
|
15
|
+
*/
|
|
16
|
+
export declare function migrateLegacyConfig(): StoredCredential | null;
|
|
17
|
+
export declare function readCredential(): StoredCredential | null;
|
|
18
|
+
export declare function writeCredential(credential: StoredCredential): void;
|
|
19
|
+
/**
|
|
20
|
+
* Drops the secret and forgets the session, but keeps the machine identity so
|
|
21
|
+
* the next sign-in shows up as the same device in the dashboard.
|
|
22
|
+
*/
|
|
23
|
+
export declare function clearCredential(): void;
|
|
24
|
+
export declare function readOrCreateDeviceId(generate: () => string): string;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getConfigPath = getConfigPath;
|
|
4
|
+
exports.getCredentialPath = getCredentialPath;
|
|
5
|
+
exports.getLegacyConfigPath = getLegacyConfigPath;
|
|
6
|
+
exports.migrateLegacyConfig = migrateLegacyConfig;
|
|
7
|
+
exports.readCredential = readCredential;
|
|
8
|
+
exports.writeCredential = writeCredential;
|
|
9
|
+
exports.clearCredential = clearCredential;
|
|
10
|
+
exports.readOrCreateDeviceId = readOrCreateDeviceId;
|
|
11
|
+
const fs = require("fs");
|
|
12
|
+
const os = require("os");
|
|
13
|
+
const path = require("path");
|
|
14
|
+
/**
|
|
15
|
+
* Local credential storage.
|
|
16
|
+
*
|
|
17
|
+
* Two files, deliberately. The secret lives alone in a 0600 file; everything
|
|
18
|
+
* else is plain state that is fine to read. The legacy `config.json` held both
|
|
19
|
+
* at whatever the umask allowed, and is removed once its contents are carried
|
|
20
|
+
* over: a CLI old enough to still read it would otherwise find a file with no
|
|
21
|
+
* key in it and refuse to log in while every other command failed.
|
|
22
|
+
*
|
|
23
|
+
* The OS keychain backends drop in behind this module later; callers only ever
|
|
24
|
+
* see the functions below.
|
|
25
|
+
*/
|
|
26
|
+
const DIRECTORY_NAME = ".aether";
|
|
27
|
+
const CONFIG_FILE = "cli.json";
|
|
28
|
+
const CREDENTIAL_FILE = "credentials.json";
|
|
29
|
+
const LEGACY_CONFIG_FILE = "config.json";
|
|
30
|
+
const SECRET_FILE_MODE = 0o600;
|
|
31
|
+
const DIRECTORY_MODE = 0o700;
|
|
32
|
+
function baseDirectory() {
|
|
33
|
+
return path.join(process.env.LOCALAPPDATA || process.env.HOME || os.homedir(), DIRECTORY_NAME);
|
|
34
|
+
}
|
|
35
|
+
function getConfigPath() {
|
|
36
|
+
return path.join(baseDirectory(), CONFIG_FILE);
|
|
37
|
+
}
|
|
38
|
+
function getCredentialPath() {
|
|
39
|
+
return path.join(baseDirectory(), CREDENTIAL_FILE);
|
|
40
|
+
}
|
|
41
|
+
function getLegacyConfigPath() {
|
|
42
|
+
return path.join(baseDirectory(), LEGACY_CONFIG_FILE);
|
|
43
|
+
}
|
|
44
|
+
function readJson(filePath) {
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(fs.readFileSync(filePath, { encoding: "utf8" }));
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function writeJson(filePath, value, mode) {
|
|
53
|
+
const directory = path.dirname(filePath);
|
|
54
|
+
fs.mkdirSync(directory, { recursive: true, mode: DIRECTORY_MODE });
|
|
55
|
+
// mkdirSync applies its mode only to directories it creates, so a ~/.aether
|
|
56
|
+
// left behind by an older CLI keeps whatever the old umask gave it.
|
|
57
|
+
try {
|
|
58
|
+
fs.chmodSync(directory, DIRECTORY_MODE);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// No POSIX mode to set here.
|
|
62
|
+
}
|
|
63
|
+
if (mode === undefined) {
|
|
64
|
+
fs.writeFileSync(filePath, JSON.stringify(value), { encoding: "utf8" });
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
// O_NOFOLLOW refuses a symlink planted at the path, and narrowing the mode
|
|
68
|
+
// before the write means the secret is never briefly world-readable in an
|
|
69
|
+
// existing file.
|
|
70
|
+
let flags = fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_TRUNC;
|
|
71
|
+
if (typeof fs.constants.O_NOFOLLOW === "number") {
|
|
72
|
+
flags |= fs.constants.O_NOFOLLOW;
|
|
73
|
+
}
|
|
74
|
+
const handle = fs.openSync(filePath, flags, mode);
|
|
75
|
+
try {
|
|
76
|
+
try {
|
|
77
|
+
fs.fchmodSync(handle, mode);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Windows and some network filesystems have no POSIX mode to set.
|
|
81
|
+
}
|
|
82
|
+
fs.writeFileSync(handle, JSON.stringify(value), { encoding: "utf8" });
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
fs.closeSync(handle);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** Returns false only when the file is still there afterwards. */
|
|
89
|
+
function removeFile(filePath) {
|
|
90
|
+
try {
|
|
91
|
+
fs.unlinkSync(filePath);
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return !fs.existsSync(filePath);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Moves a pre-0.5 `config.json` into the split files. Returns what it found so
|
|
100
|
+
* the caller can keep using it in the same run.
|
|
101
|
+
*/
|
|
102
|
+
function migrateLegacyConfig() {
|
|
103
|
+
const legacy = readJson(getLegacyConfigPath());
|
|
104
|
+
if (!legacy) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
const accessKey = legacy.accessKey || legacy.accessKeyName;
|
|
108
|
+
if (!accessKey) {
|
|
109
|
+
removeFile(getLegacyConfigPath());
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
const credential = {
|
|
113
|
+
accessKey,
|
|
114
|
+
serverUrl: legacy.customServerUrl,
|
|
115
|
+
preserveAccessKeyOnLogout: legacy.preserveAccessKeyOnLogout,
|
|
116
|
+
deviceId: readConfig().deviceId,
|
|
117
|
+
};
|
|
118
|
+
try {
|
|
119
|
+
writeCredential(credential);
|
|
120
|
+
if (!removeFile(getLegacyConfigPath())) {
|
|
121
|
+
// The key is now in two places, one of them world-readable.
|
|
122
|
+
console.warn(`[Aether] Could not delete ${getLegacyConfigPath()}. It still holds your access key; remove it by hand.`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// A read-only home must not break every command. Serve the credential from
|
|
127
|
+
// memory this run and try the migration again next time.
|
|
128
|
+
}
|
|
129
|
+
return credential;
|
|
130
|
+
}
|
|
131
|
+
function readConfig() {
|
|
132
|
+
return readJson(getConfigPath()) || {};
|
|
133
|
+
}
|
|
134
|
+
function readCredential() {
|
|
135
|
+
const secret = readJson(getCredentialPath());
|
|
136
|
+
if (!secret || !secret.accessKey) {
|
|
137
|
+
return migrateLegacyConfig();
|
|
138
|
+
}
|
|
139
|
+
const config = readConfig();
|
|
140
|
+
return {
|
|
141
|
+
accessKey: secret.accessKey,
|
|
142
|
+
serverUrl: config.serverUrl,
|
|
143
|
+
credentialId: config.credentialId,
|
|
144
|
+
deviceId: config.deviceId,
|
|
145
|
+
accountEmail: config.accountEmail,
|
|
146
|
+
preserveAccessKeyOnLogout: config.preserveAccessKeyOnLogout,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function writeCredential(credential) {
|
|
150
|
+
const config = {
|
|
151
|
+
serverUrl: credential.serverUrl,
|
|
152
|
+
credentialId: credential.credentialId,
|
|
153
|
+
deviceId: credential.deviceId || readConfig().deviceId,
|
|
154
|
+
accountEmail: credential.accountEmail,
|
|
155
|
+
preserveAccessKeyOnLogout: credential.preserveAccessKeyOnLogout,
|
|
156
|
+
};
|
|
157
|
+
writeJson(getConfigPath(), config);
|
|
158
|
+
writeJson(getCredentialPath(), { accessKey: credential.accessKey }, SECRET_FILE_MODE);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Drops the secret and forgets the session, but keeps the machine identity so
|
|
162
|
+
* the next sign-in shows up as the same device in the dashboard.
|
|
163
|
+
*/
|
|
164
|
+
function clearCredential() {
|
|
165
|
+
const deviceId = readConfig().deviceId;
|
|
166
|
+
const removed = removeFile(getCredentialPath());
|
|
167
|
+
removeFile(getLegacyConfigPath());
|
|
168
|
+
if (deviceId) {
|
|
169
|
+
writeJson(getConfigPath(), { deviceId });
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
removeFile(getConfigPath());
|
|
173
|
+
}
|
|
174
|
+
if (!removed) {
|
|
175
|
+
throw new Error(`Could not delete ${getCredentialPath()}. Remove it by hand to finish signing out.`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function readOrCreateDeviceId(generate) {
|
|
179
|
+
const config = readConfig();
|
|
180
|
+
if (config.deviceId) {
|
|
181
|
+
return config.deviceId;
|
|
182
|
+
}
|
|
183
|
+
const deviceId = generate();
|
|
184
|
+
try {
|
|
185
|
+
writeJson(getConfigPath(), { ...config, deviceId });
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
// A home we cannot write to still gets a working sign-in; the machine just
|
|
189
|
+
// shows up as a new device next time.
|
|
190
|
+
}
|
|
191
|
+
return deviceId;
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=key-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"key-store.js","sourceRoot":"","sources":["../../../script/auth/key-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface LoopbackCallback {
|
|
2
|
+
code: string;
|
|
3
|
+
state: string | null;
|
|
4
|
+
error: string | null;
|
|
5
|
+
}
|
|
6
|
+
export interface LoopbackListener {
|
|
7
|
+
redirectUri: string;
|
|
8
|
+
waitForCallback(timeoutMs: number): Promise<LoopbackCallback>;
|
|
9
|
+
close(): void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A one-shot HTTP listener on the loopback interface that receives the
|
|
13
|
+
* authorization code the browser is redirected to.
|
|
14
|
+
*
|
|
15
|
+
* Bound to 127.0.0.1 rather than every interface, and on an ephemeral port the
|
|
16
|
+
* server pins to this ceremony, so nothing off this machine can reach it and a
|
|
17
|
+
* second `aether login` cannot collide with the first.
|
|
18
|
+
*/
|
|
19
|
+
export declare function startLoopbackListener(): Promise<LoopbackListener>;
|