@protobase/server 0.33.3 → 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.
@@ -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 };
@@ -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";
@@ -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 };
@@ -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 authenticate(request);
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
- return invokeFunction(name, fn, request, {
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 };
@@ -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",
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.3",
36
- "@protobase/query": "0.33.3",
37
- "@protobase/schema": "0.33.3",
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",