@palbase/backend 34.0.0 → 34.1.1
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/LICENSE +21 -0
- package/dist/bin/palbase-backend.cjs +275 -30
- package/dist/bin/palbase-backend.cjs.map +1 -1
- package/dist/bin/palbase-backend.js +4 -4
- package/dist/{chunk-SEM36BWY.js → chunk-5UGPXKMJ.js} +60 -12
- package/dist/chunk-5UGPXKMJ.js.map +1 -0
- package/dist/{chunk-FOWA7PF4.js → chunk-P7MGSAFT.js} +6 -1
- package/dist/{chunk-FOWA7PF4.js.map → chunk-P7MGSAFT.js.map} +1 -1
- package/dist/{chunk-YX7UA7VC.js → chunk-X2UYIXDS.js} +8 -2
- package/dist/chunk-X2UYIXDS.js.map +1 -0
- package/dist/{chunk-MNGZE2MM.js → chunk-XZPDPYVY.js} +279 -34
- package/dist/chunk-XZPDPYVY.js.map +1 -0
- package/dist/{chunk-LOGL2ZHD.js → chunk-YO5GYM73.js} +2 -2
- package/dist/db/index.cjs +34 -10
- package/dist/db/index.cjs.map +1 -1
- package/dist/db/index.d.cts +2 -2
- package/dist/db/index.d.ts +2 -2
- package/dist/db/index.js +2 -2
- package/dist/{endpoint-DpNUx-ON.d.ts → endpoint-BXPdsTA5.d.ts} +56 -2
- package/dist/{endpoint-kYgEHrrV.d.cts → endpoint-DqhUP7RT.d.cts} +56 -2
- package/dist/engine/index.cjs +275 -30
- package/dist/engine/index.cjs.map +1 -1
- package/dist/engine/index.d.cts +3 -3
- package/dist/engine/index.d.ts +3 -3
- package/dist/engine/index.js +4 -4
- package/dist/{index-pnDsEfxJ.d.cts → index-Co0Hj_aO.d.cts} +2 -2
- package/dist/{index-JrHqNsjQ.d.ts → index-MhCL7ll4.d.ts} +2 -2
- package/dist/index.cjs +79 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +6 -4
- package/dist/index.js.map +1 -1
- package/dist/openapi/index.d.cts +2 -2
- package/dist/openapi/index.d.ts +2 -2
- package/dist/{registry-BcOd8Zhf.d.cts → registry-BkIVHPzB.d.cts} +1 -1
- package/dist/{registry-BoX5iNyY.d.ts → registry-CvGLR5j1.d.ts} +1 -1
- package/dist/test/index.cjs.map +1 -1
- package/dist/test/index.d.cts +1 -1
- package/dist/test/index.d.ts +1 -1
- package/dist/test/index.js +2 -2
- package/dist/test/index.js.map +1 -1
- package/docs/auth.md +58 -0
- package/docs/llms-full.txt +73 -0
- package/docs/schema.md +15 -0
- package/package.json +15 -15
- package/template/db/public.ts +12 -0
- package/template/scripts/test.sh +0 -0
- package/dist/chunk-MNGZE2MM.js.map +0 -1
- package/dist/chunk-SEM36BWY.js.map +0 -1
- package/dist/chunk-YX7UA7VC.js.map +0 -1
- /package/dist/{chunk-LOGL2ZHD.js.map → chunk-YO5GYM73.js.map} +0 -0
package/docs/auth.md
CHANGED
|
@@ -155,6 +155,64 @@ Two helpers are available in every policy body:
|
|
|
155
155
|
Both take the caller from `auth.uid()` and accept no user id, so a policy cannot
|
|
156
156
|
ask about somebody else.
|
|
157
157
|
|
|
158
|
+
### `can` — what a row says you may do
|
|
159
|
+
|
|
160
|
+
Every row a table hands back carries `can: string[]`: the names of the table's
|
|
161
|
+
named **update** policies whose `using` expression is true for THIS row and THIS
|
|
162
|
+
caller. It is computed in the same `SELECT` that fetched the row — the policy is
|
|
163
|
+
the only copy of the rule, and `can` is its echo.
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
const transfer = await Database.public.transfers.findById(id);
|
|
167
|
+
transfer?.can; // e.g. ["transfers_approve"] — typed from db/public.ts
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
To put it on the wire, name it in the response schema — the enum comes from the
|
|
171
|
+
policy declaration, never from a hand-typed list:
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
import { z, can } from "@palbase/backend";
|
|
175
|
+
import publicSchema from "../../db/public";
|
|
176
|
+
|
|
177
|
+
export const TransferSchema = z.object({
|
|
178
|
+
id: z.string(),
|
|
179
|
+
status: z.string(),
|
|
180
|
+
can: can(publicSchema.tables.transfers),
|
|
181
|
+
});
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
The contract, word for word: **`can` is for the interface; it cannot be used for
|
|
185
|
+
an authorization decision; it may be incomplete; the server decides again on
|
|
186
|
+
every request.** A policy whose `using` the engine cannot select as a column is
|
|
187
|
+
dropped from `can` with a `warn` line — it never becomes a silent `true`. In a
|
|
188
|
+
raw-string `using`, write column names unqualified (`status`, not
|
|
189
|
+
`transfers.status` or `public.transfers.status`) and wrap every
|
|
190
|
+
`auth.has_permission(...)` in `(select …)`; a comment, an unbalanced parenthesis
|
|
191
|
+
or an unterminated quote drops the policy the same way.
|
|
192
|
+
|
|
193
|
+
Rows read under `Database.$asService()` carry `can: []`: no policy applies to
|
|
194
|
+
the service role and every `TO` gate is false for it, so a row fetched there and
|
|
195
|
+
handed to a user says nothing about what that user may do — read it as the user.
|
|
196
|
+
|
|
197
|
+
Only `.for("update")` policies are echoed. A `.for("all")` policy such as
|
|
198
|
+
`notes_owner` above governs UPDATE too, but it is not a named update policy and
|
|
199
|
+
never appears in `can` — a table whose only policy is `for("all")` gets
|
|
200
|
+
`can: []` on every row, and `can(table)` refuses it by name. To get an
|
|
201
|
+
affordance, declare a separate `.for("update")` policy beside it (the template's
|
|
202
|
+
`notes_edit_own` next to `notes_owner`).
|
|
203
|
+
|
|
204
|
+
The name `can` is reserved for this echo. A table that declares a column named
|
|
205
|
+
`can` keeps its column and gets no echo (with a `warn`); a **relation** named
|
|
206
|
+
`can` — a table named `can`, a `can_id` foreign key, `as: "can"` or
|
|
207
|
+
`reverseAs: "can"` — is refused by `defineTable` itself, because `row.can` and
|
|
208
|
+
`relations.can` cannot both be true of one row.
|
|
209
|
+
|
|
210
|
+
When the server refuses, the 403 body names the cure: `required: "transfers.approve"`.
|
|
211
|
+
`pb.auth.onPermissionDenied { required in … }` (iOS) / `pb.auth.onPermissionDenied(required => …)`
|
|
212
|
+
(web) fires on it, so a screen drawn from a stale `can` refetches and repairs itself.
|
|
213
|
+
`pb.auth.permissions.holds("transfers.approve")` is the live, observable set of the
|
|
214
|
+
caller's permissions — pushed over realtime the moment a role changes.
|
|
215
|
+
|
|
158
216
|
## Email verification
|
|
159
217
|
|
|
160
218
|
The platform handles verification end to end. **You do not configure a sender**,
|
package/docs/llms-full.txt
CHANGED
|
@@ -931,6 +931,64 @@ Two helpers are available in every policy body:
|
|
|
931
931
|
Both take the caller from `auth.uid()` and accept no user id, so a policy cannot
|
|
932
932
|
ask about somebody else.
|
|
933
933
|
|
|
934
|
+
### `can` — what a row says you may do
|
|
935
|
+
|
|
936
|
+
Every row a table hands back carries `can: string[]`: the names of the table's
|
|
937
|
+
named **update** policies whose `using` expression is true for THIS row and THIS
|
|
938
|
+
caller. It is computed in the same `SELECT` that fetched the row — the policy is
|
|
939
|
+
the only copy of the rule, and `can` is its echo.
|
|
940
|
+
|
|
941
|
+
```ts
|
|
942
|
+
const transfer = await Database.public.transfers.findById(id);
|
|
943
|
+
transfer?.can; // e.g. ["transfers_approve"] — typed from db/public.ts
|
|
944
|
+
```
|
|
945
|
+
|
|
946
|
+
To put it on the wire, name it in the response schema — the enum comes from the
|
|
947
|
+
policy declaration, never from a hand-typed list:
|
|
948
|
+
|
|
949
|
+
```ts
|
|
950
|
+
import { z, can } from "@palbase/backend";
|
|
951
|
+
import publicSchema from "../../db/public";
|
|
952
|
+
|
|
953
|
+
export const TransferSchema = z.object({
|
|
954
|
+
id: z.string(),
|
|
955
|
+
status: z.string(),
|
|
956
|
+
can: can(publicSchema.tables.transfers),
|
|
957
|
+
});
|
|
958
|
+
```
|
|
959
|
+
|
|
960
|
+
The contract, word for word: **`can` is for the interface; it cannot be used for
|
|
961
|
+
an authorization decision; it may be incomplete; the server decides again on
|
|
962
|
+
every request.** A policy whose `using` the engine cannot select as a column is
|
|
963
|
+
dropped from `can` with a `warn` line — it never becomes a silent `true`. In a
|
|
964
|
+
raw-string `using`, write column names unqualified (`status`, not
|
|
965
|
+
`transfers.status` or `public.transfers.status`) and wrap every
|
|
966
|
+
`auth.has_permission(...)` in `(select …)`; a comment, an unbalanced parenthesis
|
|
967
|
+
or an unterminated quote drops the policy the same way.
|
|
968
|
+
|
|
969
|
+
Rows read under `Database.$asService()` carry `can: []`: no policy applies to
|
|
970
|
+
the service role and every `TO` gate is false for it, so a row fetched there and
|
|
971
|
+
handed to a user says nothing about what that user may do — read it as the user.
|
|
972
|
+
|
|
973
|
+
Only `.for("update")` policies are echoed. A `.for("all")` policy such as
|
|
974
|
+
`notes_owner` above governs UPDATE too, but it is not a named update policy and
|
|
975
|
+
never appears in `can` — a table whose only policy is `for("all")` gets
|
|
976
|
+
`can: []` on every row, and `can(table)` refuses it by name. To get an
|
|
977
|
+
affordance, declare a separate `.for("update")` policy beside it (the template's
|
|
978
|
+
`notes_edit_own` next to `notes_owner`).
|
|
979
|
+
|
|
980
|
+
The name `can` is reserved for this echo. A table that declares a column named
|
|
981
|
+
`can` keeps its column and gets no echo (with a `warn`); a **relation** named
|
|
982
|
+
`can` — a table named `can`, a `can_id` foreign key, `as: "can"` or
|
|
983
|
+
`reverseAs: "can"` — is refused by `defineTable` itself, because `row.can` and
|
|
984
|
+
`relations.can` cannot both be true of one row.
|
|
985
|
+
|
|
986
|
+
When the server refuses, the 403 body names the cure: `required: "transfers.approve"`.
|
|
987
|
+
`pb.auth.onPermissionDenied { required in … }` (iOS) / `pb.auth.onPermissionDenied(required => …)`
|
|
988
|
+
(web) fires on it, so a screen drawn from a stale `can` refetches and repairs itself.
|
|
989
|
+
`pb.auth.permissions.holds("transfers.approve")` is the live, observable set of the
|
|
990
|
+
caller's permissions — pushed over realtime the moment a role changes.
|
|
991
|
+
|
|
934
992
|
## Email verification
|
|
935
993
|
|
|
936
994
|
The platform handles verification end to end. **You do not configure a sender**,
|
|
@@ -2743,6 +2801,21 @@ calling user's rows — no `WHERE owner = …` needed in the handler. To read or
|
|
|
2743
2801
|
write across all users (e.g. an admin job), use the explicit bypass:
|
|
2744
2802
|
`Database.$asService()` (see [database.md](./database.md#bypassing-rls--databaseasservice)).
|
|
2745
2803
|
|
|
2804
|
+
Name your **update** policies deliberately: each name becomes a value in the row's
|
|
2805
|
+
`can` array (`row.can: ("transfers_approve" | "transfers_cancel")[]` in
|
|
2806
|
+
`palbase-env.d.ts`), and `can(table)` puts the same names into a response schema
|
|
2807
|
+
as an enum. Only `.for("update")` counts: a `.for("all")` policy like the
|
|
2808
|
+
owner policies above governs UPDATE but is never echoed, so a table with only
|
|
2809
|
+
`for("all")` policies carries `can: []`. In a raw-string `using`, write column
|
|
2810
|
+
names UNQUALIFIED (`status = 'pending_approval'`, not `transfers.status` or
|
|
2811
|
+
`public.transfers.status`) and wrap `auth.has_permission(...)` in `(select …)`:
|
|
2812
|
+
the engine selects the expression under a row alias, and a `using` it cannot
|
|
2813
|
+
carry there — a table qualifier, a comment, an unwrapped `has_permission`, a
|
|
2814
|
+
policy name longer than 59 bytes — is dropped from `can` with a `warn`, never
|
|
2815
|
+
spliced. A column or relation named `can` is reserved (see [auth.md](./auth.md)).
|
|
2816
|
+
`can` is for the interface; it cannot be used for an authorization decision; it
|
|
2817
|
+
may be incomplete; the server decides again on every request.
|
|
2818
|
+
|
|
2746
2819
|
### How policies are applied
|
|
2747
2820
|
|
|
2748
2821
|
On deploy, Palbase diffs your declared schema against the live database and
|
package/docs/schema.md
CHANGED
|
@@ -676,6 +676,21 @@ calling user's rows — no `WHERE owner = …` needed in the handler. To read or
|
|
|
676
676
|
write across all users (e.g. an admin job), use the explicit bypass:
|
|
677
677
|
`Database.$asService()` (see [database.md](./database.md#bypassing-rls--databaseasservice)).
|
|
678
678
|
|
|
679
|
+
Name your **update** policies deliberately: each name becomes a value in the row's
|
|
680
|
+
`can` array (`row.can: ("transfers_approve" | "transfers_cancel")[]` in
|
|
681
|
+
`palbase-env.d.ts`), and `can(table)` puts the same names into a response schema
|
|
682
|
+
as an enum. Only `.for("update")` counts: a `.for("all")` policy like the
|
|
683
|
+
owner policies above governs UPDATE but is never echoed, so a table with only
|
|
684
|
+
`for("all")` policies carries `can: []`. In a raw-string `using`, write column
|
|
685
|
+
names UNQUALIFIED (`status = 'pending_approval'`, not `transfers.status` or
|
|
686
|
+
`public.transfers.status`) and wrap `auth.has_permission(...)` in `(select …)`:
|
|
687
|
+
the engine selects the expression under a row alias, and a `using` it cannot
|
|
688
|
+
carry there — a table qualifier, a comment, an unwrapped `has_permission`, a
|
|
689
|
+
policy name longer than 59 bytes — is dropped from `can` with a `warn`, never
|
|
690
|
+
spliced. A column or relation named `can` is reserved (see [auth.md](./auth.md)).
|
|
691
|
+
`can` is for the interface; it cannot be used for an authorization decision; it
|
|
692
|
+
may be incomplete; the server decides again on every request.
|
|
693
|
+
|
|
679
694
|
### How policies are applied
|
|
680
695
|
|
|
681
696
|
On deploy, Palbase diffs your declared schema against the live database and
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@palbase/backend",
|
|
3
|
-
"version": "34.
|
|
3
|
+
"version": "34.1.1",
|
|
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": {
|
|
@@ -94,24 +94,12 @@
|
|
|
94
94
|
"stager",
|
|
95
95
|
"template"
|
|
96
96
|
],
|
|
97
|
-
"scripts": {
|
|
98
|
-
"build": "tsup && npm run build:stager",
|
|
99
|
-
"test": "vitest run",
|
|
100
|
-
"test:watch": "vitest",
|
|
101
|
-
"typecheck": "tsc --noEmit",
|
|
102
|
-
"docs:build": "node scripts/build-llms.mjs",
|
|
103
|
-
"emit:openapi-fixture": "tsx scripts/emit-openapi-fixture.mjs",
|
|
104
|
-
"check:api": "node scripts/api-surface.mjs",
|
|
105
|
-
"api:update": "node scripts/api-surface.mjs --update",
|
|
106
|
-
"build:stager": "mkdir -p stager && for f in src/stager/*.js; do case \"$f\" in *.test.js) ;; *) cp \"$f\" stager/ ;; esac; done && cp src/stager/package.json stager/"
|
|
107
|
-
},
|
|
108
97
|
"dependencies": {
|
|
109
98
|
"@asteasolutions/zod-to-openapi": "^7.3.4",
|
|
110
99
|
"reflect-metadata": "^0.2.2",
|
|
111
100
|
"zod": "^3.24.0"
|
|
112
101
|
},
|
|
113
102
|
"devDependencies": {
|
|
114
|
-
"@palbase/core": "workspace:^",
|
|
115
103
|
"@swc/core": "^1.16.1",
|
|
116
104
|
"@types/node": "^25.5.2",
|
|
117
105
|
"bun-types": "^1.4.0",
|
|
@@ -121,12 +109,24 @@
|
|
|
121
109
|
"tsx": "^4.22.4",
|
|
122
110
|
"typescript": "^5.7.0",
|
|
123
111
|
"unplugin-swc": "^1.5.11",
|
|
124
|
-
"vitest": "^3.0.0"
|
|
112
|
+
"vitest": "^3.0.0",
|
|
113
|
+
"@palbase/core": "^2.4.0"
|
|
125
114
|
},
|
|
126
115
|
"publishConfig": {
|
|
127
116
|
"access": "public"
|
|
128
117
|
},
|
|
129
118
|
"bin": {
|
|
130
119
|
"palbase-backend": "./dist/bin/palbase-backend.js"
|
|
120
|
+
},
|
|
121
|
+
"scripts": {
|
|
122
|
+
"build": "tsup && npm run build:stager",
|
|
123
|
+
"test": "vitest run",
|
|
124
|
+
"test:watch": "vitest",
|
|
125
|
+
"typecheck": "tsc --noEmit",
|
|
126
|
+
"docs:build": "node scripts/build-llms.mjs",
|
|
127
|
+
"emit:openapi-fixture": "tsx scripts/emit-openapi-fixture.mjs",
|
|
128
|
+
"check:api": "node scripts/api-surface.mjs",
|
|
129
|
+
"api:update": "node scripts/api-surface.mjs --update",
|
|
130
|
+
"build:stager": "mkdir -p stager && for f in src/stager/*.js; do case \"$f\" in *.test.js) ;; *) cp \"$f\" stager/ ;; esac; done && cp src/stager/package.json stager/"
|
|
131
131
|
}
|
|
132
|
-
}
|
|
132
|
+
}
|
package/template/db/public.ts
CHANGED
|
@@ -31,6 +31,18 @@ const notes = defineTable("notes", {
|
|
|
31
31
|
.using("user_id = (select auth.uid())")
|
|
32
32
|
.withCheck("user_id = (select auth.uid())"),
|
|
33
33
|
|
|
34
|
+
// A NAMED update policy is also what the row's `can` array echoes back:
|
|
35
|
+
// `note.can` contains "notes_edit_own" when THIS caller may edit THIS row
|
|
36
|
+
// (typed from this file). Put it on the wire with
|
|
37
|
+
// `can(publicSchema.tables.notes)` in the response schema — the schema's
|
|
38
|
+
// table definition, not this handle. `can` is for the interface; it cannot be used for an
|
|
39
|
+
// authorization decision; it may be incomplete; the server decides again
|
|
40
|
+
// on every request.
|
|
41
|
+
policy("notes_edit_own")
|
|
42
|
+
.for("update")
|
|
43
|
+
.to("authenticated")
|
|
44
|
+
.using("user_id = (select auth.uid())"),
|
|
45
|
+
|
|
34
46
|
// MODERATION, when you want it — delete these two if you do not.
|
|
35
47
|
//
|
|
36
48
|
// Roles are yours and there is no built-in "admin": you declare them from
|
package/template/scripts/test.sh
CHANGED
|
File without changes
|