@protobase/server 0.37.1 → 0.38.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.
@@ -0,0 +1,21 @@
1
+ import { ResourceModel } from "@protobase/schema";
2
+ import { Db } from "@protobase/query";
3
+ //#region src/assistant/query-runner/query-runner.d.ts
4
+ /** A table as config names it: the search path decides when it names no schema. */
5
+ export type TableName = {
6
+ schema?: string;
7
+ name: string;
8
+ };
9
+ /**
10
+ * The SQL that makes the role the assistant's read-only queries run as, and its runner, and grants it exactly the
11
+ * resources' tables, never an auth table; `undefined` when the database is current.
12
+ */
13
+ export declare const queryRunnerMigration: (db: Db, models: readonly ResourceModel[], authTables: readonly TableName[]) => Promise<string | undefined>;
14
+ /**
15
+ * Refuses to go on while the runner of the assistant's read-only queries is missing or out of date, or runs as a role
16
+ * that is not weaker than the app's: the app's role itself or one it is a member of, a superuser, a login role, or one
17
+ * that can read an auth table, or while the role may call a security-definer function that runs as a role able to
18
+ * read one. It checks the owner's privileges, not its name, so a database cloned under another name still passes. A resource added since the last migration is not checked: Postgres refuses to read its table until then.
19
+ */
20
+ export declare const checkQueryRunner: (db: Db, authTables: readonly TableName[]) => Promise<void>;
21
+ //#endregion
@@ -0,0 +1,79 @@
1
+ import { modelVariables } from "../model-settings.js";
2
+ import { migrationCommand } from "../../jobs/schema.js";
3
+ import { queryRoleSql, queryRunnerSignature, queryRunnerSql, runnerBody } from "./runner-sql.js";
4
+ import { plannedGrants } from "./runner-plan.js";
5
+ import { sql } from "kysely";
6
+ //#region src/assistant/query-runner/query-runner.ts
7
+ const quoted = (name) => `"${name.replaceAll("\"", "\"\"")}"`;
8
+ const regclassText = ({ schema, name }) => schema ? `${quoted(schema)}.${quoted(name)}` : quoted(name);
9
+ const roleSql = sql.raw(queryRoleSql);
10
+ const rowsOf = async (db, query) => (await query.execute(db)).rows;
11
+ const resolved = (db, tables) => rowsOf(db, sql`select n.nspname as schema, c.relname as name from unnest(${tables.map(regclassText)}::text[]) with ordinality as t(name, n)
12
+ join pg_class c on c.oid = to_regclass(t.name) join pg_namespace n on n.oid = c.relnamespace order by t.n`);
13
+ const readRunner = async (db) => {
14
+ const [runner] = await rowsOf(db, sql`select p.prosrc as source, p.prosecdef as definer, pg_get_userbyid(p.proowner) as owner,
15
+ (p.proacl is null or exists (select from aclexplode(p.proacl) a where a.grantee = 0 and a.privilege_type = 'EXECUTE')) as executable
16
+ from pg_proc p where p.oid = to_regprocedure(${queryRunnerSignature})`);
17
+ return runner;
18
+ };
19
+ /** What the database holds of the query role and its runner, and the tables the role should read and must not. Only reads the catalogue. */
20
+ const readQueryRunner = async (db, models, authTables) => {
21
+ const [role] = await rowsOf(db, sql`select ${roleSql} as name, exists (select from pg_roles where rolname = ${roleSql}) as exists`);
22
+ const tables = await rowsOf(db, sql`select n.nspname as schema, c.relname as name from pg_class c join pg_namespace n on n.oid = c.relnamespace, aclexplode(c.relacl) a
23
+ where a.grantee = (select oid from pg_roles where rolname = ${roleSql}) and a.privilege_type = 'SELECT' order by 1, 2`);
24
+ const schemas = await rowsOf(db, sql`select n.nspname as schema from pg_namespace n, aclexplode(n.nspacl) a
25
+ where a.grantee = (select oid from pg_roles where rolname = ${roleSql}) and a.privilege_type = 'USAGE' order by 1`);
26
+ return {
27
+ role,
28
+ runner: await readRunner(db),
29
+ resources: await resolved(db, models.map((model) => model.table)),
30
+ authTables: await resolved(db, authTables),
31
+ granted: {
32
+ tables,
33
+ schemas: schemas.map((found) => found.schema)
34
+ }
35
+ };
36
+ };
37
+ /**
38
+ * The SQL that makes the role the assistant's read-only queries run as, and its runner, and grants it exactly the
39
+ * resources' tables, never an auth table; `undefined` when the database is current.
40
+ */
41
+ const queryRunnerMigration = async (db, models, authTables) => {
42
+ const { role, runner, resources, authTables: auth, granted } = await readQueryRunner(db, models, authTables);
43
+ const plan = plannedGrants({
44
+ resources,
45
+ authTables: auth,
46
+ granted
47
+ });
48
+ return role.exists && runner?.source === runnerBody && runner.definer && runner.owner === role.name && !runner.executable && plan.grants.length === 0 && plan.revokes.length === 0 ? void 0 : queryRunnerSql(plan);
49
+ };
50
+ const refusal = (state) => /* @__PURE__ */ new Error(`The query role of the assistant's read-only queries ${state}. Write it as a migration with \`${migrationCommand}\`, run with the built-in assistant's key set (${modelVariables.apiKey} or options.assistant.apiKey), apply it with the project's migrations, then start again.`);
51
+ const leakingFunctions = (db, authTables) => rowsOf(db, sql`with runner as (select proowner as role from pg_proc where oid = to_regprocedure(${queryRunnerSignature}))
52
+ select format('%I.%I(%s)', n.nspname, p.proname, pg_get_function_identity_arguments(p.oid)) as name
53
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace, runner
54
+ where p.prosecdef and p.prorettype <> 'trigger'::regtype and p.proowner <> runner.role
55
+ and has_schema_privilege(runner.role, p.pronamespace, 'USAGE') and has_function_privilege(runner.role, p.oid, 'EXECUTE')
56
+ and exists (select from unnest(${authTables.map(regclassText)}::text[]) as t(name) join pg_class c on c.oid = to_regclass(t.name)
57
+ where has_schema_privilege(p.proowner, c.relnamespace, 'USAGE') and has_any_column_privilege(p.proowner, c.oid, 'SELECT'))
58
+ order by 1`);
59
+ /**
60
+ * Refuses to go on while the runner of the assistant's read-only queries is missing or out of date, or runs as a role
61
+ * that is not weaker than the app's: the app's role itself or one it is a member of, a superuser, a login role, or one
62
+ * that can read an auth table, or while the role may call a security-definer function that runs as a role able to
63
+ * read one. It checks the owner's privileges, not its name, so a database cloned under another name still passes. A resource added since the last migration is not checked: Postgres refuses to read its table until then.
64
+ */
65
+ const checkQueryRunner = async (db, authTables) => {
66
+ const [runner] = await rowsOf(db, sql`select p.prosrc as source, p.prosecdef as definer,
67
+ (o.rolsuper or o.rolcanlogin or pg_has_role(p.proowner, current_user, 'MEMBER')) as strong,
68
+ exists (select from unnest(${authTables.map(regclassText)}::text[]) as t(name) join pg_class c on c.oid = to_regclass(t.name)
69
+ where has_schema_privilege(p.proowner, c.relnamespace, 'USAGE') and has_any_column_privilege(p.proowner, c.oid, 'SELECT')) as reads_auth
70
+ from pg_proc p join pg_roles o on o.oid = p.proowner where p.oid = to_regprocedure(${queryRunnerSignature})`);
71
+ if (!runner || !runner.definer || runner.source !== runnerBody) throw refusal("is missing");
72
+ if (runner.strong) throw refusal("is not weaker than the app's database role");
73
+ if (runner.reads_auth) throw refusal("can read the auth schema");
74
+ const leaking = await leakingFunctions(db, authTables);
75
+ if (leaking.length === 0) return;
76
+ throw new Error(`The query role of the assistant's read-only queries can read the auth schema through the security-definer functions ${leaking.map((found) => found.name).join(", ")}. Revoke execute on them from PUBLIC, or move them to a schema without a resource, then start again.`);
77
+ };
78
+ //#endregion
79
+ export { checkQueryRunner, queryRunnerMigration, readQueryRunner };
@@ -0,0 +1,30 @@
1
+ //#region src/assistant/query-runner/runner-plan.ts
2
+ const same = (a, b) => a.schema === b.schema && a.name === b.name;
3
+ const unique = (items, equal) => items.filter((item, index) => items.findIndex((other) => equal(item, other)) === index);
4
+ /**
5
+ * What the query role must be given and what taken back so that it reads exactly the resources' tables, never an auth
6
+ * table, with usage on the schemas those are in. `resources` and `authTables` are the tables that exist, `granted`
7
+ * what the role holds now.
8
+ */
9
+ const plannedGrants = ({ resources, authTables, granted }) => {
10
+ const tables = unique(resources.filter((table) => !authTables.some((auth) => same(table, auth))), same);
11
+ const schemas = [...new Set(tables.map((table) => table.schema))];
12
+ return {
13
+ grants: [...schemas.filter((schema) => !granted.schemas.includes(schema)).map((schema) => ({
14
+ on: "schema",
15
+ schema
16
+ })), ...tables.filter((table) => !granted.tables.some((found) => same(found, table))).map((table) => ({
17
+ on: "table",
18
+ ...table
19
+ }))],
20
+ revokes: [...granted.tables.filter((found) => !tables.some((table) => same(table, found))).map((table) => ({
21
+ on: "table",
22
+ ...table
23
+ })), ...granted.schemas.filter((schema) => !schemas.includes(schema)).map((schema) => ({
24
+ on: "schema",
25
+ schema
26
+ }))]
27
+ };
28
+ };
29
+ //#endregion
30
+ export { plannedGrants };
@@ -0,0 +1,68 @@
1
+ //#region src/assistant/query-runner/runner-sql.ts
2
+ /** The schema the read-only query runner lives in. */
3
+ const queryRunnerSchema = "query";
4
+ /** The function every read-only query runs through, as the query role. */
5
+ const queryRunnerFunction = `${queryRunnerSchema}.run_read_only`;
6
+ /** Its signature, as `to_regprocedure` and `revoke` name it. */
7
+ const queryRunnerSignature = `${queryRunnerFunction}(text, integer)`;
8
+ /**
9
+ * The query role's name, computed by Postgres when the SQL runs, so one migration file serves every environment's
10
+ * database. Roles belong to the whole cluster, so the name carries the database's, cut to leave room for the suffix.
11
+ */
12
+ const queryRoleSql = "left(current_database(), 57) || '_query'";
13
+ /**
14
+ * The runner's plpgsql body. A cursor runs the statement, which Postgres refuses when the text holds more than one
15
+ * query. Each row comes back as JSON, a number that is not a plain integer of at most 15 digits as its exact text,
16
+ * which `JSON.parse` would otherwise round or reformat. The newlines keep a trailing `--` comment from swallowing the
17
+ * wrapper.
18
+ */
19
+ const runnerBody = `
20
+ declare
21
+ rows refcursor;
22
+ row json;
23
+ begin
24
+ open rows no scroll for execute 'select (select json_object_agg(e.key, case when json_typeof(e.value) = ''number'' and e.value::text !~ ''^-?[0-9]{1,15}$'' then to_json(e.value::text) else e.value end order by e.n) from json_each(to_json(r)) with ordinality as e(key, value, n)) from (' || E'\\n' || statement || E'\\n' || ') r limit ' || (row_limit + 1);
25
+ loop
26
+ fetch rows into row;
27
+ exit when not found;
28
+ return next row;
29
+ end loop;
30
+ close rows;
31
+ end
32
+ `;
33
+ const literal = (text) => `'${text.replaceAll("'", "''")}'`;
34
+ const grantSql = ({ grants, revokes }) => [...grants.map((grant) => grant.on === "schema" ? ` execute format('grant usage on schema %I to %I', ${literal(grant.schema)}, query_role);` : ` execute format('grant select on table %I.%I to %I', ${literal(grant.schema)}, ${literal(grant.name)}, query_role);`), ...revokes.map((revoke) => revoke.on === "schema" ? ` execute format('revoke usage on schema %I from %I', ${literal(revoke.schema)}, query_role);` : ` execute format('revoke select on table %I.%I from %I', ${literal(revoke.schema)}, ${literal(revoke.name)}, query_role);`)];
35
+ /**
36
+ * The SQL that makes the query role and the runner, and gives the role exactly `grants`, taking back `revokes`. It
37
+ * applies again on a database that has them. The migrating role becomes a member of the role, which Postgres 16 needs
38
+ * to hand it the function, and so may execute the runner; PUBLIC may not. The role bypasses row-level security only
39
+ * when the migrating role may give it that: a superuser, or a role with BYPASSRLS on Postgres 16 or later.
40
+ */
41
+ const queryRunnerSql = (plan) => [
42
+ `create schema if not exists ${queryRunnerSchema};`,
43
+ [
44
+ "do $$",
45
+ "declare",
46
+ ` query_role text := ${queryRoleSql};`,
47
+ "begin",
48
+ " if not exists (select from pg_roles where rolname = query_role) then execute format('create role %I nologin', query_role); end if;",
49
+ " execute format('grant %I to current_user', query_role);",
50
+ " if (select rolsuper or (rolbypassrls and current_setting('server_version_num')::int >= 160000) from pg_roles where rolname = current_user) then execute format('alter role %I bypassrls', query_role); end if;",
51
+ ...grantSql(plan),
52
+ "end $$;"
53
+ ].join("\n"),
54
+ `create or replace function ${queryRunnerFunction}(statement text, row_limit integer) returns setof json language plpgsql security definer as $f$${runnerBody}$f$;`,
55
+ [
56
+ "do $$",
57
+ "declare",
58
+ ` query_role text := ${queryRoleSql};`,
59
+ "begin",
60
+ ` execute format('grant create on schema ${queryRunnerSchema} to %I', query_role);`,
61
+ ` execute format('alter function ${queryRunnerSignature} owner to %I', query_role);`,
62
+ ` execute format('revoke create on schema ${queryRunnerSchema} from %I', query_role);`,
63
+ "end $$;"
64
+ ].join("\n"),
65
+ `revoke execute on function ${queryRunnerSignature} from public;`
66
+ ].join("\n\n");
67
+ //#endregion
68
+ export { queryRoleSql, queryRunnerFunction, queryRunnerSchema, queryRunnerSignature, queryRunnerSql, runnerBody };
@@ -4,7 +4,8 @@ import { Db } from "@protobase/query";
4
4
  //#region src/assistant/query-tools.d.ts
5
5
  /**
6
6
  * Lets the model read the app's database: one SELECT per call, in a read-only transaction (see `runReadOnlyQuery`).
7
- * The rows show as a table in the chat. It runs as the app's database role, outside Protobase's access rules.
7
+ * The rows show as a table in the chat. It runs as the query role, outside Protobase's access rules: it reads the
8
+ * resources' tables only, never the auth schema.
8
9
  */
9
10
  export declare const readOnlyQueryTool: (db: Db, options?: QueryOptions) => AssistantTool;
10
11
  /**
@@ -26,11 +26,12 @@ const shown = (result, statement, context) => {
26
26
  const statementOf = (args) => typeof args.sql === "string" && args.sql.trim() ? args.sql : void 0;
27
27
  /**
28
28
  * Lets the model read the app's database: one SELECT per call, in a read-only transaction (see `runReadOnlyQuery`).
29
- * The rows show as a table in the chat. It runs as the app's database role, outside Protobase's access rules.
29
+ * The rows show as a table in the chat. It runs as the query role, outside Protobase's access rules: it reads the
30
+ * resources' tables only, never the auth schema.
30
31
  */
31
32
  const readOnlyQueryTool = (db, options = {}) => defineTool({
32
33
  name: "run_read_only_query",
33
- description: `Runs one PostgreSQL SELECT on the app's database in a read-only transaction and returns up to ${options.rowLimit ?? 200} rows as JSON ({ columns, rows, truncated }). The user sees the rows as a table. Use it to answer questions about the data. It cannot change anything.`,
34
+ description: `Runs one PostgreSQL SELECT on the app's database in a read-only transaction and returns up to ${options.rowLimit ?? 200} rows as JSON ({ columns, rows, truncated }). The user sees the rows as a table. Use it to answer questions about the data. It reads the app's resource tables only. It cannot change anything.`,
34
35
  parameters: {
35
36
  type: "object",
36
37
  properties: { sql: sqlParameter("One SELECT statement, without a trailing semicolon.") },
@@ -13,7 +13,9 @@ export type QueryResult = {
13
13
  };
14
14
  /**
15
15
  * Runs one SELECT (or VALUES, or a WITH query) in a READ ONLY transaction that is always rolled back, so nothing it
16
- * does, settings included, outlives it. Postgres refuses any write in it, and the statement cannot leave it.
16
+ * does, settings included, outlives it. Postgres refuses any write in it, and the statement cannot leave it. It runs
17
+ * through the runner `protobase migration` makes, as the query role, which reads only the resources' tables and never
18
+ * the auth schema, and cannot switch back to the app's role; its cursor runs one statement only.
17
19
  */
18
20
  export declare const runReadOnlyQuery: (db: Db, statement: string, options?: QueryOptions) => Promise<QueryResult>;
19
21
  /**
@@ -1,6 +1,8 @@
1
+ import { queryRunnerFunction } from "./query-runner/runner-sql.js";
1
2
  import { sql } from "kysely";
2
3
  //#region src/assistant/run-query.ts
3
- const wrapped = (statement, rowLimit) => sql`with result as (\n${sql.raw(statement.trim().replace(/;\s*$/, ""))}\n) select * from result limit ${rowLimit + 1}`;
4
+ const withoutSemicolon = (statement) => statement.trim().replace(/;\s*$/, "");
5
+ const wrapped = (statement, rowLimit) => sql`with result as (\n${sql.raw(withoutSemicolon(statement))}\n) select * from result limit ${rowLimit + 1}`;
4
6
  const cell = (value) => {
5
7
  if (typeof value === "bigint") return value.toString();
6
8
  if (value instanceof Date) return value.toISOString();
@@ -12,28 +14,40 @@ const resultOf = (rows, rowLimit) => ({
12
14
  rows: rows.slice(0, rowLimit).map((row) => Object.values(row).map(cell)),
13
15
  truncated: rows.length > rowLimit
14
16
  });
15
- const run = (db, statement, options, begin, end) => db.connection().execute(async (connection) => {
17
+ const run = (db, query, options, begin, end) => db.connection().execute(async (connection) => {
16
18
  const rowLimit = options.rowLimit ?? 200;
17
19
  await sql.raw(begin).execute(connection);
18
20
  let succeeded = false;
19
21
  try {
20
22
  await sql`select set_config('statement_timeout', ${String(options.timeoutMs ?? 5e3)}, true)`.execute(connection);
21
- const { rows } = await wrapped(statement, rowLimit).execute(connection);
23
+ const { rows } = await query(rowLimit).execute(connection);
22
24
  succeeded = true;
23
- return resultOf(rows, rowLimit);
25
+ return {
26
+ rows,
27
+ rowLimit
28
+ };
24
29
  } finally {
25
30
  await sql.raw(end(succeeded)).execute(connection);
26
31
  }
27
32
  });
33
+ const throughRunner = (statement) => (rowLimit) => sql`select row::text as row from ${sql.raw(queryRunnerFunction)}(${withoutSemicolon(statement)}, ${rowLimit}) as row`;
28
34
  /**
29
35
  * Runs one SELECT (or VALUES, or a WITH query) in a READ ONLY transaction that is always rolled back, so nothing it
30
- * does, settings included, outlives it. Postgres refuses any write in it, and the statement cannot leave it.
36
+ * does, settings included, outlives it. Postgres refuses any write in it, and the statement cannot leave it. It runs
37
+ * through the runner `protobase migration` makes, as the query role, which reads only the resources' tables and never
38
+ * the auth schema, and cannot switch back to the app's role; its cursor runs one statement only.
31
39
  */
32
- const runReadOnlyQuery = (db, statement, options = {}) => run(db, statement, options, "begin transaction read only", () => "rollback");
40
+ const runReadOnlyQuery = async (db, statement, options = {}) => {
41
+ const { rows, rowLimit } = await run(db, throughRunner(statement), options, "begin transaction read only", () => "rollback");
42
+ return resultOf(rows.map(({ row }) => JSON.parse(row ?? "{}")), rowLimit);
43
+ };
33
44
  /**
34
45
  * Runs one INSERT, UPDATE or DELETE with a RETURNING clause (or a query) in a READ WRITE transaction, committed when it
35
46
  * succeeds. Only call it for a statement the user approved.
36
47
  */
37
- const runReadWriteQuery = (db, statement, options = {}) => run(db, statement, options, "begin transaction read write", (succeeded) => succeeded ? "commit" : "rollback");
48
+ const runReadWriteQuery = async (db, statement, options = {}) => {
49
+ const { rows, rowLimit } = await run(db, (limit) => wrapped(statement, limit), options, "begin transaction read write", (succeeded) => succeeded ? "commit" : "rollback");
50
+ return resultOf(rows, rowLimit);
51
+ };
38
52
  //#endregion
39
53
  export { runReadOnlyQuery, runReadWriteQuery };
@@ -1,3 +1,4 @@
1
+ import { storeSchema } from "./auth-tables.js";
1
2
  import { dropMcpTables, leftoverMcpTables, mcpTableNames, withMcpTables } from "./mcp/mcp-tables.js";
2
3
  import { snakeCaseMigration } from "./snake-case-migration.js";
3
4
  import { getMigrations } from "better-auth/db/migration";
@@ -15,7 +16,6 @@ const skipExisting = (sql) => sql.split(";\n\n").map((statement) => statement.re
15
16
  * the old ones.
16
17
  */
17
18
  const hasCamelCaseNames = ({ toBeAdded }) => toBeAdded.some(({ table, fields }) => table === "user" && "email_verified" in fields);
18
- const storeSchema = ({ options: { database } }) => database && "schemaName" in database ? database.schemaName : void 0;
19
19
  /**
20
20
  * The SQL that brings the admin store up to what this version of Protobase and the project's Better Auth plugins need:
21
21
  * for a store with the camelCase names of Protobase 0.6, their rename to snake_case (`snakeCaseMigration`); otherwise
@@ -0,0 +1,8 @@
1
+ import { AdminAuth } from "./create-auth.js";
2
+ //#region src/better-auth/auth-tables.d.ts
3
+ /** The admin store's tables with every plugin the project's auth has, which the assistant's queries must never read. */
4
+ export declare const authStoreTables: (auth: AdminAuth) => {
5
+ schema: string | undefined;
6
+ name: string;
7
+ }[];
8
+ //#endregion
@@ -0,0 +1,11 @@
1
+ import { getSchema } from "better-auth/db";
2
+ //#region src/better-auth/auth-tables.ts
3
+ /** The schema the admin store lives in; the search path decides when its options name none. */
4
+ const storeSchema = ({ options: { database } }) => database && "schemaName" in database ? database.schemaName : void 0;
5
+ /** The admin store's tables with every plugin the project's auth has, which the assistant's queries must never read. */
6
+ const authStoreTables = (auth) => Object.keys(getSchema(auth.options)).map((name) => ({
7
+ schema: storeSchema(auth),
8
+ name
9
+ }));
10
+ //#endregion
11
+ export { authStoreTables, storeSchema };
@@ -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) 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.
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;
@@ -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, such as `@hono/node-server`'s, with which an app with keys and live resources serves
79
- * supabase-js's Realtime at `/api/sb/realtime/v1/websocket`; without it, the app serves none.
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. 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.
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
- if (recheck) stopRecheck = recheckWhileOpen({
65
- check: recheck,
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
  });
@@ -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; a protected one's caller is checked again
22
- * every minute while it is open.
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: () => caller ??= callerOf(c.req.raw),
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 = 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
- };
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 isRefusal = (error) => error instanceof HttpProblem && (error.status === 401 || error.status === 403);
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 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.
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 = () => check().then(() => {
20
- if (!stopped) schedule();
21
- }, end);
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/dist/index.d.ts CHANGED
@@ -94,6 +94,8 @@ import { McpGateInput, mcpGate, sessionOf } from "./mcp/mcp-gate.js";
94
94
  import { McpHandlerInput, mcpHandler } from "./mcp/mcp-handler.js";
95
95
  import { McpRoutesInput, mcpRoutes } from "./mcp/mcp-routes.js";
96
96
  import { mcpToolContext } from "./mcp/mcp-context.js";
97
+ import { checkQueryRunner, queryRunnerMigration } from "./assistant/query-runner/query-runner.js";
98
+ import { authStoreTables } from "./better-auth/auth-tables.js";
97
99
  import { ErrorReporter } from "./error-reporting/sentry-reporter.js";
98
100
  import { startErrorReporting } from "./error-reporting/error-reporter.js";
99
101
  import { FunctionEnv, PublicFunctionEnv, functionApp } from "./functions/function-app.js";
@@ -120,4 +122,4 @@ import { isSchedule } from "./jobs/schedule.js";
120
122
  import { DrainResult, JobsRuntime, StartJobsOptions, startJobs } from "./jobs/runtime.js";
121
123
  import { jobsDrainApp } from "./jobs/drain-route.js";
122
124
  import { checkJobsSchema, jobsSchema, jobsSchemaMigration } from "./jobs/schema.js";
123
- export { type Admin, type AdminAuth, type AdminOptions, type ApiFunction, type ApiKeys, type ApiKeysOptions, type AppIcons, type AppIdentity, type ApplyOptions, type ApplyResult, type ApprovalAnswer, type ApprovalRequest, type AssistantBackend, type AssistantOptions, type AssistantSettings, type AssistantTool, type AuditEvent, type AuditQueue, type Authenticator, type BetterAuthAuthenticator, type BetterAuthAuthenticatorOptions, type Caller, type CallerOptions, type CapturedRow, type CascadingKey, 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 EncryptionStatus, type EncryptionTools, type EncryptionToolsInput, type EnqueueOptions, type ErrorReporter, type ErrorReportingOptions, type ErrorReportingSettings, type EventData, type EventsOptions, FatalJobError, type FieldApplied, type FieldStatus, 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 McpGateInput, type McpHandlerInput, type McpRoutesInput, type McpServerInput, type McpToolsInput, 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 ReadableResources, type ReasoningDetail, type ReasoningEffort, type RebindOption, 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 StrayValues, type SubscriptionDefinition, type SubscriptionEvent, type SubscriptionOptions, type TenantValue, type ToolAnnotations, type ToolCall, type ToolContext, type ToolRecord, type ToolRecords, type ToolSpec, type TrustedApp, type TurnOptions, type UnreadableEntry, 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, createEncryptionTools, createFileCleanup, createOrganization, createUser, defaultAppName, defaultSignInPolicy, defineConfig, defineEmail, defineFunction, defineJob, defineTool, deleteUser, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, getRecordTool, goTruePath, handleDroppedConnections, hasUsers, imageSize, isBackgroundWork, isCaptured, isJobDefinition, isOAuthWellKnownPath, isSchedule, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, jwtVerifier, limitedFileStore, listOrganizations, listRecordsTool, listResourcesTool, listUsers, localFileStore, localFiles, mapSettings, mapStyleUrl, mapStyleVariable, maxTokenTtlSeconds, maxWidgetPropsBytes, mcpGate, mcpHandler, mcpRoutes, mcpServer, mcpToolContext, mcpTools, mergeConfig, migrationCommand, newWebhookSecret, normalizeRoles, notFound, oauthWellKnownPaths, objectKey, parseColumns, parseRoles, passwordLength, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, pngIcons, processor, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readRecordTools, readWriteQueryTool, readableResources, realtimeLimits, recordOf, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runInBackground, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, s3Files, seesAssistant, seesRuntime, sessionOf, 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 };
125
+ export { type Admin, type AdminAuth, type AdminOptions, type ApiFunction, type ApiKeys, type ApiKeysOptions, type AppIcons, type AppIdentity, type ApplyOptions, type ApplyResult, type ApprovalAnswer, type ApprovalRequest, type AssistantBackend, type AssistantOptions, type AssistantSettings, type AssistantTool, type AuditEvent, type AuditQueue, type Authenticator, type BetterAuthAuthenticator, type BetterAuthAuthenticatorOptions, type Caller, type CallerOptions, type CapturedRow, type CascadingKey, 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 EncryptionStatus, type EncryptionTools, type EncryptionToolsInput, type EnqueueOptions, type ErrorReporter, type ErrorReportingOptions, type ErrorReportingSettings, type EventData, type EventsOptions, FatalJobError, type FieldApplied, type FieldStatus, 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 McpGateInput, type McpHandlerInput, type McpRoutesInput, type McpServerInput, type McpToolsInput, 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 ReadableResources, type ReasoningDetail, type ReasoningEffort, type RebindOption, 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 StrayValues, type SubscriptionDefinition, type SubscriptionEvent, type SubscriptionOptions, type TenantValue, type ToolAnnotations, type ToolCall, type ToolContext, type ToolRecord, type ToolRecords, type ToolSpec, type TrustedApp, type TurnOptions, type UnreadableEntry, 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, authStoreTables, backgroundSettled, badRequest, beforeSignInPluginsVariable, betterAuthAuthenticator, betterAuthTokens, changesMigration, checkAi, checkAuthSchema, checkChanges, checkComments, checkComputed, checkJobsSchema, checkQueryRunner, checkSourceNames, checkViewQueries, checkWebhooksSchema, coalesce, commentsMigration, computedMigration, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createEncryptionTools, createFileCleanup, createOrganization, createUser, defaultAppName, defaultSignInPolicy, defineConfig, defineEmail, defineFunction, defineJob, defineTool, deleteUser, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, getRecordTool, goTruePath, handleDroppedConnections, hasUsers, imageSize, isBackgroundWork, isCaptured, isJobDefinition, isOAuthWellKnownPath, isSchedule, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, jwtVerifier, limitedFileStore, listOrganizations, listRecordsTool, listResourcesTool, listUsers, localFileStore, localFiles, mapSettings, mapStyleUrl, mapStyleVariable, maxTokenTtlSeconds, maxWidgetPropsBytes, mcpGate, mcpHandler, mcpRoutes, mcpServer, mcpToolContext, mcpTools, mergeConfig, migrationCommand, newWebhookSecret, normalizeRoles, notFound, oauthWellKnownPaths, objectKey, parseColumns, parseRoles, passwordLength, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, pngIcons, processor, queryRunnerMigration, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readRecordTools, readWriteQueryTool, readableResources, realtimeLimits, recordOf, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runInBackground, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, s3Files, seesAssistant, seesRuntime, sessionOf, 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/index.js CHANGED
@@ -72,12 +72,14 @@ import { defineConfig, mergeConfig } from "./project-config.js";
72
72
  import { withTrustedApps } from "./better-auth/app-tokens.js";
73
73
  import { betterAuthAuthenticator, betterAuthTokens } from "./better-auth/authenticator.js";
74
74
  import { issueToken, maxTokenTtlSeconds } from "./better-auth/tokens.js";
75
+ import { authStoreTables } from "./better-auth/auth-tables.js";
75
76
  import { authSchemaMigration, checkAuthSchema } from "./better-auth/auth-schema.js";
76
77
  import { checkComputed, computedMigration } from "./computed/computed-schema.js";
77
78
  import { webhooks } from "./webhooks/table/plugin.js";
78
79
  import { checkWebhooksSchema, webhooksMigration } from "./webhooks/table/schema.js";
79
80
  import { checkAi } from "./ai/check-ai.js";
80
81
  import { maxWidgetPropsBytes, widget } from "./assistant/widget.js";
82
+ import { checkQueryRunner, queryRunnerMigration } from "./assistant/query-runner/query-runner.js";
81
83
  import { startErrorReporting } from "./error-reporting/error-reporter.js";
82
84
  import { functionApp } from "./functions/function-app.js";
83
85
  import { readJson } from "./functions/read-json.js";
@@ -91,4 +93,4 @@ import { aspectRatio, fileSize, fileType, imageSize, processor } from "./files/p
91
93
  import { defineEmail } from "./mail/define-email.js";
92
94
  import { startJobs } from "./jobs/runtime.js";
93
95
  import { jobsDrainApp } from "./jobs/drain-route.js";
94
- export { FatalJobError, HttpProblem, MissingCredentials, ModelError, 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, createEncryptionTools, createFileCleanup, createOrganization, createUser, defaultAppName, defaultSignInPolicy, defineConfig, defineEmail, defineFunction, defineJob, defineTool, deleteUser, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, getRecordTool, goTruePath, handleDroppedConnections, hasUsers, imageSize, isBackgroundWork, isCaptured, isJobDefinition, isOAuthWellKnownPath, isSchedule, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, jwtVerifier, limitedFileStore, listOrganizations, listRecordsTool, listResourcesTool, listUsers, localFileStore, localFiles, mapSettings, mapStyleUrl, mapStyleVariable, maxTokenTtlSeconds, maxWidgetPropsBytes, mcpGate, mcpHandler, mcpRoutes, mcpServer, mcpToolContext, mcpTools, mergeConfig, migrationCommand, newWebhookSecret, normalizeRoles, notFound, oauthWellKnownPaths, objectKey, parseColumns, parseRoles, passwordLength, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, pngIcons, processor, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readRecordTools, readWriteQueryTool, readableResources, realtimeLimits, recordOf, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runInBackground, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, s3Files, seesAssistant, seesRuntime, sessionOf, 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 };
96
+ export { FatalJobError, HttpProblem, MissingCredentials, ModelError, addAppRoleToOwners, addMember, apiKeyCaller, apiKeySettings, apiKeyVariables, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaMigration, authStoreTables, backgroundSettled, badRequest, beforeSignInPluginsVariable, betterAuthAuthenticator, betterAuthTokens, changesMigration, checkAi, checkAuthSchema, checkChanges, checkComments, checkComputed, checkJobsSchema, checkQueryRunner, checkSourceNames, checkViewQueries, checkWebhooksSchema, coalesce, commentsMigration, computedMigration, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createEncryptionTools, createFileCleanup, createOrganization, createUser, defaultAppName, defaultSignInPolicy, defineConfig, defineEmail, defineFunction, defineJob, defineTool, deleteUser, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, getRecordTool, goTruePath, handleDroppedConnections, hasUsers, imageSize, isBackgroundWork, isCaptured, isJobDefinition, isOAuthWellKnownPath, isSchedule, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, jwtVerifier, limitedFileStore, listOrganizations, listRecordsTool, listResourcesTool, listUsers, localFileStore, localFiles, mapSettings, mapStyleUrl, mapStyleVariable, maxTokenTtlSeconds, maxWidgetPropsBytes, mcpGate, mcpHandler, mcpRoutes, mcpServer, mcpToolContext, mcpTools, mergeConfig, migrationCommand, newWebhookSecret, normalizeRoles, notFound, oauthWellKnownPaths, objectKey, parseColumns, parseRoles, passwordLength, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, pngIcons, processor, queryRunnerMigration, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readRecordTools, readWriteQueryTool, readableResources, realtimeLimits, recordOf, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runInBackground, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, s3Files, seesAssistant, seesRuntime, sessionOf, 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/server",
3
- "version": "0.37.1",
3
+ "version": "0.38.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": {
@@ -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.1",
40
- "@protobase/query": "0.37.1",
41
- "@protobase/schema": "0.37.1",
39
+ "@protobase/layout": "0.38.0",
40
+ "@protobase/query": "0.38.0",
41
+ "@protobase/schema": "0.38.0",
42
42
  "@react-email/render": "^2.1.0",
43
43
  "@scalar/hono-api-reference": "^0.12.10",
44
44
  "@sentry/core": "^11.7.0",