@palbase/backend 38.0.4 → 38.0.5

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/docs/auth.md CHANGED
@@ -40,17 +40,21 @@ interface UserT { // exported as `UserT`; the value name `User` is the
40
40
  }
41
41
  ```
42
42
 
43
- Every field is **server-resolved** from the verified profile — nothing here is
44
- client-settable. `metadata` is your own `auth.users.metadata` (set through the
45
- admin users API); `role` is the **database** role RLS reads and is always
46
- `"authenticated"` for a signed-in user, so `user.role === "admin"` is always
47
- false and reads like a working check.
43
+ Every field is **server-resolved** — nothing here is client-settable. `role` is
44
+ the **database** role RLS reads and is always `"authenticated"` for a signed-in
45
+ user, so `user.role === "admin"` is always false and reads like a working check.
46
+
47
+ **`metadata` is always `{}`.** Its only source is a `metadata` claim, and the
48
+ access token has none — so an `auth.users.metadata` row you filled through the
49
+ admin users API never reaches `user.metadata`, and `user.metadata.plan` is
50
+ `undefined` for every caller with nothing raised anywhere to say so. Keep your
51
+ own per-user attributes in a table of your own and read them with `Database`.
48
52
 
49
53
  Application roles are **`user.roles`** — a `string[]` resolved per request from
50
54
  `auth.user_roles`, the table `palbase roles` writes. They were once kept in
51
55
  `metadata`; they are not any more, and a role written there gates nothing.
52
56
 
53
- `emailVerified`, `email`, `role` and `metadata` come from the **verified token**,
57
+ `emailVerified`, `email` and `role` come from the **verified token**,
54
58
  so they say what was true when it was minted: a user who confirms their address
55
59
  mid-session keeps reporting `false` until their next token. `roles` is the
56
60
  exception, and deliberately so — it is read from the table on every request, so
package/docs/database.md CHANGED
@@ -714,10 +714,12 @@ policies, every `Database.*` call runs as the request's verified user, so the
714
714
  database filters out rows the user's policies don't allow. That is the secure
715
715
  default.
716
716
 
717
- The Postgres role it connects as is **`backend_authenticated`** (or
718
- `backend_anon` when there is no signed-in user) — not `authenticated`. You
717
+ The Postgres role it connects as is **`backend_authenticated`** — always, for
718
+ every request, whether or not somebody is signed in — not `authenticated`. You
719
719
  rarely need to know that, because a policy declared in `db/*.ts` is
720
- deployed targeting both. It matters in exactly one place: **hand-written
720
+ deployed targeting both. What it does mean is that the role never tells a
721
+ policy whether the caller is signed in: put that in the `using` expression,
722
+ never in the role. It matters in exactly one place: **hand-written
721
723
  `CREATE POLICY` SQL in a migration must name both roles**, or it applies to
722
724
  nothing your code does. See
723
725
  [Row-Level Security](./schema.md#row-level-security-rls).
package/docs/endpoints.md CHANGED
@@ -103,9 +103,13 @@ query (RFC semantics), so a QUERY route carries its whole input there.
103
103
 
104
104
  `@Body`/`@QueryParams`/`@Headers` take a zod schema (validation + codegen source); the
105
105
  developer writes the matching type annotation (same name). `UserT` is the
106
- authenticated-user type (`{ id: string; email?: string; role: string; metadata:
107
- Record<string, unknown> }`) — exported under the alias `UserT` because the value
108
- name `User` is the `@User()` decorator.
106
+ authenticated-user type (`{ id: string; email?: string; emailVerified: boolean;
107
+ role: string; roles: string[]; metadata: Record<string, unknown>; device:
108
+ VerifiedDevice | null }`) exported under the alias `UserT` because the value
109
+ name `User` is the `@User()` decorator. **`roles` is the one to reach for**:
110
+ `role` is the DATABASE role and is always `"authenticated"` for a signed-in
111
+ caller, so `user.role === "admin"` is a check that never fires. See
112
+ [Authentication](./auth.md#what-user-gives-you).
109
113
 
110
114
  ## Output = return type
111
115
 
@@ -118,12 +122,22 @@ error. A method that returns nothing annotates `: void` (or `: Promise<void>`).
118
122
  ## Auth — controller default + route override cascade
119
123
 
120
124
  **Secure by default:** a route requires authentication UNLESS it explicitly opts
121
- out. Resolution order (most specific wins):
125
+ out. Resolution order (most specific wins) — **four links, not three**:
122
126
 
123
127
  1. Route-level `@Post("", { auth })` — wins if present.
124
128
  2. Controller-level `@Controller("/x", { auth })` — applies to routes without
125
129
  their own `auth`.
126
- 3. Default `true` (secure-by-default).
130
+ 3. **Application-level `defineDefaultAuth(...)`** — one declaration, every
131
+ controller that did not speak for itself. This is the link to know about,
132
+ because it can TIGHTEN what a route you are reading looks like it does:
133
+ `defineDefaultAuth({ verifiedEmail: true })` makes every undeclared route
134
+ demand a confirmed address, and nothing at the route says so.
135
+ 4. Default `true` (secure-by-default).
136
+
137
+ The whole chain is one function — `resolveEffectiveAuth(routeAuth,
138
+ controllerAuth)` — and both the router and the spec emitter ask it rather than
139
+ spelling the cascade out, so the build-time answer and the runtime answer cannot
140
+ drift apart about who may call an endpoint.
127
141
 
128
142
  ```ts
129
143
  @Controller("/public", { auth: false }) // all routes default public
package/docs/errors.md CHANGED
@@ -26,14 +26,15 @@ import { BadRequest, Unauthorized, Forbidden, NotFound, Conflict, TooManyRequest
26
26
 
27
27
  throw new Conflict("title taken"); // → 409 ("conflict" code by default)
28
28
  throw new NotFound(); // → 404 ("not_found", "Not found")
29
- throw new BadRequest("missing field"); // → 400
29
+ throw new BadRequest({ fields: [] }); // → 400 (data-first, see below)
30
30
  throw new Unauthorized(); // → 401
31
31
  throw new Forbidden(); // → 403
32
- throw new TooManyRequests(); // → 429
32
+ throw new TooManyRequests({ retryAfter: 30 }); // → 429 (data-first)
33
33
  ```
34
34
 
35
- Each class fixes its HTTP status. The constructor is
36
- `new <Class>(message?, code?, data?)`:
35
+ Each class fixes its HTTP status. Four of them take
36
+ `new <Class>(message?, code?, data?)` — `Unauthorized`, `Forbidden`, `NotFound`,
37
+ `Conflict`:
37
38
 
38
39
  - `message` overrides the human-readable `error_description` (defaults to a label
39
40
  derived from the class name).
@@ -41,6 +42,13 @@ Each class fixes its HTTP status. The constructor is
41
42
  code, e.g. `not_found`).
42
43
  - `data` rides along under the envelope's `data` field for structured context.
43
44
 
45
+ **`BadRequest` and `TooManyRequests` are DATA-FIRST, and their data is
46
+ required** — `new BadRequest(data, message?)`, `new TooManyRequests(data,
47
+ message?)`. Both carry a fixed typed payload so codegen can surface it on the
48
+ client: `BadRequestData` is `{ fields: FieldError[] }` and
49
+ `TooManyRequestsData` is `{ retryAfter: number }`. Passing a string, or nothing,
50
+ does not compile.
51
+
44
52
  ```ts
45
53
  throw new NotFound("Room does not exist", "room_not_found");
46
54
  throw new Conflict("locked", "title_locked", { retryAfter: 30 });
@@ -702,9 +702,13 @@ query (RFC semantics), so a QUERY route carries its whole input there.
702
702
 
703
703
  `@Body`/`@QueryParams`/`@Headers` take a zod schema (validation + codegen source); the
704
704
  developer writes the matching type annotation (same name). `UserT` is the
705
- authenticated-user type (`{ id: string; email?: string; role: string; metadata:
706
- Record<string, unknown> }`) — exported under the alias `UserT` because the value
707
- name `User` is the `@User()` decorator.
705
+ authenticated-user type (`{ id: string; email?: string; emailVerified: boolean;
706
+ role: string; roles: string[]; metadata: Record<string, unknown>; device:
707
+ VerifiedDevice | null }`) exported under the alias `UserT` because the value
708
+ name `User` is the `@User()` decorator. **`roles` is the one to reach for**:
709
+ `role` is the DATABASE role and is always `"authenticated"` for a signed-in
710
+ caller, so `user.role === "admin"` is a check that never fires. See
711
+ [Authentication](./auth.md#what-user-gives-you).
708
712
 
709
713
  ## Output = return type
710
714
 
@@ -717,12 +721,22 @@ error. A method that returns nothing annotates `: void` (or `: Promise<void>`).
717
721
  ## Auth — controller default + route override cascade
718
722
 
719
723
  **Secure by default:** a route requires authentication UNLESS it explicitly opts
720
- out. Resolution order (most specific wins):
724
+ out. Resolution order (most specific wins) — **four links, not three**:
721
725
 
722
726
  1. Route-level `@Post("", { auth })` — wins if present.
723
727
  2. Controller-level `@Controller("/x", { auth })` — applies to routes without
724
728
  their own `auth`.
725
- 3. Default `true` (secure-by-default).
729
+ 3. **Application-level `defineDefaultAuth(...)`** — one declaration, every
730
+ controller that did not speak for itself. This is the link to know about,
731
+ because it can TIGHTEN what a route you are reading looks like it does:
732
+ `defineDefaultAuth({ verifiedEmail: true })` makes every undeclared route
733
+ demand a confirmed address, and nothing at the route says so.
734
+ 4. Default `true` (secure-by-default).
735
+
736
+ The whole chain is one function — `resolveEffectiveAuth(routeAuth,
737
+ controllerAuth)` — and both the router and the spec emitter ask it rather than
738
+ spelling the cascade out, so the build-time answer and the runtime answer cannot
739
+ drift apart about who may call an endpoint.
726
740
 
727
741
  ```ts
728
742
  @Controller("/public", { auth: false }) // all routes default public
@@ -816,17 +830,21 @@ interface UserT { // exported as `UserT`; the value name `User` is the
816
830
  }
817
831
  ```
818
832
 
819
- Every field is **server-resolved** from the verified profile — nothing here is
820
- client-settable. `metadata` is your own `auth.users.metadata` (set through the
821
- admin users API); `role` is the **database** role RLS reads and is always
822
- `"authenticated"` for a signed-in user, so `user.role === "admin"` is always
823
- false and reads like a working check.
833
+ Every field is **server-resolved** — nothing here is client-settable. `role` is
834
+ the **database** role RLS reads and is always `"authenticated"` for a signed-in
835
+ user, so `user.role === "admin"` is always false and reads like a working check.
836
+
837
+ **`metadata` is always `{}`.** Its only source is a `metadata` claim, and the
838
+ access token has none — so an `auth.users.metadata` row you filled through the
839
+ admin users API never reaches `user.metadata`, and `user.metadata.plan` is
840
+ `undefined` for every caller with nothing raised anywhere to say so. Keep your
841
+ own per-user attributes in a table of your own and read them with `Database`.
824
842
 
825
843
  Application roles are **`user.roles`** — a `string[]` resolved per request from
826
844
  `auth.user_roles`, the table `palbase roles` writes. They were once kept in
827
845
  `metadata`; they are not any more, and a role written there gates nothing.
828
846
 
829
- `emailVerified`, `email`, `role` and `metadata` come from the **verified token**,
847
+ `emailVerified`, `email` and `role` come from the **verified token**,
830
848
  so they say what was true when it was minted: a user who confirms their address
831
849
  mid-session keeps reporting `false` until their next token. `roles` is the
832
850
  exception, and deliberately so — it is read from the table on every request, so
@@ -1830,10 +1848,12 @@ policies, every `Database.*` call runs as the request's verified user, so the
1830
1848
  database filters out rows the user's policies don't allow. That is the secure
1831
1849
  default.
1832
1850
 
1833
- The Postgres role it connects as is **`backend_authenticated`** (or
1834
- `backend_anon` when there is no signed-in user) — not `authenticated`. You
1851
+ The Postgres role it connects as is **`backend_authenticated`** — always, for
1852
+ every request, whether or not somebody is signed in — not `authenticated`. You
1835
1853
  rarely need to know that, because a policy declared in `db/*.ts` is
1836
- deployed targeting both. It matters in exactly one place: **hand-written
1854
+ deployed targeting both. What it does mean is that the role never tells a
1855
+ policy whether the caller is signed in: put that in the `using` expression,
1856
+ never in the role. It matters in exactly one place: **hand-written
1837
1857
  `CREATE POLICY` SQL in a migration must name both roles**, or it applies to
1838
1858
  nothing your code does. See
1839
1859
  [Row-Level Security](./schema.md#row-level-security-rls).
@@ -2724,19 +2744,31 @@ policy("pb_owner_all")
2724
2744
 
2725
2745
  #### Which role your policy must target
2726
2746
 
2727
- The runtime connects to Postgres as **`backend_authenticated`** (or
2728
- `backend_anon` when anonymous), which is a **separate role from
2729
- `authenticated`** not a member of it. A policy addressed only to
2747
+ The runtime connects to Postgres as **`backend_authenticated`** — always, for
2748
+ every request, signed in or not — which is a **separate role from
2749
+ `authenticated`** and not a member of it. A policy addressed only to
2730
2750
  `authenticated` therefore applies to nothing your backend does, and with RLS on
2731
2751
  and no applicable policy, Postgres denies everything: reads come back empty and
2732
2752
  writes are refused, while your code compiles, your tests pass and the deploy
2733
2753
  reports success.
2734
2754
 
2755
+ `backend_anon` is provisioned too, but your backend never connects as it — so a
2756
+ policy written only for `anon` changes nothing about what your controllers can
2757
+ read.
2758
+
2735
2759
  You do not have to think about this when you declare policies here. `.to("authenticated")`
2736
2760
  is deployed as `TO authenticated, backend_authenticated` (and `anon` gains
2737
2761
  `backend_anon`); `service_role` is left alone because `backend_service_role` has
2738
2762
  `BYPASSRLS` and policies never apply to it.
2739
2763
 
2764
+ **And that is why `.to("authenticated")` does not mean "signed in".** Since
2765
+ every request arrives under the same role, an anonymous caller reaches an
2766
+ `authenticated` policy too. The role decides nothing about identity — the
2767
+ `using` expression does. If a policy is meant for signed-in users, say so in the
2768
+ expression (`auth.uid() is not null`, or a predicate that dereferences it); a
2769
+ policy whose `using` never mentions the caller applies to anonymous traffic as
2770
+ well.
2771
+
2740
2772
  **Hand-written SQL is the case to watch.** A `CREATE POLICY` in a
2741
2773
  `db/migrations/*.sql` file is applied verbatim, so write both roles yourself:
2742
2774
 
@@ -3038,11 +3070,13 @@ column builders, the policy DSL, and typed `Database.public.*` access.
3038
3070
 
3039
3071
  Two things the generated path handles for you and raw SQL does not.
3040
3072
 
3041
- **Name both roles.** The runtime connects as `backend_authenticated` /
3042
- `backend_anon`, which are *not* members of `authenticated` / `anon`. A policy
3073
+ **Name both roles.** The runtime connects as `backend_authenticated` — every
3074
+ request, signed in or not — and it is *not* a member of `authenticated`. A policy
3043
3075
  addressed only to `authenticated` applies to nothing your backend does — and with
3044
3076
  RLS on and no applicable policy, Postgres denies everything: empty reads, refused
3045
- writes, no error anywhere that says why.
3077
+ writes, no error anywhere that says why. (`backend_anon` is provisioned and gets
3078
+ the same twin treatment, but your backend never connects as it, so naming it
3079
+ does not fence anonymous callers — the `using` expression is what does that.)
3046
3080
 
3047
3081
  **Guard the CREATE.** Postgres has no `CREATE POLICY IF NOT EXISTS`, so a
3048
3082
  migration that replays — a fresh Environment, or `palbase start --reset`, which
@@ -3534,14 +3568,15 @@ import { BadRequest, Unauthorized, Forbidden, NotFound, Conflict, TooManyRequest
3534
3568
 
3535
3569
  throw new Conflict("title taken"); // → 409 ("conflict" code by default)
3536
3570
  throw new NotFound(); // → 404 ("not_found", "Not found")
3537
- throw new BadRequest("missing field"); // → 400
3571
+ throw new BadRequest({ fields: [] }); // → 400 (data-first, see below)
3538
3572
  throw new Unauthorized(); // → 401
3539
3573
  throw new Forbidden(); // → 403
3540
- throw new TooManyRequests(); // → 429
3574
+ throw new TooManyRequests({ retryAfter: 30 }); // → 429 (data-first)
3541
3575
  ```
3542
3576
 
3543
- Each class fixes its HTTP status. The constructor is
3544
- `new <Class>(message?, code?, data?)`:
3577
+ Each class fixes its HTTP status. Four of them take
3578
+ `new <Class>(message?, code?, data?)` — `Unauthorized`, `Forbidden`, `NotFound`,
3579
+ `Conflict`:
3545
3580
 
3546
3581
  - `message` overrides the human-readable `error_description` (defaults to a label
3547
3582
  derived from the class name).
@@ -3549,6 +3584,13 @@ Each class fixes its HTTP status. The constructor is
3549
3584
  code, e.g. `not_found`).
3550
3585
  - `data` rides along under the envelope's `data` field for structured context.
3551
3586
 
3587
+ **`BadRequest` and `TooManyRequests` are DATA-FIRST, and their data is
3588
+ required** — `new BadRequest(data, message?)`, `new TooManyRequests(data,
3589
+ message?)`. Both carry a fixed typed payload so codegen can surface it on the
3590
+ client: `BadRequestData` is `{ fields: FieldError[] }` and
3591
+ `TooManyRequestsData` is `{ retryAfter: number }`. Passing a string, or nothing,
3592
+ does not compile.
3593
+
3552
3594
  ```ts
3553
3595
  throw new NotFound("Room does not exist", "room_not_found");
3554
3596
  throw new Conflict("locked", "title_locked", { retryAfter: 30 });
@@ -175,11 +175,13 @@ column builders, the policy DSL, and typed `Database.public.*` access.
175
175
 
176
176
  Two things the generated path handles for you and raw SQL does not.
177
177
 
178
- **Name both roles.** The runtime connects as `backend_authenticated` /
179
- `backend_anon`, which are *not* members of `authenticated` / `anon`. A policy
178
+ **Name both roles.** The runtime connects as `backend_authenticated` — every
179
+ request, signed in or not — and it is *not* a member of `authenticated`. A policy
180
180
  addressed only to `authenticated` applies to nothing your backend does — and with
181
181
  RLS on and no applicable policy, Postgres denies everything: empty reads, refused
182
- writes, no error anywhere that says why.
182
+ writes, no error anywhere that says why. (`backend_anon` is provisioned and gets
183
+ the same twin treatment, but your backend never connects as it, so naming it
184
+ does not fence anonymous callers — the `using` expression is what does that.)
183
185
 
184
186
  **Guard the CREATE.** Postgres has no `CREATE POLICY IF NOT EXISTS`, so a
185
187
  migration that replays — a fresh Environment, or `palbase start --reset`, which
package/docs/schema.md CHANGED
@@ -570,19 +570,31 @@ policy("pb_owner_all")
570
570
 
571
571
  #### Which role your policy must target
572
572
 
573
- The runtime connects to Postgres as **`backend_authenticated`** (or
574
- `backend_anon` when anonymous), which is a **separate role from
575
- `authenticated`** not a member of it. A policy addressed only to
573
+ The runtime connects to Postgres as **`backend_authenticated`** — always, for
574
+ every request, signed in or not — which is a **separate role from
575
+ `authenticated`** and not a member of it. A policy addressed only to
576
576
  `authenticated` therefore applies to nothing your backend does, and with RLS on
577
577
  and no applicable policy, Postgres denies everything: reads come back empty and
578
578
  writes are refused, while your code compiles, your tests pass and the deploy
579
579
  reports success.
580
580
 
581
+ `backend_anon` is provisioned too, but your backend never connects as it — so a
582
+ policy written only for `anon` changes nothing about what your controllers can
583
+ read.
584
+
581
585
  You do not have to think about this when you declare policies here. `.to("authenticated")`
582
586
  is deployed as `TO authenticated, backend_authenticated` (and `anon` gains
583
587
  `backend_anon`); `service_role` is left alone because `backend_service_role` has
584
588
  `BYPASSRLS` and policies never apply to it.
585
589
 
590
+ **And that is why `.to("authenticated")` does not mean "signed in".** Since
591
+ every request arrives under the same role, an anonymous caller reaches an
592
+ `authenticated` policy too. The role decides nothing about identity — the
593
+ `using` expression does. If a policy is meant for signed-in users, say so in the
594
+ expression (`auth.uid() is not null`, or a predicate that dereferences it); a
595
+ policy whose `using` never mentions the caller applies to anonymous traffic as
596
+ well.
597
+
586
598
  **Hand-written SQL is the case to watch.** A `CREATE POLICY` in a
587
599
  `db/migrations/*.sql` file is applied verbatim, so write both roles yourself:
588
600
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@palbase/backend",
3
- "version": "38.0.4",
3
+ "version": "38.0.5",
4
4
  "description": "Palbase Backend SDK — class controllers (@Controller/@Get/@Post + @Body/@QueryParams/@Param), error classes, schema DSL",
5
5
  "license": "MIT",
6
6
  "repository": {