naro-cli 0.10.0 → 0.11.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 +181 -45
- package/dist/artifact-types.d.ts +5 -0
- package/dist/{chunk-5U6YPVZS.js → chunk-L7JOGWP3.js} +2341 -114
- package/dist/deploy.js +1 -1
- package/dist/git-info.d.ts +14 -1
- package/dist/index.js +2061 -1815
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -144,12 +144,19 @@ naro backend auth redirects remove https://app.example.com/auth/
|
|
|
144
144
|
Exits 1, changing nothing, when it is not in the list
|
|
145
145
|
naro backend auth require-verification on
|
|
146
146
|
Refuse sign-in until the email is confirmed (needs email)
|
|
147
|
-
naro policy list
|
|
147
|
+
naro policy list [--table todos] Row-level security policies, as CREATE POLICY
|
|
148
|
+
naro policy create 'CREATE POLICY "own todos" ON todos TO authenticated USING (user_id = auth.uid()) WITH CHECK (user_id = auth.uid())'
|
|
149
|
+
naro policy create --file policies.sql
|
|
150
|
+
CREATE POLICY and DROP POLICY statements, run in order;
|
|
151
|
+
--allow-unindexed accepts a subquery on the outer row
|
|
152
|
+
that no index serves (refused otherwise)
|
|
153
|
+
naro policy drop "own todos" --table todos
|
|
154
|
+
DROP POLICY (asks first)
|
|
148
155
|
naro policy set todos --select owner --insert owner --update owner --delete owner --owner user_id
|
|
149
156
|
naro policy set posts --select-when 'or(is_public.eq.true,user_id.eq.auth.uid())' --insert-when 'user_id.eq.auth.uid()'
|
|
150
157
|
naro policy set posts --from-file policy.json
|
|
151
|
-
|
|
152
|
-
naro policy remove todos
|
|
158
|
+
Shorthand: writes the policies naro_select … naro_delete
|
|
159
|
+
naro policy remove todos Drop every policy on the table: back to deny (asks first)
|
|
153
160
|
|
|
154
161
|
naro promote <deployment-id> Publish a build to production
|
|
155
162
|
naro rollback [deployment-id] Back to the previous production build
|
|
@@ -225,11 +232,28 @@ A column is `name:type[:modifier…]`:
|
|
|
225
232
|
Every primary key but a single `integer` one is `NOT NULL`; a foreign key must
|
|
226
233
|
point at its table's primary key or a unique column. `--owner user_id` adds
|
|
227
234
|
`user_id TEXT NOT NULL` and, when the backend is on and ready, an owner policy for all
|
|
228
|
-
four actions; any other new table is closed to the Data API until you
|
|
229
|
-
`naro policy
|
|
230
|
-
drop removes the table's
|
|
231
|
-
|
|
232
|
-
|
|
235
|
+
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).
|
|
237
|
+
Policies are kept by table name: a drop removes the table's policies whenever
|
|
238
|
+
the project has a backend, on or off (the command says so). A rename leaves
|
|
239
|
+
them under the old name, where they apply to no table — the command prints
|
|
240
|
+
what to run to move them. Move them then: the next time `naro db table`
|
|
241
|
+
creates a table as that name or renames one to it, renaming the table back
|
|
242
|
+
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
|
|
245
|
+
so it can be made again: as `CREATE POLICY` statements, which
|
|
246
|
+
`naro policy create` takes back, and — for owner rules, whose owner column
|
|
247
|
+
`CREATE POLICY` cannot say — as the `naro policy set <table> … --owner <column>`
|
|
248
|
+
command that sets them again. A statement that cannot be shown as it is (a
|
|
249
|
+
control character, or a line break inside a string) prints as a `--` comment
|
|
250
|
+
in its place; `--json` has the exact text.
|
|
251
|
+
|
|
252
|
+
A policy reads columns and, in a subquery, other tables. When a rename or a
|
|
253
|
+
drop takes one of them away, the Data API answers `500 POLICY_INVALID` to the
|
|
254
|
+
requests that policy applies to — on another table, when the policy of `docs`
|
|
255
|
+
reads `members`. The command names each policy the change breaks and why;
|
|
256
|
+
`--dry-run` names them before anything is applied.
|
|
233
257
|
|
|
234
258
|
SQLite cannot add a PRIMARY KEY, UNIQUE or NOT-NULL-without-default column, or
|
|
235
259
|
drop a primary key, a unique or indexed column, or a table another table
|
|
@@ -411,21 +435,139 @@ A backend turned on before user management answers the `users` commands
|
|
|
411
435
|
`409 BACKEND_OUTDATED`: run `naro backend enable` once to update this
|
|
412
436
|
project's backend, then run the command again.
|
|
413
437
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
438
|
+
### Row-level security
|
|
439
|
+
|
|
440
|
+
Policies are Postgres's row-level security, as in Supabase. A table without a
|
|
441
|
+
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:
|
|
444
|
+
|
|
445
|
+
```sql
|
|
446
|
+
-- Each signed-in user reads and changes their own todos.
|
|
447
|
+
CREATE POLICY "own todos" ON todos TO authenticated
|
|
448
|
+
USING (user_id = auth.uid()) WITH CHECK (user_id = auth.uid());
|
|
449
|
+
|
|
450
|
+
-- Anyone reads published posts.
|
|
451
|
+
CREATE POLICY "published posts" ON posts FOR SELECT USING (published);
|
|
452
|
+
|
|
453
|
+
-- Members read their team's docs. The subquery reads members through
|
|
454
|
+
-- members' own select policies, so members needs one.
|
|
455
|
+
CREATE POLICY "own memberships" ON members FOR SELECT TO authenticated
|
|
456
|
+
USING (user_id = auth.uid());
|
|
457
|
+
CREATE POLICY "team docs" ON docs FOR SELECT TO authenticated
|
|
458
|
+
USING (EXISTS (SELECT 1 FROM members m
|
|
459
|
+
WHERE m.team_id = docs.team_id AND m.user_id = auth.uid()));
|
|
460
|
+
|
|
461
|
+
-- Deleted docs stay hidden, whatever else lets them through.
|
|
462
|
+
CREATE POLICY "hide deleted" ON docs AS RESTRICTIVE FOR SELECT
|
|
463
|
+
USING (deleted_at IS NULL);
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
A request passes when at least one permissive policy for its command and role
|
|
467
|
+
passes and every restrictive one does. `FOR ALL` (the default) is every
|
|
468
|
+
command; `TO public` (the default) is anon and signed-in users. As in
|
|
469
|
+
Postgres:
|
|
470
|
+
|
|
471
|
+
- select reaches the rows the select policies pass;
|
|
472
|
+
- insert writes a row only when it passes `WITH CHECK`, and with `.select()`
|
|
473
|
+
the select policies too;
|
|
474
|
+
- update reaches the rows its `USING` and the select policies pass, and each
|
|
475
|
+
changed row must pass `WITH CHECK` (`USING` when there is none) and the
|
|
476
|
+
select policies — so an update that moves a row out of what select shows,
|
|
477
|
+
like a soft delete, is refused;
|
|
478
|
+
- delete reaches the rows its `USING` and the select policies pass.
|
|
479
|
+
|
|
480
|
+
A row outside `USING` is skipped without a word. A written row that misses a
|
|
481
|
+
check writes nothing and answers `403 PERMISSION_DENIED` "New row violates
|
|
482
|
+
row-level security policy for table todos." (with the name of a restrictive
|
|
483
|
+
policy it missed). With no permissive policy for the command, the request is
|
|
484
|
+
refused — `403 PERMISSION_DENIED` "This request is not allowed by the update
|
|
485
|
+
policy of todos." — where Postgres would answer no rows.
|
|
486
|
+
|
|
487
|
+
Expressions: columns, literals, comparisons, `AND`/`OR`/`NOT`, `IS [NOT] NULL`,
|
|
488
|
+
`IS [NOT] DISTINCT FROM`, `[NOT] IN (…)`, `[NOT] LIKE`/`ILIKE`, `BETWEEN`,
|
|
489
|
+
`EXISTS (SELECT …)` and `IN (SELECT …)`, `auth.uid()`, `auth.role()`,
|
|
490
|
+
`auth.jwt() ->> 'sub' | 'role' | 'email' | 'email_verified'`, and `lower`,
|
|
491
|
+
`upper`, `coalesce`, `length`, `now()`. Supabase's `(select auth.uid())` reads
|
|
492
|
+
as `auth.uid()`. A subquery's table is read through its own select policies;
|
|
493
|
+
policies that would expand themselves are refused ("Infinite recursion
|
|
494
|
+
detected in policy for relation members."). A statement's policies bind at
|
|
495
|
+
most 50 values and write at most 50,000 bytes of SQL, leaving the rest of
|
|
496
|
+
D1's 100 values and 100,000 bytes to the request. A `LIKE` or `ILIKE` pattern
|
|
497
|
+
is at most 50 bytes of UTF-8, which is D1's limit; `LIKE` counts each `*`,
|
|
498
|
+
`?` and `[` as 3.
|
|
499
|
+
|
|
500
|
+
A subquery that reads the outer row (`m.team_id = docs.team_id`) runs once for
|
|
501
|
+
every row the statement looks at, so it needs an index to find its rows: the
|
|
502
|
+
control plane refuses such a policy until a column its WHERE compares with `=`
|
|
503
|
+
(or `IN`) to a value from outside it starts an index, and names the index to
|
|
504
|
+
create — `naro db index create members team_id`. `--allow-unindexed` accepts
|
|
505
|
+
it anyway; every run then reads the whole table.
|
|
506
|
+
|
|
507
|
+
For anon, `auth.uid()` is NULL and `auth.jwt()` holds only `role` `'anon'`, as
|
|
508
|
+
in Supabase, and NULL logic is Postgres's: `auth.uid() IS NULL` is true for
|
|
509
|
+
anon, `user_id = auth.uid()` and its `NOT` match no row, and a `NOT EXISTS`
|
|
510
|
+
over another table's policy reads that policy the same way. Target anon with
|
|
511
|
+
`TO anon` or `auth.role() = 'anon'`. `auth.jwt() ->> 'email'` is the user's
|
|
512
|
+
address only once it is verified, and NULL before — sign-up signs a user in
|
|
513
|
+
before the address is confirmed, so an unverified one is only what was typed;
|
|
514
|
+
`email_verified` says which. It is the text `'true'` or `'false'`: write
|
|
515
|
+
`(auth.jwt() ->> 'email_verified') = 'true'`. `IS TRUE`, `IS FALSE`, `NOT x`
|
|
516
|
+
and a value alone read a number or boolean column; on a claim, `auth.uid()`,
|
|
517
|
+
a string, a function or a TEXT column they are refused, with the comparison
|
|
518
|
+
to write — as text, `'true'` would read as false. So is comparing text with
|
|
519
|
+
`TRUE` or `FALSE` — `<> false`, `= true`, `IN (true)`, `BETWEEN`,
|
|
520
|
+
`IS DISTINCT FROM`, also through `coalesce` — since the text `'true'` is not
|
|
521
|
+
`TRUE`: `<> false` would hold for everyone.
|
|
522
|
+
|
|
523
|
+
Where naro differs from Postgres: no policy for a command is a 403, not zero
|
|
524
|
+
rows; row-level security is always on; roles are only `anon` and
|
|
525
|
+
`authenticated`, with no `security definer` functions; comparisons follow
|
|
526
|
+
SQLite (column affinity and collation), and `lower`, `upper` and `ILIKE` fold
|
|
527
|
+
ASCII only; `now()` is UTC text as `CURRENT_TIMESTAMP` stores it.
|
|
528
|
+
|
|
529
|
+
`naro policy create --file` parses every statement first, so a syntax error
|
|
530
|
+
sends none; then it runs them in order and stops at the first one refused,
|
|
531
|
+
keeping those before it. `naro policy list` prints what `naro policy create`
|
|
532
|
+
takes back, except the owner comment below. A policy whose name, table or
|
|
533
|
+
string holds a control character (a line break, an escape) is not printed as
|
|
534
|
+
a statement — with the character replaced it would create a different policy
|
|
535
|
+
— but as a `--` comment that says so; `naro policy list --json` has its exact
|
|
536
|
+
text.
|
|
537
|
+
|
|
538
|
+
`naro policy drop <name>` takes the name as you type it: the policy of exactly
|
|
539
|
+
that name, or else the one of its ASCII lower case. `CREATE POLICY MyPolicy`
|
|
540
|
+
stores `mypolicy`, as in Postgres, and `naro policy drop MyPolicy --table
|
|
541
|
+
todos` drops it; a policy created as `"MyPolicy"` is the one dropped when both
|
|
542
|
+
exist.
|
|
543
|
+
|
|
544
|
+
`naro policy set` is shorthand: it writes the policies `naro_select`,
|
|
545
|
+
`naro_insert`, `naro_update` and `naro_delete` of a table and leaves its other
|
|
546
|
+
policies alone. Each of `--select`, `--insert`, `--update` and `--delete`
|
|
547
|
+
takes `none` (drops that policy), `public`, `authenticated` or `owner`;
|
|
548
|
+
`owner` lets a signed-in user reach only rows whose `--owner` column holds
|
|
549
|
+
their user id, and fills that column in on insert — a Naro extension SQL
|
|
550
|
+
cannot say, so `naro policy list` prints it as a `-- naro: owner` comment.
|
|
551
|
+
What a `policy set` leaves out keeps its stored value. `naro gen types` reads
|
|
552
|
+
the policies too: with `--insert owner --owner user_id`, `user_id` is optional
|
|
553
|
+
in the generated `Insert` even when it is `NOT NULL` with no default — run it
|
|
422
554
|
again after changing a policy.
|
|
423
555
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
556
|
+
A backend turned on before named policies answers the commands that write a
|
|
557
|
+
policy — `naro policy create`, `naro policy drop` and `naro policy set` — with
|
|
558
|
+
`409 BACKEND_OUTDATED`:
|
|
559
|
+
``The project backend is older than named policies. Run `naro backend enable` to update it, then try again.``
|
|
560
|
+
`naro backend enable` uploads the current backend and turns the stored rules
|
|
561
|
+
into named policies; it also turns a backend that is off back on, so run
|
|
562
|
+
`naro backend disable` again afterwards if it should stay off. `naro policy
|
|
563
|
+
list` and `naro policy remove` work in the meantime.
|
|
564
|
+
|
|
565
|
+
`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
|
+
|
|
567
|
+
Instead of a rule, an action can hold a condition on the row, written in the
|
|
568
|
+
filter syntax of `naro-js`'s `.or()` (commas are AND) with `auth.uid()` for
|
|
569
|
+
the signed-in user's id — unquoted; `"auth.uid()"` is the text, and
|
|
570
|
+
`auth.uid` without `()` is refused rather than read as text:
|
|
429
571
|
|
|
430
572
|
- `--select-when` and `--delete-when`: the rows reads and deletes reach.
|
|
431
573
|
- `--insert-when`: what every new row must meet.
|
|
@@ -437,10 +579,9 @@ rather than read as text:
|
|
|
437
579
|
so `not(and(is_public.eq.1,user_id.eq.auth.uid()))` matches every row that is
|
|
438
580
|
not public, anon included — write what a condition lets in.
|
|
439
581
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
and compares it with the column's collation (`COLLATE NOCASE`, `RTRIM`). So:
|
|
582
|
+
A check — `WITH CHECK`, and the select policies an update's new row must
|
|
583
|
+
pass — reads a row as it will be stored and compares it with the column's
|
|
584
|
+
collation (`COLLATE NOCASE`, `RTRIM`). So:
|
|
444
585
|
|
|
445
586
|
- a number column it reads takes numbers, booleans and null, not strings, and
|
|
446
587
|
cannot be compared with `like` or `ilike`;
|
|
@@ -451,28 +592,21 @@ and compares it with the column's collation (`COLLATE NOCASE`, `RTRIM`). So:
|
|
|
451
592
|
another expression must be sent;
|
|
452
593
|
- it cannot read a generated column or a virtual table's column.
|
|
453
594
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
takes the PUT body, or a policy object from `--json`
|
|
595
|
+
The 50 values count an update's check once more for each column it reads, and
|
|
596
|
+
a write with a check binds the table's schema too (48 left), to make sure it
|
|
597
|
+
checks against the table as it is now. `--from-file` takes the PUT body, or a
|
|
598
|
+
policy object from `--json`
|
|
459
599
|
(`naro policy set posts --json | jq .policy > policy.json`), where
|
|
460
600
|
`auth.uid()` is `{"auth":"uid"}`; its `table`, if it has one, must name the
|
|
461
601
|
table being set. It cannot be combined with the other policy options.
|
|
462
|
-
`naro policy
|
|
463
|
-
the same condition, or as JSON where filter syntax cannot
|
|
464
|
-
(`deleted_at.eq.null` would be the text `"null"`, and `= NULL`
|
|
465
|
-
`IS NULL`)
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
`.
|
|
470
|
-
rows it cannot select apart without `.select()`: an update that sets a value
|
|
471
|
-
the check refuses answers 403 when a hidden row matches its filters and 200
|
|
472
|
-
when none does, so filter by filter it learns what those rows hold. Keep
|
|
473
|
-
update and delete conditions no broader than select's. And an `--update-check`
|
|
474
|
-
you give replaces the default: without `user_id.eq.auth.uid()` in it, a user
|
|
475
|
-
can give a row away by changing its `user_id`. Write
|
|
602
|
+
`naro policy set` prints the table's rules — each condition in filter syntax
|
|
603
|
+
that reads back as the same condition, or as JSON where filter syntax cannot
|
|
604
|
+
say it exactly (`deleted_at.eq.null` would be the text `"null"`, and `= NULL`
|
|
605
|
+
is not `IS NULL`) — and then its policies as `CREATE POLICY`.
|
|
606
|
+
|
|
607
|
+
An `--update-check` you give replaces the default: without
|
|
608
|
+
`user_id.eq.auth.uid()` in it, a user can give a row away by changing its
|
|
609
|
+
`user_id`. Write
|
|
476
610
|
`--update-when 'user_id.eq.auth.uid()' --update-check 'user_id.eq.auth.uid(),status.eq.draft'`.
|
|
477
611
|
|
|
478
612
|
## Machine-readable output (CI / AI agents)
|
|
@@ -497,6 +631,7 @@ Failures carry stable codes (`NOT_LOGGED_IN`, `PROJECT_NOT_LINKED`,
|
|
|
497
631
|
`ENV_NOT_FOUND`, `DOMAIN_NOT_FOUND`, `DOMAIN_NOT_READY`,
|
|
498
632
|
`BACKEND_DISABLED`, `BACKEND_NOT_READY`, `BACKEND_BUSY`,
|
|
499
633
|
`BACKEND_ROTATION_FAILED`, `BACKEND_PROVISION_FAILED`, `POLICY_NOT_FOUND`,
|
|
634
|
+
`POLICY_EXISTS`,
|
|
500
635
|
`VALIDATION_FAILED`, `CONFIRMATION_REQUIRED`, `CANCELED`, …). A destructive
|
|
501
636
|
command run non-interactively without `--yes` fails with
|
|
502
637
|
`CONFIRMATION_REQUIRED` instead of silently doing nothing:
|
|
@@ -512,7 +647,8 @@ command run non-interactively without `--yes` fails with
|
|
|
512
647
|
|
|
513
648
|
`--yes` (or `-y`) suppresses every prompt — destructive actions
|
|
514
649
|
(production rollback/promote, env/domain removal, database reset, backend key
|
|
515
|
-
rotation, policy removal) run unconfirmed, so use it deliberately in
|
|
650
|
+
rotation, policy removal and drop) run unconfirmed, so use it deliberately in
|
|
651
|
+
automation.
|
|
516
652
|
|
|
517
653
|
## CI authentication
|
|
518
654
|
|
package/dist/artifact-types.d.ts
CHANGED
|
@@ -21,6 +21,11 @@ export interface DeploymentArtifact {
|
|
|
21
21
|
assetsConfig?: {
|
|
22
22
|
notFoundHandling: "single-page-application" | "404-page";
|
|
23
23
|
};
|
|
24
|
+
/**
|
|
25
|
+
* Next.js routes ("/posts/[id]") for Speed Insights, read from the app's
|
|
26
|
+
* folders. Absent for other frameworks.
|
|
27
|
+
*/
|
|
28
|
+
routes?: string[];
|
|
24
29
|
}
|
|
25
30
|
/** Mirrors the build log shape from the internal build package. */
|
|
26
31
|
export interface BuildLog {
|