@protobase/server 0.28.4 → 0.29.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/dist/create-admin.js
CHANGED
|
@@ -270,6 +270,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
270
270
|
basePath: `${systemPath}/functions`,
|
|
271
271
|
functions: apiFunctions,
|
|
272
272
|
authenticate,
|
|
273
|
+
access: deps.accessOptions,
|
|
273
274
|
db,
|
|
274
275
|
records: (session) => callerRecords(deps, session),
|
|
275
276
|
onError,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Session } from "../types.js";
|
|
2
2
|
import { FunctionRecords } from "./records.js";
|
|
3
|
+
import { Capability } from "@protobase/schema";
|
|
3
4
|
import { Db } from "@protobase/query";
|
|
4
5
|
//#region src/functions/define-function.d.ts
|
|
5
6
|
/** What every function gets: its name and the raw database. */
|
|
@@ -47,6 +48,11 @@ export type ProtectedFunctionOptions = SharedOptions & {
|
|
|
47
48
|
public?: false;
|
|
48
49
|
/** The caller needs one of these roles, otherwise the call is a 403 problem. */
|
|
49
50
|
roles?: string[];
|
|
51
|
+
/**
|
|
52
|
+
* The caller's roles must grant all of this capability (`roles.holds`), such as `vms.update`, otherwise the call is a
|
|
53
|
+
* 403 problem. Needs the app's `roles`; not together with `roles` here.
|
|
54
|
+
*/
|
|
55
|
+
capability?: Capability;
|
|
50
56
|
};
|
|
51
57
|
export type FunctionOptions = PublicFunctionOptions | ProtectedFunctionOptions;
|
|
52
58
|
/** A function as `createAdmin` serves it: its options and what answers its requests. */
|
|
@@ -63,7 +69,8 @@ export type ApiFunction = {
|
|
|
63
69
|
export type FunctionSource = ApiFunction | FunctionHandler | FunctionApp;
|
|
64
70
|
/**
|
|
65
71
|
* A function of your own, served at `/api/functions/<name>`. Without options it needs a signed-in caller, through the
|
|
66
|
-
* app's `authenticate
|
|
72
|
+
* app's `authenticate`, and with the app's `roles` one whose roles hold `*`; `{ roles }` or `{ capability }` opens it
|
|
73
|
+
* to more, `{ public: true }` to anyone, as for a webhook.
|
|
67
74
|
*/
|
|
68
75
|
export declare function defineFunction(handler: FunctionHandler | FunctionApp): ApiFunction;
|
|
69
76
|
export declare function defineFunction(options: PublicFunctionOptions, handler: PublicFunctionHandler | FunctionApp<PublicFunctionContext>): ApiFunction;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { forbidden } from "../problem.js";
|
|
2
|
+
import { parseCapability } from "@protobase/schema/internal";
|
|
3
|
+
//#region src/functions/function-gate.ts
|
|
4
|
+
const open = () => true;
|
|
5
|
+
const holdsOf = (name, access) => {
|
|
6
|
+
const holds = access.roles?.holds;
|
|
7
|
+
if (!holds) throw new Error(`The function "${name}" is gated by the app's roles, but they have no holds(user, capability): define them with defineRoles`);
|
|
8
|
+
return holds;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Who may call a function, checked at startup: anyone, for a public one; one of its `roles`, or a caller holding all of its
|
|
12
|
+
* `capability`. Without either, an app with roles lets in only callers holding `*`, unless `defaultAccess` is `allow`;
|
|
13
|
+
* an app without roles lets in every signed-in caller.
|
|
14
|
+
*/
|
|
15
|
+
const functionGate = (name, options, access) => {
|
|
16
|
+
if (options.public) return open;
|
|
17
|
+
const { roles, capability } = options;
|
|
18
|
+
if (roles && capability) throw new Error(`The function "${name}" has both roles and a capability: give one`);
|
|
19
|
+
if (roles) return (session) => roles.some((role) => session.user.roles.includes(role));
|
|
20
|
+
if (capability) {
|
|
21
|
+
if (!access.roles) throw new Error(`The function "${name}" needs the capability "${capability}", but the app defines no roles`);
|
|
22
|
+
parseCapability(capability);
|
|
23
|
+
const holds = holdsOf(name, access);
|
|
24
|
+
return (session) => holds(session.user, capability);
|
|
25
|
+
}
|
|
26
|
+
if (!access.roles || access.defaultAccess === "allow") return open;
|
|
27
|
+
const holds = holdsOf(name, access);
|
|
28
|
+
return (session) => holds(session.user, "*");
|
|
29
|
+
};
|
|
30
|
+
/** Throws the 403 problem for a caller the gate refuses. */
|
|
31
|
+
const requireGate = (name, gate, session) => {
|
|
32
|
+
if (gate(session)) return;
|
|
33
|
+
throw forbidden("function-forbidden", `You may not call the function "${name}"`);
|
|
34
|
+
};
|
|
35
|
+
//#endregion
|
|
36
|
+
export { functionGate, requireGate };
|
package/dist/functions/routes.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { notFound } from "../problem.js";
|
|
2
2
|
import { invokeFunction } from "./define-function.js";
|
|
3
3
|
import { runInBackground } from "../background-work.js";
|
|
4
|
+
import { functionGate, requireGate } from "./function-gate.js";
|
|
4
5
|
import { Hono } from "hono";
|
|
5
6
|
import { cors } from "hono/cors";
|
|
6
7
|
//#region src/functions/routes.ts
|
|
@@ -9,17 +10,13 @@ const relativeRequest = (request, prefix) => {
|
|
|
9
10
|
url.pathname = url.pathname.slice(prefix.length) || "/";
|
|
10
11
|
return new Request(url, request);
|
|
11
12
|
};
|
|
12
|
-
const requireRoles = (name, session, roles) => {
|
|
13
|
-
if (!roles || roles.some((role) => session.user.roles.includes(role))) return;
|
|
14
|
-
throw forbidden("function-forbidden", `You may not call the function "${name}"`);
|
|
15
|
-
};
|
|
16
13
|
/**
|
|
17
14
|
* Every function at `<basePath>/<name>` and below, any method. A protected function authenticates the request first, so
|
|
18
|
-
* a call without a valid token is the authenticator's 401 problem; a public one gets the caller only when it asks. Each
|
|
15
|
+
* a call without a valid token is the authenticator's 401 problem, and one its gate refuses a 403; a public one gets the caller only when it asks. Each
|
|
19
16
|
* call runs as background work, so calls that hold the database for long leave a connection to page requests. Once a
|
|
20
17
|
* call ends, failed or not, `wake` runs, so what it wrote reaches open pages without waiting for the dispatcher's poll.
|
|
21
18
|
*/
|
|
22
|
-
const functionRoutes = ({ basePath, functions, authenticate, db, records, onError, wake }) => {
|
|
19
|
+
const functionRoutes = ({ basePath, functions, authenticate, access, db, records, onError, wake }) => {
|
|
23
20
|
const app = new Hono().basePath(basePath);
|
|
24
21
|
app.onError(onError);
|
|
25
22
|
const callerOf = async (request) => {
|
|
@@ -33,6 +30,7 @@ const functionRoutes = ({ basePath, functions, authenticate, db, records, onErro
|
|
|
33
30
|
for (const [name, fn] of Object.entries(functions)) {
|
|
34
31
|
const prefix = `${basePath}/${name}`;
|
|
35
32
|
const { options } = fn;
|
|
33
|
+
const gate = functionGate(name, options, access);
|
|
36
34
|
if (options.cors) app.use(`/${name}/*`, cors(options.cors));
|
|
37
35
|
app.all(`/${name}/*`, (c) => runInBackground(() => call(async () => {
|
|
38
36
|
const request = relativeRequest(c.req.raw, prefix);
|
|
@@ -45,7 +43,7 @@ const functionRoutes = ({ basePath, functions, authenticate, db, records, onErro
|
|
|
45
43
|
});
|
|
46
44
|
}
|
|
47
45
|
const caller = await callerOf(c.req.raw);
|
|
48
|
-
|
|
46
|
+
requireGate(name, gate, caller.session);
|
|
49
47
|
const context = {
|
|
50
48
|
name,
|
|
51
49
|
db,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"description": "The Protobase REST API as a Hono app, with Better Auth sign-in.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@better-auth/passkey": "^1.7.7",
|
|
34
34
|
"@better-auth/sso": "^1.7.7",
|
|
35
|
-
"@protobase/layout": "0.
|
|
36
|
-
"@protobase/query": "0.
|
|
37
|
-
"@protobase/schema": "0.
|
|
35
|
+
"@protobase/layout": "0.29.0",
|
|
36
|
+
"@protobase/query": "0.29.0",
|
|
37
|
+
"@protobase/schema": "0.29.0",
|
|
38
38
|
"@react-email/render": "^2.1.0",
|
|
39
39
|
"@scalar/hono-api-reference": "^0.12.10",
|
|
40
40
|
"@sentry/core": "^11.7.0",
|