naro-cli 0.9.1 → 0.10.0
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 +67 -7
- package/dist/{chunk-Z757IOF3.js → chunk-5U6YPVZS.js} +114 -19
- package/dist/deploy.js +1 -1
- package/dist/index.js +285 -22
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -124,7 +124,16 @@ naro backend keys --reveal Print the service key (a secret; needs databa
|
|
|
124
124
|
naro backend keys rotate --service Replace a key (--anon or --service); the old one stops
|
|
125
125
|
working at once (asks first)
|
|
126
126
|
naro backend auth Sign-in settings: email, Google, GitHub, redirects,
|
|
127
|
-
|
|
127
|
+
the callback URL to register for each production host,
|
|
128
|
+
sign-ups and the minimum password length
|
|
129
|
+
naro backend auth signups off Nobody signs up on their own (on opens them again)
|
|
130
|
+
naro backend auth password --min-length 10
|
|
131
|
+
The fewest characters a new password takes (6–128)
|
|
132
|
+
naro backend users The project's users: id, username, email, verified, created
|
|
133
|
+
naro backend users create --username admin --password-stdin < password.txt
|
|
134
|
+
A confirmed account (--email too, or instead); works
|
|
135
|
+
while sign-ups are closed
|
|
136
|
+
naro backend users delete admin By id, username or email; ends its sessions (asks first)
|
|
128
137
|
naro backend auth email set --provider resend --from "App <no-reply@example.com>" --api-key-stdin < resend-key.txt
|
|
129
138
|
Confirmation and password reset emails from your Resend account
|
|
130
139
|
naro backend auth oauth set github --client-id Iv1.abc --client-secret-stdin < github-secret.txt
|
|
@@ -316,10 +325,12 @@ message that the worker may still expect the new key — run
|
|
|
316
325
|
`naro backend enable`; the CLI prints that hint (JSON `hint`) and exits 1.
|
|
317
326
|
For a few seconds during a rotation the worker can already refuse the old key.
|
|
318
327
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
328
|
+
Sign-in is naro Auth, the project's own (like Supabase Auth). Email and
|
|
329
|
+
password sign-up and sign-in work as soon as the backend is on, with nothing
|
|
330
|
+
to set up. `naro backend auth` adds the rest with the project's own
|
|
331
|
+
accounts — Naro runs no shared sender. Sending email (confirmation and
|
|
332
|
+
password reset links) needs a Resend API key and a `from` address; Google
|
|
333
|
+
and GitHub need the project's own OAuth app. `naro backend auth` prints a callback URL for
|
|
323
334
|
each production host — `https://<project>.naro.sh/_naro/v1/auth/callback/<provider>`
|
|
324
335
|
and the same path on each custom domain. Register, as the app's redirect URI,
|
|
325
336
|
the one for the origin your app passes to `createClient({ url })`: the
|
|
@@ -347,10 +358,59 @@ code or reset token rides in the URL fragment, which any script on the page
|
|
|
347
358
|
— or an open redirect like `/logout?next=…` — can pass on: allow a path of
|
|
348
359
|
sign-in pages rather than a whole origin. Each change uploads
|
|
349
360
|
the Naro worker again; when that upload stops, the change is saved all the
|
|
350
|
-
same and the command exits 1 with `BACKEND_PROVISION_FAILED
|
|
351
|
-
`naro backend enable` to finish.
|
|
361
|
+
same and the command exits 1 with `BACKEND_PROVISION_FAILED`, saying
|
|
362
|
+
whether the worker runs it yet — run `naro backend enable` to finish. Until
|
|
363
|
+
then `naro backend auth` shows the saved settings with a warning that the
|
|
364
|
+
worker may still run older ones (`applied: false` with `--json`), so a
|
|
365
|
+
`signups off` that did not upload never reads back as simply closed.
|
|
366
|
+
`require-verification on` needs email set
|
|
352
367
|
up (`VERIFICATION_NEEDS_EMAIL`), and email cannot be removed while it is on.
|
|
353
368
|
|
|
369
|
+
For an app only its admins sign in to, close sign-ups and create the
|
|
370
|
+
accounts yourself — with sign-ups closed, `authenticated` in a policy means
|
|
371
|
+
exactly the users you made:
|
|
372
|
+
|
|
373
|
+
```bash
|
|
374
|
+
naro backend auth signups off
|
|
375
|
+
naro backend users create --username admin --password-stdin < password.txt
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
`signups off` makes every sign-up answer `403 SIGNUPS_DISABLED` ("Sign-ups
|
|
379
|
+
are closed for this project."), and a first Google or GitHub sign-in too;
|
|
380
|
+
users that exist keep signing in. `naro backend users create` takes
|
|
381
|
+
`--username`, `--email` or both, and the password from stdin
|
|
382
|
+
(`--password-stdin`: a pipe, or a hidden prompt in a terminal). `--password
|
|
383
|
+
<p>` works too, with a warning on stderr: it lands in shell history. The
|
|
384
|
+
account is confirmed and signs in at once — by username with
|
|
385
|
+
`naro.auth.signIn({ username, password })` — also while sign-ups are closed
|
|
386
|
+
and while sign-in waits for a confirmed email; a username-only account has
|
|
387
|
+
`email: null`. The output never carries the password. `naro backend users`
|
|
388
|
+
lists every user (id, username, email, verified, created; `--json` for all
|
|
389
|
+
fields), and `naro backend users delete <id|username|email>` removes one and
|
|
390
|
+
ends its sessions — it asks first, and without a terminal needs `--yes`
|
|
391
|
+
(`CONFIRMATION_REQUIRED` otherwise, exit 1, nothing deleted). A username is
|
|
392
|
+
3–30 letters, digits, `_` and `.`, in any case. The backend's refusals pass
|
|
393
|
+
through: `409 USERNAME_TAKEN` "That username is already taken.",
|
|
394
|
+
`422 USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL`, `400 PASSWORD_TOO_SHORT`
|
|
395
|
+
"Use at least {n} characters.", `429 WRITE_QUOTA_EXCEEDED`.
|
|
396
|
+
|
|
397
|
+
While sign-ups are open, a sign-up with a username someone has answers
|
|
398
|
+
`409 USERNAME_TAKEN`, so anyone can learn which usernames exist, as fast as
|
|
399
|
+
sign-up's per-address rate limit allows. With sign-ups closed, every
|
|
400
|
+
sign-up answers `403 SIGNUPS_DISABLED` first.
|
|
401
|
+
|
|
402
|
+
`naro backend auth password --min-length <n>` sets the fewest characters a
|
|
403
|
+
new password takes, 6 to 128 (6 by default). Sign-up, password reset and
|
|
404
|
+
`users create` refuse a shorter one with "Use at least {n} characters.";
|
|
405
|
+
sign-in never checks it, so passwords set before keep working. Both
|
|
406
|
+
settings re-upload the Naro worker, like the other `naro backend auth`
|
|
407
|
+
changes, and `naro backend auth` shows them (`Sign-ups:`, `Minimum password
|
|
408
|
+
length:`).
|
|
409
|
+
|
|
410
|
+
A backend turned on before user management answers the `users` commands
|
|
411
|
+
`409 BACKEND_OUTDATED`: run `naro backend enable` once to update this
|
|
412
|
+
project's backend, then run the command again.
|
|
413
|
+
|
|
354
414
|
Policies are default deny: a table without one refuses every anon and
|
|
355
415
|
signed-in request. Each of `--select`, `--insert`, `--update` and `--delete`
|
|
356
416
|
takes `none`, `public`, `authenticated` or `owner`; `owner` lets a signed-in
|
|
@@ -18598,6 +18598,15 @@ var TERMINAL_DEPLOYMENT_STATUSES = [
|
|
|
18598
18598
|
function isTerminalStatus(status) {
|
|
18599
18599
|
return TERMINAL_DEPLOYMENT_STATUSES.includes(status);
|
|
18600
18600
|
}
|
|
18601
|
+
var BACKEND_STEPS = [
|
|
18602
|
+
"database",
|
|
18603
|
+
"keys",
|
|
18604
|
+
"secrets",
|
|
18605
|
+
"worker",
|
|
18606
|
+
"routes",
|
|
18607
|
+
"migrate",
|
|
18608
|
+
"health"
|
|
18609
|
+
];
|
|
18601
18610
|
var STALLED_GRACE_MS = 2 * 6e4;
|
|
18602
18611
|
function isStalledBackend(info3, now = Date.now()) {
|
|
18603
18612
|
if (typeof info3.stalled === "boolean") return info3.stalled;
|
|
@@ -19315,6 +19324,35 @@ function backendInfo(body) {
|
|
|
19315
19324
|
turningOff: body.turningOff === true
|
|
19316
19325
|
};
|
|
19317
19326
|
}
|
|
19327
|
+
function authConfig(body) {
|
|
19328
|
+
return {
|
|
19329
|
+
...body,
|
|
19330
|
+
signupsEnabled: typeof body.signupsEnabled === "boolean" ? body.signupsEnabled : true,
|
|
19331
|
+
passwordMinLength: typeof body.passwordMinLength === "number" ? body.passwordMinLength : 6
|
|
19332
|
+
};
|
|
19333
|
+
}
|
|
19334
|
+
function isRecord(value) {
|
|
19335
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
19336
|
+
}
|
|
19337
|
+
async function authChange(answer) {
|
|
19338
|
+
let body;
|
|
19339
|
+
try {
|
|
19340
|
+
body = await answer;
|
|
19341
|
+
} catch (error51) {
|
|
19342
|
+
const details = isNaroError(error51) ? error51.details : void 0;
|
|
19343
|
+
if (isNaroError(error51) && error51.code === "BACKEND_PROVISION_FAILED" && isRecord(details?.auth) && isRecord(details.backend))
|
|
19344
|
+
throw new NaroError(error51.code, error51.message, {
|
|
19345
|
+
...error51.status !== void 0 ? { status: error51.status } : {},
|
|
19346
|
+
details: {
|
|
19347
|
+
...details,
|
|
19348
|
+
auth: authConfig(details.auth),
|
|
19349
|
+
backend: backendInfo(details.backend)
|
|
19350
|
+
}
|
|
19351
|
+
});
|
|
19352
|
+
throw error51;
|
|
19353
|
+
}
|
|
19354
|
+
return { auth: authConfig(body.auth), backend: backendInfo(body.backend) };
|
|
19355
|
+
}
|
|
19318
19356
|
var BackendApi = class {
|
|
19319
19357
|
constructor(client) {
|
|
19320
19358
|
this.client = client;
|
|
@@ -19447,11 +19485,13 @@ var BackendApi = class {
|
|
|
19447
19485
|
* and the callback URLs to register with the providers. Never a secret.
|
|
19448
19486
|
*/
|
|
19449
19487
|
async auth(projectId) {
|
|
19450
|
-
return
|
|
19451
|
-
this.
|
|
19452
|
-
|
|
19453
|
-
|
|
19454
|
-
|
|
19488
|
+
return authConfig(
|
|
19489
|
+
await this.client.request(
|
|
19490
|
+
this.path(projectId, "/auth"),
|
|
19491
|
+
{
|
|
19492
|
+
context: "Read sign-in settings"
|
|
19493
|
+
}
|
|
19494
|
+
)
|
|
19455
19495
|
);
|
|
19456
19496
|
}
|
|
19457
19497
|
/**
|
|
@@ -19460,32 +19500,86 @@ var BackendApi = class {
|
|
|
19460
19500
|
* (nothing is saved), `BACKEND_NOT_READY` before the first provision,
|
|
19461
19501
|
* `BACKEND_OFF` while the backend is turned off (nothing is saved),
|
|
19462
19502
|
* `VERIFICATION_NEEDS_EMAIL` for a state where nobody could sign in. A
|
|
19463
|
-
* failed upload
|
|
19464
|
-
*
|
|
19503
|
+
* failed upload throws `BACKEND_PROVISION_FAILED` (502): the settings are
|
|
19504
|
+
* saved, the message says whether the worker runs them yet, `details`
|
|
19505
|
+
* holds `{ auth, backend }`, and `enable` finishes. (A control plane from
|
|
19506
|
+
* before that resolves with `backend.status` `error` instead.) Two
|
|
19507
|
+
* minutes, like enable.
|
|
19465
19508
|
*/
|
|
19466
19509
|
async setAuth(projectId, patch) {
|
|
19467
|
-
|
|
19468
|
-
this.path(projectId, "/auth"),
|
|
19469
|
-
{
|
|
19510
|
+
return authChange(
|
|
19511
|
+
this.client.request(this.path(projectId, "/auth"), {
|
|
19470
19512
|
method: "PUT",
|
|
19471
19513
|
body: patch,
|
|
19472
19514
|
context: "Change sign-in settings",
|
|
19473
19515
|
timeoutMs: PROVISION_TIMEOUT_MS
|
|
19474
|
-
}
|
|
19516
|
+
})
|
|
19475
19517
|
);
|
|
19476
|
-
return { auth: answer.auth, backend: backendInfo(answer.backend) };
|
|
19477
19518
|
}
|
|
19478
19519
|
/** Removes email or one provider; the same answer as `setAuth`. */
|
|
19479
19520
|
async removeAuth(projectId, target) {
|
|
19521
|
+
return authChange(
|
|
19522
|
+
this.client.request(
|
|
19523
|
+
this.path(projectId, `/auth?target=${encodeURIComponent(target)}`),
|
|
19524
|
+
{
|
|
19525
|
+
method: "DELETE",
|
|
19526
|
+
context: "Change sign-in settings",
|
|
19527
|
+
timeoutMs: PROVISION_TIMEOUT_MS
|
|
19528
|
+
}
|
|
19529
|
+
)
|
|
19530
|
+
);
|
|
19531
|
+
}
|
|
19532
|
+
/**
|
|
19533
|
+
* The project's users, newest first (spec 2026-09-26 §2.2): page 1 and 50
|
|
19534
|
+
* a page unless given, at most 1000. `database.read`. `BACKEND_OFF` while
|
|
19535
|
+
* the backend is turned off, `BACKEND_NOT_READY` before it is ready,
|
|
19536
|
+
* `BACKEND_OUTDATED` while its Naro worker predates user management
|
|
19537
|
+
* (`enable` updates it), `BACKEND_UNAVAILABLE` when it cannot be reached.
|
|
19538
|
+
*/
|
|
19539
|
+
async listUsers(projectId, options = {}) {
|
|
19540
|
+
const query = new URLSearchParams();
|
|
19541
|
+
if (options.page !== void 0) query.set("page", String(options.page));
|
|
19542
|
+
if (options.perPage !== void 0)
|
|
19543
|
+
query.set("perPage", String(options.perPage));
|
|
19544
|
+
const suffix = query.size > 0 ? `?${query.toString()}` : "";
|
|
19545
|
+
const body = await this.client.request(
|
|
19546
|
+
this.path(projectId, `/users${suffix}`),
|
|
19547
|
+
{ context: "List users" }
|
|
19548
|
+
);
|
|
19549
|
+
return { users: body.users, total: body.total };
|
|
19550
|
+
}
|
|
19551
|
+
/**
|
|
19552
|
+
* Creates a user with a password, confirmed unless `emailConfirm: false`
|
|
19553
|
+
* — also while sign-ups are closed and while sign-in waits for a
|
|
19554
|
+
* confirmed email. A username-only user answers `email: null`.
|
|
19555
|
+
* `database.write`. The Naro worker's refusals come through as it said
|
|
19556
|
+
* them: `USERNAME_TAKEN` (409), `USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL`
|
|
19557
|
+
* (422), `PASSWORD_TOO_SHORT` ("Use at least {n} characters."),
|
|
19558
|
+
* `VALIDATION_ERROR`, `WRITE_QUOTA_EXCEEDED` (429, `details.retryAfter`).
|
|
19559
|
+
*/
|
|
19560
|
+
async createUser(projectId, input) {
|
|
19561
|
+
const body = { password: input.password };
|
|
19562
|
+
if (input.email !== void 0) body.email = input.email;
|
|
19563
|
+
if (input.username !== void 0) body.username = input.username;
|
|
19564
|
+
if (input.emailConfirm !== void 0)
|
|
19565
|
+
body.emailConfirm = input.emailConfirm;
|
|
19480
19566
|
const answer = await this.client.request(
|
|
19481
|
-
this.path(projectId,
|
|
19482
|
-
{
|
|
19483
|
-
|
|
19484
|
-
|
|
19485
|
-
|
|
19486
|
-
|
|
19567
|
+
this.path(projectId, "/users"),
|
|
19568
|
+
{ method: "POST", body, context: "Create user" }
|
|
19569
|
+
);
|
|
19570
|
+
return answer.user;
|
|
19571
|
+
}
|
|
19572
|
+
/**
|
|
19573
|
+
* Deletes a user with its sign-in methods, and ends its sessions. Answers
|
|
19574
|
+
* the user it was. `USER_NOT_FOUND` (404) for an id that is not one.
|
|
19575
|
+
* `database.write`.
|
|
19576
|
+
*/
|
|
19577
|
+
async deleteUser(projectId, userId) {
|
|
19578
|
+
const answer = await this.client.request(
|
|
19579
|
+
this.path(projectId, `/users/${encodeURIComponent(userId)}`),
|
|
19580
|
+
{ method: "DELETE", context: "Delete user" }
|
|
19487
19581
|
);
|
|
19488
|
-
return
|
|
19582
|
+
return answer.user;
|
|
19489
19583
|
}
|
|
19490
19584
|
/** Every stored policy, by table name. Tables without one deny anon and signed-in requests. */
|
|
19491
19585
|
async policies(projectId) {
|
|
@@ -21380,6 +21474,7 @@ export {
|
|
|
21380
21474
|
resolveAuth,
|
|
21381
21475
|
openBrowser,
|
|
21382
21476
|
isTerminalStatus,
|
|
21477
|
+
BACKEND_STEPS,
|
|
21383
21478
|
isStalledBackend,
|
|
21384
21479
|
POLICY_RULES,
|
|
21385
21480
|
POLICY_ACTIONS,
|
package/dist/deploy.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
BACKEND_STEPS,
|
|
3
4
|
BOOKMARK_FORMAT_MESSAGE,
|
|
4
5
|
NaroClient,
|
|
5
6
|
NaroError,
|
|
@@ -52,7 +53,7 @@ import {
|
|
|
52
53
|
systemTablePredicate,
|
|
53
54
|
toNaroError,
|
|
54
55
|
writeProjectLink
|
|
55
|
-
} from "./chunk-
|
|
56
|
+
} from "./chunk-5U6YPVZS.js";
|
|
56
57
|
import "./chunk-K3NQKI34.js";
|
|
57
58
|
|
|
58
59
|
// src/args.ts
|
|
@@ -83,7 +84,8 @@ var BOOLEAN_FLAGS = {
|
|
|
83
84
|
"--no-write": "noWrite",
|
|
84
85
|
"--signed-in": "signedIn",
|
|
85
86
|
"--api-key-stdin": "apiKeyStdin",
|
|
86
|
-
"--client-secret-stdin": "clientSecretStdin"
|
|
87
|
+
"--client-secret-stdin": "clientSecretStdin",
|
|
88
|
+
"--password-stdin": "passwordStdin"
|
|
87
89
|
};
|
|
88
90
|
var REPEATED_FLAGS = {
|
|
89
91
|
"--column": "columns"
|
|
@@ -120,7 +122,11 @@ var STRING_FLAGS = {
|
|
|
120
122
|
"--from-file": "fromFile",
|
|
121
123
|
"--provider": "provider",
|
|
122
124
|
"--from": "from",
|
|
123
|
-
"--client-id": "clientId"
|
|
125
|
+
"--client-id": "clientId",
|
|
126
|
+
"--username": "username",
|
|
127
|
+
"--email": "email",
|
|
128
|
+
"--password": "password",
|
|
129
|
+
"--min-length": "minLength"
|
|
124
130
|
};
|
|
125
131
|
var SECRET_FLAGS = {
|
|
126
132
|
"--api-key": "--api-key-stdin",
|
|
@@ -985,12 +991,24 @@ var AUTH_USAGE = {
|
|
|
985
991
|
oauth: "Usage: naro backend auth oauth set google|github --client-id <id> --client-secret-stdin",
|
|
986
992
|
oauthRemove: "Usage: naro backend auth oauth remove google|github",
|
|
987
993
|
redirect: "Usage: naro backend auth redirects add|remove <url>",
|
|
988
|
-
verification: "Usage: naro backend auth require-verification on|off"
|
|
994
|
+
verification: "Usage: naro backend auth require-verification on|off",
|
|
995
|
+
signups: "Usage: naro backend auth signups on|off",
|
|
996
|
+
password: "Usage: naro backend auth password --min-length <n>"
|
|
989
997
|
};
|
|
998
|
+
var MIN_LENGTH_FLOOR = 6;
|
|
999
|
+
var MIN_LENGTH_CEILING = 128;
|
|
990
1000
|
function sentence2(text) {
|
|
991
1001
|
const line = printable(text).trim();
|
|
992
1002
|
return /[.!?…]$/.test(line) ? line : `${line}.`;
|
|
993
1003
|
}
|
|
1004
|
+
function stoppedMessage(backend) {
|
|
1005
|
+
const step = backend.step ?? "worker";
|
|
1006
|
+
const reason = backend.error?.trim() ? `: ${sentence2(backend.error)}` : ".";
|
|
1007
|
+
if (BACKEND_STEPS.indexOf(step) > BACKEND_STEPS.indexOf("worker"))
|
|
1008
|
+
return `Saved and live on the Naro worker, but updating the backend stopped after the upload, at the ${step} step${reason} Run \`naro backend enable\` to finish.`;
|
|
1009
|
+
return `Saved, but not live yet: updating the Naro worker stopped at the ${step} step${reason} The worker keeps its previous settings until \`naro backend enable\` puts the saved ones on it.`;
|
|
1010
|
+
}
|
|
1011
|
+
var NOT_APPLIED_WARNING = "\u26A0 These are the saved settings. The Naro worker's last update did not get past its upload, so it may still run older ones \u2014 run `naro backend enable` to put these on it.";
|
|
994
1012
|
function authLines(auth) {
|
|
995
1013
|
const lines = [
|
|
996
1014
|
auth.email ? `Email: ${auth.email.provider} \u2014 ${printable(auth.email.from)} (API key ${printable(auth.email.apiKeyPrefix)}\u2026)` : "Email: not set up"
|
|
@@ -1003,7 +1021,9 @@ function authLines(auth) {
|
|
|
1003
1021
|
}
|
|
1004
1022
|
lines.push(
|
|
1005
1023
|
`Redirects: ${[`${productionOrigins(auth).join(", ")} (production hosts, always)`, ...auth.redirectOrigins].join(", ")}`,
|
|
1006
|
-
`Email verification: ${auth.requireEmailVerification ? "required to sign in" : "not required"}
|
|
1024
|
+
`Email verification: ${auth.requireEmailVerification ? "required to sign in" : "not required"}`,
|
|
1025
|
+
`Sign-ups: ${auth.signupsEnabled ? "open" : "closed (only `naro backend users create` makes accounts)"}`,
|
|
1026
|
+
`Minimum password length: ${String(auth.passwordMinLength)}`
|
|
1007
1027
|
);
|
|
1008
1028
|
return lines;
|
|
1009
1029
|
}
|
|
@@ -1026,9 +1046,9 @@ function adviceLines2(auth) {
|
|
|
1026
1046
|
"Redirect URIs to register with each provider \u2014 the one for the origin your app passes to createClient({ url }):"
|
|
1027
1047
|
);
|
|
1028
1048
|
for (const provider of ["google", "github"]) {
|
|
1029
|
-
const
|
|
1049
|
+
const label2 = ` ${LABELS[provider]}: `;
|
|
1030
1050
|
callbacksOf(auth, provider).forEach(
|
|
1031
|
-
(url, index) => lines.push(`${index === 0 ?
|
|
1051
|
+
(url, index) => lines.push(`${index === 0 ? label2 : " ".repeat(label2.length)}${url}`)
|
|
1032
1052
|
);
|
|
1033
1053
|
}
|
|
1034
1054
|
return lines;
|
|
@@ -1037,6 +1057,7 @@ async function runBackendAuthStatus(context) {
|
|
|
1037
1057
|
const client = await context.client();
|
|
1038
1058
|
const projectId = await context.resolveProjectId();
|
|
1039
1059
|
const auth = await client.backend.auth(projectId);
|
|
1060
|
+
if (auth.applied === false) context.output.warn(NOT_APPLIED_WARNING);
|
|
1040
1061
|
context.output.result({ projectId, auth }, [
|
|
1041
1062
|
...authLines(auth),
|
|
1042
1063
|
...adviceLines2(auth)
|
|
@@ -1066,17 +1087,22 @@ async function change(context, send, done, after) {
|
|
|
1066
1087
|
"The change did not answer within 2 minutes. `naro backend auth` shows what is saved, and `naro backend enable` finishes putting it on the Naro worker.",
|
|
1067
1088
|
{ cause: error }
|
|
1068
1089
|
);
|
|
1090
|
+
const saved = isNaroError(error) ? error.details?.auth : void 0;
|
|
1091
|
+
if (isNaroError(error) && error.code === "BACKEND_PROVISION_FAILED" && typeof saved === "object" && saved !== null) {
|
|
1092
|
+
for (const line of authLines(saved))
|
|
1093
|
+
context.output.log(line);
|
|
1094
|
+
throw new NaroError(error.code, error.message, {
|
|
1095
|
+
details: { projectId, ...error.details }
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1069
1098
|
throw error;
|
|
1070
1099
|
}
|
|
1071
1100
|
const { auth, backend } = answer;
|
|
1072
1101
|
if (backend.status === "error") {
|
|
1073
1102
|
for (const line of authLines(auth)) context.output.log(line);
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
`Saved, but updating the Naro worker stopped at ${step}${backend.error ? `: ${sentence2(backend.error)}` : "."} Run \`naro backend enable\` to finish.`,
|
|
1078
|
-
{ details: { projectId, auth, backend } }
|
|
1079
|
-
);
|
|
1103
|
+
throw new NaroError("BACKEND_PROVISION_FAILED", stoppedMessage(backend), {
|
|
1104
|
+
details: { projectId, auth, backend }
|
|
1105
|
+
});
|
|
1080
1106
|
}
|
|
1081
1107
|
if (backend.status !== "ready")
|
|
1082
1108
|
context.output.warn(
|
|
@@ -1272,6 +1298,199 @@ async function runBackendAuthRequireVerification(context) {
|
|
|
1272
1298
|
]
|
|
1273
1299
|
);
|
|
1274
1300
|
}
|
|
1301
|
+
async function runBackendAuthSignups(context) {
|
|
1302
|
+
const [value] = context.positionals;
|
|
1303
|
+
if (value !== "on" && value !== "off")
|
|
1304
|
+
throw new NaroError("VALIDATION_FAILED", AUTH_USAGE.signups);
|
|
1305
|
+
const open = value === "on";
|
|
1306
|
+
const client = await context.client();
|
|
1307
|
+
await change(
|
|
1308
|
+
context,
|
|
1309
|
+
(projectId) => client.backend.setAuth(projectId, { signupsEnabled: open }),
|
|
1310
|
+
[
|
|
1311
|
+
open ? "\u2705 Sign-ups are open: anyone can create an account, by email or with Google or GitHub." : "\u2705 Sign-ups are closed: sign-up answers 403 SIGNUPS_DISABLED, and a first Google or GitHub sign-in is refused. Existing users still sign in; create accounts with `naro backend users create`."
|
|
1312
|
+
]
|
|
1313
|
+
);
|
|
1314
|
+
}
|
|
1315
|
+
async function runBackendAuthPassword(context) {
|
|
1316
|
+
const given = context.flags.minLength;
|
|
1317
|
+
if (given === void 0 || context.positionals.length > 0)
|
|
1318
|
+
throw new NaroError("VALIDATION_FAILED", AUTH_USAGE.password);
|
|
1319
|
+
const length = /^\d{1,3}$/.test(given) ? Number(given) : Number.NaN;
|
|
1320
|
+
if (!Number.isInteger(length) || length < MIN_LENGTH_FLOOR || length > MIN_LENGTH_CEILING)
|
|
1321
|
+
throw new NaroError(
|
|
1322
|
+
"VALIDATION_FAILED",
|
|
1323
|
+
`--min-length must be a whole number from ${String(MIN_LENGTH_FLOOR)} to ${String(MIN_LENGTH_CEILING)}. ${AUTH_USAGE.password}`
|
|
1324
|
+
);
|
|
1325
|
+
const client = await context.client();
|
|
1326
|
+
await change(
|
|
1327
|
+
context,
|
|
1328
|
+
(projectId) => client.backend.setAuth(projectId, { passwordMinLength: length }),
|
|
1329
|
+
[
|
|
1330
|
+
`\u2705 New passwords take at least ${String(length)} characters: sign-up, password reset and \`naro backend users create\`. Passwords set before still sign in.`
|
|
1331
|
+
]
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
// src/commands/backend-users.ts
|
|
1336
|
+
var USERS_USAGE = {
|
|
1337
|
+
create: "Usage: naro backend users create (--username <name> | --email <address>) (--password-stdin | --password <password>)",
|
|
1338
|
+
delete: "Usage: naro backend users delete <id|username|email> [--yes]"
|
|
1339
|
+
};
|
|
1340
|
+
var PASSWORD_FLAG_WARNING = "\u26A0 --password puts the password in your shell history. Pipe it in with --password-stdin instead.";
|
|
1341
|
+
var OUTDATED_MESSAGE = "This project's backend is older than user management. Run `naro backend enable` once to update this project's backend. Then run this command again.";
|
|
1342
|
+
var PAGE_SIZE = 1e3;
|
|
1343
|
+
var processStdin3 = {
|
|
1344
|
+
stdinIsTty: () => process.stdin.isTTY === true,
|
|
1345
|
+
readStdin
|
|
1346
|
+
};
|
|
1347
|
+
async function usersCall(call) {
|
|
1348
|
+
try {
|
|
1349
|
+
return await call();
|
|
1350
|
+
} catch (error) {
|
|
1351
|
+
if (isNaroError(error) && error.code === "BACKEND_OUTDATED")
|
|
1352
|
+
throw new NaroError("BACKEND_OUTDATED", OUTDATED_MESSAGE, {
|
|
1353
|
+
...error.status !== void 0 ? { status: error.status } : {},
|
|
1354
|
+
cause: error
|
|
1355
|
+
});
|
|
1356
|
+
throw error;
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
async function allUsers(client, projectId) {
|
|
1360
|
+
const users = [];
|
|
1361
|
+
let total = 0;
|
|
1362
|
+
for (let page = 1; ; page++) {
|
|
1363
|
+
const listed = await usersCall(
|
|
1364
|
+
() => client.backend.listUsers(projectId, { page, perPage: PAGE_SIZE })
|
|
1365
|
+
);
|
|
1366
|
+
total = listed.total;
|
|
1367
|
+
users.push(...listed.users);
|
|
1368
|
+
if (listed.users.length < PAGE_SIZE || users.length >= total)
|
|
1369
|
+
return { users, total };
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
function orDash(value) {
|
|
1373
|
+
return value === null ? "-" : printable(value);
|
|
1374
|
+
}
|
|
1375
|
+
function label(user) {
|
|
1376
|
+
return printable(user.username ?? user.email ?? user.id);
|
|
1377
|
+
}
|
|
1378
|
+
function userTable(users) {
|
|
1379
|
+
const rows = users.map((user) => [
|
|
1380
|
+
printable(user.id),
|
|
1381
|
+
orDash(user.username),
|
|
1382
|
+
orDash(user.email),
|
|
1383
|
+
user.emailVerified ? "yes" : "no",
|
|
1384
|
+
printable(user.createdAt)
|
|
1385
|
+
]);
|
|
1386
|
+
const header = ["ID", "USERNAME", "EMAIL", "VERIFIED", "CREATED"];
|
|
1387
|
+
const widths = header.map(
|
|
1388
|
+
(title, column) => Math.max(title.length, ...rows.map((row) => row[column]?.length ?? 0))
|
|
1389
|
+
);
|
|
1390
|
+
return [header, ...rows].map(
|
|
1391
|
+
(row) => row.map(
|
|
1392
|
+
(cell, column) => column === row.length - 1 ? cell : cell.padEnd(widths[column] ?? 0)
|
|
1393
|
+
).join(" ")
|
|
1394
|
+
);
|
|
1395
|
+
}
|
|
1396
|
+
async function runBackendUsersList(context) {
|
|
1397
|
+
const client = await context.client();
|
|
1398
|
+
const projectId = await context.resolveProjectId();
|
|
1399
|
+
const { users, total } = await allUsers(client, projectId);
|
|
1400
|
+
context.output.result(
|
|
1401
|
+
{ projectId, users, total },
|
|
1402
|
+
users.length === 0 ? [
|
|
1403
|
+
"No users yet. Create one with `naro backend users create --username <name> --password-stdin`."
|
|
1404
|
+
] : [
|
|
1405
|
+
`${String(total)} ${total === 1 ? "user" : "users"}, newest first:`,
|
|
1406
|
+
...userTable(users)
|
|
1407
|
+
]
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1410
|
+
async function passwordOf(context, input) {
|
|
1411
|
+
const { password, passwordStdin } = context.flags;
|
|
1412
|
+
if (password !== void 0 && passwordStdin)
|
|
1413
|
+
throw new NaroError(
|
|
1414
|
+
"VALIDATION_FAILED",
|
|
1415
|
+
`Pass the password one way: --password-stdin or --password. ${USERS_USAGE.create}`
|
|
1416
|
+
);
|
|
1417
|
+
if (password !== void 0) {
|
|
1418
|
+
context.output.warn(PASSWORD_FLAG_WARNING);
|
|
1419
|
+
if (password === "")
|
|
1420
|
+
throw new NaroError("VALIDATION_FAILED", "The password is empty.");
|
|
1421
|
+
return password;
|
|
1422
|
+
}
|
|
1423
|
+
if (!passwordStdin)
|
|
1424
|
+
throw new NaroError(
|
|
1425
|
+
"VALIDATION_FAILED",
|
|
1426
|
+
`Pass the password on stdin with --password-stdin. ${USERS_USAGE.create}`
|
|
1427
|
+
);
|
|
1428
|
+
const value = input.stdinIsTty() ? await context.prompter.askHidden("? Password: ") : await input.readStdin();
|
|
1429
|
+
if (value === "")
|
|
1430
|
+
throw new NaroError("VALIDATION_FAILED", "No password came in on stdin.");
|
|
1431
|
+
return value;
|
|
1432
|
+
}
|
|
1433
|
+
function userLines(user) {
|
|
1434
|
+
return [
|
|
1435
|
+
`ID: ${printable(user.id)}`,
|
|
1436
|
+
`Username: ${orDash(user.username)}`,
|
|
1437
|
+
`Email: ${orDash(user.email)}`,
|
|
1438
|
+
`Verified: ${user.emailVerified ? "yes" : "no"}`,
|
|
1439
|
+
`Created: ${printable(user.createdAt)}`
|
|
1440
|
+
];
|
|
1441
|
+
}
|
|
1442
|
+
async function runBackendUsersCreate(context, input = processStdin3) {
|
|
1443
|
+
const { username, email } = context.flags;
|
|
1444
|
+
if (username === void 0 && email === void 0 || context.positionals.length > 0)
|
|
1445
|
+
throw new NaroError("VALIDATION_FAILED", USERS_USAGE.create);
|
|
1446
|
+
const password = await passwordOf(context, input);
|
|
1447
|
+
const client = await context.client();
|
|
1448
|
+
const projectId = await context.resolveProjectId();
|
|
1449
|
+
const body = { password };
|
|
1450
|
+
if (username !== void 0) body.username = username;
|
|
1451
|
+
if (email !== void 0) body.email = email;
|
|
1452
|
+
const user = await usersCall(
|
|
1453
|
+
() => client.backend.createUser(projectId, body)
|
|
1454
|
+
);
|
|
1455
|
+
const signIn = user.username === null ? "naro.auth.signIn({ email, password })" : `naro.auth.signIn({ username: ${JSON.stringify(printable(user.username))}, password })`;
|
|
1456
|
+
context.output.result({ projectId, user }, [
|
|
1457
|
+
`\u2705 Created user ${label(user)}. It signs in at once with ${signIn}.`,
|
|
1458
|
+
...userLines(user)
|
|
1459
|
+
]);
|
|
1460
|
+
}
|
|
1461
|
+
function find(users, given) {
|
|
1462
|
+
const byId = users.find((user) => user.id === given);
|
|
1463
|
+
if (byId) return byId;
|
|
1464
|
+
const wanted = given.toLowerCase();
|
|
1465
|
+
return users.find(
|
|
1466
|
+
(user) => given.includes("@") ? user.email?.toLowerCase() === wanted : user.username?.toLowerCase() === wanted
|
|
1467
|
+
);
|
|
1468
|
+
}
|
|
1469
|
+
async function runBackendUsersDelete(context) {
|
|
1470
|
+
const [given] = context.positionals;
|
|
1471
|
+
if (given === void 0 || context.positionals.length > 1)
|
|
1472
|
+
throw new NaroError("VALIDATION_FAILED", USERS_USAGE.delete);
|
|
1473
|
+
const client = await context.client();
|
|
1474
|
+
const projectId = await context.resolveProjectId();
|
|
1475
|
+
const { users } = await allUsers(client, projectId);
|
|
1476
|
+
const user = find(users, given);
|
|
1477
|
+
if (!user)
|
|
1478
|
+
throw new NaroError(
|
|
1479
|
+
"USER_NOT_FOUND",
|
|
1480
|
+
`No user has the id, username or email ${printable(given)}. \`naro backend users\` lists them.`
|
|
1481
|
+
);
|
|
1482
|
+
const named = user.id === label(user) ? label(user) : `${label(user)} (${printable(user.id)})`;
|
|
1483
|
+
await confirmOrAbort(
|
|
1484
|
+
context,
|
|
1485
|
+
`Delete the user ${named}? Their sessions end at once and the account cannot be brought back.`
|
|
1486
|
+
);
|
|
1487
|
+
const deleted = await usersCall(
|
|
1488
|
+
() => client.backend.deleteUser(projectId, user.id)
|
|
1489
|
+
);
|
|
1490
|
+
context.output.result({ projectId, user: deleted }, [
|
|
1491
|
+
`\u2705 Deleted user ${named}. Their sessions have ended.`
|
|
1492
|
+
]);
|
|
1493
|
+
}
|
|
1275
1494
|
|
|
1276
1495
|
// src/commands/cancel.ts
|
|
1277
1496
|
async function runCancel(context) {
|
|
@@ -1300,7 +1519,7 @@ async function runCancel(context) {
|
|
|
1300
1519
|
// src/commands/db.ts
|
|
1301
1520
|
import { readFileSync as readFileSync2 } from "fs";
|
|
1302
1521
|
import { resolve } from "path";
|
|
1303
|
-
var
|
|
1522
|
+
var processStdin4 = {
|
|
1304
1523
|
stdinIsTty: () => process.stdin.isTTY === true,
|
|
1305
1524
|
readStdin
|
|
1306
1525
|
};
|
|
@@ -1418,7 +1637,7 @@ async function readSql(context, deps) {
|
|
|
1418
1637
|
throw new NaroError("VALIDATION_FAILED", USAGE);
|
|
1419
1638
|
return sql;
|
|
1420
1639
|
}
|
|
1421
|
-
async function runDbQuery(context, deps =
|
|
1640
|
+
async function runDbQuery(context, deps = processStdin4) {
|
|
1422
1641
|
const sql = await readSql(context, deps);
|
|
1423
1642
|
const client = await context.client();
|
|
1424
1643
|
const projectId = await context.resolveProjectId();
|
|
@@ -1682,7 +1901,7 @@ async function runDbRestore(context) {
|
|
|
1682
1901
|
target = resolved.bookmark;
|
|
1683
1902
|
at = resolved.timestamp ?? given.timestamp;
|
|
1684
1903
|
}
|
|
1685
|
-
const
|
|
1904
|
+
const label2 = at === null ? `bookmark ${target}` : `${at} (bookmark ${target})`;
|
|
1686
1905
|
if (context.flags.dryRun) {
|
|
1687
1906
|
context.output.result(
|
|
1688
1907
|
{
|
|
@@ -1690,13 +1909,13 @@ async function runDbRestore(context) {
|
|
|
1690
1909
|
bookmark: target,
|
|
1691
1910
|
...at === null ? {} : { timestamp: at }
|
|
1692
1911
|
},
|
|
1693
|
-
[`Would restore to ${
|
|
1912
|
+
[`Would restore to ${label2}. Nothing was changed.`]
|
|
1694
1913
|
);
|
|
1695
1914
|
return;
|
|
1696
1915
|
}
|
|
1697
1916
|
await confirmOrAbort(
|
|
1698
1917
|
context,
|
|
1699
|
-
`Restore the project database to ${
|
|
1918
|
+
`Restore the project database to ${label2}? Everything written after that point is rolled back \u2014 migration history included \u2014 and queries running right now are cancelled.`
|
|
1700
1919
|
);
|
|
1701
1920
|
const before = await bookmarkNow(context, client, projectId);
|
|
1702
1921
|
let restored;
|
|
@@ -2487,9 +2706,9 @@ async function resolveTeam(context, teams, deps = {}) {
|
|
|
2487
2706
|
);
|
|
2488
2707
|
const account = deps.accountLabel ? await deps.accountLabel() : "this account";
|
|
2489
2708
|
if (only) {
|
|
2490
|
-
const
|
|
2709
|
+
const label2 = only.isPersonal ? `${only.name} (personal)` : only.name;
|
|
2491
2710
|
context.output.log(`
|
|
2492
|
-
${account} \u2192 ${
|
|
2711
|
+
${account} \u2192 ${label2}`);
|
|
2493
2712
|
const ok = await context.prompter.confirm(
|
|
2494
2713
|
"Deploy to this account and team?"
|
|
2495
2714
|
);
|
|
@@ -4191,9 +4410,16 @@ Usage:
|
|
|
4191
4410
|
naro backend keys [--reveal] API keys; --reveal prints the service key (a secret; needs database write)
|
|
4192
4411
|
naro backend keys rotate --anon|--service
|
|
4193
4412
|
Replace a key; the old one stops working at once (asks first)
|
|
4194
|
-
naro backend auth Sign-in settings: email, Google, GitHub, redirects, callback URLs
|
|
4413
|
+
naro backend auth Sign-in settings: email, Google, GitHub, redirects, callback URLs,
|
|
4414
|
+
sign-ups and the minimum password length
|
|
4415
|
+
naro backend auth signups <on|off>
|
|
4416
|
+
off: nobody signs up on their own, by email or Google/GitHub;
|
|
4417
|
+
the accounts you create (naro backend users create) sign in
|
|
4418
|
+
naro backend auth password --min-length <n>
|
|
4419
|
+
The fewest characters a new password takes (6-128; 6 by default)
|
|
4195
4420
|
naro backend auth email set --provider resend --from "App <no-reply@example.com>" --api-key-stdin
|
|
4196
4421
|
Confirmation and reset emails from your Resend account
|
|
4422
|
+
(email and password sign-in needs no setup)
|
|
4197
4423
|
naro backend auth email remove Turn email off (asks first)
|
|
4198
4424
|
naro backend auth oauth set <google|github> --client-id <id> --client-secret-stdin
|
|
4199
4425
|
naro backend auth oauth remove <google|github> Turn a provider off (asks first)
|
|
@@ -4205,7 +4431,14 @@ Usage:
|
|
|
4205
4431
|
Exits 1, changing nothing, when <url> is not in the list
|
|
4206
4432
|
naro backend auth require-verification <on|off>
|
|
4207
4433
|
Refuse sign-in until the email is confirmed (needs email)
|
|
4208
|
-
naro
|
|
4434
|
+
naro backend users The project's users: id, username, email, verified, created
|
|
4435
|
+
naro backend users create --username <name> --password-stdin
|
|
4436
|
+
Create a confirmed account (--email <address> instead of or with
|
|
4437
|
+
--username); works while sign-ups are closed. --password <p>
|
|
4438
|
+
works too, but lands in shell history
|
|
4439
|
+
naro backend users delete <id|username|email>
|
|
4440
|
+
Delete a user and end its sessions (asks first; --yes to skip)
|
|
4441
|
+
naro policy list Who the Data API lets through, per table (no policy = deny)
|
|
4209
4442
|
naro policy set <table> [--select <rule>] [--insert <rule>] [--update <rule>] [--delete <rule>]
|
|
4210
4443
|
[--owner <column>] Rules: none | public | authenticated | owner (owner needs --owner)
|
|
4211
4444
|
[--select-when <filter>] [--insert-when <filter>] [--update-when <filter>]
|
|
@@ -4249,6 +4482,11 @@ Global options:
|
|
|
4249
4482
|
--client-id <id> The OAuth app's client ID (naro backend auth oauth set)
|
|
4250
4483
|
--api-key-stdin | --client-secret-stdin
|
|
4251
4484
|
Read the secret from stdin (a pipe, or a hidden prompt); never from argv
|
|
4485
|
+
--username <name> | --email <address>
|
|
4486
|
+
The new user's username (3-30 of a-z, 0-9, _ and .) and/or email (naro backend users create)
|
|
4487
|
+
--password-stdin Read the new user's password from stdin (a pipe, or a hidden prompt)
|
|
4488
|
+
--password <p> The password in argv: works, but lands in shell history (naro backend users create)
|
|
4489
|
+
--min-length <n> Fewest characters of a new password, 6-128 (naro backend auth password)
|
|
4252
4490
|
--select <rule> Rule for reads; --insert, --update, --delete likewise (naro policy set)
|
|
4253
4491
|
--owner <column> Column that holds the signed-in user's id, for the owner rule (naro policy set, db table create)
|
|
4254
4492
|
--select-when <filter> Condition for reads, e.g. 'user_id.eq.auth.uid()'; --insert-when (what a new
|
|
@@ -4315,6 +4553,29 @@ function routeBackendAuth(words) {
|
|
|
4315
4553
|
handler: runBackendAuthRequireVerification,
|
|
4316
4554
|
positionals: words.slice(1)
|
|
4317
4555
|
};
|
|
4556
|
+
// Spec 2026-09-26 §2.1, §2.4.
|
|
4557
|
+
case "signups":
|
|
4558
|
+
return { handler: runBackendAuthSignups, positionals: words.slice(1) };
|
|
4559
|
+
case "password":
|
|
4560
|
+
return { handler: runBackendAuthPassword, positionals: words.slice(1) };
|
|
4561
|
+
default:
|
|
4562
|
+
return null;
|
|
4563
|
+
}
|
|
4564
|
+
}
|
|
4565
|
+
function routeBackendUsers(words) {
|
|
4566
|
+
const [action] = words;
|
|
4567
|
+
const rest = words.slice(1);
|
|
4568
|
+
switch (action) {
|
|
4569
|
+
case void 0:
|
|
4570
|
+
case "list":
|
|
4571
|
+
case "ls":
|
|
4572
|
+
return rest.length === 0 ? { handler: runBackendUsersList, positionals: [] } : null;
|
|
4573
|
+
case "create":
|
|
4574
|
+
return { handler: runBackendUsersCreate, positionals: rest };
|
|
4575
|
+
case "delete":
|
|
4576
|
+
case "remove":
|
|
4577
|
+
case "rm":
|
|
4578
|
+
return { handler: runBackendUsersDelete, positionals: rest };
|
|
4318
4579
|
default:
|
|
4319
4580
|
return null;
|
|
4320
4581
|
}
|
|
@@ -4484,6 +4745,8 @@ function route(parsed) {
|
|
|
4484
4745
|
return null;
|
|
4485
4746
|
case "auth":
|
|
4486
4747
|
return routeBackendAuth(positionals.slice(1));
|
|
4748
|
+
case "users":
|
|
4749
|
+
return routeBackendUsers(positionals.slice(1));
|
|
4487
4750
|
default:
|
|
4488
4751
|
return null;
|
|
4489
4752
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "naro-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"naro": "./dist/index.js"
|
|
@@ -33,10 +33,10 @@
|
|
|
33
33
|
"tsx": "4.23.12",
|
|
34
34
|
"typescript": "7.0.2",
|
|
35
35
|
"vitest": "3.2.7",
|
|
36
|
-
"
|
|
36
|
+
"@next-deploy/config": "0.1.0",
|
|
37
|
+
"naro-js": "0.2.0",
|
|
37
38
|
"@next-deploy/build": "0.1.0",
|
|
38
39
|
"@next-deploy/core": "0.1.0",
|
|
39
|
-
"@next-deploy/config": "0.1.0",
|
|
40
40
|
"@next-deploy/data-core": "0.1.0",
|
|
41
41
|
"@next-deploy/sdk": "0.1.0"
|
|
42
42
|
},
|