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 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
  }
@@ -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 / Google / Yandex).")
31
- .option("-p, --provider <provider>", "OAuth provider hint (github | google | yandex)", "github")
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({ provider: opts.provider, port: opts.port });
33
+ await loginCmd(opts);
36
34
  });
37
35
  program
38
36
  .command("logout")
@@ -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
- const LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
8
- /**
9
- * Browser-based OAuth login. Runs a one-shot loopback HTTP server on
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 state = randomBytes(16).toString("hex");
22
- const provider = opts.provider ?? "github";
23
- const desiredPort = opts.port ?? Number(process.env.LAYERO_LOGIN_PORT ?? 0);
24
- const tokenPromise = new Promise((resolve, reject) => {
25
- let timeoutHandle;
26
- const server = http.createServer(async (req, res) => {
27
- const url = new URL(req.url ?? "/", "http://127.0.0.1");
28
- if (url.pathname !== "/callback") {
29
- res.writeHead(404).end("not found");
30
- return;
31
- }
32
- // Accept token via POST body (preferred) or GET query (fallback).
33
- let body = "";
34
- if (req.method === "POST") {
35
- for await (const chunk of req)
36
- body += chunk;
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
- let token;
39
- let receivedState;
40
- if (body) {
41
- try {
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
- const port = addr.port;
80
- const callback = `http://127.0.0.1:${port}/callback`;
81
- const startUrl = `${cfg.apiUrl.replace(/\/+$/, "")}/auth/cli/start` +
82
- `?provider=${provider}` +
83
- `&callback=${encodeURIComponent(callback)}` +
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.0",
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
  },