@protobase/server 0.37.1 → 0.37.2
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/better-auth/session-token.js +8 -3
- package/dist/create-admin.d.ts +3 -2
- package/dist/functions/define-function.d.ts +5 -3
- package/dist/functions/function-socket.d.ts +6 -0
- package/dist/functions/function-socket.js +6 -2
- package/dist/functions/routes.js +16 -9
- package/dist/functions/socket-recheck.js +19 -7
- package/package.json +4 -4
|
@@ -7,11 +7,16 @@ const messageOf = (body) => {
|
|
|
7
7
|
};
|
|
8
8
|
/**
|
|
9
9
|
* The bearer token of the session a request's cookie carries, minted as `GET /api/auth/token` would, with the same
|
|
10
|
-
* plugin hooks (the sign-in policy's hold-back included)
|
|
11
|
-
* signature
|
|
12
|
-
*
|
|
10
|
+
* plugin hooks (the sign-in policy's hold-back included). The direct call has no rate limit, so nothing before the
|
|
11
|
+
* signature check may read the store: the session is read first, which refuses a cookie that is missing, forged or for
|
|
12
|
+
* no session before any store read, and the policy is read only for a session that exists. No session is a 401
|
|
13
|
+
* problem, a refusal a 403 with Better Auth's message; any other error, such as a store failure, stays as it is.
|
|
13
14
|
*/
|
|
14
15
|
const betterAuthSessionToken = (auth) => async (request) => {
|
|
16
|
+
if (await auth.api.getSession({
|
|
17
|
+
headers: request.headers,
|
|
18
|
+
query: { disableRefresh: true }
|
|
19
|
+
}) === null) throw unauthorized("Sign in first");
|
|
15
20
|
try {
|
|
16
21
|
const { token } = await auth.api.getToken({ headers: request.headers });
|
|
17
22
|
return token;
|
package/dist/create-admin.d.ts
CHANGED
|
@@ -75,8 +75,9 @@ export type CreateAdminInput = {
|
|
|
75
75
|
/** @internal Verifies a user's token on `/api/sb/rest/v1`, `/api/sb/realtime/v1` and `/api/sb/storage/v1` and gives its claims, which tests fake; those `/api/sb/auth/v1` signs otherwise. */
|
|
76
76
|
verifyToken?: (token: string) => Promise<JWTPayload>;
|
|
77
77
|
/**
|
|
78
|
-
* The host's WebSocket upgrade,
|
|
79
|
-
*
|
|
78
|
+
* The host's WebSocket upgrade, `@hono/node-server`'s `upgradeWebSocket`, the only one supported: function sockets use
|
|
79
|
+
* its `ws` socket's send callback and `bufferedAmount`. With it, functions accept sockets, and an app with keys and
|
|
80
|
+
* live resources serves supabase-js's Realtime at `/api/sb/realtime/v1/websocket`; without it, the app serves none.
|
|
80
81
|
*/
|
|
81
82
|
upgradeWebSocket?: UpgradeWebSocket;
|
|
82
83
|
};
|
|
@@ -14,9 +14,11 @@ export type PublicFunctionContext = {
|
|
|
14
14
|
/** The caller, through the app's own `authenticate`: rejects with a 401 problem when the request carries no valid token. */
|
|
15
15
|
caller: () => Promise<FunctionCaller>;
|
|
16
16
|
/**
|
|
17
|
-
* Answers a WebSocket upgrade with a socket: return its response. A request that is no upgrade is a 426 problem.
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* Answers a WebSocket upgrade with a socket: return its response. A request that is no upgrade is a 426 problem. While
|
|
18
|
+
* the socket is open, the caller of a protected function, and of a public one once `caller()` resolved, is checked
|
|
19
|
+
* again every minute: their credential, the function's roles, and the same user, tenant, organization and roles. The
|
|
20
|
+
* socket closes with 1008 once they would be refused. The function's own checks, such as access to one record, are
|
|
21
|
+
* not repeated unless it passes them as `recheck`.
|
|
20
22
|
*/
|
|
21
23
|
acceptWebSocket: (events: SocketEvents) => Response;
|
|
22
24
|
};
|
|
@@ -14,5 +14,11 @@ export type SocketEvents = {
|
|
|
14
14
|
/** Text arrives as a string, binary as an ArrayBuffer. The next message waits until a returned promise settles. */
|
|
15
15
|
onMessage?: (data: string | ArrayBuffer, socket: FunctionSocket) => void | Promise<void>;
|
|
16
16
|
onClose?: (code: number, reason: string) => void | Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Checked every minute after the framework's own check, while the socket is open; what it returns is ignored. Throw
|
|
19
|
+
* or reject with a 401, 403 or 404 problem to close the socket with 1008; any other error is reported and closes it
|
|
20
|
+
* with 1011.
|
|
21
|
+
*/
|
|
22
|
+
recheck?: () => unknown;
|
|
17
23
|
};
|
|
18
24
|
//#endregion
|
|
@@ -61,8 +61,12 @@ const socketEvents = (events, { report, recheck }) => {
|
|
|
61
61
|
return raw.bufferedAmount;
|
|
62
62
|
}
|
|
63
63
|
};
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
const check = async () => {
|
|
65
|
+
await recheck?.();
|
|
66
|
+
await events.recheck?.();
|
|
67
|
+
};
|
|
68
|
+
if (recheck || events.recheck) stopRecheck = recheckWhileOpen({
|
|
69
|
+
check,
|
|
66
70
|
close: end,
|
|
67
71
|
report
|
|
68
72
|
});
|
package/dist/functions/routes.js
CHANGED
|
@@ -18,8 +18,8 @@ const isUpgrade = (request) => request.headers.get("upgrade")?.toLowerCase() ===
|
|
|
18
18
|
* 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
19
|
* call runs as background work, so calls that hold the database for long leave a connection to page requests. Once a
|
|
20
20
|
* call ends, failed or not, `wake` runs, so what it wrote reaches open pages without waiting for the dispatcher's poll.
|
|
21
|
-
* A function may answer an upgrade with a socket, only once it returned
|
|
22
|
-
* every minute
|
|
21
|
+
* A function may answer an upgrade with a socket, only once it returned. While it is open, its caller is checked again
|
|
22
|
+
* every minute: a protected function's always, a public one's once `caller()` resolved.
|
|
23
23
|
*/
|
|
24
24
|
const functionRoutes = ({ basePath, functions, authenticate, access, db, records, onError, wake, upgradeWebSocket, upgradeAuthenticate = authenticate, report }) => {
|
|
25
25
|
const app = new Hono().basePath(basePath);
|
|
@@ -33,6 +33,11 @@ const functionRoutes = ({ basePath, functions, authenticate, access, db, records
|
|
|
33
33
|
};
|
|
34
34
|
};
|
|
35
35
|
const call = (work) => work().finally(wake);
|
|
36
|
+
const recheckCaller = async (name, gate, request, first) => {
|
|
37
|
+
const again = await upgradeAuthenticate(request);
|
|
38
|
+
requireGate(name, gate, again);
|
|
39
|
+
if (!sameCaller(first, again)) throw forbidden("caller-changed", "Your access changed: open the socket again");
|
|
40
|
+
};
|
|
36
41
|
for (const [name, fn] of Object.entries(functions)) {
|
|
37
42
|
const prefix = `${basePath}/${name}`;
|
|
38
43
|
const { options } = fn;
|
|
@@ -42,26 +47,28 @@ const functionRoutes = ({ basePath, functions, authenticate, access, db, records
|
|
|
42
47
|
const request = relativeRequest(c.req.raw, prefix);
|
|
43
48
|
if (options.public) {
|
|
44
49
|
let caller;
|
|
50
|
+
let admitted;
|
|
51
|
+
const resolveCaller = () => caller ??= callerOf(c.req.raw).then((resolved) => (admitted = resolved.session, resolved));
|
|
52
|
+
const recheck = async () => {
|
|
53
|
+
if (admitted) await recheckCaller(name, gate, c.req.raw, admitted);
|
|
54
|
+
};
|
|
45
55
|
const acceptor = socketAcceptor({
|
|
46
56
|
c,
|
|
47
57
|
upgradeWebSocket,
|
|
48
|
-
report
|
|
58
|
+
report,
|
|
59
|
+
recheck
|
|
49
60
|
});
|
|
50
61
|
const context = {
|
|
51
62
|
name,
|
|
52
63
|
db,
|
|
53
|
-
caller:
|
|
64
|
+
caller: resolveCaller,
|
|
54
65
|
acceptWebSocket: acceptor.accept
|
|
55
66
|
};
|
|
56
67
|
return acceptor.answer(name, await invokeFunction(name, fn, request, context));
|
|
57
68
|
}
|
|
58
69
|
const caller = await callerOf(c.req.raw);
|
|
59
70
|
requireGate(name, gate, caller.session);
|
|
60
|
-
const recheck =
|
|
61
|
-
const again = await upgradeAuthenticate(c.req.raw);
|
|
62
|
-
requireGate(name, gate, again);
|
|
63
|
-
if (!sameCaller(caller.session, again)) throw forbidden("caller-changed", "Your access changed: open the socket again");
|
|
64
|
-
};
|
|
71
|
+
const recheck = () => recheckCaller(name, gate, c.req.raw, caller.session);
|
|
65
72
|
const acceptor = socketAcceptor({
|
|
66
73
|
c,
|
|
67
74
|
upgradeWebSocket,
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { HttpProblem } from "../problem.js";
|
|
2
2
|
//#region src/functions/socket-recheck.ts
|
|
3
|
-
const
|
|
3
|
+
const refusals = /* @__PURE__ */ new Set([
|
|
4
|
+
401,
|
|
5
|
+
403,
|
|
6
|
+
404
|
|
7
|
+
]);
|
|
8
|
+
const isRefusal = (error) => error instanceof HttpProblem && refusals.has(error.status);
|
|
4
9
|
/**
|
|
5
|
-
* Runs `check` every `everyMs` while a socket is open, one check after the other. A refusal
|
|
6
|
-
* (Policy Violation) and the problem's title; any other error is
|
|
7
|
-
* stops the check ends the access too.
|
|
10
|
+
* Runs `check` every `everyMs` while a socket is open, one check after the other. A refusal, such as a record the caller
|
|
11
|
+
* can no longer see (404), closes the socket with 1008 (Policy Violation) and the problem's title; any other error is
|
|
12
|
+
* reported and closes it with 1011, so an outage that stops the check ends the access too. A check still pending after
|
|
13
|
+
* another `everyMs` counts as such an error, so a hung store never leaves a socket open unchecked. Returns the function
|
|
14
|
+
* that stops it.
|
|
8
15
|
*/
|
|
9
16
|
const recheckWhileOpen = ({ check, close, report, everyMs = 6e4 }) => {
|
|
10
17
|
let stopped = false;
|
|
@@ -16,9 +23,14 @@ const recheckWhileOpen = ({ check, close, report, everyMs = 6e4 }) => {
|
|
|
16
23
|
report(error);
|
|
17
24
|
close(1011, "Internal Error");
|
|
18
25
|
};
|
|
19
|
-
const run = () =>
|
|
20
|
-
|
|
21
|
-
|
|
26
|
+
const run = () => {
|
|
27
|
+
timer = setTimeout(() => end(/* @__PURE__ */ new Error(`A socket's check did not finish within ${everyMs} ms`)), everyMs);
|
|
28
|
+
timer.unref?.();
|
|
29
|
+
check().then(() => {
|
|
30
|
+
clearTimeout(timer);
|
|
31
|
+
if (!stopped) schedule();
|
|
32
|
+
}, end);
|
|
33
|
+
};
|
|
22
34
|
const schedule = () => {
|
|
23
35
|
timer = setTimeout(run, everyMs);
|
|
24
36
|
timer.unref?.();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/server",
|
|
3
|
-
"version": "0.37.
|
|
3
|
+
"version": "0.37.2",
|
|
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": {
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
"@better-auth/passkey": "^1.7.12",
|
|
37
37
|
"@better-auth/sso": "^1.7.12",
|
|
38
38
|
"@modelcontextprotocol/server": "^2.3.1",
|
|
39
|
-
"@protobase/layout": "0.37.
|
|
40
|
-
"@protobase/query": "0.37.
|
|
41
|
-
"@protobase/schema": "0.37.
|
|
39
|
+
"@protobase/layout": "0.37.2",
|
|
40
|
+
"@protobase/query": "0.37.2",
|
|
41
|
+
"@protobase/schema": "0.37.2",
|
|
42
42
|
"@react-email/render": "^2.1.0",
|
|
43
43
|
"@scalar/hono-api-reference": "^0.12.10",
|
|
44
44
|
"@sentry/core": "^11.7.0",
|