layero 0.5.0 → 0.5.2
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/api.js +6 -0
- package/dist/bin/layero.js +3 -5
- package/dist/commands/login.js +45 -96
- package/package.json +25 -2
package/dist/api.js
CHANGED
|
@@ -81,6 +81,12 @@ export class ApiClient {
|
|
|
81
81
|
rollbackProject(projectId, input) {
|
|
82
82
|
return this.request("POST", `/projects/${projectId}/rollback`, input);
|
|
83
83
|
}
|
|
84
|
+
startDeviceAuth() {
|
|
85
|
+
return this.request("POST", "/auth/cli/device");
|
|
86
|
+
}
|
|
87
|
+
pollDeviceAuth(device_code) {
|
|
88
|
+
return this.request("POST", "/auth/cli/device/poll", { device_code });
|
|
89
|
+
}
|
|
84
90
|
setUsername(value) {
|
|
85
91
|
return this.request("POST", "/auth/me/username", { value });
|
|
86
92
|
}
|
package/dist/bin/layero.js
CHANGED
|
@@ -27,12 +27,10 @@ async function main() {
|
|
|
27
27
|
.option("--json", "emit machine-readable JSON-lines on stdout (for agents and CI)");
|
|
28
28
|
program
|
|
29
29
|
.command("login")
|
|
30
|
-
.description("Authenticate via browser (GitHub /
|
|
31
|
-
.
|
|
32
|
-
.option("--port <port>", "fixed loopback port (default: random)", (v) => Number(v))
|
|
33
|
-
.addHelpText("after", "\nExamples:\n $ layero login\n $ layero login --provider google")
|
|
30
|
+
.description("Authenticate via browser (GitHub / Yandex). Opens a one-time URL — no localhost server required.")
|
|
31
|
+
.addHelpText("after", "\nExamples:\n $ layero login\n $ npx layero login")
|
|
34
32
|
.action(async (opts) => {
|
|
35
|
-
await loginCmd(
|
|
33
|
+
await loginCmd(opts);
|
|
36
34
|
});
|
|
37
35
|
program
|
|
38
36
|
.command("logout")
|
package/dist/commands/login.js
CHANGED
|
@@ -1,106 +1,55 @@
|
|
|
1
|
-
import http from "node:http";
|
|
2
|
-
import { randomBytes } from "node:crypto";
|
|
3
1
|
import chalk from "chalk";
|
|
4
2
|
import open from "open";
|
|
5
3
|
import { loadConfig, saveConfig } from "../config.js";
|
|
6
4
|
import { ApiClient } from "../api.js";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
* 127.0.0.1, opens the browser at /auth/cli/start, waits for the
|
|
11
|
-
* frontend's CliBridge page to POST the JWT back. Server stops as soon
|
|
12
|
-
* as a valid token arrives or LOGIN_TIMEOUT_MS elapses.
|
|
13
|
-
*
|
|
14
|
-
* NOTE: this command depends on backend endpoints that may not be live yet
|
|
15
|
-
* (`/auth/cli/start`, frontend `/oauth/cli-bridge`). If you can already
|
|
16
|
-
* obtain a JWT through the web UI, use `layero token set <jwt>` as a
|
|
17
|
-
* temporary alternative.
|
|
18
|
-
*/
|
|
19
|
-
export async function loginCmd(opts) {
|
|
5
|
+
import { LayeroError, detectMode, emit } from "../agent.js";
|
|
6
|
+
const MAX_WAIT_MS = 15 * 60 * 1000;
|
|
7
|
+
export async function loginCmd(_opts) {
|
|
20
8
|
const cfg = await loadConfig();
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
9
|
+
const mode = detectMode();
|
|
10
|
+
const api = new ApiClient(cfg);
|
|
11
|
+
const device = await api.startDeviceAuth();
|
|
12
|
+
const { device_code, user_code, verification_url, poll_interval } = device;
|
|
13
|
+
const pollMs = (poll_interval ?? 2) * 1000;
|
|
14
|
+
if (mode.json) {
|
|
15
|
+
emit({ event: "auth_required", url: verification_url, user_code });
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
console.log(chalk.cyan("\nOpen this URL to sign in:"));
|
|
19
|
+
console.log(chalk.bold(` ${verification_url}`));
|
|
20
|
+
console.log(chalk.dim(`\n Confirmation code: `) + chalk.white.bold(user_code));
|
|
21
|
+
console.log(chalk.dim(` (expires in ${device.expires_in}s)\n`));
|
|
22
|
+
try {
|
|
23
|
+
await open(verification_url);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// non-fatal — user can paste the URL manually
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const deadline = Date.now() + MAX_WAIT_MS;
|
|
30
|
+
while (Date.now() < deadline) {
|
|
31
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
32
|
+
const poll = await api.pollDeviceAuth(device_code);
|
|
33
|
+
if (poll.status === "approved" && poll.token) {
|
|
34
|
+
cfg.token = poll.token;
|
|
35
|
+
const probe = new ApiClient(cfg);
|
|
36
|
+
const me = await probe.me();
|
|
37
|
+
cfg.user = { id: me.id, username: me.username, email: me.email };
|
|
38
|
+
await saveConfig(cfg);
|
|
39
|
+
if (mode.json) {
|
|
40
|
+
emit({ event: "authorized", user: me.username ?? me.email ?? me.id });
|
|
37
41
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const parsed = JSON.parse(body);
|
|
43
|
-
token = parsed.token;
|
|
44
|
-
receivedState = parsed.state;
|
|
42
|
+
else {
|
|
43
|
+
console.log(chalk.green(`Logged in as ${me.username ?? me.email ?? me.id}`));
|
|
44
|
+
if (!me.username) {
|
|
45
|
+
console.log(chalk.yellow("No username set — open https://app.layero.ru/onboarding to pick one."));
|
|
45
46
|
}
|
|
46
|
-
catch {
|
|
47
|
-
// ignore; fall through to query
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
token ??= url.searchParams.get("token") ?? undefined;
|
|
51
|
-
receivedState ??= url.searchParams.get("state") ?? undefined;
|
|
52
|
-
if (!token || receivedState !== state) {
|
|
53
|
-
res.writeHead(400, { "Access-Control-Allow-Origin": "*" }).end("bad state");
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
res
|
|
57
|
-
.writeHead(200, {
|
|
58
|
-
"Content-Type": "text/plain",
|
|
59
|
-
"Access-Control-Allow-Origin": "*",
|
|
60
|
-
})
|
|
61
|
-
.end("ok — you can close this tab");
|
|
62
|
-
if (timeoutHandle)
|
|
63
|
-
clearTimeout(timeoutHandle);
|
|
64
|
-
server.close();
|
|
65
|
-
resolve(token);
|
|
66
|
-
});
|
|
67
|
-
server.on("error", reject);
|
|
68
|
-
server.listen(desiredPort, "127.0.0.1", () => {
|
|
69
|
-
const addr = server.address();
|
|
70
|
-
// Defence-in-depth: confirm we actually bound to loopback before
|
|
71
|
-
// emitting `callback=…` to the browser. If listen() somehow returned
|
|
72
|
-
// a non-loopback address (Node bug, weird /etc/hosts) we'd otherwise
|
|
73
|
-
// hand the JWT redirect target out to whoever owns that interface.
|
|
74
|
-
if (!addr || addr.address !== "127.0.0.1") {
|
|
75
|
-
server.close();
|
|
76
|
-
reject(new Error(`expected to bind 127.0.0.1, got ${addr?.address ?? "null"}`));
|
|
77
|
-
return;
|
|
78
47
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
`&state=${state}`;
|
|
85
|
-
console.log(chalk.cyan(`opening browser for ${provider} login...`));
|
|
86
|
-
console.log(chalk.dim(`if it doesn't open, paste this URL: ${startUrl}`));
|
|
87
|
-
open(startUrl).catch(() => {
|
|
88
|
-
// open failure is non-fatal — user can paste the URL manually.
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
timeoutHandle = setTimeout(() => {
|
|
92
|
-
server.close();
|
|
93
|
-
reject(new Error(`login timed out after ${LOGIN_TIMEOUT_MS / 1000}s`));
|
|
94
|
-
}, LOGIN_TIMEOUT_MS);
|
|
95
|
-
});
|
|
96
|
-
const token = await tokenPromise;
|
|
97
|
-
cfg.token = token;
|
|
98
|
-
const probe = new ApiClient(cfg);
|
|
99
|
-
const me = await probe.me();
|
|
100
|
-
cfg.user = { id: me.id, username: me.username, email: me.email };
|
|
101
|
-
await saveConfig(cfg);
|
|
102
|
-
console.log(chalk.green(`logged in as ${me.username ?? me.email ?? me.id}`));
|
|
103
|
-
if (!me.username) {
|
|
104
|
-
console.log(chalk.yellow("no username set — open https://app.layero.ru/onboarding to pick one."));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (poll.status === "expired") {
|
|
51
|
+
throw new LayeroError("auth_expired", "Login timed out — code expired. Run `layero login` again.", "run_login");
|
|
52
|
+
}
|
|
105
53
|
}
|
|
54
|
+
throw new LayeroError("auth_timeout", "Login timed out. Run `layero login` again.", "run_login");
|
|
106
55
|
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "layero",
|
|
3
|
-
"version": "0.5.
|
|
4
|
-
"description": "Layero CLI — publish a local site with one command.",
|
|
3
|
+
"version": "0.5.2",
|
|
4
|
+
"description": "Layero CLI — publish a local site with one command. No git, no GitHub, agent-friendly (Cursor, Claude Code).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
|
+
"homepage": "https://layero.ru",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/LayeroInfra/core.git",
|
|
11
|
+
"directory": "cli"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/LayeroInfra/core/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"layero",
|
|
18
|
+
"deploy",
|
|
19
|
+
"deployment",
|
|
20
|
+
"hosting",
|
|
21
|
+
"cdn",
|
|
22
|
+
"cli",
|
|
23
|
+
"static-site",
|
|
24
|
+
"spa",
|
|
25
|
+
"ssr",
|
|
26
|
+
"ai-agents",
|
|
27
|
+
"cursor",
|
|
28
|
+
"claude-code"
|
|
29
|
+
],
|
|
7
30
|
"engines": {
|
|
8
31
|
"node": ">=20"
|
|
9
32
|
},
|