@protobase/server 0.33.2 → 0.33.4
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 +26 -0
- package/dist/check-owners.js +12 -6
- package/dist/create-admin.js +15 -3
- package/dist/functions/accept-socket.js +32 -0
- package/dist/functions/define-function.d.ts +7 -0
- package/dist/functions/function-socket.d.ts +18 -0
- package/dist/functions/function-socket.js +81 -0
- package/dist/functions/routes.js +33 -8
- package/dist/functions/same-caller.js +9 -0
- package/dist/functions/socket-recheck.js +33 -0
- package/dist/functions/upgrade-caller.js +25 -0
- package/dist/index.d.ts +2 -1
- package/dist/registry.d.ts +1 -1
- package/dist/types.d.ts +9 -3
- package/dist/webhooks/attempt.js +5 -1
- package/package.json +4 -4
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { forbidden, unauthorized } from "../problem.js";
|
|
2
|
+
import { APIError } from "better-auth/api";
|
|
3
|
+
//#region src/better-auth/session-token.ts
|
|
4
|
+
const messageOf = (body) => {
|
|
5
|
+
const message = body?.message;
|
|
6
|
+
return typeof message === "string" ? message : "Signing in is refused";
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
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) but without its rate limit: a forged cookie fails its
|
|
11
|
+
* signature before any store read. No session is a 401 problem, a refusal a 403 with Better Auth's message; any other
|
|
12
|
+
* error stays as it is.
|
|
13
|
+
*/
|
|
14
|
+
const betterAuthSessionToken = (auth) => async (request) => {
|
|
15
|
+
try {
|
|
16
|
+
const { token } = await auth.api.getToken({ headers: request.headers });
|
|
17
|
+
return token;
|
|
18
|
+
} catch (error) {
|
|
19
|
+
if (!(error instanceof APIError)) throw error;
|
|
20
|
+
if (error.statusCode === 401) throw unauthorized("Sign in first");
|
|
21
|
+
if (error.statusCode === 403) throw forbidden("sign-in-refused", messageOf(error.body));
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
//#endregion
|
|
26
|
+
export { betterAuthSessionToken };
|
package/dist/check-owners.js
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
//#region src/check-owners.ts
|
|
2
|
+
const holds = (field, id) => field === "text" || field === id || field === "bigint" && id === "integer";
|
|
2
3
|
/**
|
|
3
|
-
* Stops startup on an `.owner()` field that cannot hold the ids of the
|
|
4
|
-
*
|
|
5
|
-
* ids come from the app's own authenticator, which this
|
|
4
|
+
* Stops startup on an `.owner()` field that cannot hold the ids of the signed-in users. The authenticator's
|
|
5
|
+
* `userIdType` declares their type; without it, the auth instance's users have Better Auth's own ids, random text,
|
|
6
|
+
* unless `generateUserId` makes the ids. Without either, the ids come from the app's own authenticator, which this
|
|
7
|
+
* cannot see.
|
|
6
8
|
*/
|
|
7
|
-
const checkOwnerFields = (registry, auth) => {
|
|
9
|
+
const checkOwnerFields = (registry, userIdType, auth) => {
|
|
10
|
+
if (userIdType) return checkAgainst(registry, userIdType, (ownerType) => `the authenticator's users have ${userIdType} ids: make it a ${userIdType} or text column, or declare the authenticator's userIdType`);
|
|
8
11
|
if (!auth || auth.generatesUserIds) return;
|
|
12
|
+
checkAgainst(registry, "text", (ownerType) => `Better Auth's user ids are random text: make it a text column, or give createAuth a generateUserId that makes ${ownerType} ids`);
|
|
13
|
+
};
|
|
14
|
+
const checkAgainst = (registry, userIdType, fix) => {
|
|
9
15
|
for (const entry of registry.entries) {
|
|
10
|
-
if (entry.ownerType === void 0 || entry.ownerType
|
|
11
|
-
throw new Error(`${entry.name}.${entry.model.owner} is the owner of its records, so it holds the signed-in user's id, but it holds ${entry.ownerType} values and
|
|
16
|
+
if (entry.ownerType === void 0 || holds(entry.ownerType, userIdType)) continue;
|
|
17
|
+
throw new Error(`${entry.name}.${entry.model.owner} is the owner of its records, so it holds the signed-in user's id, but it holds ${entry.ownerType} values and ${fix(entry.ownerType)}`);
|
|
12
18
|
}
|
|
13
19
|
};
|
|
14
20
|
//#endregion
|
package/dist/create-admin.js
CHANGED
|
@@ -25,6 +25,7 @@ import { authBasePath } from "./better-auth/auth-base-path.js";
|
|
|
25
25
|
import { withoutSessionTokens } from "./better-auth/without-session-tokens.js";
|
|
26
26
|
import { organizationRoutes } from "./better-auth/organizations/admin-routes.js";
|
|
27
27
|
import { statusRoute } from "./better-auth/status-route.js";
|
|
28
|
+
import { betterAuthSessionToken } from "./better-auth/session-token.js";
|
|
28
29
|
import { goTrueTokens } from "./gotrue/access-tokens.js";
|
|
29
30
|
import { goTrueRoutes } from "./gotrue/routes.js";
|
|
30
31
|
import { changesSchema } from "./changes/events-options.js";
|
|
@@ -58,6 +59,7 @@ import { formRoutes } from "./forms/routes.js";
|
|
|
58
59
|
import { createFormsRuntime } from "./forms/runtime.js";
|
|
59
60
|
import { functionRoutes } from "./functions/routes.js";
|
|
60
61
|
import { supabaseFunctionRoutes } from "./functions/supabase-routes.js";
|
|
62
|
+
import { upgradeAuthenticator } from "./functions/upgrade-caller.js";
|
|
61
63
|
import { localeSetting } from "./locale/locale-option.js";
|
|
62
64
|
import { mapSettings } from "./maps/map-settings.js";
|
|
63
65
|
import { pluginOptions, pluginsBeforeSignIn } from "./plugins/plugin-settings.js";
|
|
@@ -115,7 +117,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
115
117
|
checkBottomBars(registry, bottomBarModels, pageModels.map((entry) => entry.name), declaredRoles(auth, options.roles));
|
|
116
118
|
checkViews(registry, viewModels, pageModels.map((entry) => entry.name), fieldTypes);
|
|
117
119
|
checkRoleLimits(registry, options.roles?.limits ?? []);
|
|
118
|
-
checkOwnerFields(registry, auth);
|
|
120
|
+
checkOwnerFields(registry, authenticate.userIdType, auth);
|
|
119
121
|
checkPositionAfter(registry);
|
|
120
122
|
checkParents(registry);
|
|
121
123
|
checkPages(pageModels, {
|
|
@@ -272,6 +274,14 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
272
274
|
report: options.onUnhandledError
|
|
273
275
|
}));
|
|
274
276
|
}
|
|
277
|
+
const report = options.onUnhandledError ?? ((error) => console.error(error));
|
|
278
|
+
const upgradeAuthenticate = upgradeAuthenticator({
|
|
279
|
+
authenticate,
|
|
280
|
+
...auth && {
|
|
281
|
+
origins: auth.origins,
|
|
282
|
+
sessionToken: betterAuthSessionToken(auth)
|
|
283
|
+
}
|
|
284
|
+
});
|
|
275
285
|
app.route("/", functionRoutes({
|
|
276
286
|
basePath: `${systemPath}/functions`,
|
|
277
287
|
functions: apiFunctions,
|
|
@@ -280,7 +290,10 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
280
290
|
db,
|
|
281
291
|
records: (session) => callerRecords(deps, session),
|
|
282
292
|
onError,
|
|
283
|
-
wake: () => wakeDispatch(deps)
|
|
293
|
+
wake: () => wakeDispatch(deps),
|
|
294
|
+
upgradeWebSocket,
|
|
295
|
+
upgradeAuthenticate,
|
|
296
|
+
report
|
|
284
297
|
}));
|
|
285
298
|
const verifyGoTrue = auth ? jwtVerifier(goTrueTokens({ auth })) : () => Promise.reject(unauthorized("This app signs no user tokens"));
|
|
286
299
|
app.route("/", supabaseFunctionRoutes({
|
|
@@ -296,7 +309,6 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
296
309
|
app.route("/", batchRoute(deps, authenticate, meta));
|
|
297
310
|
app.route("/", fileRoutes(deps, authenticate));
|
|
298
311
|
if (forms) app.route(`${systemPath}/forms`, formRoutes(deps, forms));
|
|
299
|
-
const report = options.onUnhandledError ?? ((error) => console.error(error));
|
|
300
312
|
const feed = tableFeed({
|
|
301
313
|
db,
|
|
302
314
|
schema: changes,
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { HttpProblem } from "../problem.js";
|
|
2
|
+
import { socketEvents } from "./function-socket.js";
|
|
3
|
+
//#region src/functions/accept-socket.ts
|
|
4
|
+
const marker = "protobase-websocket";
|
|
5
|
+
/**
|
|
6
|
+
* One call's `acceptWebSocket` and what turns its answer into the upgrade. `accept` only records the events and returns
|
|
7
|
+
* a marker response; `answer` upgrades once the function has returned it, so a caller that is refused, or a function
|
|
8
|
+
* that throws, never gets a socket.
|
|
9
|
+
*/
|
|
10
|
+
const socketAcceptor = ({ c, upgradeWebSocket, report, recheck }) => {
|
|
11
|
+
let accepted;
|
|
12
|
+
const accept = (events) => {
|
|
13
|
+
if (c.req.header("upgrade")?.toLowerCase() !== "websocket") throw new HttpProblem(426, "upgrade-required", "Upgrade Required", "Open this function with a WebSocket", {}, { upgrade: "websocket" });
|
|
14
|
+
if (!upgradeWebSocket) throw new Error("This server does not accept WebSockets");
|
|
15
|
+
accepted = events;
|
|
16
|
+
return new Response(null, { headers: { [marker]: "accept" } });
|
|
17
|
+
};
|
|
18
|
+
const answer = (name, response) => {
|
|
19
|
+
if (!accepted) return response;
|
|
20
|
+
if (!response.headers.has(marker)) throw new Error(`The function "${name}" accepted a WebSocket but answered with another response`);
|
|
21
|
+
return upgradeWebSocket(c, socketEvents(accepted, {
|
|
22
|
+
report,
|
|
23
|
+
...recheck && { recheck }
|
|
24
|
+
}), { onError: report });
|
|
25
|
+
};
|
|
26
|
+
return {
|
|
27
|
+
accept,
|
|
28
|
+
answer
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
//#endregion
|
|
32
|
+
export { socketAcceptor };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Session } from "../types.js";
|
|
2
2
|
import { Caller } from "../api-keys/resolve-caller.js";
|
|
3
|
+
import { SocketEvents } from "./function-socket.js";
|
|
3
4
|
import { FunctionRecords } from "./records.js";
|
|
4
5
|
import { Capability } from "@protobase/schema";
|
|
5
6
|
import { Db } from "@protobase/query";
|
|
@@ -12,6 +13,12 @@ export type PublicFunctionContext = {
|
|
|
12
13
|
db: Db;
|
|
13
14
|
/** The caller, through the app's own `authenticate`: rejects with a 401 problem when the request carries no valid token. */
|
|
14
15
|
caller: () => Promise<FunctionCaller>;
|
|
16
|
+
/**
|
|
17
|
+
* Answers a WebSocket upgrade with a socket: return its response. A request that is no upgrade is a 426 problem. A
|
|
18
|
+
* protected function's caller is checked again every minute while the socket is open, which closes with 1008 once
|
|
19
|
+
* they would be refused; a public function checks its own callers.
|
|
20
|
+
*/
|
|
21
|
+
acceptWebSocket: (events: SocketEvents) => Response;
|
|
15
22
|
};
|
|
16
23
|
/** Who called a function, and the app's resources as them. */
|
|
17
24
|
export type FunctionCaller = {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import "hono/ws";
|
|
2
|
+
//#region src/functions/function-socket.d.ts
|
|
3
|
+
/** An open WebSocket as a function sees it. */
|
|
4
|
+
export type FunctionSocket = {
|
|
5
|
+
/** Resolves once the data is written to the connection, or at once when the socket is closed; it never rejects. */
|
|
6
|
+
send(data: string | ArrayBuffer | Uint8Array): Promise<void>;
|
|
7
|
+
close(code?: number, reason?: string): void;
|
|
8
|
+
/** The bytes queued to send but not yet written. */
|
|
9
|
+
readonly bufferedAmount: number;
|
|
10
|
+
};
|
|
11
|
+
/** What a function's socket does: see `acceptWebSocket`. */
|
|
12
|
+
export type SocketEvents = {
|
|
13
|
+
onOpen?: (socket: FunctionSocket) => void | Promise<void>;
|
|
14
|
+
/** Text arrives as a string, binary as an ArrayBuffer. The next message waits until a returned promise settles. */
|
|
15
|
+
onMessage?: (data: string | ArrayBuffer, socket: FunctionSocket) => void | Promise<void>;
|
|
16
|
+
onClose?: (code: number, reason: string) => void | Promise<void>;
|
|
17
|
+
};
|
|
18
|
+
//#endregion
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { recheckWhileOpen } from "./socket-recheck.js";
|
|
2
|
+
//#region src/functions/function-socket.ts
|
|
3
|
+
const openState = 1;
|
|
4
|
+
/** Received bytes waiting for `onMessage` above which reading pauses. */
|
|
5
|
+
const pauseAboveBytes = 1 << 20;
|
|
6
|
+
/** Where reading cannot pause, the waiting bytes above which the socket closes with 1013 (Try Again Later). */
|
|
7
|
+
const closeAboveBytes = 16 << 20;
|
|
8
|
+
const sizeOf = (data) => typeof data === "string" ? data.length : data.byteLength;
|
|
9
|
+
/**
|
|
10
|
+
* A function's socket events as Hono's, over the host's raw socket. Messages reach `onMessage` one at a time, in order,
|
|
11
|
+
* after `onOpen`, and `onClose` comes after the last of them. A handler that throws or rejects is reported and closes
|
|
12
|
+
* the socket with 1011.
|
|
13
|
+
*/
|
|
14
|
+
const socketEvents = (events, { report, recheck }) => {
|
|
15
|
+
let raw;
|
|
16
|
+
let socket;
|
|
17
|
+
let chain = Promise.resolve();
|
|
18
|
+
let waiting = 0;
|
|
19
|
+
let paused = false;
|
|
20
|
+
let ended = false;
|
|
21
|
+
let stopRecheck = () => {};
|
|
22
|
+
const end = (code, reason) => {
|
|
23
|
+
ended = true;
|
|
24
|
+
stopRecheck();
|
|
25
|
+
raw.close(code, reason);
|
|
26
|
+
};
|
|
27
|
+
const fail = (error) => {
|
|
28
|
+
report(error);
|
|
29
|
+
if (!ended) end(1011, "Internal Error");
|
|
30
|
+
};
|
|
31
|
+
const enqueue = (step, always = false) => {
|
|
32
|
+
chain = chain.then(() => ended && !always ? void 0 : step()).catch(fail);
|
|
33
|
+
};
|
|
34
|
+
const received = (data) => {
|
|
35
|
+
const size = sizeOf(data);
|
|
36
|
+
waiting += size;
|
|
37
|
+
if (waiting > 1048576 && raw.pause && !paused) {
|
|
38
|
+
paused = true;
|
|
39
|
+
raw.pause();
|
|
40
|
+
}
|
|
41
|
+
if (waiting > 16777216 && !raw.pause && !ended) end(1013, "Try Again Later");
|
|
42
|
+
enqueue(async () => {
|
|
43
|
+
await events.onMessage?.(data, socket);
|
|
44
|
+
waiting -= size;
|
|
45
|
+
if (paused && waiting === 0) {
|
|
46
|
+
paused = false;
|
|
47
|
+
raw.resume?.();
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
onOpen: (_event, ws) => {
|
|
53
|
+
raw = ws.raw;
|
|
54
|
+
socket = {
|
|
55
|
+
send: (data) => new Promise((resolve) => {
|
|
56
|
+
if (raw.readyState !== openState) return resolve();
|
|
57
|
+
raw.send(data, () => resolve());
|
|
58
|
+
}),
|
|
59
|
+
close: (code, reason) => raw.close(code, reason),
|
|
60
|
+
get bufferedAmount() {
|
|
61
|
+
return raw.bufferedAmount;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
if (recheck) stopRecheck = recheckWhileOpen({
|
|
65
|
+
check: recheck,
|
|
66
|
+
close: end,
|
|
67
|
+
report
|
|
68
|
+
});
|
|
69
|
+
enqueue(() => events.onOpen?.(socket));
|
|
70
|
+
},
|
|
71
|
+
onMessage: (event) => {
|
|
72
|
+
if (typeof event.data === "string" || event.data instanceof ArrayBuffer) received(event.data);
|
|
73
|
+
},
|
|
74
|
+
onClose: (event) => {
|
|
75
|
+
stopRecheck();
|
|
76
|
+
enqueue(() => events.onClose?.(event.code, event.reason), true);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
//#endregion
|
|
81
|
+
export { closeAboveBytes, pauseAboveBytes, socketEvents };
|
package/dist/functions/routes.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { notFound } from "../problem.js";
|
|
1
|
+
import { forbidden, notFound } from "../problem.js";
|
|
2
2
|
import { invokeFunction } from "./define-function.js";
|
|
3
3
|
import { runInBackground } from "../background-work.js";
|
|
4
|
+
import { socketAcceptor } from "./accept-socket.js";
|
|
4
5
|
import { functionGate, requireGate } from "./function-gate.js";
|
|
6
|
+
import { sameCaller } from "./same-caller.js";
|
|
5
7
|
import { Hono } from "hono";
|
|
6
8
|
import { cors } from "hono/cors";
|
|
7
9
|
//#region src/functions/routes.ts
|
|
@@ -10,17 +12,21 @@ const relativeRequest = (request, prefix) => {
|
|
|
10
12
|
url.pathname = url.pathname.slice(prefix.length) || "/";
|
|
11
13
|
return new Request(url, request);
|
|
12
14
|
};
|
|
15
|
+
const isUpgrade = (request) => request.headers.get("upgrade")?.toLowerCase() === "websocket";
|
|
13
16
|
/**
|
|
14
17
|
* Every function at `<basePath>/<name>` and below, any method. A protected function authenticates the request first, so
|
|
15
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
|
|
16
19
|
* call runs as background work, so calls that hold the database for long leave a connection to page requests. Once a
|
|
17
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; a protected one's caller is checked again
|
|
22
|
+
* every minute while it is open.
|
|
18
23
|
*/
|
|
19
|
-
const functionRoutes = ({ basePath, functions, authenticate, access, db, records, onError, wake }) => {
|
|
24
|
+
const functionRoutes = ({ basePath, functions, authenticate, access, db, records, onError, wake, upgradeWebSocket, upgradeAuthenticate = authenticate, report }) => {
|
|
20
25
|
const app = new Hono().basePath(basePath);
|
|
21
26
|
app.onError(onError);
|
|
27
|
+
const sessionOf = (request) => isUpgrade(request) ? upgradeAuthenticate(request) : authenticate(request);
|
|
22
28
|
const callerOf = async (request) => {
|
|
23
|
-
const session = await
|
|
29
|
+
const session = await sessionOf(request);
|
|
24
30
|
return {
|
|
25
31
|
session,
|
|
26
32
|
records: records(session)
|
|
@@ -36,21 +42,40 @@ const functionRoutes = ({ basePath, functions, authenticate, access, db, records
|
|
|
36
42
|
const request = relativeRequest(c.req.raw, prefix);
|
|
37
43
|
if (options.public) {
|
|
38
44
|
let caller;
|
|
39
|
-
|
|
45
|
+
const acceptor = socketAcceptor({
|
|
46
|
+
c,
|
|
47
|
+
upgradeWebSocket,
|
|
48
|
+
report
|
|
49
|
+
});
|
|
50
|
+
const context = {
|
|
40
51
|
name,
|
|
41
52
|
db,
|
|
42
|
-
caller: () => caller ??= callerOf(c.req.raw)
|
|
43
|
-
|
|
53
|
+
caller: () => caller ??= callerOf(c.req.raw),
|
|
54
|
+
acceptWebSocket: acceptor.accept
|
|
55
|
+
};
|
|
56
|
+
return acceptor.answer(name, await invokeFunction(name, fn, request, context));
|
|
44
57
|
}
|
|
45
58
|
const caller = await callerOf(c.req.raw);
|
|
46
59
|
requireGate(name, gate, caller.session);
|
|
60
|
+
const recheck = async () => {
|
|
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
|
+
};
|
|
65
|
+
const acceptor = socketAcceptor({
|
|
66
|
+
c,
|
|
67
|
+
upgradeWebSocket,
|
|
68
|
+
report,
|
|
69
|
+
recheck
|
|
70
|
+
});
|
|
47
71
|
const context = {
|
|
48
72
|
name,
|
|
49
73
|
db,
|
|
50
74
|
...caller,
|
|
51
|
-
caller: async () => caller
|
|
75
|
+
caller: async () => caller,
|
|
76
|
+
acceptWebSocket: acceptor.accept
|
|
52
77
|
};
|
|
53
|
-
return invokeFunction(name, fn, request, context);
|
|
78
|
+
return acceptor.answer(name, await invokeFunction(name, fn, request, context));
|
|
54
79
|
})));
|
|
55
80
|
}
|
|
56
81
|
app.all("/*", (c) => {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//#region src/functions/same-caller.ts
|
|
2
|
+
const sameRoles = (a, b) => {
|
|
3
|
+
const roles = new Set(a);
|
|
4
|
+
return roles.size === new Set(b).size && b.every((role) => roles.has(role));
|
|
5
|
+
};
|
|
6
|
+
/** Whether two resolutions of a caller give the same user, tenant, organization and roles, in any order. */
|
|
7
|
+
const sameCaller = (a, b) => a.user.id === b.user.id && a.tenant === b.tenant && a.organization?.id === b.organization?.id && sameRoles(a.user.roles, b.user.roles);
|
|
8
|
+
//#endregion
|
|
9
|
+
export { sameCaller };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { HttpProblem } from "../problem.js";
|
|
2
|
+
//#region src/functions/socket-recheck.ts
|
|
3
|
+
const isRefusal = (error) => error instanceof HttpProblem && (error.status === 401 || error.status === 403);
|
|
4
|
+
/**
|
|
5
|
+
* Runs `check` every `everyMs` while a socket is open, one check after the other. A refusal closes the socket with 1008
|
|
6
|
+
* (Policy Violation) and the problem's title; any other error is reported and closes it with 1011, so an outage that
|
|
7
|
+
* stops the check ends the access too. Returns the function that stops it.
|
|
8
|
+
*/
|
|
9
|
+
const recheckWhileOpen = ({ check, close, report, everyMs = 6e4 }) => {
|
|
10
|
+
let stopped = false;
|
|
11
|
+
let timer;
|
|
12
|
+
const end = (error) => {
|
|
13
|
+
if (stopped) return;
|
|
14
|
+
stopped = true;
|
|
15
|
+
if (isRefusal(error)) return close(1008, error.title);
|
|
16
|
+
report(error);
|
|
17
|
+
close(1011, "Internal Error");
|
|
18
|
+
};
|
|
19
|
+
const run = () => check().then(() => {
|
|
20
|
+
if (!stopped) schedule();
|
|
21
|
+
}, end);
|
|
22
|
+
const schedule = () => {
|
|
23
|
+
timer = setTimeout(run, everyMs);
|
|
24
|
+
timer.unref?.();
|
|
25
|
+
};
|
|
26
|
+
schedule();
|
|
27
|
+
return () => {
|
|
28
|
+
stopped = true;
|
|
29
|
+
clearTimeout(timer);
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
//#endregion
|
|
33
|
+
export { recheckWhileOpen };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { forbidden } from "../problem.js";
|
|
2
|
+
//#region src/functions/upgrade-caller.ts
|
|
3
|
+
const hostOf = (url) => URL.canParse(url) ? new URL(url).host : void 0;
|
|
4
|
+
const allowedOrigin = (request, origins) => {
|
|
5
|
+
const origin = request.headers.get("origin");
|
|
6
|
+
if (origin === null) return true;
|
|
7
|
+
if (origins) return origins.includes(origin);
|
|
8
|
+
const host = hostOf(origin);
|
|
9
|
+
return host !== void 0 && host === new URL(request.url).host;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* The caller of a WebSocket upgrade, which a browser opens with its session cookie and no headers of its own: refused
|
|
13
|
+
* with 403 from a foreign origin (cross-site WebSocket hijacking), then by its bearer token when it has one, otherwise
|
|
14
|
+
* by the bearer token its cookie's session mints, which goes through `authenticate` with all the request's other
|
|
15
|
+
* headers, as a plain call would.
|
|
16
|
+
*/
|
|
17
|
+
const upgradeAuthenticator = ({ authenticate, origins, sessionToken }) => async (request) => {
|
|
18
|
+
if (!allowedOrigin(request, origins)) throw forbidden("foreign-origin", "This page’s origin may not open a socket to this app");
|
|
19
|
+
if (request.headers.has("authorization") || !request.headers.has("cookie") || !sessionToken) return authenticate(request);
|
|
20
|
+
const headers = new Headers(request.headers);
|
|
21
|
+
headers.set("authorization", `Bearer ${await sessionToken(request)}`);
|
|
22
|
+
return authenticate(new Request(request, { headers }));
|
|
23
|
+
};
|
|
24
|
+
//#endregion
|
|
25
|
+
export { upgradeAuthenticator };
|
package/dist/index.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { FileProvider, FileStore, StoredObject } from "./files/store.js";
|
|
|
24
24
|
import { FilesOptions } from "./files/options.js";
|
|
25
25
|
import { ApiKeys, ApiKeysOptions, apiKeySettings, apiKeyVariables } from "./api-keys/api-key-settings.js";
|
|
26
26
|
import { Caller, CallerOptions, apiKeyCaller } from "./api-keys/resolve-caller.js";
|
|
27
|
+
import { FunctionSocket, SocketEvents } from "./functions/function-socket.js";
|
|
27
28
|
import { ErrorReportingOptions, ErrorReportingSettings, errorReportingSettings, errorReportingVariable } from "./error-reporting/error-reporting-settings.js";
|
|
28
29
|
import { ChatMessage, ChatTextPart, Completion, CompletionOptions, ModelError, ModelSettings, ReasoningDetail, ReasoningEffort, ToolCall, ToolSpec, streamChatCompletion } from "./assistant/chat-completions.js";
|
|
29
30
|
import { CleanupEntry, CleanupSchedule, ScheduledDelete, storageCleanupSchedule } from "./files/cleanup-schedule.js";
|
|
@@ -105,4 +106,4 @@ import { isSchedule } from "./jobs/schedule.js";
|
|
|
105
106
|
import { DrainResult, JobsRuntime, StartJobsOptions, startJobs } from "./jobs/runtime.js";
|
|
106
107
|
import { jobsDrainApp } from "./jobs/drain-route.js";
|
|
107
108
|
import { checkJobsSchema, jobsSchema, jobsSchemaMigration } from "./jobs/schema.js";
|
|
108
|
-
export { type Admin, type AdminAuth, type AdminOptions, type ApiFunction, type ApiKeys, type ApiKeysOptions, type AppIcons, type AppIdentity, type ApprovalAnswer, type ApprovalRequest, type AssistantBackend, type AssistantOptions, type AssistantSettings, type AssistantTool, type AuditEvent, type AuditQueue, type Authenticator, type BetterAuthAuthenticatorOptions, type Caller, type CallerOptions, type CapturedRow, type ChatMessage, type ChatTextPart, type CheckAiInput, type CleanupEntry, type CleanupReport, type CleanupSchedule, type CoalescedChange, type CommentItem, type CommentUsers, type Completion, type CompletionOptions, type Conversation, type ConversationRecord, type ConversationStore, type ConversationsOptions, type CorsOptions, type CorsOrigins, type CreateAdminInput, type CreateAuthOptions, type DrainResult, type EmailComponent, type EmailOptions, type EmailTemplate, type EnqueueOptions, type ErrorReporter, type ErrorReportingOptions, type ErrorReportingSettings, type EventData, type EventsOptions, FatalJobError, type FileCleanupInput, type FileLimits, type FileObject, type FileProvider, type FileRef, type FileStore, type FileVariantObject, type FilesOptions, type FunctionApp, type FunctionCaller, type FunctionContext, type FunctionEnv, type FunctionHandler, type FunctionOptions, type FunctionRecords, type FunctionSource, type HistoryOptions, HttpProblem, type IssueTokenOptions, type JobContext, type JobDefinition, type JobOptions, type JobsRuntime, type JwtOptions, type JwtVerifyOptions, type KeyFunctionContext, type LinkedAccount, type ListParams, type LocalFilesOptions, type MailMessage, type Mailer, type MapLayer, type MapOptions, type MapSettings, type MemberInput, MissingCredentials, type MobileConfig, ModelError, type ModelSettings, type NewOrganization, type NewUser, type OperatorProvider, type OrganizationRole, type OrganizationsOptions, type PipelineHook, type PlatformSignIn, type PluginOption, type PluginRef, type PngIcon, type ProjectConfig, type ProtectedFunctionOptions, type PublicFunctionContext, type PublicFunctionEnv, type PublicFunctionHandler, type PublicFunctionOptions, type QueryOptions, type QueryResult, type ReasoningDetail, type ReasoningEffort, type RecordKeyInput, type RecordPage, type RenderedEmail, type ResourceChange, type ResourceSource, type RoleDefinition, type RolesInput, type RunContext, type RuntimeOptions, type S3Bucket, type S3FilesOptions, type S3ListPage, type S3Object, type ScanGuardMode, type ScheduledDelete, type Session, type SessionOrganization, type Sharp, type SignInPolicy, type SignInRule, type SignatureOptions, type SourceOption, type SourceRef, type StaffAccessRule, type StaffSignIn, type StandardSchema, type StandardWebhookOptions, type StartJobsOptions, type StoreFileInput, type StoredMember, type StoredObject, type StoredOrganization, type StoredUser, type SubscriptionDefinition, type SubscriptionEvent, type SubscriptionOptions, type TenantValue, type ToolCall, type ToolContext, type ToolRecord, type ToolRecords, type ToolSpec, type TrustedApp, type TurnOptions, type User, type UserCreatedHook, type ViewSource, type WebhooksOptions, type WidgetOptions, type WriteEvent, type WriteOperation, addAppRoleToOwners, addMember, apiKeyCaller, apiKeySettings, apiKeyVariables, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaMigration, backgroundSettled, badRequest, beforeSignInPluginsVariable, betterAuthAuthenticator, betterAuthTokens, changesMigration, checkAi, checkAuthSchema, checkChanges, checkComments, checkComputed, checkJobsSchema, checkSourceNames, checkViewQueries, checkWebhooksSchema, coalesce, commentsMigration, computedMigration, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createFileCleanup, createOrganization, createUser, defaultAppName, defaultSignInPolicy, defineConfig, defineEmail, defineFunction, defineJob, defineTool, deleteUser, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, goTruePath, handleDroppedConnections, hasUsers, imageSize, isBackgroundWork, isCaptured, isJobDefinition, isSchedule, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, jwtVerifier, limitedFileStore, listOrganizations, listUsers, localFileStore, localFiles, mapSettings, mapStyleUrl, mapStyleVariable, maxTokenTtlSeconds, maxWidgetPropsBytes, mergeConfig, migrationCommand, newWebhookSecret, normalizeRoles, notFound, objectKey, parseColumns, parseRoles, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, pngIcons, processor, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readWriteQueryTool, recordOf, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runInBackground, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, s3Files, seesAssistant, seesRuntime, setMemberRoles, setUserBanned, setUserRole, signInPolicyMethods, sourceOptions, sourcesFor, sourcesVariable, startErrorReporting, startJobs, storageCleanupSchedule, storageProvider, storeFile, storeUsage, streamChatCompletion, subscription, supabasePath, thumbHash, unauthorized, variantPath, verifySignature, verifyStandardWebhook, waitUntil, webauthnPath, webhooks, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps };
|
|
109
|
+
export { type Admin, type AdminAuth, type AdminOptions, type ApiFunction, type ApiKeys, type ApiKeysOptions, type AppIcons, type AppIdentity, type ApprovalAnswer, type ApprovalRequest, type AssistantBackend, type AssistantOptions, type AssistantSettings, type AssistantTool, type AuditEvent, type AuditQueue, type Authenticator, type BetterAuthAuthenticatorOptions, type Caller, type CallerOptions, type CapturedRow, type ChatMessage, type ChatTextPart, type CheckAiInput, type CleanupEntry, type CleanupReport, type CleanupSchedule, type CoalescedChange, type CommentItem, type CommentUsers, type Completion, type CompletionOptions, type Conversation, type ConversationRecord, type ConversationStore, type ConversationsOptions, type CorsOptions, type CorsOrigins, type CreateAdminInput, type CreateAuthOptions, type DrainResult, type EmailComponent, type EmailOptions, type EmailTemplate, type EnqueueOptions, type ErrorReporter, type ErrorReportingOptions, type ErrorReportingSettings, type EventData, type EventsOptions, FatalJobError, type FileCleanupInput, type FileLimits, type FileObject, type FileProvider, type FileRef, type FileStore, type FileVariantObject, type FilesOptions, type FunctionApp, type FunctionCaller, type FunctionContext, type FunctionEnv, type FunctionHandler, type FunctionOptions, type FunctionRecords, type FunctionSocket, type FunctionSource, type HistoryOptions, HttpProblem, type IssueTokenOptions, type JobContext, type JobDefinition, type JobOptions, type JobsRuntime, type JwtOptions, type JwtVerifyOptions, type KeyFunctionContext, type LinkedAccount, type ListParams, type LocalFilesOptions, type MailMessage, type Mailer, type MapLayer, type MapOptions, type MapSettings, type MemberInput, MissingCredentials, type MobileConfig, ModelError, type ModelSettings, type NewOrganization, type NewUser, type OperatorProvider, type OrganizationRole, type OrganizationsOptions, type PipelineHook, type PlatformSignIn, type PluginOption, type PluginRef, type PngIcon, type ProjectConfig, type ProtectedFunctionOptions, type PublicFunctionContext, type PublicFunctionEnv, type PublicFunctionHandler, type PublicFunctionOptions, type QueryOptions, type QueryResult, type ReasoningDetail, type ReasoningEffort, type RecordKeyInput, type RecordPage, type RenderedEmail, type ResourceChange, type ResourceSource, type RoleDefinition, type RolesInput, type RunContext, type RuntimeOptions, type S3Bucket, type S3FilesOptions, type S3ListPage, type S3Object, type ScanGuardMode, type ScheduledDelete, type Session, type SessionOrganization, type Sharp, type SignInPolicy, type SignInRule, type SignatureOptions, type SocketEvents, type SourceOption, type SourceRef, type StaffAccessRule, type StaffSignIn, type StandardSchema, type StandardWebhookOptions, type StartJobsOptions, type StoreFileInput, type StoredMember, type StoredObject, type StoredOrganization, type StoredUser, type SubscriptionDefinition, type SubscriptionEvent, type SubscriptionOptions, type TenantValue, type ToolCall, type ToolContext, type ToolRecord, type ToolRecords, type ToolSpec, type TrustedApp, type TurnOptions, type User, type UserCreatedHook, type ViewSource, type WebhooksOptions, type WidgetOptions, type WriteEvent, type WriteOperation, addAppRoleToOwners, addMember, apiKeyCaller, apiKeySettings, apiKeyVariables, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaMigration, backgroundSettled, badRequest, beforeSignInPluginsVariable, betterAuthAuthenticator, betterAuthTokens, changesMigration, checkAi, checkAuthSchema, checkChanges, checkComments, checkComputed, checkJobsSchema, checkSourceNames, checkViewQueries, checkWebhooksSchema, coalesce, commentsMigration, computedMigration, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createFileCleanup, createOrganization, createUser, defaultAppName, defaultSignInPolicy, defineConfig, defineEmail, defineFunction, defineJob, defineTool, deleteUser, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, goTruePath, handleDroppedConnections, hasUsers, imageSize, isBackgroundWork, isCaptured, isJobDefinition, isSchedule, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, jwtVerifier, limitedFileStore, listOrganizations, listUsers, localFileStore, localFiles, mapSettings, mapStyleUrl, mapStyleVariable, maxTokenTtlSeconds, maxWidgetPropsBytes, mergeConfig, migrationCommand, newWebhookSecret, normalizeRoles, notFound, objectKey, parseColumns, parseRoles, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, pngIcons, processor, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readWriteQueryTool, recordOf, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runInBackground, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, s3Files, seesAssistant, seesRuntime, setMemberRoles, setUserBanned, setUserRole, signInPolicyMethods, sourceOptions, sourcesFor, sourcesVariable, startErrorReporting, startJobs, storageCleanupSchedule, storageProvider, storeFile, storeUsage, streamChatCompletion, subscription, supabasePath, thumbHash, unauthorized, variantPath, verifySignature, verifyStandardWebhook, waitUntil, webauthnPath, webhooks, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps };
|
package/dist/registry.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import "./resource-source.js";
|
|
2
|
-
import { ResourceModel } from "@protobase/schema";
|
|
2
|
+
import { KeyType, ResourceModel } from "@protobase/schema";
|
package/dist/types.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { JobDefinition } from "./jobs/job-types.js";
|
|
2
2
|
import { Hono } from "hono";
|
|
3
3
|
import { PageModel } from "@protobase/layout";
|
|
4
|
-
import { ResourceModel, ViewModel } from "@protobase/schema";
|
|
4
|
+
import { KeyType, ResourceModel, ViewModel } from "@protobase/schema";
|
|
5
5
|
import { Db } from "@protobase/query";
|
|
6
6
|
//#region src/types.d.ts
|
|
7
7
|
export type TenantValue = string | number;
|
|
@@ -20,8 +20,14 @@ export type Session = {
|
|
|
20
20
|
organization?: SessionOrganization;
|
|
21
21
|
anonymous?: true;
|
|
22
22
|
};
|
|
23
|
-
/**
|
|
24
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Resolves the caller of a request, or throws a 401 problem (see `unauthorized`). `userIdType` declares the type of
|
|
25
|
+
* the ids of the users it returns, which `.owner()` fields are checked against at startup, such as `'uuid'` for an
|
|
26
|
+
* authenticator that maps the signed-in account to the app's own user row.
|
|
27
|
+
*/
|
|
28
|
+
export type Authenticator = ((request: Request) => Promise<Session> | Session) & {
|
|
29
|
+
userIdType?: KeyType;
|
|
30
|
+
};
|
|
25
31
|
export type AdminEnv = {
|
|
26
32
|
Variables: {
|
|
27
33
|
session: Session;
|
package/dist/webhooks/attempt.js
CHANGED
|
@@ -47,10 +47,14 @@ const callFunction = async (name, subscription, event, options, context, signal)
|
|
|
47
47
|
session,
|
|
48
48
|
records: context.records(session)
|
|
49
49
|
};
|
|
50
|
+
const acceptWebSocket = () => {
|
|
51
|
+
throw new Error(`The function "${name}" accepted a WebSocket, but a webhook delivery is no upgrade`);
|
|
52
|
+
};
|
|
50
53
|
const functionContext = {
|
|
51
54
|
name,
|
|
52
55
|
db: context.db,
|
|
53
|
-
caller: async () => caller
|
|
56
|
+
caller: async () => caller,
|
|
57
|
+
acceptWebSocket
|
|
54
58
|
};
|
|
55
59
|
return invokeFunction(name, fn, await request("http://protobase.internal/", event, options, signal), fn.options.public ? functionContext : {
|
|
56
60
|
...functionContext,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/server",
|
|
3
|
-
"version": "0.33.
|
|
3
|
+
"version": "0.33.4",
|
|
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.33.
|
|
36
|
-
"@protobase/query": "0.33.
|
|
37
|
-
"@protobase/schema": "0.33.
|
|
35
|
+
"@protobase/layout": "0.33.4",
|
|
36
|
+
"@protobase/query": "0.33.4",
|
|
37
|
+
"@protobase/schema": "0.33.4",
|
|
38
38
|
"@react-email/render": "^2.1.0",
|
|
39
39
|
"@scalar/hono-api-reference": "^0.12.10",
|
|
40
40
|
"@sentry/core": "^11.7.0",
|