layero 0.11.23 → 0.11.25

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 CHANGED
@@ -368,6 +368,12 @@ Full reference: <https://docs.layero.ru/cli/json-events>
368
368
  `node_modules`, `.git`, `dist`, `build`, `.next`, `.env*`, `.DS_Store`, and
369
369
  the rule files themselves. Maximum archive size is 500 MB.
370
370
 
371
+ Env files (`.env`, `.env.*`) stay out even if `.gitignore` un-ignores them
372
+ (`!.env.example`, `!.env`): those rules are written for git, not for a
373
+ deploy. To upload an env file on purpose, name it in `.layeroignore`:
374
+ `!.env.production`. Broad negations like `!*` do not bring env files back,
375
+ and nothing brings back `.git`, `.layero` or the rule files.
376
+
371
377
  ## Config
372
378
 
373
379
  - Auth token: `~/.layero/config.json` (chmod 600). Claim tokens of temporary
package/dist/api.js CHANGED
@@ -285,6 +285,19 @@ export class ApiClient {
285
285
  return this.request("POST", `/organizations/${org}/databases/${dbId}/api/storage/session`);
286
286
  }
287
287
  /** Настройки входа: незаданное не трогается (R2-08 — имя отправителя писем). */
288
+ /** Всё о входе базы: провайдеры (без ключей), способы, адреса возврата (R2-18). */
289
+ getDataAuthState(org, dbId) {
290
+ return this.request("GET", `/organizations/${org}/databases/${dbId}/api/auth/providers`);
291
+ }
292
+ setDataAuthProvider(org, dbId, input) {
293
+ return this.request("PUT", `/organizations/${org}/databases/${dbId}/api/auth/providers`, input);
294
+ }
295
+ setDataAuthProviderOptions(org, dbId, provider, input) {
296
+ return this.request("PUT", `/organizations/${org}/databases/${dbId}/api/auth/providers/${encodeURIComponent(provider)}/options`, input);
297
+ }
298
+ deleteDataAuthProvider(org, dbId, provider) {
299
+ return this.request("DELETE", `/organizations/${org}/databases/${dbId}/api/auth/providers/${encodeURIComponent(provider)}`);
300
+ }
288
301
  setDataAuthSettings(org, dbId, input) {
289
302
  return this.request("PUT", `/organizations/${org}/databases/${dbId}/api/auth/settings`, input);
290
303
  }
@@ -0,0 +1,209 @@
1
+ // `layero data auth …` — вход пользователей приложения из терминала (R2-18, 27.10.2026).
2
+ //
3
+ // Раньше провайдеры (Яндекс, VK ID, Сбер ID), их адреса возврата и способы
4
+ // входа включались только в панели — единственный шаг эталонного приложения
5
+ // без команды. Ручки сервера те же, что у панели.
6
+ //
7
+ // ⚠️ Секрет провайдера НЕ принимается аргументом: аргумент оседает в истории
8
+ // shell и в списке процессов. Только stdin или переменная окружения.
9
+ import chalk from "chalk";
10
+ import { LayeroError, detectMode, emit } from "../agent.js";
11
+ import { target } from "./data-api.js";
12
+ const PROVIDERS = ["yandex", "vkid", "sber"];
13
+ const TITLE = { yandex: "Яндекс", vkid: "VK ID", sber: "Сбер ID" };
14
+ function asJson(opts) {
15
+ return detectMode().json || Boolean(opts.json);
16
+ }
17
+ function provider(name) {
18
+ const p = name.trim().toLowerCase();
19
+ if (!PROVIDERS.includes(p)) {
20
+ throw new LayeroError("invalid_choice", `провайдер «${name}» не поддержан`, `доступны: ${PROVIDERS.join(", ")}`);
21
+ }
22
+ return p;
23
+ }
24
+ function onOff(flag, value) {
25
+ if (value === undefined)
26
+ return undefined;
27
+ const v = value.trim().toLowerCase();
28
+ if (["on", "yes", "true", "1"].includes(v))
29
+ return true;
30
+ if (["off", "no", "false", "0"].includes(v))
31
+ return false;
32
+ throw new LayeroError("invalid_choice", `${flag}: ждали on или off, а пришло «${value}»`, `пример: ${flag} on`);
33
+ }
34
+ async function readStdin() {
35
+ const chunks = [];
36
+ for await (const chunk of process.stdin)
37
+ chunks.push(chunk);
38
+ return Buffer.concat(chunks).toString("utf8").trim();
39
+ }
40
+ export async function dataAuthStatusCmd(opts) {
41
+ const { api, org, db, ref } = await target(opts);
42
+ const s = await api.getDataAuthState(org, db.id);
43
+ const providers = (s.providers ?? []).map((p) => ({
44
+ provider: p.provider, configured: Boolean(p.configured), enabled: Boolean(p.enabled),
45
+ checked_at: p.checked_at ?? null, return_urls: p.return_urls ?? [],
46
+ }));
47
+ const methods = {
48
+ password: s.password_enabled ?? null, link: s.magiclink_enabled ?? null,
49
+ signup_open: s.signup_open ?? null, confirm_email: s.confirm_email ?? null,
50
+ };
51
+ if (asJson(opts)) {
52
+ emit({ event: "data_auth_status", org, database: ref, callback_url: s.callback_url ?? null,
53
+ providers, methods, email_return_urls: s.email_return_urls ?? [] });
54
+ return;
55
+ }
56
+ const yn = (v) => (v === null ? "—" : v ? "да" : "нет");
57
+ console.log(chalk.bold("Способы входа"));
58
+ console.log(` по паролю: ${yn(methods.password)}, по ссылке из письма: ${yn(methods.link)}, ` +
59
+ `регистрация открыта: ${yn(methods.signup_open)}, подтверждение почты: ${yn(methods.confirm_email)}`);
60
+ console.log(chalk.bold("\nПровайдеры"));
61
+ for (const p of providers) {
62
+ const state = p.enabled ? chalk.green("включён") : p.configured ? chalk.yellow("ключи есть, выключен") : chalk.dim("не подключён");
63
+ console.log(` ${TITLE[p.provider] ?? p.provider}: ${state}`);
64
+ }
65
+ if (s.callback_url)
66
+ console.log(chalk.dim(`\nАдрес возврата для консоли провайдера: ${s.callback_url}`));
67
+ }
68
+ export async function dataAuthProviderSetCmd(name, opts) {
69
+ const p = provider(name);
70
+ if (!opts.clientId) {
71
+ throw new LayeroError("bad_format", "не задан Client ID", `layero data auth provider set ${p} --client-id <id>`);
72
+ }
73
+ let secret = "";
74
+ if (p !== "vkid") {
75
+ if (opts.secretFromEnv) {
76
+ secret = process.env[opts.secretFromEnv] ?? "";
77
+ if (!secret)
78
+ throw new LayeroError("bad_format", `переменная окружения ${opts.secretFromEnv} пуста или не задана`, "задайте её или передайте секрет через stdin");
79
+ }
80
+ else if (!process.stdin.isTTY) {
81
+ secret = await readStdin();
82
+ }
83
+ if (!secret) {
84
+ throw new LayeroError("bad_format", `у ${TITLE[p]} нужен секрет`, `передайте его через stdin или --secret-from-env: printf %s "$SECRET" | layero data auth provider set ${p} --client-id <id>`);
85
+ }
86
+ }
87
+ const { api, org, db, ref } = await target(opts);
88
+ await api.setDataAuthProvider(org, db.id, { provider: p, client_id: opts.clientId, client_secret: secret });
89
+ const s = await api.getDataAuthState(org, db.id);
90
+ if (asJson(opts)) {
91
+ emit({ event: "data_auth_provider_set", org, database: ref, provider: p, callback_url: s.callback_url ?? null });
92
+ return;
93
+ }
94
+ console.log(`${chalk.green("✓")} вход через ${TITLE[p]} подключён`);
95
+ if (s.callback_url)
96
+ console.log(chalk.dim(` впишите в консоли провайдера адрес возврата: ${s.callback_url}`));
97
+ console.log(chalk.dim(" проверьте вход в панели: Авторизация → Способы входа → «Проверить вход»"));
98
+ }
99
+ export async function dataAuthProviderToggleCmd(name, enabled, opts) {
100
+ const p = provider(name);
101
+ const { api, org, db, ref } = await target(opts);
102
+ await api.setDataAuthProviderOptions(org, db.id, p, { enabled });
103
+ if (asJson(opts)) {
104
+ emit({ event: "data_auth_provider_toggled", org, database: ref, provider: p, enabled });
105
+ return;
106
+ }
107
+ console.log(`${chalk.green("✓")} вход через ${TITLE[p]} ${enabled ? "включён" : "выключен, ключи сохранены"}`);
108
+ }
109
+ export async function dataAuthProviderRemoveCmd(name, opts) {
110
+ const p = provider(name);
111
+ const { api, org, db, ref } = await target(opts);
112
+ if (!opts.yes) {
113
+ throw new LayeroError("confirmation_required", `удаление ключей ${TITLE[p]} не подтверждено`, `повторите с --yes: layero data auth provider remove ${p} --yes`);
114
+ }
115
+ await api.deleteDataAuthProvider(org, db.id, p);
116
+ if (asJson(opts)) {
117
+ emit({ event: "data_auth_provider_removed", org, database: ref, provider: p });
118
+ return;
119
+ }
120
+ console.log(`${chalk.green("✓")} ключи ${TITLE[p]} удалены, вход через него закрыт`);
121
+ }
122
+ /** `email` — адреса возврата писем входа, иначе — провайдера. */
123
+ export async function dataAuthRedirectsCmd(action, method, url, opts) {
124
+ const m = method.trim().toLowerCase() === "email" ? "email" : provider(method);
125
+ if (action !== "list" && !url) {
126
+ throw new LayeroError("bad_format", "не задан адрес", `layero data auth redirects ${action} ${m} https://shop.example/auth`);
127
+ }
128
+ if (url && !/^https?:\/\//i.test(url)) {
129
+ throw new LayeroError("bad_format", `адрес «${url}» должен начинаться с http:// или https://`, "пример: https://shop.example/auth");
130
+ }
131
+ const { api, org, db, ref } = await target(opts);
132
+ const s = await api.getDataAuthState(org, db.id);
133
+ const current = m === "email"
134
+ ? (s.email_return_urls ?? [])
135
+ : ((s.providers ?? []).find((p) => p.provider === m)?.return_urls ?? []);
136
+ let next = current;
137
+ if (action === "add")
138
+ next = current.includes(url) ? current : [...current, url];
139
+ if (action === "remove")
140
+ next = current.filter((u) => u !== url);
141
+ if (action !== "list") {
142
+ if (m === "email")
143
+ await api.setDataAuthSettings(org, db.id, { email_return_urls: next });
144
+ else
145
+ await api.setDataAuthProviderOptions(org, db.id, m, { return_urls: next });
146
+ }
147
+ if (asJson(opts)) {
148
+ emit({ event: "data_auth_redirects", org, database: ref, method: m, urls: next });
149
+ return;
150
+ }
151
+ console.log(chalk.bold(`Адреса возврата: ${m === "email" ? "письма входа" : TITLE[m]}`));
152
+ for (const u of next)
153
+ console.log(` ${u}`);
154
+ if (!next.length)
155
+ console.log(chalk.dim(" нет — после входа человек вернётся на адрес подключённого проекта"));
156
+ }
157
+ export async function dataAuthMethodsCmd(opts) {
158
+ const patch = {};
159
+ const set = (key, flag, value) => {
160
+ const v = onOff(flag, value);
161
+ if (v !== undefined)
162
+ patch[key] = v;
163
+ };
164
+ set("password_enabled", "--password", opts.password);
165
+ set("magiclink_enabled", "--link", opts.link);
166
+ set("signup_open", "--signup", opts.signup);
167
+ set("confirm_email", "--confirm-email", opts.confirmEmail);
168
+ if (!Object.keys(patch).length) {
169
+ throw new LayeroError("nothing_to_set", "не задан ни один способ", "пример: layero data auth methods --password on --link off --signup on");
170
+ }
171
+ const { api, org, db, ref } = await target(opts);
172
+ await api.setDataAuthSettings(org, db.id, patch);
173
+ if (asJson(opts)) {
174
+ emit({ event: "data_auth_methods_set", org, database: ref, changed: patch });
175
+ return;
176
+ }
177
+ console.log(`${chalk.green("✓")} способы входа обновлены: ${Object.entries(patch).map(([k, v]) => `${k}=${v ? "on" : "off"}`).join(", ")}`);
178
+ }
179
+ export function registerDataAuthCommands(auth, withDb, json) {
180
+ withDb(auth.command("status").description("Sign-in methods, providers and the callback URL for the provider console."))
181
+ .action(async (opts) => dataAuthStatusCmd(json(opts)));
182
+ const prov = auth.command("provider").description("External providers: yandex, vkid, sber.");
183
+ withDb(prov
184
+ .command("set <provider>")
185
+ .description("Save the provider's keys and enable it. The secret is read from stdin or --secret-from-env (VK ID has none).")
186
+ .requiredOption("--client-id <id>", "Client ID from the provider's console")
187
+ .option("--secret-from-env <var>", "take the secret from this environment variable")
188
+ .addHelpText("after", '\nExample:\n $ printf %s "$YANDEX_SECRET" | layero data auth provider set yandex --client-id 1a2b3c')).action(async (name, opts) => dataAuthProviderSetCmd(name, json(opts)));
189
+ withDb(prov.command("on <provider>").description("Enable a provider whose keys are saved."))
190
+ .action(async (name, opts) => dataAuthProviderToggleCmd(name, true, json(opts)));
191
+ withDb(prov.command("off <provider>").description("Disable a provider, keeping its keys."))
192
+ .action(async (name, opts) => dataAuthProviderToggleCmd(name, false, json(opts)));
193
+ withDb(prov.command("remove <provider>").description("Delete the provider's keys.").option("-y, --yes", "do not ask for confirmation"))
194
+ .action(async (name, opts) => dataAuthProviderRemoveCmd(name, json(opts)));
195
+ const red = auth.command("redirects").description("Where the app may send people back after sign-in: per provider, or `email` for sign-in emails.");
196
+ withDb(red.command("list <method>").description("Return addresses of a provider or of `email`."))
197
+ .action(async (method, opts) => dataAuthRedirectsCmd("list", method, undefined, json(opts)));
198
+ withDb(red.command("add <method> <url>").description("Allow a return address."))
199
+ .action(async (method, url, opts) => dataAuthRedirectsCmd("add", method, url, json(opts)));
200
+ withDb(red.command("remove <method> <url>").description("Remove a return address."))
201
+ .action(async (method, url, opts) => dataAuthRedirectsCmd("remove", method, url, json(opts)));
202
+ withDb(auth
203
+ .command("methods")
204
+ .description("Turn sign-in methods on or off.")
205
+ .option("--password <on|off>", "email and password")
206
+ .option("--link <on|off>", "link or code from an email")
207
+ .option("--signup <on|off>", "new users may register")
208
+ .option("--confirm-email <on|off>", "require email confirmation")).action(async (opts) => dataAuthMethodsCmd(json(opts)));
209
+ }
@@ -6,6 +6,7 @@
6
6
  import chalk from "chalk";
7
7
  import { LayeroError, detectMode, emit } from "../agent.js";
8
8
  import { target } from "./data-api.js";
9
+ import { registerDataAuthCommands } from "./data-auth.js";
9
10
  function asJson(opts) {
10
11
  return detectMode().json || Boolean(opts.json);
11
12
  }
@@ -124,7 +125,8 @@ export function registerDataFilesCommands(data, withDb, json) {
124
125
  .option("--max <size>", "file size limit: 5MB, 512KB or bytes")
125
126
  .option("--types <list>", "allowed MIME types, comma-separated: image/jpeg,image/png")
126
127
  .addHelpText("after", "\nExample:\n $ layero data buckets create screenshots --private --max 5MB --types image/jpeg,image/png")).action(async (name, opts) => dataBucketsCreateCmd(name, json(opts)));
127
- const auth = data.command("auth").description("Sign-in settings that used to live only in the dashboard.");
128
+ const auth = data.command("auth").description("Sign-in of the app's users: status, providers (Yandex, VK ID, Sber ID), return addresses, methods, sender name.");
129
+ registerDataAuthCommands(auth, withDb, json);
128
130
  withDb(auth
129
131
  .command("sender <name>")
130
132
  .description("The sender name of sign-in emails (codes, links, password reset).")
package/dist/pack.js CHANGED
@@ -60,11 +60,50 @@ const FORCE_INCLUDE_LOCKFILES = [
60
60
  "pnpm-lock.yaml",
61
61
  "npm-shrinkwrap.json",
62
62
  ];
63
- async function buildIgnore(cwd) {
63
+ // Env files carry secrets. In the `ignore` package the last matching rule
64
+ // wins, so a `.gitignore` written for git — `!.env.example`, or `!.env` in a
65
+ // monorepo that commits a shared one — used to pull env files back into the
66
+ // archive. A static site without a build serves the archive as-is, so that
67
+ // `.env` ended up readable at the site's address.
68
+ //
69
+ // The exclusion is therefore re-applied after each user layer. The only way
70
+ // back in is an explicit negation in `.layeroignore` that names an env file
71
+ // (`!.env.production`): a deliberate, Layero-specific choice. `.gitignore`
72
+ // never overrides it, and neither do broad `.layeroignore` negations such as
73
+ // `!*`.
74
+ //
75
+ // The same broad negation would also have pulled back the other things that
76
+ // must never reach a site: VCS metadata (remote credentials live in
77
+ // `.git/config`), the CLI's own `.layero/` (claim tokens) and the rule files
78
+ // themselves. They get the same treatment, with no way back in at all.
79
+ const ENV_EXCLUDE = [".env", ".env.*"];
80
+ const ALWAYS_EXCLUDE = [
81
+ ...ENV_EXCLUDE,
82
+ ".git",
83
+ ".svn",
84
+ ".hg",
85
+ ".layero",
86
+ ".gitignore",
87
+ ".layeroignore",
88
+ ];
89
+ /** Negations from `.layeroignore` that explicitly name an env file. */
90
+ function explicitEnvNegations(lines) {
91
+ return lines.filter((l) => l.startsWith("!") && l.includes(".env"));
92
+ }
93
+ async function userLayers(cwd) {
94
+ const gitignore = await readIgnoreFile(path.join(cwd, ".gitignore"));
95
+ const layeroignore = await readIgnoreFile(path.join(cwd, ".layeroignore"));
64
96
  const ig = ignore();
65
97
  ig.add(DEFAULT_IGNORE);
66
- ig.add(await readIgnoreFile(path.join(cwd, ".gitignore")));
67
- ig.add(await readIgnoreFile(path.join(cwd, ".layeroignore")));
98
+ ig.add(gitignore);
99
+ ig.add(ALWAYS_EXCLUDE);
100
+ ig.add(layeroignore);
101
+ ig.add(ALWAYS_EXCLUDE);
102
+ ig.add(explicitEnvNegations(layeroignore));
103
+ return ig;
104
+ }
105
+ async function buildIgnore(cwd) {
106
+ const ig = await userLayers(cwd);
68
107
  // Negations last so they win over any ignore rule above.
69
108
  ig.add(FORCE_INCLUDE_LOCKFILES.map((f) => `!/${f}`));
70
109
  return ig;
@@ -106,10 +145,7 @@ async function sha256OfFile(p) {
106
145
  * negations, so it reflects exactly what the user's .gitignore/.layeroignore
107
146
  * would have excluded. */
108
147
  async function detectForcedLockfiles(cwd) {
109
- const probe = ignore();
110
- probe.add(DEFAULT_IGNORE);
111
- probe.add(await readIgnoreFile(path.join(cwd, ".gitignore")));
112
- probe.add(await readIgnoreFile(path.join(cwd, ".layeroignore")));
148
+ const probe = await userLayers(cwd);
113
149
  const forced = [];
114
150
  for (const lf of FORCE_INCLUDE_LOCKFILES) {
115
151
  if (!probe.ignores(lf))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "layero",
3
- "version": "0.11.23",
3
+ "version": "0.11.25",
4
4
  "description": "Layero CLI — deploy a directory or a repo to hosting with build servers in Russia. JSON events and exit codes for AI agents and CI.",
5
5
  "license": "MIT",
6
6
  "type": "module",