@protobase/server 0.37.2 → 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 };
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.2",
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.2",
40
- "@protobase/query": "0.37.2",
41
- "@protobase/schema": "0.37.2",
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",