layero 0.11.22 → 0.11.24
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 +13 -0
- package/dist/bin/layero.js +1 -0
- package/dist/commands/data-auth.js +209 -0
- package/dist/commands/data-files.js +3 -1
- package/dist/commands/deploy.js +13 -0
- package/dist/exit-codes.js +1 -0
- package/package.json +1 -1
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
|
}
|
package/dist/bin/layero.js
CHANGED
|
@@ -452,6 +452,7 @@ async function main() {
|
|
|
452
452
|
"(layero.json buildCommand or the package.json build script) and serves the folder with index.html")
|
|
453
453
|
.option("--name <name>", "project name (only used on first deploy)")
|
|
454
454
|
.option("--db <database>", "connect the organization's database before the build, so the first build already gets the Data API URL and key")
|
|
455
|
+
.option("--address <label>", "site address of a NEW project: inbox → inbox.layero.app (ignored for an existing project)")
|
|
455
456
|
.option("--project <id_or_slug>", "deploy into an existing project, ignoring local config")
|
|
456
457
|
.option("-y, --yes", "non-interactive: accept defaults and skip the --prod confirmation")
|
|
457
458
|
.option("--config", "(legacy alias of the default behaviour — auto-detect + .layero/project.json values)")
|
|
@@ -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
|
|
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/commands/deploy.js
CHANGED
|
@@ -728,6 +728,7 @@ export async function deployCmd(opts) {
|
|
|
728
728
|
runtime_kind_origin: setup.runtime_kind_origin,
|
|
729
729
|
root_directory: opts.root ?? null,
|
|
730
730
|
env_vars: existing?.env_vars ?? {},
|
|
731
|
+
...(opts.address ? { address: opts.address } : {}),
|
|
731
732
|
commit_message: prebuiltDir
|
|
732
733
|
? `CLI prebuilt deploy (${prebuiltDir})`
|
|
733
734
|
: "CLI deploy",
|
|
@@ -743,6 +744,18 @@ export async function deployCmd(opts) {
|
|
|
743
744
|
// подменять его на `internal` значит ломать их обработку.
|
|
744
745
|
throw new LayeroError("project_not_found", `no project with id/slug "${opts.project}"`, "run `layero projects list` to see available projects");
|
|
745
746
|
}
|
|
747
|
+
// «Адрес по умолчанию недоступен» — с кодом, а не голым 409, и с флагом,
|
|
748
|
+
// которым его задать: раньше совет «укажите вручную» указывал в никуда (R2-13).
|
|
749
|
+
if (err instanceof ApiError && err.status === 409 && err.body.includes("address_required")) {
|
|
750
|
+
let message = "адрес по умолчанию недоступен";
|
|
751
|
+
try {
|
|
752
|
+
message = JSON.parse(err.body)?.detail?.message ?? message;
|
|
753
|
+
}
|
|
754
|
+
catch {
|
|
755
|
+
// тело не JSON — оставляем общий текст
|
|
756
|
+
}
|
|
757
|
+
throw new LayeroError("address_required", message, "задайте адрес сайта: layero deploy --address <имя> (станет <имя>.layero.app)");
|
|
758
|
+
}
|
|
746
759
|
if (err instanceof ApiError && err.status === 403) {
|
|
747
760
|
throw new LayeroError("forbidden", `access denied: ${err.body.slice(0, 200)}`, "check that your token has the required scope and that you're a member of the organization");
|
|
748
761
|
}
|
package/dist/exit-codes.js
CHANGED
package/package.json
CHANGED