@protobase/server 0.3.5 → 0.5.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/admin-options.d.ts +35 -0
- package/dist/aggregate-service.js +1 -1
- package/dist/assistant/assistant-settings.d.ts +4 -7
- package/dist/assistant/built-in-assistant.js +21 -5
- package/dist/assistant/chat-completions.d.ts +7 -1
- package/dist/assistant/conversations.d.ts +1 -1
- package/dist/assistant/records.d.ts +23 -0
- package/dist/assistant/records.js +53 -0
- package/dist/assistant/tools.d.ts +3 -0
- package/dist/assistant/turn.d.ts +4 -2
- package/dist/assistant/turn.js +3 -1
- package/dist/assistant/widget.d.ts +15 -0
- package/dist/assistant/widget.js +32 -0
- package/dist/batch/reorder.js +3 -3
- package/dist/better-auth/admin-only.js +13 -0
- package/dist/better-auth/create-auth.d.ts +2357 -30
- package/dist/better-auth/create-auth.js +70 -24
- package/dist/better-auth/operator-provider.d.ts +29 -0
- package/dist/better-auth/operator-provider.js +130 -0
- package/dist/better-auth/parse-roles.d.ts +7 -0
- package/dist/better-auth/parse-roles.js +12 -0
- package/dist/better-auth/policy-plugin.js +142 -0
- package/dist/better-auth/policy-store.js +65 -0
- package/dist/better-auth/sign-in-enforcement.js +65 -0
- package/dist/better-auth/sign-in-mail.js +78 -0
- package/dist/better-auth/sign-in-policy.d.ts +49 -0
- package/dist/better-auth/sign-in-policy.js +83 -0
- package/dist/better-auth/staff-checks.js +65 -0
- package/dist/better-auth/staff-log.d.ts +14 -0
- package/dist/better-auth/staff-log.js +96 -0
- package/dist/better-auth/staff-plugin.js +61 -0
- package/dist/better-auth/staff-sign-in-endpoints.js +162 -0
- package/dist/better-auth/status-route.js +23 -8
- package/dist/better-auth/tokens.js +1 -1
- package/dist/better-auth/two-factor-challenge.js +23 -0
- package/dist/better-auth/users.d.ts +5 -7
- package/dist/better-auth/users.js +3 -10
- package/dist/create-admin.d.ts +2 -1
- package/dist/create-admin.js +15 -8
- package/dist/deps.d.ts +6 -0
- package/dist/index.d.ts +17 -9
- package/dist/index.js +7 -2
- package/dist/list-service.js +4 -4
- package/dist/project-config.d.ts +28 -0
- package/dist/project-config.js +111 -0
- package/dist/read-service.js +3 -3
- package/dist/registry.d.ts +2 -0
- package/dist/routes/record.js +2 -2
- package/dist/row-permissions.js +2 -2
- package/dist/scan-guard.d.ts +4 -0
- package/dist/types.d.ts +1 -32
- package/dist/write-pipeline.js +2 -2
- package/dist/write-shared.js +2 -2
- package/dist/write-update.js +2 -2
- package/package.json +6 -4
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { AuditQueue, PipelineHook, ScanGuardMode } from "./types.js";
|
|
2
|
+
import { AssistantOptions } from "./assistant/assistant-settings.js";
|
|
3
|
+
import { RuntimeOptions } from "./runtime/runtime-settings.js";
|
|
4
|
+
import { ResolveOptions } from "@protobase/schema";
|
|
5
|
+
//#region src/admin-options.d.ts
|
|
6
|
+
export type AdminOptions = {
|
|
7
|
+
/** Default `/api/v1`. */
|
|
8
|
+
basePath?: string;
|
|
9
|
+
/** Applied to every request's transaction with SET LOCAL. Default 15000. */
|
|
10
|
+
statementTimeoutMs?: number;
|
|
11
|
+
/** List shapes that would sequentially scan a table of more than `seqScanRows` rows (default 10000). Default mode `reject`. */
|
|
12
|
+
scanGuard?: {
|
|
13
|
+
mode?: ScanGuardMode;
|
|
14
|
+
seqScanRows?: number;
|
|
15
|
+
};
|
|
16
|
+
/** Runs inside the write transaction after each create, update and delete. */
|
|
17
|
+
writeHooks?: PipelineHook[];
|
|
18
|
+
/** The project's roles (from `defineRoles`): an operation without an access rule needs the matching capability. */
|
|
19
|
+
roles?: ResolveOptions['roles'];
|
|
20
|
+
/** What an operation without a rule does; overrides the capability check. Without `roles` the default is allow. */
|
|
21
|
+
defaultAccess?: ResolveOptions['defaultAccess'];
|
|
22
|
+
/** Whether list items carry their `permissions` (update, delete): true by default, or `{ except: [...] }` for resources that opt out. */
|
|
23
|
+
rowPermissions?: boolean | {
|
|
24
|
+
except: string[];
|
|
25
|
+
};
|
|
26
|
+
/** Receives an event for every reveal of a sensitive field. Default: printed to the console, nothing kept. */
|
|
27
|
+
audit?: AuditQueue;
|
|
28
|
+
/** Called with every error the API turns into a 500, so the host can report it. */
|
|
29
|
+
onUnhandledError?: (error: unknown) => void;
|
|
30
|
+
/** The assistant in the admin app's dock: a backend elsewhere or the built-in one (see `AssistantOptions`); `false` turns it off. */
|
|
31
|
+
assistant?: AssistantOptions | false;
|
|
32
|
+
/** The runtime updates endpoint behind the top bar's update button (see `RuntimeOptions`); `false` turns it off. */
|
|
33
|
+
runtime?: RuntimeOptions | false;
|
|
34
|
+
};
|
|
35
|
+
//#endregion
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { badRequest } from "./problem.js";
|
|
2
|
+
import { columnId, filterCondition, scopeConditions, tableId, whereClause } from "./sql-parts.js";
|
|
2
3
|
import { checkedFilter } from "./filter-input.js";
|
|
3
4
|
import { readScope, requestAccess, requireOperation } from "./request-access.js";
|
|
4
5
|
import { readTransaction } from "./transactions.js";
|
|
5
|
-
import { columnId, filterCondition, scopeConditions, tableId, whereClause } from "./sql-parts.js";
|
|
6
6
|
import { parseParams } from "./params.js";
|
|
7
7
|
import { facetCounts, histogram, series } from "@protobase/query";
|
|
8
8
|
import { sql } from "kysely";
|
|
@@ -1,16 +1,12 @@
|
|
|
1
|
+
import { ModelSettings } from "./chat-completions.js";
|
|
2
|
+
import { AssistantTool } from "./tools.js";
|
|
1
3
|
//#region src/assistant/assistant-settings.d.ts
|
|
2
|
-
/** An OpenAI-compatible chat completions endpoint for the built-in assistant. */
|
|
3
|
-
export type ModelSettings = {
|
|
4
|
-
baseUrl: string;
|
|
5
|
-
apiKey: string;
|
|
6
|
-
model: string;
|
|
7
|
-
fetch?: typeof fetch;
|
|
8
|
-
};
|
|
9
4
|
/**
|
|
10
5
|
* `url`: an assistant backend elsewhere, which the dock talks to instead of the built-in one.
|
|
11
6
|
* `apiKey`, `model`, `baseUrl`: the built-in assistant, which answers questions about the app with a chat model.
|
|
12
7
|
* `chats`: the directory the built-in assistant keeps conversations in, one file each.
|
|
13
8
|
* Each falls back to its environment variable; `fetch` replaces the model endpoint, for tests.
|
|
9
|
+
* `tools`: the app's own tools, which the built-in assistant offers after its own; a backend elsewhere has its own.
|
|
14
10
|
*/
|
|
15
11
|
export type AssistantOptions = {
|
|
16
12
|
url?: string;
|
|
@@ -19,6 +15,7 @@ export type AssistantOptions = {
|
|
|
19
15
|
baseUrl?: string;
|
|
20
16
|
chats?: string;
|
|
21
17
|
fetch?: typeof fetch;
|
|
18
|
+
tools?: AssistantTool[];
|
|
22
19
|
};
|
|
23
20
|
export type AssistantSettings = {
|
|
24
21
|
kind: 'external';
|
|
@@ -6,16 +6,31 @@ import { assistantProtocolRoutes } from "./protocol-routes.js";
|
|
|
6
6
|
import { readOnlyQueryTool, readWriteQueryTool } from "./query-tools.js";
|
|
7
7
|
import { runTurn } from "./turn.js";
|
|
8
8
|
//#region src/assistant/built-in-assistant.ts
|
|
9
|
+
const toolName = /^[A-Za-z0-9_-]{1,64}$/;
|
|
10
|
+
/** The tools in order; a name a model endpoint refuses, or one taken twice, stops the server at startup. */
|
|
11
|
+
const checkedTools = (tools) => {
|
|
12
|
+
const seen = /* @__PURE__ */ new Set();
|
|
13
|
+
for (const tool of tools) {
|
|
14
|
+
if (!toolName.test(tool.name)) throw new Error(`Assistant tool name "${tool.name}" must be 1 to 64 letters, digits, _ or -`);
|
|
15
|
+
if (seen.has(tool.name)) throw new Error(`Two assistant tools are named "${tool.name}"; give one of them another name`);
|
|
16
|
+
seen.add(tool.name);
|
|
17
|
+
}
|
|
18
|
+
return tools;
|
|
19
|
+
};
|
|
9
20
|
const pageContext = (page) => page && `The user is on the page ${page} of the app (/<resource> lists records, /<resource>/<key> shows one).`;
|
|
10
21
|
/**
|
|
11
22
|
* Protobase's own assistant: answers questions about the app, reads its data with read-only queries and proposes
|
|
12
|
-
* changes the user approves, for callers with the `admin` or `ai` role. Made only of
|
|
13
|
-
* its prompt, which describes what the caller may see of the app. Conversations are
|
|
14
|
-
* On OpenRouter it caches the prompt and asks for little reasoning.
|
|
23
|
+
* changes the user approves, with the app's own tools besides, for callers with the `admin` or `ai` role. Made only of
|
|
24
|
+
* the exported building blocks and its prompt, which describes what the caller may see of the app. Conversations are
|
|
25
|
+
* files in the `chats` directory. On OpenRouter it caches the prompt and asks for little reasoning.
|
|
15
26
|
*/
|
|
16
|
-
const builtInAssistant = ({ model, chats, db, meta, authenticate, report }) => {
|
|
27
|
+
const builtInAssistant = ({ model, chats, db, meta, authenticate, tools: appTools = [], records, report }) => {
|
|
17
28
|
const conversations = createConversations({ store: fileConversationStore(chats) });
|
|
18
|
-
const tools = [
|
|
29
|
+
const tools = checkedTools([
|
|
30
|
+
readOnlyQueryTool(db),
|
|
31
|
+
readWriteQueryTool(db),
|
|
32
|
+
...appTools
|
|
33
|
+
]);
|
|
19
34
|
const onOpenRouter = model.baseUrl === openRouterUrl;
|
|
20
35
|
return assistantProtocolRoutes({
|
|
21
36
|
authenticate,
|
|
@@ -30,6 +45,7 @@ const builtInAssistant = ({ model, chats, db, meta, authenticate, report }) => {
|
|
|
30
45
|
session,
|
|
31
46
|
system: appContext(body.resources, body.views),
|
|
32
47
|
tools,
|
|
48
|
+
...records && { records: records(session) },
|
|
33
49
|
report,
|
|
34
50
|
...page && { context: pageContext(page) },
|
|
35
51
|
...onOpenRouter && {
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import { ModelSettings } from "./assistant-settings.js";
|
|
2
1
|
//#region src/assistant/chat-completions.d.ts
|
|
2
|
+
/** An OpenAI-compatible chat completions endpoint for the built-in assistant. */
|
|
3
|
+
export type ModelSettings = {
|
|
4
|
+
baseUrl: string;
|
|
5
|
+
apiKey: string;
|
|
6
|
+
model: string;
|
|
7
|
+
fetch?: typeof fetch;
|
|
8
|
+
};
|
|
3
9
|
export type ToolCall = {
|
|
4
10
|
id: string;
|
|
5
11
|
type: 'function';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Session } from "../types.js";
|
|
2
1
|
import { ChatMessage } from "./chat-completions.js";
|
|
2
|
+
import { Session } from "../types.js";
|
|
3
3
|
import { ConversationStore } from "./conversation-store.js";
|
|
4
4
|
import { AssistantEvent, AssistantState } from "@protobase/schema";
|
|
5
5
|
//#region src/assistant/conversations.d.ts
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Row } from "../types.js";
|
|
2
|
+
import "../deps.js";
|
|
3
|
+
//#region src/assistant/records.d.ts
|
|
4
|
+
/** A record's key: its URL text (`a,b` when composite), a scalar, or its parts. */
|
|
5
|
+
export type RecordKeyInput = string | number | Array<string | number>;
|
|
6
|
+
/** A record as the user may see it, with the ETag a later update can pass to fail when someone changed it since. */
|
|
7
|
+
export type ToolRecord = {
|
|
8
|
+
record: Row;
|
|
9
|
+
etag: string;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* The app's resources as the session's user: their access rules, tenant scope, validation and write hooks apply as
|
|
13
|
+
* in the REST API. A refusal or a missing record rejects with an `HttpProblem`, which a tool can turn into text.
|
|
14
|
+
*/
|
|
15
|
+
export type ToolRecords = {
|
|
16
|
+
get: (resource: string, key: RecordKeyInput) => Promise<ToolRecord>;
|
|
17
|
+
create: (resource: string, data: Row) => Promise<ToolRecord>;
|
|
18
|
+
/** With `etag`, fails with 412 when the record changed since it was read; without, it updates any version. */
|
|
19
|
+
update: (resource: string, key: RecordKeyInput, data: Row, options?: {
|
|
20
|
+
etag?: string;
|
|
21
|
+
}) => Promise<ToolRecord>;
|
|
22
|
+
};
|
|
23
|
+
//#endregion
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { notFound } from "../problem.js";
|
|
2
|
+
import { decodeKeyText } from "../key.js";
|
|
3
|
+
import { readRecord } from "../read-service.js";
|
|
4
|
+
import { runWrite } from "../write-pipeline.js";
|
|
5
|
+
import { encodeKey } from "@protobase/schema";
|
|
6
|
+
//#region src/assistant/records.ts
|
|
7
|
+
/** `ToolRecords` over the admin's resources for `session`. */
|
|
8
|
+
const assistantRecords = (deps, session) => {
|
|
9
|
+
const entryOf = (resource) => {
|
|
10
|
+
const entry = deps.registry.find(resource);
|
|
11
|
+
if (!entry) throw notFound(`There is no resource "${resource}"`);
|
|
12
|
+
return entry;
|
|
13
|
+
};
|
|
14
|
+
const keyOf = (resource, key) => {
|
|
15
|
+
const entry = entryOf(resource);
|
|
16
|
+
return {
|
|
17
|
+
entry,
|
|
18
|
+
key: decodeKeyText(entry, Array.isArray(key) ? encodeKey(key) : String(key))
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
return {
|
|
22
|
+
get: async (resource, input) => {
|
|
23
|
+
const { entry, key } = keyOf(resource, input);
|
|
24
|
+
const { record, etag } = await readRecord(deps, entry, session, key);
|
|
25
|
+
return {
|
|
26
|
+
record,
|
|
27
|
+
etag
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
create: async (resource, data) => runWrite(deps, entryOf(resource), session, {
|
|
31
|
+
operation: "create",
|
|
32
|
+
body: data
|
|
33
|
+
}),
|
|
34
|
+
update: async (resource, input, data, { etag } = {}) => {
|
|
35
|
+
const { entry, key } = keyOf(resource, input);
|
|
36
|
+
return runWrite(deps, entry, session, {
|
|
37
|
+
operation: "update",
|
|
38
|
+
key,
|
|
39
|
+
body: data,
|
|
40
|
+
ifMatch: etag ?? "*"
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
const withoutAdmin = () => Promise.reject(/* @__PURE__ */ new Error("This assistant has no records: ToolContext.records works in the built-in assistant"));
|
|
46
|
+
/** Records for a turn without an admin around it, such as in a backend of its own: every call fails. */
|
|
47
|
+
const noRecords = {
|
|
48
|
+
get: withoutAdmin,
|
|
49
|
+
create: withoutAdmin,
|
|
50
|
+
update: withoutAdmin
|
|
51
|
+
};
|
|
52
|
+
//#endregion
|
|
53
|
+
export { assistantRecords, noRecords };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Session } from "../types.js";
|
|
2
2
|
import { Conversation } from "./conversations.js";
|
|
3
3
|
import { ApprovalAnswer, ApprovalRequest } from "./approval.js";
|
|
4
|
+
import { ToolRecords } from "./records.js";
|
|
4
5
|
import { AssistantPart } from "@protobase/schema";
|
|
5
6
|
//#region src/assistant/tools.d.ts
|
|
6
7
|
export type ToolContext = {
|
|
@@ -10,6 +11,8 @@ export type ToolContext = {
|
|
|
10
11
|
show: (part: AssistantPart) => void;
|
|
11
12
|
/** Asks the user to approve with a card in that message (see `requestApproval`). */
|
|
12
13
|
approve: (request: ApprovalRequest) => Promise<ApprovalAnswer>;
|
|
14
|
+
/** The app's resources as the session's user, through their access rules and write hooks (built-in assistant only). */
|
|
15
|
+
records: ToolRecords;
|
|
13
16
|
};
|
|
14
17
|
/**
|
|
15
18
|
* A function the model may call during a turn. `parameters` is the JSON Schema of its arguments; `run` gets them
|
package/dist/assistant/turn.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { ModelSettings } from "./
|
|
1
|
+
import { ModelSettings, ReasoningEffort } from "./chat-completions.js";
|
|
2
2
|
import { Session } from "../types.js";
|
|
3
|
-
import { ReasoningEffort } from "./chat-completions.js";
|
|
4
3
|
import { Conversation } from "./conversations.js";
|
|
4
|
+
import { ToolRecords } from "./records.js";
|
|
5
5
|
import { AssistantTool } from "./tools.js";
|
|
6
6
|
//#region src/assistant/turn.d.ts
|
|
7
7
|
export type TurnOptions = {
|
|
@@ -11,6 +11,8 @@ export type TurnOptions = {
|
|
|
11
11
|
/** The system prompt. */
|
|
12
12
|
system: string;
|
|
13
13
|
tools?: AssistantTool[];
|
|
14
|
+
/** What tools get as `records`; without it, their calls fail. */
|
|
15
|
+
records?: ToolRecords;
|
|
14
16
|
/** Model calls in one turn before it stops. Default 8. */
|
|
15
17
|
maxSteps?: number;
|
|
16
18
|
/** Gets errors other than the model endpoint's refusals, which the user only sees as a failure. */
|
package/dist/assistant/turn.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { requestApproval } from "./approval.js";
|
|
2
2
|
import { withCacheBreakpoints } from "./cache-breakpoints.js";
|
|
3
3
|
import { ModelError, streamChatCompletion } from "./chat-completions.js";
|
|
4
|
+
import { noRecords } from "./records.js";
|
|
4
5
|
//#region src/assistant/turn.ts
|
|
5
6
|
const toolResult = async (call, tools, context) => {
|
|
6
7
|
const tool = tools.find((entry) => entry.name === call.function.name);
|
|
@@ -39,7 +40,8 @@ const runTurn = async (options, text) => {
|
|
|
39
40
|
session,
|
|
40
41
|
conversation,
|
|
41
42
|
show,
|
|
42
|
-
approve: (request) => requestApproval(conversation, messageId, request)
|
|
43
|
+
approve: (request) => requestApproval(conversation, messageId, request),
|
|
44
|
+
records: options.records ?? noRecords
|
|
43
45
|
};
|
|
44
46
|
const specs = tools.map((tool) => ({
|
|
45
47
|
type: "function",
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { AssistantWidgetPart } from "@protobase/schema";
|
|
2
|
+
//#region src/assistant/widget.d.ts
|
|
3
|
+
/** The most a widget's props may take, as JSON in UTF-8: room for ids and settings, not for the state it shows. */
|
|
4
|
+
export declare const maxWidgetPropsBytes = 2048;
|
|
5
|
+
export type WidgetOptions = {
|
|
6
|
+
id?: string;
|
|
7
|
+
fallback?: AssistantWidgetPart['fallback'];
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* A widget part for `show`: the app's component `name` draws it with `props`, fetching what it shows itself. A name
|
|
11
|
+
* that is not PascalCase, or props that are not a JSON object of at most 2 KB, throw, so a tool cannot keep state in
|
|
12
|
+
* the chat by accident. Without an `id`, the part gets a new one; show a part with the same id again to replace it.
|
|
13
|
+
*/
|
|
14
|
+
export declare const widget: (name: string, props: Record<string, unknown>, options?: WidgetOptions) => AssistantWidgetPart;
|
|
15
|
+
//#endregion
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//#region src/assistant/widget.ts
|
|
2
|
+
const componentName = /^[A-Z][A-Za-z0-9]*$/;
|
|
3
|
+
/** The most a widget's props may take, as JSON in UTF-8: room for ids and settings, not for the state it shows. */
|
|
4
|
+
const maxWidgetPropsBytes = 2048;
|
|
5
|
+
const isPlainObject = (value) => typeof value === "object" && value !== null && Object.getPrototypeOf(value) === Object.prototype;
|
|
6
|
+
const isJson = (value) => {
|
|
7
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
8
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
9
|
+
if (Array.isArray(value)) return value.every(isJson);
|
|
10
|
+
return isPlainObject(value) && Object.values(value).every(isJson);
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* A widget part for `show`: the app's component `name` draws it with `props`, fetching what it shows itself. A name
|
|
14
|
+
* that is not PascalCase, or props that are not a JSON object of at most 2 KB, throw, so a tool cannot keep state in
|
|
15
|
+
* the chat by accident. Without an `id`, the part gets a new one; show a part with the same id again to replace it.
|
|
16
|
+
*/
|
|
17
|
+
const widget = (name, props, options = {}) => {
|
|
18
|
+
if (!componentName.test(name)) throw new Error(`Widget name "${name}" must be PascalCase, such as TaskProposal`);
|
|
19
|
+
if (!isPlainObject(props) || !isJson(props)) throw new Error(`The props of widget ${name} must be a JSON object`);
|
|
20
|
+
const bytes = new TextEncoder().encode(JSON.stringify(props)).length;
|
|
21
|
+
if (bytes > 2048) throw new Error(`The props of widget ${name} take ${bytes} bytes, more than ${maxWidgetPropsBytes}; pass ids and let the widget fetch the rest`);
|
|
22
|
+
const { id = crypto.randomUUID(), fallback } = options;
|
|
23
|
+
return {
|
|
24
|
+
type: "widget",
|
|
25
|
+
id,
|
|
26
|
+
name,
|
|
27
|
+
props,
|
|
28
|
+
...fallback && { fallback }
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
//#endregion
|
|
32
|
+
export { maxWidgetPropsBytes, widget };
|
package/dist/batch/reorder.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { badRequest } from "../problem.js";
|
|
2
|
-
import { tenantScope } from "../tenant.js";
|
|
3
|
-
import { requireOperation, targetFilter, writableFields } from "../request-access.js";
|
|
4
2
|
import { columnId, keyConditions, scopeConditions, tableId, whereClause } from "../sql-parts.js";
|
|
5
3
|
import { fetchRecord } from "../records.js";
|
|
6
|
-
import {
|
|
4
|
+
import { tenantScope } from "../tenant.js";
|
|
5
|
+
import { requireOperation, targetFilter, writableFields } from "../request-access.js";
|
|
7
6
|
import { validateBody } from "../validate-body.js";
|
|
7
|
+
import { missing, shown } from "../write-shared.js";
|
|
8
8
|
import { applyUpdate, planUpdate } from "../write-update.js";
|
|
9
9
|
import { encodeKey } from "@protobase/schema";
|
|
10
10
|
import { sql } from "kysely";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { parseRoles } from "./parse-roles.js";
|
|
2
|
+
import { APIError, createAuthMiddleware, sessionMiddleware } from "better-auth/api";
|
|
3
|
+
//#region src/better-auth/admin-only.ts
|
|
4
|
+
/** Lets a request through only with the session of an admin; others get `403 ADMIN_ONLY` with `message`. */
|
|
5
|
+
const adminOnly = (message) => createAuthMiddleware({ use: [sessionMiddleware] }, async (ctx) => {
|
|
6
|
+
if (!parseRoles(ctx.context.session.user.role).includes("admin")) throw new APIError("FORBIDDEN", {
|
|
7
|
+
code: "ADMIN_ONLY",
|
|
8
|
+
message
|
|
9
|
+
});
|
|
10
|
+
return { session: ctx.context.session };
|
|
11
|
+
});
|
|
12
|
+
//#endregion
|
|
13
|
+
export { adminOnly };
|