@protobase/server 0.40.8 → 0.40.10

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.
@@ -7,6 +7,7 @@ import { SourceOption } from "./sources/source-settings.js";
7
7
  import { CorsOrigins } from "./api-cors.js";
8
8
  import { AssistantOptions } from "./assistant/assistant-settings.js";
9
9
  import { LocaleOption } from "./locale/locale-option.js";
10
+ import { McpWrites } from "./mcp/mcp-writes.js";
10
11
  import { RuntimeOptions } from "./runtime/runtime-settings.js";
11
12
  import { ResolveOptions, RoleLimit } from "@protobase/schema";
12
13
  //#region src/admin-options.d.ts
@@ -57,12 +58,13 @@ export type AdminOptions = {
57
58
  * `run_read_only_query`, which reads the whole database as the app's database role, outside the access rules.
58
59
  * `writes: true` offers `create_record` and `update_record`, through the REST API's access rules, `validate` and write
59
60
  * hooks; the person approves each write in their client, and a client that cannot ask is refused.
61
+ * `writes: { resources: ['tasks'] }` offers them on the listed resources only, and refuses a write to any other.
60
62
  * `false`: the host serves its own MCP endpoint at `auth.mcp.path`, in front of this app. This app then answers 404
61
63
  * there and needs no `forAudience`, while the OAuth provider's discovery stays.
62
64
  */
63
65
  mcp?: {
64
66
  sql?: boolean;
65
- writes?: boolean;
67
+ writes?: McpWrites;
66
68
  } | false;
67
69
  /**
68
70
  * The websites whose browser code may call the API (`/api/*` but `/api/auth/*`) besides the app's own origins (see
@@ -1,9 +1,13 @@
1
1
  import { AssistantTool } from "./tools.js";
2
2
  //#region src/assistant/write-record-tools.d.ts
3
+ /** The resources the write tools may change: any the caller may write, or only those listed. */
4
+ export type WriteRecordToolsInput = {
5
+ resources?: string[];
6
+ };
3
7
  /** Creates a record of a resource once the user approves it, under the caller's access rules. */
4
- export declare const createRecordTool: () => AssistantTool;
8
+ export declare const createRecordTool: (input?: WriteRecordToolsInput) => AssistantTool;
5
9
  /** Updates one record by its key once the user approves it, under the caller's access rules. */
6
- export declare const updateRecordTool: () => AssistantTool;
7
- /** `create_record` and `update_record`. */
8
- export declare const writeRecordTools: () => AssistantTool[];
10
+ export declare const updateRecordTool: (input?: WriteRecordToolsInput) => AssistantTool;
11
+ /** `create_record` and `update_record`, on the resources `input` allows. */
12
+ export declare const writeRecordTools: (input?: WriteRecordToolsInput) => AssistantTool[];
9
13
  //#endregion
@@ -11,15 +11,24 @@ const dataParameter = {
11
11
  description: "Field names to values, as list_resources gives the fields."
12
12
  };
13
13
  const dataOf = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
14
+ const resourceParameterFor = ({ resources }) => resources ? {
15
+ ...resourceParameter,
16
+ description: `The resource's name: one of ${resources.join(", ")}.`
17
+ } : resourceParameter;
18
+ const onlyNote = ({ resources }) => resources ? ` It writes only ${resources.join(", ")}.` : "";
19
+ const outside = ({ resources }, resource) => resources && !resources.includes(resource) ? JSON.stringify({
20
+ error: "forbidden",
21
+ message: `Writes over MCP reach only ${resources.join(", ")}, not ${resource}; nothing was written.`
22
+ }) : void 0;
14
23
  const notApproved = (answer) => `The user did not approve this change (${answer}); nothing was written.`;
15
24
  /** Creates a record of a resource once the user approves it, under the caller's access rules. */
16
- const createRecordTool = () => defineTool({
25
+ const createRecordTool = (input = {}) => defineTool({
17
26
  name: "create_record",
18
- description: "Creates a record of a resource and returns it as JSON ({ record, etag }). The user approves each write in their client first; nothing is written without it.",
27
+ description: `Creates a record of a resource and returns it as JSON ({ record, etag }). The user approves each write in their client first; nothing is written without it.${onlyNote(input)}`,
19
28
  parameters: {
20
29
  type: "object",
21
30
  properties: {
22
- resource: resourceParameter,
31
+ resource: resourceParameterFor(input),
23
32
  data: dataParameter
24
33
  },
25
34
  required: ["resource", "data"]
@@ -29,6 +38,8 @@ const createRecordTool = () => defineTool({
29
38
  const resource = resourceOf(args);
30
39
  const data = dataOf(args.data);
31
40
  if (!resource || !data) return "Pass the resource and data as { \"resource\": \"<name>\", \"data\": { \"<field>\": <value> } }.";
41
+ const refusal = outside(input, resource);
42
+ if (refusal) return refusal;
32
43
  const answer = await context.approve({
33
44
  title: `Create a record in ${resource}?`,
34
45
  code: JSON.stringify(data, null, 2),
@@ -39,13 +50,13 @@ const createRecordTool = () => defineTool({
39
50
  }
40
51
  });
41
52
  /** Updates one record by its key once the user approves it, under the caller's access rules. */
42
- const updateRecordTool = () => defineTool({
53
+ const updateRecordTool = (input = {}) => defineTool({
43
54
  name: "update_record",
44
- description: "Updates fields of one record of a resource by its key and returns it as JSON ({ record, etag }). Pass the etag get_record gave to fail when someone changed the record since. The user approves each write in their client first; nothing is written without it.",
55
+ description: `Updates fields of one record of a resource by its key and returns it as JSON ({ record, etag }). Pass the etag get_record gave to fail when someone changed the record since. The user approves each write in their client first; nothing is written without it.${onlyNote(input)}`,
45
56
  parameters: {
46
57
  type: "object",
47
58
  properties: {
48
- resource: resourceParameter,
59
+ resource: resourceParameterFor(input),
49
60
  key: keyParameter,
50
61
  data: {
51
62
  ...dataParameter,
@@ -68,6 +79,8 @@ const updateRecordTool = () => defineTool({
68
79
  const key = keyOf(args.key);
69
80
  const data = dataOf(args.data);
70
81
  if (!resource || key === void 0 || !data) return "Pass the resource, key and data as { \"resource\": \"<name>\", \"key\": \"<key>\", \"data\": { \"<field>\": <value> } }.";
82
+ const refusal = outside(input, resource);
83
+ if (refusal) return refusal;
71
84
  const shownKey = Array.isArray(key) ? key.join(",") : String(key);
72
85
  const answer = await context.approve({
73
86
  title: `Update ${resource} ${shownKey}?`,
@@ -79,7 +92,7 @@ const updateRecordTool = () => defineTool({
79
92
  return context.records.update(resource, key, data, { etag }).then((record) => JSON.stringify(record), refused);
80
93
  }
81
94
  });
82
- /** `create_record` and `update_record`. */
83
- const writeRecordTools = () => [createRecordTool(), updateRecordTool()];
95
+ /** `create_record` and `update_record`, on the resources `input` allows. */
96
+ const writeRecordTools = (input = {}) => [createRecordTool(input), updateRecordTool(input)];
84
97
  //#endregion
85
98
  export { createRecordTool, updateRecordTool, writeRecordTools };
@@ -2,7 +2,7 @@ import { checkedUrl } from "../checked-url.js";
2
2
  import { forbidden } from "../problem.js";
3
3
  import { jwtAuthenticator } from "../auth/jwt.js";
4
4
  import { noGlobalRole } from "./organizations/role-definitions.js";
5
- import "./auth-base-path.js";
5
+ import { authBasePath } from "./auth-base-path.js";
6
6
  import { createRemoteJWKSet, decodeJwt, errors } from "jose";
7
7
  //#region src/better-auth/app-tokens.ts
8
8
  const claimedIssuer = (request) => {
@@ -19,7 +19,7 @@ const claimedIssuer = (request) => {
19
19
  const trustedAuthenticator = (app, issuer) => {
20
20
  const forms = [issuer, `${issuer}/`];
21
21
  const verify = jwtAuthenticator({
22
- keys: app.keys ?? createRemoteJWKSet(new URL(`/api/auth/jwks`, issuer)),
22
+ keys: app.keys ?? createRemoteJWKSet(new URL(`${authBasePath}/jwks`, issuer)),
23
23
  issuer: forms,
24
24
  audience: forms,
25
25
  rolesClaim: "role",
@@ -0,0 +1 @@
1
+ import "@protobase/schema/internal";
@@ -1,5 +1,2 @@
1
- //#region src/better-auth/auth-base-path.ts
2
- /** Where Better Auth's endpoints are served, below the app's origin. */
3
- const authBasePath = "/api/auth";
4
- //#endregion
1
+ import { authBasePath } from "@protobase/schema/internal";
5
2
  export { authBasePath };
@@ -1,6 +1,7 @@
1
1
  import { StaffSignIn } from "./staff-log.js";
2
2
  import { RoleDefinitions, RolesInput } from "./organizations/role-definitions.js";
3
3
  import { ConnectedApp } from "./mcp/connected-apps.js";
4
+ import "./auth-base-path.js";
4
5
  import { Mailer } from "../mail/smtp-mailer.js";
5
6
  import { OperatorProvider } from "./operator-provider.js";
6
7
  import { OrganizationRole } from "./organizations/store.js";
@@ -8649,7 +8650,7 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
8649
8650
  };
8650
8651
  basePath: string;
8651
8652
  secret: string;
8652
- trustedOrigins: string[];
8653
+ trustedOrigins: string[] | ((request?: Request) => string[]);
8653
8654
  emailAndPassword: {
8654
8655
  onPasswordReset?: ((data: {
8655
8656
  user: import("better-auth").User;
@@ -8673,6 +8674,11 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
8673
8674
  maxPasswordLength: number;
8674
8675
  };
8675
8676
  }> & {
8677
+ /**
8678
+ * Answers a request from the app's iOS or Android build: Better Auth also trusts the Capacitor origin the request
8679
+ * names. Only `createAdmin` with a `mobile` section uses it; `handler` and `api` never trust a Capacitor origin.
8680
+ */
8681
+ handleMobile: (request: Request) => Promise<Response>;
8676
8682
  /** Issuer and audience of the tokens: `baseURL`, on every origin. */
8677
8683
  issuer: string;
8678
8684
  /** Every origin the app is served on, `baseURL`'s first. */
@@ -6,6 +6,7 @@ import { roleDefinitions } from "./organizations/role-definitions.js";
6
6
  import { signInPolicyPlugin } from "./policy-plugin.js";
7
7
  import { goTrueSessionsPlugin } from "../gotrue/sessions-plugin.js";
8
8
  import { authBasePath } from "./auth-base-path.js";
9
+ import { mobileRequests } from "./mobile-requests.js";
9
10
  import { passwordLength } from "./new-account-password.js";
10
11
  import { platformMailer } from "../mail/platform-mailer.js";
11
12
  import { readOperatorSettings, resolveOperator } from "./operator-provider.js";
@@ -60,7 +61,7 @@ const organizationPaths = ["/organization/set-active", "/organization/accept-inv
60
61
  * The options `createAuth` gives Better Auth, with the roles, default role, mailer, operator provider and platform
61
62
  * sign-in provider already resolved.
62
63
  */
63
- const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, definitions, defaultRole, appName = defaultAppName, mailer, signUpRole, operator, platformSignIn, saml = [], organizations, mcp, context = noContext, signInPolicy = defaultSignInPolicy }) => {
64
+ const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, definitions, defaultRole, appName = defaultAppName, mailer, signUpRole, operator, platformSignIn, saml = [], organizations, mcp, context = noContext, signInPolicy = defaultSignInPolicy, mobile }) => {
64
65
  const secure = options.baseURL.startsWith("https://");
65
66
  const signIn = platformSignInFor(platformSignIn, Object.keys(options.socialProviders ?? {}));
66
67
  const hosts = passkeyHosts([options.baseURL, ...origins]);
@@ -104,6 +105,11 @@ const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, defini
104
105
  },
105
106
  disableSettingJwtHeader: true
106
107
  };
108
+ const trusted = [
109
+ options.baseURL,
110
+ ...origins,
111
+ ...options.trustedOrigins ?? []
112
+ ];
107
113
  const userFields = {
108
114
  ...userFieldsNamed(emailConfirmedField, users),
109
115
  ...organizations && userFieldsNamed(lastOrganizationField, users),
@@ -115,11 +121,7 @@ const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, defini
115
121
  baseURL: multiHostBaseURL(options.baseURL, origins),
116
122
  basePath: authBasePath,
117
123
  secret: options.secret,
118
- trustedOrigins: [
119
- options.baseURL,
120
- ...origins,
121
- ...options.trustedOrigins ?? []
122
- ],
124
+ trustedOrigins: mobile ? (request) => [...trusted, ...mobile.trusted(request)] : trusted,
123
125
  emailAndPassword: {
124
126
  enabled: true,
125
127
  disableSignUp: true,
@@ -310,7 +312,9 @@ const createAuth = (options) => {
310
312
  const mcp = options.mcp && resolveMcp(options.baseURL, options.mcp);
311
313
  let instance;
312
314
  const context = () => instance?.$context ?? Promise.reject(/* @__PURE__ */ new Error("The auth instance is not made yet"));
315
+ const mobile = mobileRequests();
313
316
  const auth = betterAuth(betterAuthOptions(options, {
317
+ mobile,
314
318
  origins,
315
319
  passkeyDomain,
316
320
  roles,
@@ -329,6 +333,11 @@ const createAuth = (options) => {
329
333
  }));
330
334
  instance = auth;
331
335
  return Object.assign(auth, {
336
+ /**
337
+ * Answers a request from the app's iOS or Android build: Better Auth also trusts the Capacitor origin the request
338
+ * names. Only `createAdmin` with a `mobile` section uses it; `handler` and `api` never trust a Capacitor origin.
339
+ */
340
+ handleMobile: (request) => auth.handler(mobile.grant(request)),
332
341
  /** Issuer and audience of the tokens: `baseURL`, on every origin. */
333
342
  issuer: options.baseURL,
334
343
  /** Every origin the app is served on, `baseURL`'s first. */
@@ -0,0 +1,23 @@
1
+ import { capacitorOrigins } from "../mobile-config.js";
2
+ //#region src/better-auth/mobile-requests.ts
3
+ /**
4
+ * Which requests come from the app's iOS or Android build: `grant` marks a request, and `trusted` gives the Capacitor
5
+ * origin it names as an origin Better Auth trusts, for that very request only. Any other request, or an Origin that is
6
+ * not exactly a Capacitor origin, gets nothing.
7
+ */
8
+ const mobileRequests = () => {
9
+ const granted = /* @__PURE__ */ new WeakSet();
10
+ return {
11
+ grant: (request) => {
12
+ granted.add(request);
13
+ return request;
14
+ },
15
+ trusted: (request) => {
16
+ if (!request || !granted.has(request)) return [];
17
+ const origin = request.headers.get("origin");
18
+ return origin !== null && capacitorOrigins.includes(origin) ? [origin] : [];
19
+ }
20
+ };
21
+ };
22
+ //#endregion
23
+ export { mobileRequests };
@@ -46,7 +46,10 @@ export type CreateAdminInput = {
46
46
  subscriptions?: SubscriptionDefinition[];
47
47
  /** Webhook subscriptions made in the admin app, from `extends: [webhooks()]` (see `WebhooksOptions`). */
48
48
  webhooks?: WebhooksOptions;
49
- /** The app's iOS and Android build (see `MobileConfig`): the API answers its Capacitor origins through CORS. */
49
+ /**
50
+ * The app's iOS and Android build (see `MobileConfig`): the API answers its Capacitor origins through CORS, and sign-in
51
+ * at `/api/auth` trusts the Capacitor origin a request names, without CORS, as the app calls it through native HTTP.
52
+ */
50
53
  mobile?: MobileConfig;
51
54
  /** The app's colours, which its web manifest carries (see `AppIdentity`). */
52
55
  app?: AppIdentity;
@@ -23,6 +23,7 @@ import { realtimePath, restPath, storagePath, supabaseFunctionsPath, supabasePat
23
23
  import { goTruePath } from "./gotrue/keys.js";
24
24
  import { authBasePath } from "./better-auth/auth-base-path.js";
25
25
  import { mcpDiscovery } from "./better-auth/mcp/discovery-routes.js";
26
+ import { checkMcpWrites } from "./mcp/mcp-writes.js";
26
27
  import { mcpAuthenticator } from "./mcp/mcp-authenticator.js";
27
28
  import { mcpToolContext } from "./mcp/mcp-context.js";
28
29
  import { mcpHandler } from "./mcp/mcp-handler.js";
@@ -127,6 +128,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
127
128
  checkRoleLimits(registry, options.roles?.limits ?? []);
128
129
  checkOwnerFields(registry, authenticate.userIdType, auth);
129
130
  const mcpAuthenticate = mcpAuthenticator(authenticate, auth?.mcp, options.mcp);
131
+ if (options.mcp) checkMcpWrites(registry, options.mcp.writes);
130
132
  checkPositionAfter(registry);
131
133
  checkParents(registry);
132
134
  checkPages(pageModels, {
@@ -275,7 +277,8 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
275
277
  db
276
278
  }));
277
279
  if (auth.mcp) mcpDiscovery(app, auth);
278
- app.on(["GET", "POST"], `${authBasePath}/*`, async (c) => withoutSessionTokens(c.req.raw, await auth.handler(c.req.raw)));
280
+ const answer = mobile ? auth.handleMobile : auth.handler;
281
+ app.on(["GET", "POST"], `${authBasePath}/*`, async (c) => withoutSessionTokens(c.req.raw, await answer(c.req.raw)));
279
282
  app.route(goTruePath, goTrueRoutes(auth));
280
283
  if (auth.origins.length > 1) app.get(webauthnPath, (c) => c.json({ origins: auth.origins }));
281
284
  }
@@ -299,7 +302,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
299
302
  const tools = mcpTools({
300
303
  db,
301
304
  sql: options.mcp !== false && options.mcp?.sql === true,
302
- writes: options.mcp !== false && options.mcp?.writes === true
305
+ writes: options.mcp !== false && options.mcp?.writes || false
303
306
  });
304
307
  const server = (session) => mcpServer({
305
308
  info: {
package/dist/index.d.ts CHANGED
@@ -43,6 +43,7 @@ import { Conversation, ConversationsOptions, conversationKey, createConversation
43
43
  import { ApprovalAnswer, ApprovalRequest, requestApproval } from "./assistant/approval.js";
44
44
  import { AssistantTool, ReadableResources, ToolAnnotations, ToolContext, defineTool } from "./assistant/tools.js";
45
45
  import { AssistantOptions, AssistantSettings, assistantRoles, assistantSettings, assistantVariables, seesAssistant } from "./assistant/assistant-settings.js";
46
+ import { McpWrites } from "./mcp/mcp-writes.js";
46
47
  import { RuntimeOptions, runtimeUrl, runtimeVariable, seesRuntime } from "./runtime/runtime-settings.js";
47
48
  import { AdminOptions } from "./admin-options.js";
48
49
  import { CreateAdminInput, createAdmin } from "./create-admin.js";
@@ -89,7 +90,7 @@ import { WidgetOptions, maxWidgetPropsBytes, widget } from "./assistant/widget.j
89
90
  import { QueryOptions, QueryResult, runReadOnlyQuery, runReadWriteQuery } from "./assistant/run-query.js";
90
91
  import { readOnlyQueryTool, readWriteQueryTool } from "./assistant/query-tools.js";
91
92
  import { getRecordTool, listRecordsTool, listResourcesTool, readRecordTools } from "./assistant/record-tools.js";
92
- import { createRecordTool, updateRecordTool, writeRecordTools } from "./assistant/write-record-tools.js";
93
+ import { WriteRecordToolsInput, createRecordTool, updateRecordTool, writeRecordTools } from "./assistant/write-record-tools.js";
93
94
  import { McpServerInput, mcpServer } from "./mcp/mcp-server.js";
94
95
  import { McpToolsInput, mcpTools } from "./mcp/mcp-tools.js";
95
96
  import { McpGateInput, mcpGate, sessionOf } from "./mcp/mcp-gate.js";
@@ -124,4 +125,4 @@ import { isSchedule } from "./jobs/schedule.js";
124
125
  import { DrainResult, JobsRuntime, StartJobsOptions, startJobs } from "./jobs/runtime.js";
125
126
  import { jobsDrainApp } from "./jobs/drain-route.js";
126
127
  import { checkJobsSchema, jobsSchema, jobsSchemaMigration } from "./jobs/schema.js";
127
- 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, capSocketMessages, changesMigration, checkAi, checkAuthSchema, checkChanges, checkComments, checkComputed, checkJobsSchema, checkQueryRunner, checkSourceNames, checkViewQueries, checkWebhooksSchema, coalesce, commentsMigration, computedMigration, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createEncryptionTools, createFileCleanup, createOrganization, createRecordTool, createUser, defaultAppName, defaultSignInPolicy, defineConfig, defineEmail, defineFunction, defineJob, defineTool, deleteUser, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, getRecordTool, goTruePath, handleDroppedConnections, hasUsers, honorSocketMessageCaps, 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, socketMessageCapOf, sourceOptions, sourcesFor, sourcesVariable, startErrorReporting, startJobs, storageCleanupSchedule, storageProvider, storeFile, storeUsage, streamChatCompletion, subscription, supabasePath, thumbHash, unauthorized, updateRecordTool, variantPath, verifySignature, verifyStandardWebhook, waitUntil, webauthnPath, webhooks, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps, writeRecordTools };
128
+ 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 McpWrites, 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, type WriteRecordToolsInput, addAppRoleToOwners, addMember, apiKeyCaller, apiKeySettings, apiKeyVariables, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaMigration, authStoreTables, backgroundSettled, badRequest, beforeSignInPluginsVariable, betterAuthAuthenticator, betterAuthTokens, capSocketMessages, changesMigration, checkAi, checkAuthSchema, checkChanges, checkComments, checkComputed, checkJobsSchema, checkQueryRunner, checkSourceNames, checkViewQueries, checkWebhooksSchema, coalesce, commentsMigration, computedMigration, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createEncryptionTools, createFileCleanup, createOrganization, createRecordTool, createUser, defaultAppName, defaultSignInPolicy, defineConfig, defineEmail, defineFunction, defineJob, defineTool, deleteUser, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, getRecordTool, goTruePath, handleDroppedConnections, hasUsers, honorSocketMessageCaps, 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, socketMessageCapOf, sourceOptions, sourcesFor, sourcesVariable, startErrorReporting, startJobs, storageCleanupSchedule, storageProvider, storeFile, storeUsage, streamChatCompletion, subscription, supabasePath, thumbHash, unauthorized, updateRecordTool, variantPath, verifySignature, verifyStandardWebhook, waitUntil, webauthnPath, webhooks, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps, writeRecordTools };
@@ -1,14 +1,15 @@
1
+ import { isMcpWrites } from "./mcp-writes.js";
1
2
  //#region src/mcp/mcp-authenticator.ts
2
3
  const hasAudiences = (authenticate) => typeof authenticate.forAudience === "function";
3
- const isMcpOptions = (options) => options === void 0 || options === false || typeof options === "object" && options !== null && !Array.isArray(options) && Object.entries(options).every(([name, value]) => (name === "sql" || name === "writes") && (value === void 0 || typeof value === "boolean"));
4
+ const isMcpOptions = (options) => options === void 0 || options === false || typeof options === "object" && options !== null && !Array.isArray(options) && Object.entries(options).every(([name, value]) => value === void 0 || name === "sql" && typeof value === "boolean" || name === "writes" && isMcpWrites(value));
4
5
  /**
5
6
  * The authenticator of the MCP endpoint's tokens, when MCP is on (`createAuth({ mcp })`); none when the host serves its
6
7
  * own (`options.mcp: false`). Stops the server at startup when `options.mcp` is neither `false` nor
7
- * `{ sql?: boolean; writes?: boolean }`, when it is set without MCP, or when `authenticate` cannot verify tokens for
8
+ * `{ sql?: boolean; writes?: boolean | { resources: string[] } }`, when it is set without MCP, or when `authenticate` cannot verify tokens for
8
9
  * another audience.
9
10
  */
10
11
  const mcpAuthenticator = (authenticate, mcp, options) => {
11
- if (!isMcpOptions(options)) throw new Error("options.mcp takes false or { sql?: boolean, writes?: boolean }");
12
+ if (!isMcpOptions(options)) throw new Error("options.mcp takes false or { sql?: boolean, writes?: boolean | { resources: [<resource>, ...] } }");
12
13
  if (options === false) return void 0;
13
14
  if (options && !mcp) throw new Error("options.mcp needs createAuth({ mcp }) passed as auth");
14
15
  if (!mcp) return void 0;
@@ -1,13 +1,17 @@
1
1
  import { Session } from "../types.js";
2
2
  import { AssistantTool } from "../assistant/tools.js";
3
+ import { McpWrites } from "./mcp-writes.js";
3
4
  import { Db } from "@protobase/query";
4
5
  //#region src/mcp/mcp-tools.d.ts
5
6
  export type McpToolsInput = {
6
7
  db: Db;
7
8
  /** Offers `run_read_only_query` to callers who see the assistant (`admin` or `ai`); it reads outside the access rules. */
8
9
  sql: boolean;
9
- /** Offers `create_record` and `update_record`; each write still needs the caller's role and their approval in the client. */
10
- writes: boolean;
10
+ /**
11
+ * Offers `create_record` and `update_record`, on the listed resources only when given `{ resources }`; each write still
12
+ * needs the caller's role and their approval in the client.
13
+ */
14
+ writes: McpWrites;
11
15
  };
12
16
  /**
13
17
  * The default MCP tools for a caller: the record tools, the write tools when `writes` is on, and read-only SQL when `sql`
@@ -9,7 +9,7 @@ import { writeRecordTools } from "../assistant/write-record-tools.js";
9
9
  */
10
10
  const mcpTools = ({ db, sql, writes }) => (session) => [
11
11
  ...readRecordTools(),
12
- ...writes ? writeRecordTools() : [],
12
+ ...writes === false ? [] : writeRecordTools(writes === true ? {} : { resources: writes.resources }),
13
13
  ...sql && seesAssistant(session.user.roles) ? [readOnlyQueryTool(db)] : []
14
14
  ];
15
15
  //#endregion
@@ -0,0 +1,7 @@
1
+ import "../registry.js";
2
+ //#region src/mcp/mcp-writes.d.ts
3
+ /** Which resources the MCP write tools may change: none, every one the caller may write, or the ones named. */
4
+ export type McpWrites = boolean | {
5
+ resources: string[];
6
+ };
7
+ //#endregion
@@ -0,0 +1,16 @@
1
+ //#region src/mcp/mcp-writes.ts
2
+ const isResourceList = (value) => Array.isArray(value) && value.length > 0 && value.every((name) => typeof name === "string" && name !== "");
3
+ const isMcpWrites = (value) => {
4
+ if (typeof value === "boolean") return true;
5
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
6
+ const entries = Object.entries(value);
7
+ return entries.length === 1 && entries[0][0] === "resources" && isResourceList(entries[0][1]);
8
+ };
9
+ /** Stops startup when `options.mcp.writes` names a resource the app does not have. */
10
+ const checkMcpWrites = (registry, writes) => {
11
+ if (typeof writes !== "object") return;
12
+ const unknown = writes.resources.filter((name) => !registry.find(name));
13
+ if (unknown.length > 0) throw new Error(`options.mcp.writes names resources the app does not have: ${unknown.join(", ")}`);
14
+ };
15
+ //#endregion
16
+ export { checkMcpWrites, isMcpWrites };
@@ -1,8 +1,6 @@
1
+ import { capacitorOrigin } from "@protobase/schema/internal";
1
2
  //#region src/mobile-config.ts
2
- /**
3
- * The origins Capacitor's WebView serves the app's files on: `capacitor://localhost` on iOS, `https://localhost` on
4
- * Android. They hold because `protobase mobile` never sets Capacitor's `server.hostname` or schemes.
5
- */
6
- const capacitorOrigins = ["capacitor://localhost", "https://localhost"];
3
+ /** The origins Capacitor's WebView serves the app's files on: `capacitor://localhost` on iOS, `https://localhost` on Android. */
4
+ const capacitorOrigins = Object.values(capacitorOrigin);
7
5
  //#endregion
8
6
  export { capacitorOrigins };
package/dist/package.js CHANGED
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "0.40.8";
2
+ var version = "0.40.10";
3
3
  //#endregion
4
4
  export { version };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/server",
3
- "version": "0.40.8",
3
+ "version": "0.40.10",
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.8",
40
- "@protobase/query": "0.40.8",
41
- "@protobase/schema": "0.40.8",
39
+ "@protobase/layout": "0.40.10",
40
+ "@protobase/query": "0.40.10",
41
+ "@protobase/schema": "0.40.10",
42
42
  "@react-email/render": "^2.1.0",
43
43
  "@scalar/hono-api-reference": "^0.12.10",
44
44
  "@sentry/core": "^11.7.0",