naro-cli 0.11.0 → 0.13.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
@@ -69,6 +69,15 @@ naro logs <deployment-id> Build logs of a deployment
69
69
  naro logs --type build --follow Stream build logs until READY/FAILED
70
70
  naro logs --env preview Preview-surface runtime logs
71
71
 
72
+ naro insights Speed Insights: Web Vitals p75 (--metric lcp, --range 7d)
73
+ naro insights --route '/posts/[id]' The paths under one route
74
+ naro insights --env preview The same for the preview worker
75
+ naro insights enable | disable Turn measuring on or off
76
+
77
+ naro protection Preview protection: on or off, and whether a bypass secret is set
78
+ naro protection enable | disable Protect the preview URL (members only) or open it to anyone with the link (disable asks first; --yes in CI)
79
+ naro protection bypass [remove] Create (shown once) or remove the secret automation sends as x-naro-protection-bypass (replacing or removing asks first; --yes in CI)
80
+
72
81
  naro env list [--reveal] Secrets masked unless --reveal
73
82
  naro env add DATABASE_URL production
74
83
  naro env remove DATABASE_URL
@@ -91,9 +100,11 @@ naro db dump --output backup.sql SQL dump, streamed from Cloudflare (stdout wi
91
100
  naro db restore --bookmark <id> Time Travel, last 30 days (or --timestamp <time>);
92
101
  asks first, prints the command that undoes it
93
102
  naro db pull Existing database, no migrations yet: writes its schema
94
- as migrations/0001_remote_schema.sql, recorded as applied
103
+ and policies as migrations/0001_remote_schema.sql,
104
+ recorded as applied
95
105
  naro db reset [--dry-run] Drop every table, view and trigger, clear the ledger and
96
- apply ./migrations again (asks first; --yes in CI)
106
+ the policies, and apply ./migrations again (asks first;
107
+ --yes in CI; refuses files the server would refuse)
97
108
  naro db lint [--strict] Check the schema for what D1 actually trips on; exits 1
98
109
  on an error, and on a warning too with --strict
99
110
  naro db table create posts --column "id:integer:pk:autoincrement" --column "title:text:notnull" --column "created_at:datetime:notnull:default=current_timestamp" --owner user_id
@@ -158,6 +169,18 @@ naro policy set posts --from-file policy.json
158
169
  Shorthand: writes the policies naro_select … naro_delete
159
170
  naro policy remove todos Drop every policy on the table: back to deny (asks first)
160
171
 
172
+ naro storage Naro Storage: on or off, limits, usage, buckets
173
+ naro storage enable Turn it on: the project's R2 bucket, bound to the backend
174
+ naro storage bucket create avatars --public --file-size-limit 5MiB --allowed-mime-types image/png,image/jpeg
175
+ naro storage bucket update avatars --private | --file-size-limit none | --allowed-mime-types any
176
+ naro storage bucket update docs --public (asks first: anyone with an object's URL can download it)
177
+ naro storage bucket list | empty <name> | remove <name> [--empty] (empty and remove ask first)
178
+ naro storage ls naro://avatars/u1 [--recursive]
179
+ naro storage cp ./me.png naro://avatars/u1/me.png [--upsert] Up to 50 MiB, with the service key
180
+ naro storage cp naro://docs/team-1/plan.pdf ./ Replaces a local file of the same name
181
+ naro storage rm naro://avatars/u1/old.png [--recursive] (asks first)
182
+ naro storage rm naro://avatars/u1/ --recursive Everything under u1/; written u1, an object u1 too
183
+
161
184
  naro promote <deployment-id> Publish a build to production
162
185
  naro rollback [deployment-id] Back to the previous production build
163
186
  ```
@@ -191,12 +214,43 @@ reads the bookmark from just before the drops and prints it, so
191
214
  `naro db restore --bookmark <that>` puts the data back. `--dry-run` lists what
192
215
  it would drop and re-apply without touching anything.
193
216
 
217
+ The backend's accounts, sessions, settings and the other `_naro_` tables are
218
+ kept — except its policies: they are cleared with the schema and the migration
219
+ files create them again. Before it reads anything, `db reset` checks the files
220
+ as `naro db push --dry-run` does: when the control plane would refuse one, the
221
+ re-apply would stop there with the schema already dropped, so the reset is
222
+ refused with `VALIDATION_FAILED`, each file as `<file>: Statement <i> of <n>:
223
+ …` (`--dry-run` prints them and exits 1). A policy that exists only in the
224
+ database would be lost — one made with `naro policy create` or
225
+ `naro policy set`, and the owner policy that `naro db table create --owner`,
226
+ MCP's `create_table` and the table editor add, which no migration file holds.
227
+ So before it asks, `db reset` lists the policies your files do not make, each
228
+ with the SQL that makes it again — its `CREATE POLICY` and, for an owner rule,
229
+ the `COMMENT ON POLICY … IS 'naro:owner <column>'` that keeps its column
230
+ (`--json`: `policiesNotInFiles[].sql`); add it to a migration first to keep
231
+ them. The policies of a table your files do not create go with that table and
232
+ are named apart, with no SQL (`--json`: `policiesOfTablesNotInFiles`): a
233
+ migration that made them would stop at a table that is not there. A file the
234
+ re-apply would stop at that the check above does not catch (one whose SQL
235
+ cannot be split) is named instead — `<file> would be refused: <reason>`
236
+ (`--json`: `wouldBeRefused`) — and then no policy is called lost. A policy row
237
+ that cannot be read is listed as a `-- Policy … cannot be read` comment
238
+ instead: copying it keeps nothing, so drop that policy and create it again. A
239
+ backend whose worker predates named policies is refused with
240
+ `409 BACKEND_OUTDATED`: run `naro backend enable` first.
241
+
194
242
  `naro db lint` reads the schema and runs the checks D1 allows. Its rules are
195
243
  the ones D1 actually trips on — orphaned rows, a failed `quick_check`, a
196
244
  missing or nullable primary key, a foreign key no index starts with, a trigger
197
245
  the REST API cannot recreate, a virtual table that makes `db dump` refuse the
198
- whole database. There is no "unused index" guess: D1 keeps no execution
199
- statistics to base one on. An error exits 1, a warning exits 0 — `--strict`
246
+ whole database. On a project with a backend it also checks the policies:
247
+ `rls_no_policy` (a table the Data API refuses entirely),
248
+ `rls_policy_text_default` (a number column a policy reads as a boolean whose
249
+ `DEFAULT` is text, such as `BOOLEAN DEFAULT 'true'`) and
250
+ `rls_policy_unindexed` (warnings), and `rls_policy_invalid`, a policy the
251
+ backend cannot apply — one that reads a text column as true or false among
252
+ them (an error). There is no "unused index" guess: D1 keeps
253
+ no execution statistics to base one on. An error exits 1, a warning exits 0 — `--strict`
200
254
  makes warnings fail too, so CI can gate on it:
201
255
 
202
256
  ```bash
@@ -217,7 +271,9 @@ from a fixed list — and applies it as one migration, recorded in
217
271
  `d1_migrations` and written to `./migrations` as `NNNN_schema_<op>_<table>.sql`,
218
272
  numbered after your local files and the ledger. `naro migration list`,
219
273
  `db pull` and `db reset` see it like any other migration; with `--no-write`
220
- there is no file, so `db reset` will not re-apply it. `--dry-run` prints the
274
+ there is no file, so `db reset` will not re-apply it. The owner policy
275
+ `--owner` adds is not in that file either: `db reset` lists it, with its SQL,
276
+ before it removes it — add that SQL to a migration to keep it. `--dry-run` prints the
221
277
  SQL and changes nothing. While a local migration is not in the ledger yet, a
222
278
  change is refused — `naro db push` it first, or the change would run before it.
223
279
 
@@ -233,15 +289,17 @@ Every primary key but a single `integer` one is `NOT NULL`; a foreign key must
233
289
  point at its table's primary key or a unique column. `--owner user_id` adds
234
290
  `user_id TEXT NOT NULL` and, when the backend is on and ready, an owner policy for all
235
291
  four actions; any other new table is closed to the Data API until you give it
236
- a policy (`naro policy create` or `naro policy set`; the command says so).
292
+ a policy (`CREATE POLICY` in a migration, or `naro policy create` or
293
+ `naro policy set`; the command says so).
237
294
  Policies are kept by table name: a drop removes the table's policies whenever
238
295
  the project has a backend, on or off (the command says so). A rename leaves
239
296
  them under the old name, where they apply to no table — the command prints
240
297
  what to run to move them. Move them then: the next time `naro db table`
241
298
  creates a table as that name or renames one to it, renaming the table back
242
299
  included, the policies stored under the name are removed first, so a new
243
- table never inherits an earlier one's (a table made by `naro db push` or
244
- `naro db query` gets them as they are). Whatever a command removes it prints
300
+ table never inherits an earlier one's (except, on a project without a
301
+ backend, one changed through `naro db table`, the table editor or MCP) — and
302
+ neither does a table made by `naro db push` or `naro db query`. Whatever a command removes it prints
245
303
  so it can be made again: as `CREATE POLICY` statements, which
246
304
  `naro policy create` takes back, and — for owner rules, whose owner column
247
305
  `CREATE POLICY` cannot say — as the `naro policy set <table> … --owner <column>`
@@ -439,8 +497,112 @@ project's backend, then run the command again.
439
497
 
440
498
  Policies are Postgres's row-level security, as in Supabase. A table without a
441
499
  policy refuses every anon and signed-in request; the service key skips
442
- policies. Write them as `CREATE POLICY`, a change is live within about 10
443
- seconds, and `naro policy list` prints them back as SQL:
500
+ policies. Write a table's policies in the migration that creates it —
501
+ `CREATE POLICY` after the `CREATE TABLE`, in the same file. `naro db push`,
502
+ `naro migration up`, `naro db query` and the MCP tools `apply_migration` and
503
+ `execute_sql` apply them together with the table, all or nothing. Three shapes
504
+ cover most apps:
505
+
506
+ ```sql
507
+ -- migrations/0002_todos.sql: each signed-in user reads and changes their own rows.
508
+ CREATE TABLE todos (
509
+ id integer PRIMARY KEY,
510
+ user_id uuid NOT NULL,
511
+ title text NOT NULL
512
+ );
513
+ CREATE POLICY "own rows" ON todos FOR ALL TO authenticated
514
+ USING ((select auth.uid()) = user_id)
515
+ WITH CHECK ((select auth.uid()) = user_id);
516
+
517
+ -- migrations/0003_posts.sql: anyone reads, signed in or not.
518
+ CREATE TABLE posts (id integer PRIMARY KEY, title text NOT NULL);
519
+ CREATE POLICY "read" ON posts FOR SELECT TO anon, authenticated USING (true);
520
+
521
+ -- migrations/0004_reports.sql: an app only its admins sign in to. First run
522
+ -- naro backend auth signups off and create the admin accounts: authenticated
523
+ -- then means exactly those accounts.
524
+ CREATE TABLE reports (id integer PRIMARY KEY, body text NOT NULL);
525
+ CREATE POLICY "admins" ON reports FOR ALL TO authenticated USING (true) WITH CHECK (true);
526
+ ```
527
+
528
+ A migration or a query takes `CREATE POLICY`; `DROP POLICY [IF EXISTS]` (`IF
529
+ EXISTS` skips a missing table too, as in Postgres); `COMMENT ON POLICY p ON t
530
+ IS 'naro:owner <column>'`, which makes `p` the owner rule `naro policy set
531
+ --owner` writes (any other comment is ignored); and `ALTER TABLE t ENABLE |
532
+ FORCE | NO FORCE ROW LEVEL SECURITY`, which changes nothing because
533
+ row-level security is always on. Before anything runs, naro refuses these with
534
+ `400 VALIDATION_FAILED` "Statement <i> of <n>: …":
535
+
536
+ - `ALTER TABLE … DISABLE ROW LEVEL SECURITY` — it is always on; to let
537
+ everyone through, create a policy: `CREATE POLICY "open" ON t USING (true)
538
+ WITH CHECK (true);`
539
+ - `GRANT` and `REVOKE` — every table is behind row-level security, so write a
540
+ policy instead;
541
+ - `ALTER POLICY` — `DROP POLICY` and `CREATE POLICY` again;
542
+ - a table named `public.x` in `CREATE TABLE`, `DROP TABLE` or a rename: `SQLite
543
+ has no schema "public": write the table name without "public."`
544
+ - a `CREATE POLICY` that reads a column inside a subquery with no table in
545
+ front of it, when a later statement of the same file creates, drops, renames
546
+ or alters the policy's table or a table its subqueries read: `a later
547
+ statement changes a table, so name the table of every column inside this
548
+ policy's subqueries (for example members.user_id).` Postgres decides whose
549
+ column it is when the policy is created; naro binds it once the file is in,
550
+ when the later statement may have added that name to the subquery's table.
551
+ A later statement that changes only other tables — the next table of a file
552
+ grouped table by table — leaves the policy alone.
553
+
554
+ The statements of one request apply all-or-nothing, the policies with the
555
+ tables: a policy on a table that does not exist
556
+ (`relation "nope" does not exist`), a name already taken, or a 33rd policy on
557
+ one table fails the whole request with `400 DATABASE_QUERY_FAILED`, names the
558
+ statement, and applies nothing — not even the `CREATE TABLE` before it. Policy
559
+ statements, and statements that create, drop or rename a table, take no
560
+ `params` (`400 VALIDATION_FAILED`): write the values into the SQL.
561
+
562
+ Policies go with their table, as in Postgres: `DROP TABLE` drops them, and a
563
+ new table never inherits the leftovers of an earlier one with the same name
564
+ (except tables changed through the table editor on a project without a backend).
565
+ SQLite changes a column by building the table again — `CREATE TABLE
566
+ __new_x`, copy the rows, `DROP TABLE x`, `ALTER TABLE __new_x RENAME TO x` — so
567
+ the drop removes `x`'s policies and the rename does not bring them back. Write
568
+ the `CREATE POLICY` statements again after the rename, in the same migration.
569
+ A plain `ALTER TABLE a RENAME TO b` does not move them either: they stay under
570
+ `a`, and `b` has none until you create them again.
571
+ The answer's `warnings` lists the policies a migration removed, with SQL you
572
+ can paste; it also says when the project has no backend yet (the policies are
573
+ saved and apply once `naro backend enable` turns it on) and when a policy was
574
+ applied that the Data API refuses, with the reason. `naro db push` and
575
+ `naro db query` print each warning as `⚠ …` on stderr (`--json` has it whole
576
+ in `warnings`, a query's in `result.warnings`).
577
+
578
+ `naro db push --dry-run` parses the policy statements of the pending files on
579
+ your machine and prints what it would refuse as `<file>: Statement <i> of <n>:
580
+ …` (exit 1); whether a table or a policy exists is known only when the file is
581
+ applied. `naro db pull` writes the policies that exist at the end of the
582
+ baseline file — `ALTER TABLE … ENABLE ROW LEVEL SECURITY;` for each table,
583
+ then each `CREATE POLICY …;`, an owner rule's column as `COMMENT ON POLICY …
584
+ IS 'naro:owner <column>';` (a database without policies pulls as plain
585
+ SQLite). `naro db reset` clears the policies with the schema and creates them
586
+ again from the migration files, so a policy that exists only in the database
587
+ is lost; it lists those before it asks, each with the SQL to add to a
588
+ migration to keep it (an owner rule's column as its `COMMENT ON POLICY`; a
589
+ row that cannot be read is only a comment — drop it and create it again), and
590
+ names apart, without SQL, the policies of tables your files do not create. It
591
+ refuses to start when the control plane would refuse one of the files.
592
+ `naro db lint`, on a
593
+ project with a backend, reports `rls_no_policy` (a table the Data API refuses
594
+ entirely), `rls_policy_text_default` and `rls_policy_unindexed` as warnings,
595
+ and `rls_policy_invalid` — a
596
+ policy the backend cannot apply, such as one that reads a text column as true
597
+ or false — as an error.
598
+
599
+ `naro policy create` sends the same statements to the database directly: one
600
+ in quotes, or `--file` with several, `DROP POLICY` included. Nothing records
601
+ them in `./migrations`, so `naro db reset` removes what your files do not
602
+ make; use it for a table that already exists. A change is live within about 10
603
+ seconds, and `naro policy list` prints the policies back as SQL. More complete
604
+ policies — they give each user their own todos, everyone the published posts
605
+ and team members their team's docs, and hide deleted docs from all of them:
444
606
 
445
607
  ```sql
446
608
  -- Each signed-in user reads and changes their own todos.
@@ -560,7 +722,9 @@ policy — `naro policy create`, `naro policy drop` and `naro policy set` — wi
560
722
  `naro backend enable` uploads the current backend and turns the stored rules
561
723
  into named policies; it also turns a backend that is off back on, so run
562
724
  `naro backend disable` again afterwards if it should stay off. `naro policy
563
- list` and `naro policy remove` work in the meantime.
725
+ list` and `naro policy remove` work in the meantime. A migration or a query
726
+ that holds a `CREATE POLICY` or `DROP POLICY` gets the same answer before
727
+ anything runs; SQL without one runs as before.
564
728
 
565
729
  `naro policy set` refuses with `409 POLICY_EXISTS` when the table's `naro_<action>` policy is not one it wrote (for example one made with `naro policy create`): `Policy <name> on <table> is not one naro policy set wrote. Change it with naro policy create, or drop it first: naro policy drop <name> --table <table>.` A `naro_<action>` policy that cannot be read as stored (the table answers `500 POLICY_INVALID`) is not refused: setting a rule replaces it, and `none` drops it.
566
730
 
@@ -584,7 +748,21 @@ pass — reads a row as it will be stored and compares it with the column's
584
748
  collation (`COLLATE NOCASE`, `RTRIM`). So:
585
749
 
586
750
  - a number column it reads takes numbers, booleans and null, not strings, and
587
- cannot be compared with `like` or `ilike`;
751
+ cannot be compared with `like` or `ilike` — the Data API refuses a string
752
+ for a column that any policy of any table reads (an owner rule's own id
753
+ column excepted) with `400 INVALID_QUERY`. For a column whose declared type
754
+ has `INT`, `REAL`, `FLOA`, `DOUB`, `NUM`, `DEC`, `BOOL` or `BIT` in it, that
755
+ is every string — and even when no policy reads it, a string SQLite would
756
+ keep as text (`'abc'`, `'true'`; `'7'` is stored as 7), which Postgres
757
+ refuses as a type error and a policy written later would read inside out. A
758
+ column declared another type (`uuid`, `date`, `timestamptz`, `json`) keeps a
759
+ string as text, so Supabase's `user_id uuid` with `auth.uid() = user_id`
760
+ works; there the Data API refuses only a string SQLite would store as a
761
+ number (`'12'`), a string with a NUL character, and any string when a
762
+ policy reads the column as true or false or compares it with a number. On such a
763
+ column a `WITH CHECK` that orders it against a number (`<`, `>`, `BETWEEN`)
764
+ can judge a new text differently from the row it becomes, and `LIKE` or a
765
+ text function on it is refused as on a number column;
588
766
  - a primary key column it reads must be sent, and not as null;
589
767
  - any other column it reads may be left out only when it has no default or a
590
768
  plain one — `NULL`, `TRUE`, `FALSE`, a number, or a quoted string on a column
@@ -609,6 +787,83 @@ An `--update-check` you give replaces the default: without
609
787
  `user_id`. Write
610
788
  `--update-when 'user_id.eq.auth.uid()' --update-check 'user_id.eq.auth.uid(),status.eq.draft'`.
611
789
 
790
+ ## Release notes
791
+
792
+ ### 0.13.0
793
+
794
+ - `naro insights` shows the project's Speed Insights from the terminal: each
795
+ Web Vital's p75 with its good, needs-improvement and poor shares, then the
796
+ selected metric (`--metric`, `--range`) by page, device, the elements
797
+ behind slow visits and deployment. Production by default; `--env preview`
798
+ reads the preview worker. `--route '/posts/[id]'` lists the paths under
799
+ one route, `--json` prints the whole report, and
800
+ `naro insights enable | disable` turns measuring on or off for production
801
+ and preview together (one switch, so `--env` is refused there).
802
+ - `naro protection` shows whether the project's preview URL is protected and
803
+ whether a bypass secret is set. Protection is on by default: only members
804
+ of the project can open `<name>-preview.naro.sh`, after signing in to naro.
805
+ A GET or HEAD that is a page navigation (`Sec-Fetch-Mode: navigate`), or that
806
+ sends no `Sec-Fetch-Mode` and an `Accept` including `text/html`, is
807
+ redirected to the naro sign-in; every other request gets
808
+ `401 PREVIEW_PROTECTED`. A request the browser sends without cookies (a
809
+ `<link rel="manifest">` without `crossorigin="use-credentials"`) gets 401
810
+ too.
811
+ `naro protection enable` and `naro protection disable` switch it (the edge
812
+ picks the change up within about a minute). `naro protection bypass`
813
+ creates the secret CI or a monitor sends as the `x-naro-protection-bypass`
814
+ header (shown once; it replaces any earlier one) and
815
+ `naro protection bypass remove` deletes it. `disable`, a `bypass` that
816
+ replaces a secret and `bypass remove` ask first; without a terminal they need
817
+ `--yes` and exit 1 with `CONFIRMATION_REQUIRED` otherwise, nothing written.
818
+ The Naro backend API (`/_naro/v1`) and production are never protected.
819
+ - `naro storage` manages the project's Naro Storage: `enable` turns it on,
820
+ `bucket list | create | update | empty | remove` manage the buckets
821
+ (public or private, the largest file, the types taken), and `ls`, `cp`
822
+ and `rm` work on `naro://<bucket>/<path>` with the project's service key
823
+ (`NARO_SERVICE_KEY`, or fetched with a warning) — the bytes go to the
824
+ project's backend, not through naro. One upload is 50 MiB at most.
825
+ `bucket update --public`, `empty`, `remove` and `rm` ask first; without a
826
+ terminal they need `--yes` (`CONFIRMATION_REQUIRED` otherwise, nothing
827
+ sent).
828
+ - `naro policy create` and `naro policy set` take `storage.objects`, as
829
+ migrations already did. A backend from before Storage answers
830
+ `409 BACKEND_OUTDATED`.
831
+ - `naro db push --dry-run`, `naro db reset` and `naro db lint` read
832
+ `CREATE POLICY … ON storage.objects`. 0.12.0 refused such a file on this
833
+ machine (`schema storage does not exist`), and its `db lint` and
834
+ `db reset` did not know Storage's policies.
835
+
836
+ ### 0.12.0
837
+
838
+ - Migrations and `naro db query` take Postgres's `CREATE POLICY`,
839
+ `DROP POLICY` and `COMMENT ON POLICY … IS 'naro:owner <column>'` next to
840
+ the tables they are for (see Row-level security). `naro db reset` clears
841
+ the policies with the schema and creates them again from your migration
842
+ files; before it drops anything it checks the files as
843
+ `naro db push --dry-run` does, and lists the policies your files do not
844
+ make, with the SQL to keep them.
845
+ - A failed `db push` keeps the server's `details.warnings` of the file that
846
+ failed (the policies a table rebuild took, with their SQL) and prints them
847
+ as `⚠` lines; the warnings of the files before it are
848
+ `details.appliedWarnings`.
849
+ - **Update the CLI before you run `naro db reset`.** `naro-cli` 0.11.0
850
+ (published on 2026-10-05 from main `98bf538b`, PR #82) and every older
851
+ version know nothing of policies in migrations. On a project whose backend
852
+ has named policies, the control plane now drops a table's policies with
853
+ the table, so their `db reset` takes every policy along — without listing
854
+ first the ones your files do not make again, and without reading the
855
+ warning that names them. A table left without a policy refuses every
856
+ Data API request until you create one again.
857
+ - **Check your clients before the backend updates.** The Data API now
858
+ refuses a string that SQLite would keep as text in a column declared with
859
+ INT, REAL, FLOA, DOUB, NUM, DEC, BOOL or BIT (a form's `"true"` for a
860
+ BOOLEAN, `"high"` for an INTEGER), even when no policy reads the column.
861
+ The backend worker takes this on at the project's next deploy or
862
+ `naro backend enable`, not when naro itself updates. Before that, look for
863
+ such rows: `naro db query "select count(*) from t where typeof(c) = 'text'"`
864
+ for each number or boolean column — a count above 0 means a client writes
865
+ strings there and needs fixing first.
866
+
612
867
  ## Machine-readable output (CI / AI agents)
613
868
 
614
869
  Every command supports `--json`: stdout carries exactly one JSON object,
@@ -647,7 +902,9 @@ command run non-interactively without `--yes` fails with
647
902
 
648
903
  `--yes` (or `-y`) suppresses every prompt — destructive actions
649
904
  (production rollback/promote, env/domain removal, database reset, backend key
650
- rotation, policy removal and drop) run unconfirmed, so use it deliberately in
905
+ rotation, policy removal and drop, opening the preview with
906
+ `naro protection disable`, replacing or removing the bypass secret) run
907
+ unconfirmed, so use it deliberately in
651
908
  automation.
652
909
 
653
910
  ## CI authentication
@@ -674,4 +931,5 @@ NARO_TOKEN=$(naro token) npx naro-cli deploy --prod --yes --json
674
931
  | ------------------------ | ---------------------------------------------- |
675
932
  | `NARO_TOKEN` | CI/agent access token (skips `naro login`) |
676
933
  | `NARO_CONTROL_PLANE_URL` | Custom control plane (default https://naro.sh) |
934
+ | `NARO_SERVICE_KEY` | Service key for `naro storage ls\|cp\|rm` |
677
935
  | `NARO_SUPABASE_URL/KEY` | Auth backend override (self-hosted setups) |