naro-cli 0.9.0 → 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 CHANGED
@@ -112,7 +112,7 @@ naro migration repair 0001_init.sql --status applied
112
112
  reverted to drop its row. Its SQL is never run
113
113
  naro gen types --output src/database.types.ts
114
114
  TypeScript types for the schema, for
115
- createClient<Database>() from @naro/sdk
115
+ createClient<Database>() from naro-js
116
116
 
117
117
  naro backend status On or off, status, URL and anon key (service key hidden)
118
118
  naro backend enable Turn the backend on: D1, sign-in and the Data API; safe
@@ -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
- and the callback URL to register for each production host
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
@@ -241,7 +250,7 @@ says what you asked for, it says so and writes nothing.
241
250
 
242
251
  `naro gen types` writes TypeScript for the schema: a `…Row` interface per
243
252
  table and view, and a `Database` type that `createClient<Database>()` from
244
- `@naro/sdk` reads, with `Tables<"name">`, `TablesInsert<"name">` and
253
+ `naro-js` reads, with `Tables<"name">`, `TablesInsert<"name">` and
245
254
  `TablesUpdate<"name">` to pick one out. `--lang typescript` is the default and
246
255
  the only language.
247
256
 
@@ -250,7 +259,7 @@ naro gen types --output src/database.types.ts
250
259
  ```
251
260
 
252
261
  ```ts
253
- import { createClient } from "@naro/sdk";
262
+ import { createClient } from "naro-js";
254
263
  import type { Database } from "./database.types";
255
264
 
256
265
  const naro = createClient<Database>({ url, anonKey });
@@ -273,7 +282,7 @@ and BLOB was `ArrayBuffer`: use `Tables<"users">` now. The `…Row` interfaces
273
282
  are unchanged.
274
283
 
275
284
  `naro backend` is the project's Naro backend: its own D1, email and password
276
- sign-in, and a Data API that browser code calls through `@naro/sdk` at the
285
+ sign-in, and a Data API that browser code calls through `naro-js` at the
277
286
  project's URL. It is off until you turn it on, one project at a time:
278
287
  `naro backend enable` (or the Database page of the dashboard). The anon key
279
288
  is public — it identifies the project and grants nothing. The service key
@@ -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
- `naro backend auth` sets up email and social sign-in with the project's own
320
- accounts — Naro runs no shared sender. Email (confirmation and password reset
321
- links) needs a Resend API key and a `from` address; Google and GitHub need
322
- the project's own OAuth app. `naro backend auth` prints a callback URL for
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` — run
351
- `naro backend enable` to finish. `require-verification on` needs email set
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
@@ -362,7 +422,7 @@ the generated `Insert` even when it is `NOT NULL` with no default — run it
362
422
  again after changing a policy.
363
423
 
364
424
  Instead of a rule, an action can hold a condition on the row, like a Supabase
365
- row-level security policy, written in the filter syntax of `@naro/sdk`'s
425
+ row-level security policy, written in the filter syntax of `naro-js`'s
366
426
  `.or()` (commas are AND) with `auth.uid()` for the signed-in user's id —
367
427
  unquoted; `"auth.uid()"` is the text, and `auth.uid` without `()` is refused
368
428
  rather than read as text:
@@ -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 this.client.request(
19451
- this.path(projectId, "/auth"),
19452
- {
19453
- context: "Read sign-in settings"
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 is not an error — `backend.status` says `error` and the
19464
- * settings are saved; `enable` finishes. Two minutes, like enable.
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
- const answer = await this.client.request(
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, `/auth?target=${encodeURIComponent(target)}`),
19482
- {
19483
- method: "DELETE",
19484
- context: "Change sign-in settings",
19485
- timeoutMs: PROVISION_TIMEOUT_MS
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 { auth: answer.auth, backend: backendInfo(answer.backend) };
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
@@ -5,7 +5,7 @@ import {
5
5
  parseDotenv,
6
6
  readProjectEnv,
7
7
  slugify
8
- } from "./chunk-Z757IOF3.js";
8
+ } from "./chunk-5U6YPVZS.js";
9
9
  import "./chunk-K3NQKI34.js";
10
10
  export {
11
11
  deployDirectory,
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-Z757IOF3.js";
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 label = ` ${LABELS[provider]}: `;
1049
+ const label2 = ` ${LABELS[provider]}: `;
1030
1050
  callbacksOf(auth, provider).forEach(
1031
- (url, index) => lines.push(`${index === 0 ? label : " ".repeat(label.length)}${url}`)
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
- const step = backend.step ? `the ${backend.step} step` : "a step";
1075
- throw new NaroError(
1076
- "BACKEND_PROVISION_FAILED",
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 processStdin3 = {
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 = processStdin3) {
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 label = at === null ? `bookmark ${target}` : `${at} (bookmark ${target})`;
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 ${label}. Nothing was changed.`]
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 ${label}? Everything written after that point is rolled back \u2014 migration history included \u2014 and queries running right now are cancelled.`
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 label = only.isPersonal ? `${only.name} (personal)` : only.name;
2709
+ const label2 = only.isPersonal ? `${only.name} (personal)` : only.name;
2491
2710
  context.output.log(`
2492
- ${account} \u2192 ${label}`);
2711
+ ${account} \u2192 ${label2}`);
2493
2712
  const ok = await context.prompter.confirm(
2494
2713
  "Deploy to this account and team?"
2495
2714
  );
@@ -4179,7 +4398,7 @@ Usage:
4179
4398
  --status applied|reverted [--yes] remove its row. Its SQL is never run (asks first)
4180
4399
  naro gen types [--lang typescript] [--output <file>]
4181
4400
  TypeScript types for the schema, for createClient<Database>()
4182
- from @naro/sdk (stdout by default)
4401
+ from naro-js (stdout by default)
4183
4402
 
4184
4403
  naro backend [status] On or off, status, URL and anon key (the service key stays hidden)
4185
4404
  naro backend enable Turn the backend on: D1, sign-in and the Data API (safe to re-run;
@@ -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 policy list Who the Data API lets through, per table (no policy = deny)
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.9.0",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "naro": "./dist/index.js"
@@ -33,11 +33,11 @@
33
33
  "tsx": "4.23.12",
34
34
  "typescript": "7.0.2",
35
35
  "vitest": "3.2.7",
36
- "@naro/sdk": "0.1.0",
37
- "@next-deploy/build": "0.1.0",
38
36
  "@next-deploy/config": "0.1.0",
39
- "@next-deploy/data-core": "0.1.0",
37
+ "naro-js": "0.2.0",
38
+ "@next-deploy/build": "0.1.0",
40
39
  "@next-deploy/core": "0.1.0",
40
+ "@next-deploy/data-core": "0.1.0",
41
41
  "@next-deploy/sdk": "0.1.0"
42
42
  },
43
43
  "scripts": {