@protobase/server 0.37.2 → 0.39.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.
- package/dist/assistant/query-runner/query-runner.d.ts +21 -0
- package/dist/assistant/query-runner/query-runner.js +79 -0
- package/dist/assistant/query-runner/runner-plan.js +30 -0
- package/dist/assistant/query-runner/runner-sql.js +68 -0
- package/dist/assistant/query-tools.d.ts +2 -1
- package/dist/assistant/query-tools.js +3 -2
- package/dist/assistant/run-query.d.ts +3 -1
- package/dist/assistant/run-query.js +21 -7
- package/dist/batch/reorder.js +2 -1
- package/dist/better-auth/auth-schema.js +1 -1
- package/dist/better-auth/auth-tables.d.ts +8 -0
- package/dist/better-auth/auth-tables.js +11 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/openapi/schemas.js +2 -2
- package/dist/request-access.js +5 -3
- package/dist/validate-body.js +17 -12
- package/package.json +4 -4
|
@@ -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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
23
|
+
const { rows } = await query(rowLimit).execute(connection);
|
|
22
24
|
succeeded = true;
|
|
23
|
-
return
|
|
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 = {}) =>
|
|
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 = {}) =>
|
|
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 };
|
package/dist/batch/reorder.js
CHANGED
|
@@ -3,7 +3,7 @@ import { tenantScope } from "../tenant.js";
|
|
|
3
3
|
import { requireOperation, targetFilter, writableFields } from "../request-access.js";
|
|
4
4
|
import { columnId, keyConditions, scopeConditions, tableId, whereClause } from "../sql-parts.js";
|
|
5
5
|
import { fetchRecord } from "../records.js";
|
|
6
|
-
import { validateBody } from "../validate-body.js";
|
|
6
|
+
import { refuseUnknownField, validateBody } from "../validate-body.js";
|
|
7
7
|
import { missing, shown } from "../write-shared.js";
|
|
8
8
|
import { applyUpdate, planUpdate } from "../write-update.js";
|
|
9
9
|
import { checkRankAfter } from "../rank-after.js";
|
|
@@ -50,6 +50,7 @@ const reorderIn = async (trx, deps, access, input) => {
|
|
|
50
50
|
const { entry, full } = access;
|
|
51
51
|
requireOperation(access, "update");
|
|
52
52
|
const field = full.fields[input.field];
|
|
53
|
+
if (!Object.hasOwn(access.readable.fields, input.field)) refuseUnknownField(input.field);
|
|
53
54
|
validateBody(access, "update", { [input.field]: 1 });
|
|
54
55
|
if (field?.type !== "integer" && field?.type !== "bigint") throw badRequest("invalid-parameter", `"${input.field}" must be an integer field to order by`);
|
|
55
56
|
if (!writableFields(access, "update").includes(input.field)) throw badRequest("invalid-parameter", `"${input.field}" is read-only`);
|
|
@@ -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/dist/openapi/schemas.js
CHANGED
|
@@ -69,7 +69,7 @@ const only = (schema, names) => ({
|
|
|
69
69
|
});
|
|
70
70
|
/**
|
|
71
71
|
* Component schemas of a resource for one caller: the record with the fields they may read, and the create and update
|
|
72
|
-
* bodies with the fields they may write. Hidden fields appear nowhere.
|
|
72
|
+
* bodies with the fields they may write. Hidden fields appear nowhere; a write-only one only in the bodies.
|
|
73
73
|
*/
|
|
74
74
|
const resourceSchemas = (access) => {
|
|
75
75
|
const { entry, model, resolved } = access;
|
|
@@ -104,7 +104,7 @@ const resourceSchemas = (access) => {
|
|
|
104
104
|
}
|
|
105
105
|
};
|
|
106
106
|
record.required = [...record.required ?? [], "etag"];
|
|
107
|
-
const writable = access.
|
|
107
|
+
const writable = access.full;
|
|
108
108
|
const body = (names) => {
|
|
109
109
|
const properties = only(input.schema, names).properties;
|
|
110
110
|
const optional = new Set(names.filter((field) => writable.fields[field]?.nullable || writable.fields[field]?.default && "db" in writable.fields[field].default));
|
package/dist/request-access.js
CHANGED
|
@@ -65,11 +65,13 @@ const requireOperation = (access, operation) => {
|
|
|
65
65
|
if (access.session.anonymous) throw unauthorized(`Send a bearer token to ${operation} ${access.entry.name}`);
|
|
66
66
|
throw forbidden("access-denied", `You may not ${operation} ${access.entry.name}`);
|
|
67
67
|
};
|
|
68
|
+
const writesBlind = (access, name, mode) => access.entry.source.fieldAccess()[name]?.[mode] !== void 0 && !access.full.fields[name]?.file && name !== access.full.position?.field;
|
|
68
69
|
/**
|
|
69
|
-
* Fields the caller may write on create or update
|
|
70
|
-
*
|
|
70
|
+
* Fields the caller may write on create or update: those they read (a sensitive field counts as read), and write-only
|
|
71
|
+
* ones, which they cannot read but whose own write rule allows them (see `writesBlind`). The tenant column is always set
|
|
72
|
+
* by the server.
|
|
71
73
|
*/
|
|
72
|
-
const writableFields = (access, mode) => access.resolved.writableFields[mode].filter((name) => Object.hasOwn(access.readable.fields, name) && name !== access.full.tenant);
|
|
74
|
+
const writableFields = (access, mode) => access.resolved.writableFields[mode].filter((name) => (Object.hasOwn(access.readable.fields, name) || writesBlind(access, name, mode)) && name !== access.full.tenant);
|
|
73
75
|
const visibleFields = (access) => Object.values(access.model.fields);
|
|
74
76
|
const andFilters = (filters) => {
|
|
75
77
|
const present = filters.filter((filter) => filter !== void 0);
|
package/dist/validate-body.js
CHANGED
|
@@ -17,6 +17,15 @@ const invalidValue = (field, issue) => {
|
|
|
17
17
|
...params && { params }
|
|
18
18
|
};
|
|
19
19
|
};
|
|
20
|
+
const unknownField = (name) => ({
|
|
21
|
+
field: name,
|
|
22
|
+
code: "unknown-field",
|
|
23
|
+
message: `Unknown field "${name}"`
|
|
24
|
+
});
|
|
25
|
+
/** Refuses a field the caller may not name in this request as `validateBody` refuses an unknown one. */
|
|
26
|
+
const refuseUnknownField = (name) => {
|
|
27
|
+
throw fail([unknownField(name)]);
|
|
28
|
+
};
|
|
20
29
|
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
21
30
|
const reserved = ["etag", "permissions"];
|
|
22
31
|
const isEmptyText = (field, value) => field.type === "text" && !field.nullable && value === "";
|
|
@@ -26,11 +35,11 @@ const isEmptyText = (field, value) => field.type === "text" && !field.nullable &
|
|
|
26
35
|
*/
|
|
27
36
|
const requiredOnCreate = (field, schema) => !(field.default && "db" in field.default) && !field.nullable && !schema.safeParse(void 0).success;
|
|
28
37
|
/**
|
|
29
|
-
* Checks a create or update body for this caller. A field the caller
|
|
30
|
-
* but not write is read-only (the tenant column and the API's own `etag` and `permissions` keys are always
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* an update reports it required. Only the values written are checked, so a stored '' still reads.
|
|
38
|
+
* Checks a create or update body for this caller. A field the caller can neither read nor write is an unknown field, one
|
|
39
|
+
* they can read but not write is read-only (the tenant column and the API's own `etag` and `permissions` keys are always
|
|
40
|
+
* read-only), and a write-only one (see `writableFields`) is written like any other; every provided value goes through
|
|
41
|
+
* its field's zod schema, then `.validate()` rules run on the result. An empty text for a text field that may not be null
|
|
42
|
+
* counts as not given: a create fills in its default or reports it required, an update reports it required. Only the values written are checked, so a stored '' still reads.
|
|
34
43
|
* Returns only the values to write: on create the defaults are filled in, on update only the provided fields.
|
|
35
44
|
* `current` is the stored record an update merges into for `.validate()` rules, which get a set encrypted field of it as
|
|
36
45
|
* `encrypted`: they see an encrypted value as text only when this write gives it.
|
|
@@ -46,11 +55,7 @@ const validateBody = (access, mode, body, current = {}) => {
|
|
|
46
55
|
code: "read-only",
|
|
47
56
|
message: name === "etag" ? "The version is not part of the body; send it in the If-Match header" : `"${name}" is read-only`
|
|
48
57
|
});
|
|
49
|
-
else if (!Object.hasOwn(model.fields, name)) errors.push(
|
|
50
|
-
field: name,
|
|
51
|
-
code: "unknown-field",
|
|
52
|
-
message: `Unknown field "${name}"`
|
|
53
|
-
});
|
|
58
|
+
else if (!Object.hasOwn(model.fields, name) && !writable.has(name)) errors.push(unknownField(name));
|
|
54
59
|
else if (!writable.has(name) || name === full.tenant) errors.push({
|
|
55
60
|
field: name,
|
|
56
61
|
code: "read-only",
|
|
@@ -92,11 +97,11 @@ const validateBody = (access, mode, body, current = {}) => {
|
|
|
92
97
|
};
|
|
93
98
|
const issues = entry.source.validators.flatMap((validate) => validate(merged) ?? []);
|
|
94
99
|
if (issues.length > 0) throw fail(issues.map((issue) => ({
|
|
95
|
-
field: Object.hasOwn(model.fields, issue.field) ? issue.field : "_record",
|
|
100
|
+
field: Object.hasOwn(model.fields, issue.field) || writable.has(issue.field) ? issue.field : "_record",
|
|
96
101
|
code: "invalid-record",
|
|
97
102
|
message: issue.message
|
|
98
103
|
})));
|
|
99
104
|
return values;
|
|
100
105
|
};
|
|
101
106
|
//#endregion
|
|
102
|
-
export { requiredOnCreate, validateBody };
|
|
107
|
+
export { refuseUnknownField, requiredOnCreate, validateBody };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.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.
|
|
40
|
-
"@protobase/query": "0.
|
|
41
|
-
"@protobase/schema": "0.
|
|
39
|
+
"@protobase/layout": "0.39.0",
|
|
40
|
+
"@protobase/query": "0.39.0",
|
|
41
|
+
"@protobase/schema": "0.39.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",
|