@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.
Files changed (55) hide show
  1. package/dist/admin-options.d.ts +35 -0
  2. package/dist/aggregate-service.js +1 -1
  3. package/dist/assistant/assistant-settings.d.ts +4 -7
  4. package/dist/assistant/built-in-assistant.js +21 -5
  5. package/dist/assistant/chat-completions.d.ts +7 -1
  6. package/dist/assistant/conversations.d.ts +1 -1
  7. package/dist/assistant/records.d.ts +23 -0
  8. package/dist/assistant/records.js +53 -0
  9. package/dist/assistant/tools.d.ts +3 -0
  10. package/dist/assistant/turn.d.ts +4 -2
  11. package/dist/assistant/turn.js +3 -1
  12. package/dist/assistant/widget.d.ts +15 -0
  13. package/dist/assistant/widget.js +32 -0
  14. package/dist/batch/reorder.js +3 -3
  15. package/dist/better-auth/admin-only.js +13 -0
  16. package/dist/better-auth/create-auth.d.ts +2357 -30
  17. package/dist/better-auth/create-auth.js +70 -24
  18. package/dist/better-auth/operator-provider.d.ts +29 -0
  19. package/dist/better-auth/operator-provider.js +130 -0
  20. package/dist/better-auth/parse-roles.d.ts +7 -0
  21. package/dist/better-auth/parse-roles.js +12 -0
  22. package/dist/better-auth/policy-plugin.js +142 -0
  23. package/dist/better-auth/policy-store.js +65 -0
  24. package/dist/better-auth/sign-in-enforcement.js +65 -0
  25. package/dist/better-auth/sign-in-mail.js +78 -0
  26. package/dist/better-auth/sign-in-policy.d.ts +49 -0
  27. package/dist/better-auth/sign-in-policy.js +83 -0
  28. package/dist/better-auth/staff-checks.js +65 -0
  29. package/dist/better-auth/staff-log.d.ts +14 -0
  30. package/dist/better-auth/staff-log.js +96 -0
  31. package/dist/better-auth/staff-plugin.js +61 -0
  32. package/dist/better-auth/staff-sign-in-endpoints.js +162 -0
  33. package/dist/better-auth/status-route.js +23 -8
  34. package/dist/better-auth/tokens.js +1 -1
  35. package/dist/better-auth/two-factor-challenge.js +23 -0
  36. package/dist/better-auth/users.d.ts +5 -7
  37. package/dist/better-auth/users.js +3 -10
  38. package/dist/create-admin.d.ts +2 -1
  39. package/dist/create-admin.js +15 -8
  40. package/dist/deps.d.ts +6 -0
  41. package/dist/index.d.ts +17 -9
  42. package/dist/index.js +7 -2
  43. package/dist/list-service.js +4 -4
  44. package/dist/project-config.d.ts +28 -0
  45. package/dist/project-config.js +111 -0
  46. package/dist/read-service.js +3 -3
  47. package/dist/registry.d.ts +2 -0
  48. package/dist/routes/record.js +2 -2
  49. package/dist/row-permissions.js +2 -2
  50. package/dist/scan-guard.d.ts +4 -0
  51. package/dist/types.d.ts +1 -32
  52. package/dist/write-pipeline.js +2 -2
  53. package/dist/write-shared.js +2 -2
  54. package/dist/write-update.js +2 -2
  55. package/package.json +6 -4
@@ -0,0 +1,23 @@
1
+ import { twoFactor } from "better-auth/plugins";
2
+ //#region src/better-auth/two-factor-challenge.ts
3
+ /**
4
+ * Better Auth's two-factor plugin, whose second step also follows a sign-in with an emailed code: the plugin asks for
5
+ * it after a password only, and an inbox alone is one factor as well. A passkey is two factors already, so a passkey
6
+ * sign-in is not asked. The plugin's own hook does the work; only the paths it matches grow.
7
+ */
8
+ const twoFactorAfterPasswordOrCode = (options) => {
9
+ const plugin = twoFactor(options);
10
+ const after = plugin.hooks.after.map((hook) => ({
11
+ ...hook,
12
+ matcher: (context) => hook.matcher(context) || context.path === "/sign-in/email-otp"
13
+ }));
14
+ return {
15
+ ...plugin,
16
+ hooks: {
17
+ ...plugin.hooks,
18
+ after
19
+ }
20
+ };
21
+ };
22
+ //#endregion
23
+ export { twoFactorAfterPasswordOrCode };
@@ -1,4 +1,5 @@
1
1
  import { AdminAuth } from "./create-auth.js";
2
+ import { parseRoles } from "./parse-roles.js";
2
3
  //#region src/better-auth/users.d.ts
3
4
  /** The roles this auth instance allows, `admin` first. */
4
5
  export declare const roleChoices: (auth: AdminAuth) => string[];
@@ -19,18 +20,14 @@ export type StoredUser = {
19
20
  export declare const hasUsers: (auth: AdminAuth) => Promise<boolean>;
20
21
  /**
21
22
  * Creates a user directly in the admin store. Host side only (no HTTP route creates accounts while the store is empty):
22
- * the first user is always an `admin`, later ones get `role` (default `user`).
23
+ * the first user is always an `admin`, later ones get `role` (default `user`). The address counts as verified, so the
24
+ * user can also sign in with an emailed code.
23
25
  */
24
26
  export declare const createUser: (auth: AdminAuth, input: NewUser) => Promise<{
25
27
  id: string;
26
28
  email: string;
27
29
  role: string;
28
30
  }>;
29
- /**
30
- * The admin plugin stores roles as a comma separated string. A value written as a JS array through a Postgres driver
31
- * comes back as an array literal (`{admin,sales}`); that is read too, and rewritten as a plain string on the next change.
32
- */
33
- export declare const parseRoles: (role: string | string[] | null | undefined) => string[];
34
31
  export declare const listUsers: (auth: AdminAuth) => Promise<StoredUser[]>;
35
32
  /** Deletes the user with their sessions and accounts; refuses to delete the last active admin. */
36
33
  export declare const deleteUser: (auth: AdminAuth, email: string) => Promise<void>;
@@ -47,4 +44,5 @@ export declare const setUserBanned: (auth: AdminAuth, input: {
47
44
  email: string;
48
45
  banned: boolean;
49
46
  }) => Promise<void>;
50
- //#endregion
47
+ //#endregion
48
+ export { parseRoles };
@@ -1,3 +1,4 @@
1
+ import { parseRoles } from "./parse-roles.js";
1
2
  //#region src/better-auth/users.ts
2
3
  /** The roles this auth instance allows, `admin` first. */
3
4
  const roleChoices = (auth) => [...auth.roles];
@@ -11,7 +12,8 @@ const checkRoles = (auth, role) => {
11
12
  const hasUsers = async (auth) => (await auth.$context).adapter.count({ model: "user" }).then((count) => count > 0);
12
13
  /**
13
14
  * Creates a user directly in the admin store. Host side only (no HTTP route creates accounts while the store is empty):
14
- * the first user is always an `admin`, later ones get `role` (default `user`).
15
+ * the first user is always an `admin`, later ones get `role` (default `user`). The address counts as verified, so the
16
+ * user can also sign in with an emailed code.
15
17
  */
16
18
  const createUser = async (auth, input) => {
17
19
  const first = !await hasUsers(auth);
@@ -30,15 +32,6 @@ const createUser = async (auth, input) => {
30
32
  role: String(user.role)
31
33
  };
32
34
  };
33
- /**
34
- * The admin plugin stores roles as a comma separated string. A value written as a JS array through a Postgres driver
35
- * comes back as an array literal (`{admin,sales}`); that is read too, and rewritten as a plain string on the next change.
36
- */
37
- const parseRoles = (role) => {
38
- if (Array.isArray(role)) return role;
39
- const text = (role ?? "user").trim();
40
- return (text.startsWith("{") && text.endsWith("}") ? text.slice(1, -1) : text).split(",").map((name) => name.trim().replace(/^"|"$/g, "")).filter(Boolean);
41
- };
42
35
  const allUsers = async (auth) => {
43
36
  const { adapter } = await auth.$context;
44
37
  return adapter.findMany({
@@ -1,6 +1,7 @@
1
1
  import { AdminAuth } from "./better-auth/create-auth.js";
2
2
  import { ResourceSource } from "./resource-source.js";
3
- import { AdminEnv, AdminOptions, Authenticator, PageSource, ViewSource } from "./types.js";
3
+ import { AdminEnv, Authenticator, PageSource, ViewSource } from "./types.js";
4
+ import { AdminOptions } from "./admin-options.js";
4
5
  import { Hono } from "hono";
5
6
  import { UserMenuSource } from "@protobase/schema";
6
7
  import { Db } from "@protobase/query";
@@ -1,5 +1,6 @@
1
1
  import { assistantSettings } from "./assistant/assistant-settings.js";
2
2
  import { createErrorHandler } from "./error-handler.js";
3
+ import { assistantRecords } from "./assistant/records.js";
3
4
  import { builtInAssistant } from "./assistant/built-in-assistant.js";
4
5
  import { consoleAuditQueue } from "./audit/console-queue.js";
5
6
  import { authBasePath } from "./better-auth/create-auth.js";
@@ -84,14 +85,20 @@ const createAdmin = ({ resources, views = [], pages = [], userMenu, db, authenti
84
85
  app.route(authBasePath, statusRoute(auth));
85
86
  app.on(["GET", "POST"], `${authBasePath}/*`, (c) => auth.handler(c.req.raw));
86
87
  }
87
- if (assistant?.kind === "built-in") app.route(`${systemPath}/assistant`, builtInAssistant({
88
- model: assistant.model,
89
- chats: assistant.chats,
90
- db,
91
- meta,
92
- authenticate,
93
- report: options.onUnhandledError
94
- }));
88
+ if (assistant?.kind === "built-in") {
89
+ const tools = options.assistant ? options.assistant.tools : void 0;
90
+ const records = (session) => assistantRecords(deps, session);
91
+ app.route(`${systemPath}/assistant`, builtInAssistant({
92
+ model: assistant.model,
93
+ chats: assistant.chats,
94
+ db,
95
+ meta,
96
+ authenticate,
97
+ tools,
98
+ records,
99
+ report: options.onUnhandledError
100
+ }));
101
+ }
95
102
  app.route("/", batchRoute(deps, authenticate, meta));
96
103
  app.route(basePath, api);
97
104
  app.route(deps.systemPath || "/", system);
package/dist/deps.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ import "./types.js";
2
+ import "./registry.js";
3
+ import "./scan-guard.js";
4
+ import { PageModel } from "@protobase/layout";
5
+ import { ResolveOptions, ViewModel } from "@protobase/schema";
6
+ import { Db } from "@protobase/query";
package/dist/index.d.ts CHANGED
@@ -1,24 +1,32 @@
1
+ import { SignInPolicy, SignInRule, StaffAccessRule, defaultSignInPolicy, signInPolicyMethods } from "./better-auth/sign-in-policy.js";
2
+ import { StaffSignIn } from "./better-auth/staff-log.js";
3
+ import { OperatorProvider, readOperatorSettings } from "./better-auth/operator-provider.js";
1
4
  import { AdminAuth, CreateAuthOptions, UserCreatedHook, createAuth, normalizeRoles } from "./better-auth/create-auth.js";
2
5
  import { ResourceSource, configExports } from "./resource-source.js";
3
- import { AssistantOptions, AssistantSettings, ModelSettings, assistantRoles, assistantSettings, assistantVariables, seesAssistant } from "./assistant/assistant-settings.js";
6
+ import { ChatMessage, ChatTextPart, Completion, CompletionOptions, ModelError, ModelSettings, ReasoningDetail, ReasoningEffort, ToolCall, ToolSpec, streamChatCompletion } from "./assistant/chat-completions.js";
7
+ import { Admin, AuditEvent, AuditQueue, Authenticator, PipelineHook, ScanGuardMode, Session, TenantValue, User, ViewSource, WriteEvent, WriteOperation } from "./types.js";
8
+ import { ConversationRecord, ConversationStore } from "./assistant/conversation-store.js";
9
+ import { Conversation, ConversationsOptions, conversationKey, createConversations } from "./assistant/conversations.js";
10
+ import { ApprovalAnswer, ApprovalRequest, requestApproval } from "./assistant/approval.js";
11
+ import { RecordKeyInput, ToolRecord, ToolRecords } from "./assistant/records.js";
12
+ import { AssistantTool, ToolContext, defineTool } from "./assistant/tools.js";
13
+ import { AssistantOptions, AssistantSettings, assistantRoles, assistantSettings, assistantVariables, seesAssistant } from "./assistant/assistant-settings.js";
4
14
  import { RuntimeOptions, runtimeUrl, runtimeVariable, seesRuntime } from "./runtime/runtime-settings.js";
5
- import { Admin, AdminOptions, AuditEvent, AuditQueue, Authenticator, PipelineHook, ScanGuardMode, Session, TenantValue, User, ViewSource, WriteEvent, WriteOperation } from "./types.js";
15
+ import { AdminOptions } from "./admin-options.js";
6
16
  import { CreateAdminInput, createAdmin } from "./create-admin.js";
17
+ import { ProjectConfig, defineConfig, mergeConfig } from "./project-config.js";
7
18
  import { consoleAuditQueue } from "./audit/console-queue.js";
8
19
  import { JwtOptions, jwtAuthenticator } from "./auth/jwt.js";
9
20
  import { HttpProblem, badRequest, forbidden, notFound, unauthorized } from "./problem.js";
10
21
  import { BetterAuthAuthenticatorOptions, betterAuthAuthenticator } from "./better-auth/authenticator.js";
11
- import { NewUser, StoredUser, createUser, deleteUser, hasUsers, listUsers, parseRoles, roleChoices, setUserBanned, setUserRole } from "./better-auth/users.js";
22
+ import { parseRoles } from "./better-auth/parse-roles.js";
23
+ import { NewUser, StoredUser, createUser, deleteUser, hasUsers, listUsers, roleChoices, setUserBanned, setUserRole } from "./better-auth/users.js";
12
24
  import { IssueTokenOptions, issueToken, maxTokenTtlSeconds } from "./better-auth/tokens.js";
13
- import { ChatMessage, ChatTextPart, Completion, CompletionOptions, ModelError, ReasoningDetail, ReasoningEffort, ToolCall, ToolSpec, streamChatCompletion } from "./assistant/chat-completions.js";
14
- import { ConversationRecord, ConversationStore } from "./assistant/conversation-store.js";
15
- import { Conversation, ConversationsOptions, conversationKey, createConversations } from "./assistant/conversations.js";
16
25
  import { AssistantBackend, assistantProtocolRoutes } from "./assistant/protocol-routes.js";
17
26
  import { fileConversationStore } from "./assistant/file-conversation-store.js";
18
27
  import { withCacheBreakpoints } from "./assistant/cache-breakpoints.js";
19
- import { ApprovalAnswer, ApprovalRequest, requestApproval } from "./assistant/approval.js";
20
- import { AssistantTool, ToolContext, defineTool } from "./assistant/tools.js";
21
28
  import { TurnOptions, runTurn } from "./assistant/turn.js";
29
+ import { WidgetOptions, maxWidgetPropsBytes, widget } from "./assistant/widget.js";
22
30
  import { QueryOptions, QueryResult, runReadOnlyQuery, runReadWriteQuery } from "./assistant/run-query.js";
23
31
  import { readOnlyQueryTool, readWriteQueryTool } from "./assistant/query-tools.js";
24
- export { type Admin, type AdminAuth, type AdminOptions, type ApprovalAnswer, type ApprovalRequest, type AssistantBackend, type AssistantOptions, type AssistantSettings, type AssistantTool, type AuditEvent, type AuditQueue, type Authenticator, type BetterAuthAuthenticatorOptions, type ChatMessage, type ChatTextPart, type Completion, type CompletionOptions, type Conversation, type ConversationRecord, type ConversationStore, type ConversationsOptions, type CreateAdminInput, type CreateAuthOptions, HttpProblem, type IssueTokenOptions, type JwtOptions, ModelError, type ModelSettings, type NewUser, type PipelineHook, type QueryOptions, type QueryResult, type ReasoningDetail, type ReasoningEffort, type ResourceSource, type RuntimeOptions, type ScanGuardMode, type Session, type StoredUser, type TenantValue, type ToolCall, type ToolContext, type ToolSpec, type TurnOptions, type User, type UserCreatedHook, type ViewSource, type WriteEvent, type WriteOperation, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, badRequest, betterAuthAuthenticator, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createUser, defineTool, deleteUser, fileConversationStore, forbidden, hasUsers, issueToken, jwtAuthenticator, listUsers, maxTokenTtlSeconds, normalizeRoles, notFound, parseRoles, readOnlyQueryTool, readWriteQueryTool, requestApproval, roleChoices, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, seesAssistant, seesRuntime, setUserBanned, setUserRole, streamChatCompletion, unauthorized, withCacheBreakpoints };
32
+ export { type Admin, type AdminAuth, type AdminOptions, type ApprovalAnswer, type ApprovalRequest, type AssistantBackend, type AssistantOptions, type AssistantSettings, type AssistantTool, type AuditEvent, type AuditQueue, type Authenticator, type BetterAuthAuthenticatorOptions, type ChatMessage, type ChatTextPart, type Completion, type CompletionOptions, type Conversation, type ConversationRecord, type ConversationStore, type ConversationsOptions, type CreateAdminInput, type CreateAuthOptions, HttpProblem, type IssueTokenOptions, type JwtOptions, ModelError, type ModelSettings, type NewUser, type OperatorProvider, type PipelineHook, type ProjectConfig, type QueryOptions, type QueryResult, type ReasoningDetail, type ReasoningEffort, type RecordKeyInput, type ResourceSource, type RuntimeOptions, type ScanGuardMode, type Session, type SignInPolicy, type SignInRule, type StaffAccessRule, type StaffSignIn, type StoredUser, type TenantValue, type ToolCall, type ToolContext, type ToolRecord, type ToolRecords, type ToolSpec, type TurnOptions, type User, type UserCreatedHook, type ViewSource, type WidgetOptions, type WriteEvent, type WriteOperation, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, badRequest, betterAuthAuthenticator, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createUser, defaultSignInPolicy, defineConfig, defineTool, deleteUser, fileConversationStore, forbidden, hasUsers, issueToken, jwtAuthenticator, listUsers, maxTokenTtlSeconds, maxWidgetPropsBytes, mergeConfig, normalizeRoles, notFound, parseRoles, readOnlyQueryTool, readOperatorSettings, readWriteQueryTool, requestApproval, roleChoices, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, seesAssistant, seesRuntime, setUserBanned, setUserRole, signInPolicyMethods, streamChatCompletion, unauthorized, widget, withCacheBreakpoints };
package/dist/index.js CHANGED
@@ -11,12 +11,17 @@ import { withCacheBreakpoints } from "./assistant/cache-breakpoints.js";
11
11
  import { ModelError, streamChatCompletion } from "./assistant/chat-completions.js";
12
12
  import { runTurn } from "./assistant/turn.js";
13
13
  import { consoleAuditQueue } from "./audit/console-queue.js";
14
+ import { readOperatorSettings } from "./better-auth/operator-provider.js";
15
+ import { defaultSignInPolicy, signInPolicyMethods } from "./better-auth/sign-in-policy.js";
16
+ import { parseRoles } from "./better-auth/parse-roles.js";
14
17
  import { createAuth, normalizeRoles } from "./better-auth/create-auth.js";
15
- import { createUser, deleteUser, hasUsers, listUsers, parseRoles, roleChoices, setUserBanned, setUserRole } from "./better-auth/users.js";
18
+ import { createUser, deleteUser, hasUsers, listUsers, roleChoices, setUserBanned, setUserRole } from "./better-auth/users.js";
16
19
  import { runtimeUrl, runtimeVariable, seesRuntime } from "./runtime/runtime-settings.js";
17
20
  import { createAdmin } from "./create-admin.js";
18
21
  import { configExports } from "./resource-source.js";
22
+ import { defineConfig, mergeConfig } from "./project-config.js";
19
23
  import { jwtAuthenticator } from "./auth/jwt.js";
20
24
  import { betterAuthAuthenticator } from "./better-auth/authenticator.js";
21
25
  import { issueToken, maxTokenTtlSeconds } from "./better-auth/tokens.js";
22
- export { HttpProblem, ModelError, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, badRequest, betterAuthAuthenticator, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createUser, defineTool, deleteUser, fileConversationStore, forbidden, hasUsers, issueToken, jwtAuthenticator, listUsers, maxTokenTtlSeconds, normalizeRoles, notFound, parseRoles, readOnlyQueryTool, readWriteQueryTool, requestApproval, roleChoices, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, seesAssistant, seesRuntime, setUserBanned, setUserRole, streamChatCompletion, unauthorized, withCacheBreakpoints };
26
+ import { maxWidgetPropsBytes, widget } from "./assistant/widget.js";
27
+ export { HttpProblem, ModelError, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, badRequest, betterAuthAuthenticator, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createUser, defaultSignInPolicy, defineConfig, defineTool, deleteUser, fileConversationStore, forbidden, hasUsers, issueToken, jwtAuthenticator, listUsers, maxTokenTtlSeconds, maxWidgetPropsBytes, mergeConfig, normalizeRoles, notFound, parseRoles, readOnlyQueryTool, readOperatorSettings, readWriteQueryTool, requestApproval, roleChoices, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, seesAssistant, seesRuntime, setUserBanned, setUserRole, signInPolicyMethods, streamChatCompletion, unauthorized, widget, withCacheBreakpoints };
@@ -1,11 +1,11 @@
1
- import { checkedFilter, checkedSort } from "./filter-input.js";
2
- import { readScope, requestAccess, requireOperation } from "./request-access.js";
3
- import { readTransaction } from "./transactions.js";
4
1
  import { etagAlias } from "./sql-parts.js";
5
2
  import { etagOf } from "./etag.js";
6
3
  import { exposeRow, pickFields } from "./rows.js";
7
- import { deletedRows, listParams, pageSizeOf, parseParams, seekParams, selectedFields } from "./params.js";
4
+ import { checkedFilter, checkedSort } from "./filter-input.js";
5
+ import { readScope, requestAccess, requireOperation } from "./request-access.js";
8
6
  import { rowPermissions } from "./row-permissions.js";
7
+ import { readTransaction } from "./transactions.js";
8
+ import { deletedRows, listParams, pageSizeOf, parseParams, seekParams, selectedFields } from "./params.js";
9
9
  import { estimateCount, exactCount, listQuery, seekToPosition } from "@protobase/query";
10
10
  //#region src/list-service.ts
11
11
  /**
@@ -0,0 +1,28 @@
1
+ import { AdminAuth } from "./better-auth/create-auth.js";
2
+ import { Authenticator } from "./types.js";
3
+ import { AdminOptions } from "./admin-options.js";
4
+ import { Db } from "@protobase/query";
5
+ //#region src/project-config.d.ts
6
+ /** What `protobase.config.ts` may default-export. Everything is optional. */
7
+ export type ProjectConfig = {
8
+ /** Names the config in errors, such as two configs defining the same tool; an extension should set it. */
9
+ name?: string;
10
+ /** Configs this one extends: merged in order before it (see `mergeConfig`). */
11
+ extends?: ProjectConfig[];
12
+ /** The config module, e.g. `import * as config from './config'`; default: ./config/index.ts and ./config/*\/ui.ts. */
13
+ config?: Record<string, unknown>;
14
+ db?: Db;
15
+ authenticate?: Authenticator;
16
+ /** A Better Auth instance; createAdmin mounts its routes under /api/auth. */
17
+ auth?: AdminAuth;
18
+ options?: AdminOptions;
19
+ };
20
+ /** Typed identity, for `export default defineConfig({ extends: [...], config, auth, ... })` in `protobase.config.ts`. */
21
+ export declare const defineConfig: (config: ProjectConfig) => ProjectConfig;
22
+ /**
23
+ * One config from several, each after the ones it `extends`, the last being the app's own. Named things are all kept:
24
+ * resources, views and pages of the config modules, assistant tools; one name defined by two configs is an error naming
25
+ * both. Write hooks run in the same order. Every other value is the last config's that sets it, so the app's own wins.
26
+ */
27
+ export declare const mergeConfig: (...configs: ProjectConfig[]) => ProjectConfig;
28
+ //#endregion
@@ -0,0 +1,111 @@
1
+ import { isPageDefinition } from "@protobase/layout";
2
+ //#region src/project-config.ts
3
+ /** Typed identity, for `export default defineConfig({ extends: [...], config, auth, ... })` in `protobase.config.ts`. */
4
+ const defineConfig = (config) => config;
5
+ const lastSet = (objects) => Object.fromEntries(objects.flatMap((object) => Object.entries(object)).filter(([, value]) => value !== void 0));
6
+ const flatten = (config, label) => [...(config.extends ?? []).flatMap((parent, index) => flatten(parent, parent.name ?? `${label} extends[${index}]`)), {
7
+ config,
8
+ label
9
+ }];
10
+ const checkUnique = (kind, entries) => {
11
+ const seen = /* @__PURE__ */ new Map();
12
+ for (const { name, value, label } of entries) {
13
+ const first = seen.get(name);
14
+ if (first && first.value !== value) throw new Error(`${kind} "${name}" is defined twice, ${first.label === label ? `in ${label}` : `by ${first.label} and by ${label}`}; rename one of them`);
15
+ if (!first) seen.set(name, {
16
+ value,
17
+ label
18
+ });
19
+ }
20
+ };
21
+ const mergedModule = (named) => {
22
+ const withModule = named.filter(({ config }) => config.config);
23
+ if (withModule.length === 0) return void 0;
24
+ const exports = withModule.flatMap(({ config, label }) => Object.entries(config.config).map(([key, value]) => ({
25
+ key,
26
+ value,
27
+ label
28
+ })));
29
+ const values = exports.map((entry) => ({
30
+ ...entry,
31
+ value: entry.value
32
+ }));
33
+ checkUnique("The resource", values.filter(({ value }) => typeof value?.recordSchema === "function" && typeof value.toModel === "function").map(({ value, label }) => ({
34
+ name: String(value.toModel().name),
35
+ value,
36
+ label
37
+ })));
38
+ checkUnique("The page", exports.filter(({ value }) => isPageDefinition(value)).map(({ value, label }) => ({
39
+ name: value.name,
40
+ value,
41
+ label
42
+ })));
43
+ const menus = values.filter(({ value }) => typeof value?.toUserMenuModel === "function");
44
+ if (new Set(menus.map(({ value }) => value)).size > 1) throw new Error(`Only one config may export a user menu; ${menus.map(({ label }) => label).join(" and ")} each do`);
45
+ const module = {};
46
+ const own = withModule.at(-1).label;
47
+ for (const { key, value, label } of exports) {
48
+ if (Object.values(module).includes(value)) continue;
49
+ module[label === own ? key : `${label}:${key}`] = value;
50
+ }
51
+ return module;
52
+ };
53
+ const mergedAssistant = (named) => {
54
+ const options = named.map(({ config, label }) => ({
55
+ assistant: config.options?.assistant,
56
+ label
57
+ })).filter(({ assistant }) => assistant !== void 0);
58
+ if (options.length === 0) return void 0;
59
+ if (options.at(-1).assistant === false) return false;
60
+ const after = options.slice(options.findLastIndex(({ assistant }) => assistant === false) + 1);
61
+ const tools = after.flatMap(({ assistant, label }) => (assistant.tools ?? []).map((tool) => ({
62
+ tool,
63
+ label
64
+ })));
65
+ checkUnique("The assistant tool", tools.map(({ tool, label }) => ({
66
+ name: tool.name,
67
+ value: tool,
68
+ label
69
+ })));
70
+ return {
71
+ ...lastSet(after.map(({ assistant }) => assistant)),
72
+ ...tools.length > 0 && { tools: tools.map(({ tool }) => tool) }
73
+ };
74
+ };
75
+ const mergedOptions = (named) => {
76
+ const all = named.flatMap(({ config }) => config.options ? [config.options] : []);
77
+ if (all.length === 0) return void 0;
78
+ const writeHooks = all.flatMap((options) => options.writeHooks ?? []);
79
+ const assistant = mergedAssistant(named);
80
+ return {
81
+ ...lastSet(all),
82
+ ...writeHooks.length > 0 && { writeHooks },
83
+ ...assistant !== void 0 && { assistant }
84
+ };
85
+ };
86
+ /**
87
+ * One config from several, each after the ones it `extends`, the last being the app's own. Named things are all kept:
88
+ * resources, views and pages of the config modules, assistant tools; one name defined by two configs is an error naming
89
+ * both. Write hooks run in the same order. Every other value is the last config's that sets it, so the app's own wins.
90
+ */
91
+ const mergeConfig = (...configs) => {
92
+ const named = configs.flatMap((config, index) => flatten(config, config.name ?? `config ${index + 1}`));
93
+ const { db, authenticate, auth } = lastSet(named.map(({ config }) => ({
94
+ db: config.db,
95
+ authenticate: config.authenticate,
96
+ auth: config.auth
97
+ })));
98
+ const module = mergedModule(named);
99
+ const options = mergedOptions(named);
100
+ const name = configs.at(-1)?.name;
101
+ return {
102
+ ...name && { name },
103
+ ...module && { config: module },
104
+ ...db && { db },
105
+ ...authenticate && { authenticate },
106
+ ...auth && { auth },
107
+ ...options && { options }
108
+ };
109
+ };
110
+ //#endregion
111
+ export { defineConfig, mergeConfig };
@@ -1,10 +1,10 @@
1
1
  import { notFound } from "./problem.js";
2
- import { tenantScope } from "./tenant.js";
3
- import { recordDecision, requestAccess, requireOperation, targetFilter, visibleFields, writableFields } from "./request-access.js";
4
- import { readTransaction } from "./transactions.js";
5
2
  import { pickFields } from "./rows.js";
6
3
  import { fetchRecord } from "./records.js";
4
+ import { tenantScope } from "./tenant.js";
5
+ import { recordDecision, requestAccess, requireOperation, targetFilter, visibleFields, writableFields } from "./request-access.js";
7
6
  import { rowPermissions } from "./row-permissions.js";
7
+ import { readTransaction } from "./transactions.js";
8
8
  //#region src/read-service.ts
9
9
  /** `edit` for fields the caller may change on this record, `read` for the rest (sensitive ones included); hidden fields are not listed at all. */
10
10
  const fieldModes = (access, canUpdate) => Object.fromEntries(Object.keys(access.readable.fields).map((name) => [name, canUpdate && writableFields(access, "update").includes(name) ? "edit" : "read"]));
@@ -0,0 +1,2 @@
1
+ import "./resource-source.js";
2
+ import { ResourceModel } from "@protobase/schema";
@@ -1,11 +1,11 @@
1
1
  import { notFound } from "../problem.js";
2
+ import { keyFromPath } from "../key.js";
2
3
  import { pickFields } from "../rows.js";
4
+ import { readRecord } from "../read-service.js";
3
5
  import { runWrite } from "../write-pipeline.js";
4
- import { keyFromPath } from "../key.js";
5
6
  import { deletedRows, selectedFields } from "../params.js";
6
7
  import { returnPreference } from "./preferences.js";
7
8
  import { resolveTarget } from "./target.js";
8
- import { readRecord } from "../read-service.js";
9
9
  import { revealField } from "../reveal-service.js";
10
10
  import { Hono } from "hono";
11
11
  //#region src/routes/record.ts
@@ -1,7 +1,7 @@
1
- import { tenantScope } from "./tenant.js";
2
- import { recordDecision } from "./request-access.js";
3
1
  import { aliasedColumn, columnId, scopeConditions, tableId, whereClause } from "./sql-parts.js";
4
2
  import { fetchRecord } from "./records.js";
3
+ import { tenantScope } from "./tenant.js";
4
+ import { recordDecision } from "./request-access.js";
5
5
  import { compileFilter } from "@protobase/query";
6
6
  import { sql } from "kysely";
7
7
  //#region src/row-permissions.ts
@@ -0,0 +1,4 @@
1
+ import "./types.js";
2
+ import "./registry.js";
3
+ import { ResourceModel } from "@protobase/schema";
4
+ import { Db } from "@protobase/query";
package/dist/types.d.ts CHANGED
@@ -1,8 +1,6 @@
1
- import { AssistantOptions } from "./assistant/assistant-settings.js";
2
- import { RuntimeOptions } from "./runtime/runtime-settings.js";
3
1
  import { Hono } from "hono";
4
2
  import { PageModel } from "@protobase/layout";
5
- import { ResolveOptions, ResourceModel, ViewModel } from "@protobase/schema";
3
+ import { ResourceModel, ViewModel } from "@protobase/schema";
6
4
  import { Db } from "@protobase/query";
7
5
  //#region src/types.d.ts
8
6
  export type TenantValue = string | number;
@@ -64,35 +62,6 @@ export type AuditQueue = {
64
62
  publish(event: AuditEvent): Promise<void>;
65
63
  };
66
64
  export type ScanGuardMode = 'reject' | 'warn' | 'off';
67
- export type AdminOptions = {
68
- /** Default `/api/v1`. */
69
- basePath?: string;
70
- /** Applied to every request's transaction with SET LOCAL. Default 15000. */
71
- statementTimeoutMs?: number;
72
- /** List shapes that would sequentially scan a table of more than `seqScanRows` rows (default 10000). Default mode `reject`. */
73
- scanGuard?: {
74
- mode?: ScanGuardMode;
75
- seqScanRows?: number;
76
- };
77
- /** Runs inside the write transaction after each create, update and delete. */
78
- writeHooks?: PipelineHook[];
79
- /** The project's roles (from `defineRoles`): an operation without an access rule needs the matching capability. */
80
- roles?: ResolveOptions['roles'];
81
- /** What an operation without a rule does; overrides the capability check. Without `roles` the default is allow. */
82
- defaultAccess?: ResolveOptions['defaultAccess'];
83
- /** Whether list items carry their `permissions` (update, delete): true by default, or `{ except: [...] }` for resources that opt out. */
84
- rowPermissions?: boolean | {
85
- except: string[];
86
- };
87
- /** Receives an event for every reveal of a sensitive field. Default: printed to the console, nothing kept. */
88
- audit?: AuditQueue;
89
- /** Called with every error the API turns into a 500, so the host can report it. */
90
- onUnhandledError?: (error: unknown) => void;
91
- /** The assistant in the admin app's dock: a backend elsewhere or the built-in one (see `AssistantOptions`); `false` turns it off. */
92
- assistant?: AssistantOptions | false;
93
- /** The runtime updates endpoint behind the top bar's update button (see `RuntimeOptions`); `false` turns it off. */
94
- runtime?: RuntimeOptions | false;
95
- };
96
65
  export type ViewSource = {
97
66
  toModel(): ViewModel;
98
67
  };
@@ -1,10 +1,10 @@
1
1
  import { HttpProblem, badRequest } from "./problem.js";
2
+ import { deleteRecord, fetchRecord, insertRecord, restoreRecord } from "./records.js";
2
3
  import { tenantScope } from "./tenant.js";
3
4
  import { requestAccess, requireOperation, targetFilter, writableFields } from "./request-access.js";
4
5
  import { writeTransaction } from "./transactions.js";
5
- import { deleteRecord, fetchRecord, insertRecord, restoreRecord } from "./records.js";
6
- import { assertWritten, checkPrecondition, decide, denied, firstKeyField, missing, notifier, shown } from "./write-shared.js";
7
6
  import { validateBody } from "./validate-body.js";
7
+ import { assertWritten, checkPrecondition, decide, denied, firstKeyField, missing, notifier, shown } from "./write-shared.js";
8
8
  import { applyUpdate, planUpdate } from "./write-update.js";
9
9
  //#region src/write-pipeline.ts
10
10
  const withOwner = (access, values) => {
@@ -1,9 +1,9 @@
1
1
  import { forbidden, notFound, preconditionFailed, preconditionRequired } from "./problem.js";
2
- import { tenantScope } from "./tenant.js";
3
- import { andFilters, recordDecision } from "./request-access.js";
4
2
  import { ifMatchSatisfied } from "./etag.js";
5
3
  import { pickFields } from "./rows.js";
6
4
  import { fetchRecord } from "./records.js";
5
+ import { tenantScope } from "./tenant.js";
6
+ import { andFilters, recordDecision } from "./request-access.js";
7
7
  //#region src/write-shared.ts
8
8
  const missing = (entry) => notFound(`No ${entry.name} with this key`);
9
9
  const checkPrecondition = (current, ifMatch, required) => {
@@ -1,8 +1,8 @@
1
+ import { fetchRecord, updateRecord } from "./records.js";
1
2
  import { tenantScope } from "./tenant.js";
2
3
  import { targetFilter } from "./request-access.js";
3
- import { fetchRecord, updateRecord } from "./records.js";
4
- import { assertWritten, checkPrecondition, decide, denied, firstKeyField, missing, notifier } from "./write-shared.js";
5
4
  import { validateBody } from "./validate-body.js";
5
+ import { assertWritten, checkPrecondition, decide, denied, firstKeyField, missing, notifier } from "./write-shared.js";
6
6
  //#region src/write-update.ts
7
7
  /**
8
8
  * Everything an update needs before it writes: the row (locked, inside the caller's tenant and row filters), the ETag
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/server",
3
- "version": "0.3.5",
3
+ "version": "0.5.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": {
@@ -29,10 +29,12 @@
29
29
  "access": "public"
30
30
  },
31
31
  "dependencies": {
32
- "@protobase/layout": "0.3.5",
33
- "@protobase/query": "0.3.5",
34
- "@protobase/schema": "0.3.5",
32
+ "@better-auth/passkey": "^1.7.7",
33
+ "@protobase/layout": "0.5.0",
34
+ "@protobase/query": "0.5.0",
35
+ "@protobase/schema": "0.5.0",
35
36
  "@scalar/hono-api-reference": "^0.12.10",
37
+ "@simplewebauthn/server": "^13.3.1",
36
38
  "better-auth": "^1.7.7",
37
39
  "hono": "^4.13.13",
38
40
  "jose": "^6.2.12",