@anvil-works/anvil-cli 0.5.11 → 0.5.12
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 +14 -0
- package/dist/cli.js +198 -51
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -172,6 +172,7 @@ Useful options:
|
|
|
172
172
|
|
|
173
173
|
```bash
|
|
174
174
|
anvil login
|
|
175
|
+
anvil login anvil.company.com
|
|
175
176
|
anvil logout
|
|
176
177
|
anvil config list
|
|
177
178
|
anvil config get <key>
|
|
@@ -260,6 +261,19 @@ If you want to change the default server permanently:
|
|
|
260
261
|
anvil config set anvilUrl https://anvil.company.com
|
|
261
262
|
```
|
|
262
263
|
|
|
264
|
+
When you run `anvil login`, the CLI now supports both local and headless flows:
|
|
265
|
+
|
|
266
|
+
- press `Enter` to open the default browser
|
|
267
|
+
- or copy the printed device-login URL and code to continue in a browser of your choice
|
|
268
|
+
|
|
269
|
+
For `anvil login`, prefer the positional server argument in docs and examples:
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
anvil login anvil.company.com
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
`--url` is also supported if needed.
|
|
276
|
+
|
|
263
277
|
## Troubleshooting
|
|
264
278
|
|
|
265
279
|
### `anvil` command not found
|
package/dist/cli.js
CHANGED
|
@@ -51890,34 +51890,134 @@ var __webpack_exports__ = {};
|
|
|
51890
51890
|
</body>
|
|
51891
51891
|
</html>`;
|
|
51892
51892
|
}
|
|
51893
|
+
const CLIENT_ID = "anvil-sync";
|
|
51894
|
+
const SCOPES = "apps:read apps:write user:read";
|
|
51893
51895
|
function oauth_login_base64url(buf) {
|
|
51894
51896
|
return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
51895
51897
|
}
|
|
51896
|
-
|
|
51898
|
+
function sleep(ms) {
|
|
51899
|
+
return new Promise((resolve)=>setTimeout(resolve, ms));
|
|
51900
|
+
}
|
|
51901
|
+
function createBrowserLaunchPrompt(authUrl) {
|
|
51902
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return {
|
|
51903
|
+
waitForOpen: Promise.resolve(false),
|
|
51904
|
+
close: ()=>{}
|
|
51905
|
+
};
|
|
51897
51906
|
const rl = external_readline_namespaceObject.createInterface({
|
|
51898
51907
|
input: process.stdin,
|
|
51899
51908
|
output: process.stdout
|
|
51900
51909
|
});
|
|
51901
|
-
|
|
51902
|
-
|
|
51903
|
-
|
|
51904
|
-
|
|
51905
|
-
|
|
51906
|
-
oauthPromise.then((r)=>({
|
|
51907
|
-
oauth: r
|
|
51908
|
-
}))
|
|
51909
|
-
]);
|
|
51910
|
-
if ("enter" === result) {
|
|
51910
|
+
let closed = false;
|
|
51911
|
+
let resolveWait = null;
|
|
51912
|
+
const close = (opened = false)=>{
|
|
51913
|
+
if (closed) return;
|
|
51914
|
+
closed = true;
|
|
51911
51915
|
rl.close();
|
|
51912
|
-
|
|
51913
|
-
|
|
51914
|
-
|
|
51916
|
+
resolveWait?.(opened);
|
|
51917
|
+
resolveWait = null;
|
|
51918
|
+
};
|
|
51919
|
+
const waitForOpen = new Promise((resolve)=>{
|
|
51920
|
+
resolveWait = resolve;
|
|
51921
|
+
rl.on("SIGINT", ()=>{
|
|
51922
|
+
close();
|
|
51923
|
+
process.kill(process.pid, "SIGINT");
|
|
51924
|
+
});
|
|
51925
|
+
rl.question("", async ()=>{
|
|
51926
|
+
try {
|
|
51927
|
+
await node_modules_open(authUrl);
|
|
51928
|
+
logger_logger.info(chalk_source.dim("Opened your browser to continue login."));
|
|
51929
|
+
close(true);
|
|
51930
|
+
} catch {
|
|
51931
|
+
logger_logger.warn("Could not open a browser automatically.");
|
|
51932
|
+
close(false);
|
|
51933
|
+
}
|
|
51934
|
+
});
|
|
51935
|
+
});
|
|
51936
|
+
return {
|
|
51937
|
+
waitForOpen,
|
|
51938
|
+
close
|
|
51939
|
+
};
|
|
51940
|
+
}
|
|
51941
|
+
async function parseOAuthError(response) {
|
|
51942
|
+
const contentType = response.headers.get("content-type") || "";
|
|
51943
|
+
if (contentType.includes("application/json")) {
|
|
51944
|
+
const data = await response.json();
|
|
51945
|
+
return data.error || `HTTP ${response.status}`;
|
|
51946
|
+
}
|
|
51947
|
+
const text = (await response.text()).trim();
|
|
51948
|
+
if (!text) return `HTTP ${response.status}`;
|
|
51949
|
+
try {
|
|
51950
|
+
const parsed = JSON.parse(text);
|
|
51951
|
+
return parsed.error || text;
|
|
51952
|
+
} catch {
|
|
51953
|
+
return text;
|
|
51915
51954
|
}
|
|
51916
|
-
rl.close();
|
|
51917
|
-
process.stdout.write("\r" + " ".repeat(60) + "\r");
|
|
51918
|
-
return result.oauth;
|
|
51919
51955
|
}
|
|
51920
|
-
async function
|
|
51956
|
+
async function exchangeAuthorizationCodeForTokens(anvilUrl, redirectUri, code, codeVerifier) {
|
|
51957
|
+
const tokenResponse = await fetch(`${anvilUrl}/oauth/token`, {
|
|
51958
|
+
method: "POST",
|
|
51959
|
+
headers: {
|
|
51960
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
51961
|
+
},
|
|
51962
|
+
body: new URLSearchParams({
|
|
51963
|
+
grant_type: "authorization_code",
|
|
51964
|
+
code,
|
|
51965
|
+
redirect_uri: redirectUri,
|
|
51966
|
+
client_id: CLIENT_ID,
|
|
51967
|
+
code_verifier: codeVerifier
|
|
51968
|
+
})
|
|
51969
|
+
});
|
|
51970
|
+
if (!tokenResponse.ok) throw new Error(`Failed to exchange authorization code for token. ${await parseOAuthError(tokenResponse)}`);
|
|
51971
|
+
return await tokenResponse.json();
|
|
51972
|
+
}
|
|
51973
|
+
async function requestDeviceAuthorization(anvilUrl) {
|
|
51974
|
+
const response = await fetch(`${anvilUrl}/oauth/device_authorization`, {
|
|
51975
|
+
method: "POST",
|
|
51976
|
+
headers: {
|
|
51977
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
51978
|
+
},
|
|
51979
|
+
body: new URLSearchParams({
|
|
51980
|
+
client_id: CLIENT_ID,
|
|
51981
|
+
scope: SCOPES
|
|
51982
|
+
})
|
|
51983
|
+
});
|
|
51984
|
+
if (!response.ok) throw new Error(`Failed to start device login. ${await parseOAuthError(response)}`);
|
|
51985
|
+
return await response.json();
|
|
51986
|
+
}
|
|
51987
|
+
async function pollDeviceAuthorization(anvilUrl, deviceAuth, options) {
|
|
51988
|
+
const expiresAt = Date.now() + 1000 * deviceAuth.expires_in;
|
|
51989
|
+
let intervalMs = 1000 * Math.max(deviceAuth.interval ?? 5, 1);
|
|
51990
|
+
while(Date.now() < expiresAt){
|
|
51991
|
+
if (options?.isCancelled?.()) throw new Error("cancelled");
|
|
51992
|
+
const response = await fetch(`${anvilUrl}/oauth/token`, {
|
|
51993
|
+
method: "POST",
|
|
51994
|
+
headers: {
|
|
51995
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
51996
|
+
},
|
|
51997
|
+
body: new URLSearchParams({
|
|
51998
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
51999
|
+
device_code: deviceAuth.device_code,
|
|
52000
|
+
client_id: CLIENT_ID
|
|
52001
|
+
})
|
|
52002
|
+
});
|
|
52003
|
+
if (response.ok) return await response.json();
|
|
52004
|
+
const error = await parseOAuthError(response);
|
|
52005
|
+
if ("authorization_pending" === error) {
|
|
52006
|
+
await sleep(intervalMs);
|
|
52007
|
+
continue;
|
|
52008
|
+
}
|
|
52009
|
+
if ("slow_down" === error) {
|
|
52010
|
+
intervalMs += 1000;
|
|
52011
|
+
await sleep(intervalMs);
|
|
52012
|
+
continue;
|
|
52013
|
+
}
|
|
52014
|
+
if ("access_denied" === error) throw new Error("Device login was denied.");
|
|
52015
|
+
if ("expired_token" === error || "invalid_grant" === error) break;
|
|
52016
|
+
throw new Error(`Device login failed. ${error}`);
|
|
52017
|
+
}
|
|
52018
|
+
throw new Error("Device login expired before it was approved.");
|
|
52019
|
+
}
|
|
52020
|
+
async function createPkceLoginFlow(anvilUrl) {
|
|
51921
52021
|
const codeVerifier = external_crypto_.randomBytes(48).toString("hex");
|
|
51922
52022
|
const codeChallenge = oauth_login_base64url(external_crypto_.createHash("sha256").update(codeVerifier, "ascii").digest());
|
|
51923
52023
|
const state = external_crypto_.randomBytes(16).toString("hex");
|
|
@@ -51927,10 +52027,9 @@ var __webpack_exports__ = {};
|
|
|
51927
52027
|
if (!address || "string" == typeof address) throw new Error("No address");
|
|
51928
52028
|
const port = address.port;
|
|
51929
52029
|
const redirectUri = `http://127.0.0.1:${port}/oauth-callback`;
|
|
51930
|
-
const SCOPES = "apps:read apps:write user:read";
|
|
51931
52030
|
const authUrl = new URL(`${anvilUrl}/oauth/authorize`);
|
|
51932
52031
|
authUrl.searchParams.set("response_type", "code");
|
|
51933
|
-
authUrl.searchParams.set("client_id",
|
|
52032
|
+
authUrl.searchParams.set("client_id", CLIENT_ID);
|
|
51934
52033
|
authUrl.searchParams.set("redirect_uri", redirectUri);
|
|
51935
52034
|
authUrl.searchParams.set("scope", SCOPES);
|
|
51936
52035
|
authUrl.searchParams.set("state", state);
|
|
@@ -51964,37 +52063,84 @@ var __webpack_exports__ = {};
|
|
|
51964
52063
|
server.close();
|
|
51965
52064
|
});
|
|
51966
52065
|
});
|
|
51967
|
-
|
|
52066
|
+
let closed = false;
|
|
52067
|
+
server.on("close", ()=>{
|
|
52068
|
+
closed = true;
|
|
52069
|
+
});
|
|
52070
|
+
const close = async ()=>{
|
|
52071
|
+
if (closed) return;
|
|
52072
|
+
closed = true;
|
|
52073
|
+
server.closeAllConnections();
|
|
52074
|
+
if (!server.listening) return;
|
|
52075
|
+
await new Promise((resolve)=>server.close(()=>resolve()));
|
|
52076
|
+
};
|
|
52077
|
+
return {
|
|
52078
|
+
authUrl,
|
|
52079
|
+
close,
|
|
52080
|
+
waitForLogin: (async ()=>{
|
|
52081
|
+
const { code, error, recvState } = await codePromise;
|
|
52082
|
+
if (recvState !== state) throw new Error("Invalid state received from OAuth callback");
|
|
52083
|
+
if (error) throw new Error(`Error received from OAuth callback: ${error}`);
|
|
52084
|
+
const tokenData = await exchangeAuthorizationCodeForTokens(anvilUrl, redirectUri, code, codeVerifier);
|
|
52085
|
+
return login(anvilUrl, {
|
|
52086
|
+
access_token: tokenData.access_token,
|
|
52087
|
+
refresh_token: tokenData.refresh_token,
|
|
52088
|
+
expires_in: tokenData.expires_in,
|
|
52089
|
+
scope: tokenData.scope
|
|
52090
|
+
});
|
|
52091
|
+
})()
|
|
52092
|
+
};
|
|
52093
|
+
}
|
|
52094
|
+
function raceLoginAttempts(attempts) {
|
|
52095
|
+
return new Promise((resolve, reject)=>{
|
|
52096
|
+
let remaining = attempts.length;
|
|
52097
|
+
let lastError = null;
|
|
52098
|
+
for (const attempt of attempts)attempt.then(resolve).catch((error)=>{
|
|
52099
|
+
if ("cancelled" === error.message) return;
|
|
52100
|
+
remaining -= 1;
|
|
52101
|
+
lastError = error;
|
|
52102
|
+
if (0 === remaining && lastError) reject(lastError);
|
|
52103
|
+
});
|
|
52104
|
+
});
|
|
52105
|
+
}
|
|
52106
|
+
async function runInteractiveLoginFlow(anvilUrl) {
|
|
52107
|
+
const pkceFlow = await createPkceLoginFlow(anvilUrl);
|
|
52108
|
+
const deviceAuth = await requestDeviceAuthorization(anvilUrl);
|
|
52109
|
+
const browserPrompt = createBrowserLaunchPrompt(pkceFlow.authUrl.toString());
|
|
52110
|
+
let settled = false;
|
|
52111
|
+
let spinnerStarted = false;
|
|
52112
|
+
logger_logger.info(chalk_source.dim(`Logging in to ${anvilUrl}`));
|
|
51968
52113
|
console.log();
|
|
51969
|
-
logger_logger.info(chalk_source.
|
|
52114
|
+
logger_logger.info(chalk_source.dim("Visit:"), `${deviceAuth.verification_uri_complete || deviceAuth.verification_uri}`);
|
|
52115
|
+
logger_logger.info(chalk_source.dim("Device code:"), `${deviceAuth.user_code}`);
|
|
51970
52116
|
console.log();
|
|
51971
|
-
|
|
51972
|
-
|
|
51973
|
-
|
|
51974
|
-
|
|
51975
|
-
|
|
51976
|
-
|
|
51977
|
-
"Content-Type": "application/x-www-form-urlencoded"
|
|
51978
|
-
},
|
|
51979
|
-
body: new URLSearchParams({
|
|
51980
|
-
grant_type: "authorization_code",
|
|
51981
|
-
code: code,
|
|
51982
|
-
redirect_uri: redirectUri,
|
|
51983
|
-
client_id: "anvil-sync",
|
|
51984
|
-
code_verifier: codeVerifier
|
|
51985
|
-
})
|
|
52117
|
+
logger_logger.info("OR Press ENTER to open a browser...");
|
|
52118
|
+
browserPrompt.waitForOpen.then((opened)=>{
|
|
52119
|
+
if (opened && !settled) {
|
|
52120
|
+
spinnerStarted = true;
|
|
52121
|
+
logger_logger.progress("login", "Waiting for login to complete...");
|
|
52122
|
+
}
|
|
51986
52123
|
});
|
|
51987
|
-
|
|
51988
|
-
const
|
|
51989
|
-
|
|
52124
|
+
try {
|
|
52125
|
+
const result = await raceLoginAttempts([
|
|
52126
|
+
pkceFlow.waitForLogin,
|
|
52127
|
+
pollDeviceAuthorization(anvilUrl, deviceAuth, {
|
|
52128
|
+
isCancelled: ()=>settled
|
|
52129
|
+
}).then(async (tokenData)=>login(anvilUrl, {
|
|
52130
|
+
access_token: tokenData.access_token,
|
|
52131
|
+
refresh_token: tokenData.refresh_token,
|
|
52132
|
+
expires_in: tokenData.expires_in,
|
|
52133
|
+
scope: tokenData.scope
|
|
52134
|
+
}))
|
|
52135
|
+
]);
|
|
52136
|
+
settled = true;
|
|
52137
|
+
return result;
|
|
52138
|
+
} finally{
|
|
52139
|
+
settled = true;
|
|
52140
|
+
browserPrompt.close();
|
|
52141
|
+
if (spinnerStarted) logger_logger.progressEnd("login");
|
|
52142
|
+
await pkceFlow.close();
|
|
51990
52143
|
}
|
|
51991
|
-
const tokenData = await tokenResponse.json();
|
|
51992
|
-
return login(anvilUrl, {
|
|
51993
|
-
access_token: tokenData.access_token,
|
|
51994
|
-
refresh_token: tokenData.refresh_token,
|
|
51995
|
-
expires_in: tokenData.expires_in,
|
|
51996
|
-
scope: tokenData.scope
|
|
51997
|
-
});
|
|
51998
52144
|
}
|
|
51999
52145
|
const CHECKOUT_ERROR_VALUE = "__ERROR__";
|
|
52000
52146
|
function isAbortLikeError(error) {
|
|
@@ -52747,10 +52893,11 @@ var __webpack_exports__ = {};
|
|
|
52747
52893
|
});
|
|
52748
52894
|
}
|
|
52749
52895
|
function registerLoginCommand(program) {
|
|
52750
|
-
const loginCommand = program.command("login [anvil-server-url]").description("Log in to Anvil using OAuth").alias("l").action(async (anvilUrl)=>{
|
|
52896
|
+
const loginCommand = program.command("login [anvil-server-url]").description("Log in to Anvil using OAuth").alias("l").option("-u, --url <ANVIL_URL>", "Specify Anvil server URL").action(async (anvilUrl, options)=>{
|
|
52751
52897
|
try {
|
|
52752
|
-
|
|
52753
|
-
|
|
52898
|
+
const requestedUrl = anvilUrl || options.url;
|
|
52899
|
+
if (requestedUrl) {
|
|
52900
|
+
anvilUrl = normalizeAnvilUrl(requestedUrl.trim());
|
|
52754
52901
|
setConfig("anvilUrl", anvilUrl);
|
|
52755
52902
|
} else anvilUrl = resolveAnvilUrl();
|
|
52756
52903
|
const result = await runInteractiveLoginFlow(anvilUrl);
|
|
@@ -52760,7 +52907,7 @@ var __webpack_exports__ = {};
|
|
|
52760
52907
|
process.exit(1);
|
|
52761
52908
|
}
|
|
52762
52909
|
});
|
|
52763
|
-
loginCommand.addHelpText("after", "\n" + chalk_source.bold("Examples:") + "\n anvil login Log in to default Anvil server\n anvil login anvil.works Log in to anvil.works\n anvil login localhost Log in to local Anvil server\n");
|
|
52910
|
+
loginCommand.addHelpText("after", "\n" + chalk_source.bold("Examples:") + "\n anvil login Log in to default Anvil server\n anvil login anvil.works Log in to anvil.works\n anvil login --url localhost Log in to localhost\n anvil login localhost Log in to local Anvil server\n");
|
|
52764
52911
|
}
|
|
52765
52912
|
function getTotalLoggedInAccounts(urls) {
|
|
52766
52913
|
return urls.reduce((total, url)=>total + auth_getAccountsForUrl(url).length, 0);
|