@protobase/server 0.12.7 → 0.12.9

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.
@@ -5,7 +5,7 @@ import { SourceOption } from "./sources/source-settings.js";
5
5
  import { CorsOrigins } from "./api-cors.js";
6
6
  import { AssistantOptions } from "./assistant/assistant-settings.js";
7
7
  import { RuntimeOptions } from "./runtime/runtime-settings.js";
8
- import { ResolveOptions } from "@protobase/schema";
8
+ import { ResolveOptions, RoleLimit } from "@protobase/schema";
9
9
  //#region src/admin-options.d.ts
10
10
  export type AdminOptions = {
11
11
  /** Default `/api/v1`. */
@@ -19,8 +19,14 @@ export type AdminOptions = {
19
19
  };
20
20
  /** Runs inside the write transaction after each create, update and delete. */
21
21
  writeHooks?: PipelineHook[];
22
- /** The project's roles (from `defineRoles`): an operation without an access rule needs the matching capability. */
23
- roles?: ResolveOptions['roles'];
22
+ /**
23
+ * The project's roles (from `defineRoles`): an operation without an access rule needs the matching capability. Their
24
+ * limits are checked against the resources at startup; an `anonymous` role opens the resources API to requests
25
+ * without a token.
26
+ */
27
+ roles?: ResolveOptions['roles'] & {
28
+ limits?: readonly RoleLimit[];
29
+ };
24
30
  /** What an operation without a rule does; overrides the capability check. Without `roles` the default is allow. */
25
31
  defaultAccess?: ResolveOptions['defaultAccess'];
26
32
  /** Whether list items carry their `permissions` (update, delete): true by default, or `{ except: [...] }` for resources that opt out. */
@@ -0,0 +1,27 @@
1
+ import { MissingCredentials } from "./problem.js";
2
+ import { anonymousRole } from "@protobase/schema";
3
+ //#region src/anonymous.ts
4
+ const anonymousSession = () => ({
5
+ user: {
6
+ id: "",
7
+ roles: [anonymousRole]
8
+ },
9
+ anonymous: true
10
+ });
11
+ /**
12
+ * With an `anonymous` role in `roles`, a request that carries no credential at all (`MissingCredentials`) is served as
13
+ * a caller holding that role alone; an invalid or expired one is still a 401. Without the role, `authenticate` itself.
14
+ */
15
+ const withAnonymous = (authenticate, roles) => {
16
+ if (!Object.hasOwn(roles?.definitions ?? {}, anonymousRole)) return authenticate;
17
+ return async (request) => {
18
+ try {
19
+ return await authenticate(request);
20
+ } catch (error) {
21
+ if (error instanceof MissingCredentials) return anonymousSession();
22
+ throw error;
23
+ }
24
+ };
25
+ };
26
+ //#endregion
27
+ export { withAnonymous };
@@ -20,6 +20,6 @@ export type JwtOptions = {
20
20
  /** A fixed tenant (or resolver) used instead of any claim, for single-tenant deployments. */
21
21
  tenant?: TenantValue | (() => Promise<TenantValue | undefined>);
22
22
  };
23
- /** Verifies `Authorization: Bearer <jwt>` against a JWKS; `sub` is the user id. */
23
+ /** Verifies `Authorization: Bearer <jwt>` against a JWKS; `sub` is the user id. Without the header, `MissingCredentials`. */
24
24
  export declare const jwtAuthenticator: (options: JwtOptions) => Authenticator;
25
25
  //#endregion
package/dist/auth/jwt.js CHANGED
@@ -1,4 +1,4 @@
1
- import { unauthorized } from "../problem.js";
1
+ import { MissingCredentials, unauthorized } from "../problem.js";
2
2
  import { createRemoteJWKSet, errors, jwtVerify } from "jose";
3
3
  //#region src/auth/jwt.ts
4
4
  const asymmetric = [
@@ -17,13 +17,15 @@ const rolesOf = (claim) => {
17
17
  if (typeof claim === "string") return claim.split(/[\s,]+/).filter(Boolean);
18
18
  return Array.isArray(claim) ? claim.filter((role) => typeof role === "string") : [];
19
19
  };
20
- /** Verifies `Authorization: Bearer <jwt>` against a JWKS; `sub` is the user id. */
20
+ /** Verifies `Authorization: Bearer <jwt>` against a JWKS; `sub` is the user id. Without the header, `MissingCredentials`. */
21
21
  const jwtAuthenticator = (options) => {
22
22
  const { rolesClaim = "roles", tenantClaim = "tenant" } = options;
23
23
  const keys = options.keys ?? (options.jwksUrl ? createRemoteJWKSet(new URL(options.jwksUrl)) : void 0);
24
24
  if (!keys) throw new Error("jwtAuthenticator needs jwksUrl or keys");
25
25
  return async (request) => {
26
- const [scheme, token] = (request.headers.get("authorization") ?? "").split(" ");
26
+ const header = request.headers.get("authorization");
27
+ if (header === null) throw new MissingCredentials("Send a bearer token in the Authorization header");
28
+ const [scheme, token] = header.split(" ");
27
29
  if (scheme?.toLowerCase() !== "bearer" || !token) throw unauthorized("Send a bearer token in the Authorization header");
28
30
  const verified = await jwtVerify(token, keys, {
29
31
  algorithms: options.algorithms ?? asymmetric,
@@ -0,0 +1,29 @@
1
+ import { anonymousRole, checkFilter } from "@protobase/schema";
2
+ //#region src/check-roles.ts
3
+ /**
4
+ * Stops startup on a role's limit that does not fit its resource: fields or a filter for a name that is not a resource,
5
+ * a field it does not have, a filter it cannot check, or an anonymous capability on a tenant-scoped resource, since an
6
+ * anonymous caller works in no organization.
7
+ */
8
+ const checkRoleLimits = (registry, limits) => {
9
+ for (const { role, capability, fields, filter } of limits) {
10
+ const name = capability.slice(0, capability.indexOf("."));
11
+ const model = registry.find(name)?.model;
12
+ if (!model) throw new Error(`Role "${role}" limits "${capability}" but "${name}" is not a resource`);
13
+ const unknown = fields?.find((field) => !Object.hasOwn(model.fields, field));
14
+ if (unknown) throw new Error(`Role "${role}" lists the field "${unknown}" for "${capability}" but ${name} has no such field`);
15
+ if (role === anonymousRole && model.tenant) throw new Error(`The anonymous role reads ${name}, which is tenant-scoped, but an anonymous caller works in no organization`);
16
+ if (!filter) continue;
17
+ const trusted = {
18
+ ...model,
19
+ fields: Object.fromEntries(Object.entries(model.fields).map(([field, value]) => [field, {
20
+ ...value,
21
+ filterable: true
22
+ }]))
23
+ };
24
+ const checked = checkFilter(trusted, filter);
25
+ if (!checked.ok) throw new Error(`Role "${role}" limits "${capability}" to a filter that does not fit ${name}: ${checked.errors[0].message}`);
26
+ }
27
+ };
28
+ //#endregion
29
+ export { checkRoleLimits };
@@ -14,7 +14,9 @@ import { toApiFunction } from "./functions/define-function.js";
14
14
  import { createWebhooks } from "./webhooks/webhooks.js";
15
15
  import { webhookTable } from "./webhooks/table/webhook-table.js";
16
16
  import { subscriptionWriteCheck } from "./webhooks/table/validate-hook.js";
17
+ import { withAnonymous } from "./anonymous.js";
17
18
  import { checkBottomBars, checkShell } from "./check-shell.js";
19
+ import { checkRoleLimits } from "./check-roles.js";
18
20
  import { checkViews } from "./check-views.js";
19
21
  import { errorReportingSettings } from "./error-reporting/error-reporting-settings.js";
20
22
  import { createFilesRuntime } from "./files/runtime.js";
@@ -54,6 +56,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
54
56
  const bottomBarModels = bottomBars.map((bar) => bar.toBottomBarModel());
55
57
  checkBottomBars(registry, bottomBarModels, pageModels.map((entry) => entry.name), declaredRoles(auth, options.roles));
56
58
  checkViews(registry, viewModels, pageModels.map((entry) => entry.name), fieldTypes);
59
+ checkRoleLimits(registry, options.roles?.limits ?? []);
57
60
  checkPages(pageModels, {
58
61
  models: Object.fromEntries(registry.entries.map((entry) => [entry.name, entry.model])),
59
62
  actions: (resource) => new Set(viewModels.filter((view) => view.resource === resource).flatMap((view) => view.actions.map((action) => action.name)))
@@ -103,7 +106,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
103
106
  };
104
107
  const meta = createMeta(deps);
105
108
  const onError = createErrorHandler(options.onUnhandledError);
106
- const guarded = (routes) => {
109
+ const guarded = (routes, caller = authenticate) => {
107
110
  const app = new Hono();
108
111
  app.onError(onError);
109
112
  app.use("*", async (c, next) => {
@@ -111,7 +114,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
111
114
  c.res.headers.set("X-Meta-Version", await meta.version(c.get("session")?.user.roles ?? []));
112
115
  });
113
116
  app.use("*", async (c, next) => {
114
- c.set("session", await authenticate(c.req.raw));
117
+ c.set("session", await caller(c.req.raw));
115
118
  await next();
116
119
  });
117
120
  routes(app);
@@ -120,7 +123,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
120
123
  const api = guarded((app) => {
121
124
  app.route("/", collectionRoutes(deps));
122
125
  app.route("/", recordRoutes(deps));
123
- });
126
+ }, withAnonymous(authenticate, options.roles));
124
127
  const system = guarded((app) => {
125
128
  app.route("/", docsRoutes(deps));
126
129
  app.route("/", metaRoutes(meta));
package/dist/index.d.ts CHANGED
@@ -36,7 +36,7 @@ import { CreateAdminInput, createAdmin } from "./create-admin.js";
36
36
  import { ProjectConfig, defineConfig, mergeConfig } from "./project-config.js";
37
37
  import { consoleAuditQueue } from "./audit/console-queue.js";
38
38
  import { JwtOptions, jwtAuthenticator } from "./auth/jwt.js";
39
- import { HttpProblem, badRequest, forbidden, notFound, unauthorized } from "./problem.js";
39
+ import { HttpProblem, MissingCredentials, badRequest, forbidden, notFound, unauthorized } from "./problem.js";
40
40
  import { TrustedApp, withTrustedApps } from "./better-auth/app-tokens.js";
41
41
  import { BetterAuthAuthenticatorOptions, betterAuthAuthenticator } from "./better-auth/authenticator.js";
42
42
  import { parseRoles } from "./better-auth/parse-roles.js";
@@ -74,4 +74,4 @@ import { FatalJobError } from "./jobs/run-job.js";
74
74
  import { DrainResult, JobsRuntime, StartJobsOptions, startJobs } from "./jobs/runtime.js";
75
75
  import { jobsDrainApp } from "./jobs/drain-route.js";
76
76
  import { checkJobsSchema, jobsSchema, jobsSchemaMigration } from "./jobs/schema.js";
77
- export { type Admin, type AdminAuth, type AdminOptions, type ApiFunction, type ApprovalAnswer, type ApprovalRequest, type AssistantBackend, type AssistantOptions, type AssistantSettings, type AssistantTool, type AuditEvent, type AuditQueue, type Authenticator, type BetterAuthAuthenticatorOptions, type CapturedRow, type ChatMessage, type ChatTextPart, type CleanupEntry, type CleanupReport, type CleanupSchedule, type CoalescedChange, type Completion, type CompletionOptions, type Conversation, type ConversationRecord, type ConversationStore, type ConversationsOptions, type CorsOptions, type CorsOrigins, type CreateAdminInput, type CreateAuthOptions, type DrainResult, type EnqueueOptions, type ErrorReporter, type ErrorReportingOptions, type ErrorReportingSettings, type EventData, type EventsOptions, FatalJobError, type FileCleanupInput, type FileObject, type FileProvider, type FileStore, type FilesOptions, type FunctionApp, type FunctionCaller, type FunctionContext, type FunctionEnv, type FunctionHandler, type FunctionOptions, type FunctionRecords, type FunctionSource, HttpProblem, type IssueTokenOptions, type JobContext, type JobDefinition, type JobOptions, type JobsRuntime, type JwtOptions, type LinkedAccount, type ListParams, type LocalFilesOptions, type MemberInput, ModelError, type ModelSettings, type NewOrganization, type NewUser, type OperatorProvider, type OrganizationRole, type OrganizationsOptions, type PipelineHook, type PlatformSignIn, type PluginOption, type PluginRef, type ProjectConfig, type ProtectedFunctionOptions, type PublicFunctionContext, type PublicFunctionEnv, type PublicFunctionHandler, type PublicFunctionOptions, type QueryOptions, type QueryResult, type ReasoningDetail, type ReasoningEffort, type RecordKeyInput, type RecordPage, type ResourceChange, type ResourceSource, type RoleDefinition, type RolesInput, type RunContext, type RuntimeOptions, type ScanGuardMode, type ScheduledDelete, type Session, type SessionOrganization, type SignInPolicy, type SignInRule, type SignatureOptions, type SourceOption, type SourceRef, type StaffAccessRule, type StaffSignIn, type StandardSchema, type StandardWebhookOptions, type StartJobsOptions, type StoredMember, type StoredObject, type StoredOrganization, type StoredUser, type SubscriptionDefinition, type SubscriptionEvent, type SubscriptionOptions, type TenantValue, type ToolCall, type ToolContext, type ToolRecord, type ToolRecords, type ToolSpec, type TrustedApp, type TurnOptions, type User, type UserCreatedHook, type ViewSource, type WebhooksOptions, type WidgetOptions, type WriteEvent, type WriteOperation, addAppRoleToOwners, addMember, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaMigration, badRequest, betterAuthAuthenticator, changesMigration, checkAuthSchema, checkChanges, checkJobsSchema, checkSourceNames, checkWebhooksSchema, coalesce, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createFileCleanup, createOrganization, createUser, defaultSignInPolicy, defineConfig, defineFunction, defineJob, defineTool, deleteUser, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, functionApp, hasUsers, imageSize, isJobDefinition, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, listOrganizations, listUsers, localFileStore, localFiles, maxTokenTtlSeconds, maxWidgetPropsBytes, mergeConfig, migrationCommand, newWebhookSecret, normalizeRoles, notFound, parseColumns, parseRoles, pluginOptions, pluginsFor, pluginsVariable, processor, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readWriteQueryTool, recordOf, removeMember, requestApproval, requireEnv, resourceChange, roleChoices, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, seesAssistant, seesRuntime, setMemberRoles, setUserBanned, setUserRole, signInPolicyMethods, sourceOptions, sourcesFor, sourcesVariable, startErrorReporting, startJobs, storageCleanupSchedule, streamChatCompletion, subscription, unauthorized, verifySignature, verifyStandardWebhook, webauthnPath, webhooks, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps };
77
+ export { type Admin, type AdminAuth, type AdminOptions, type ApiFunction, type ApprovalAnswer, type ApprovalRequest, type AssistantBackend, type AssistantOptions, type AssistantSettings, type AssistantTool, type AuditEvent, type AuditQueue, type Authenticator, type BetterAuthAuthenticatorOptions, type CapturedRow, type ChatMessage, type ChatTextPart, type CleanupEntry, type CleanupReport, type CleanupSchedule, type CoalescedChange, type Completion, type CompletionOptions, type Conversation, type ConversationRecord, type ConversationStore, type ConversationsOptions, type CorsOptions, type CorsOrigins, type CreateAdminInput, type CreateAuthOptions, type DrainResult, type EnqueueOptions, type ErrorReporter, type ErrorReportingOptions, type ErrorReportingSettings, type EventData, type EventsOptions, FatalJobError, type FileCleanupInput, type FileObject, type FileProvider, type FileStore, type FilesOptions, type FunctionApp, type FunctionCaller, type FunctionContext, type FunctionEnv, type FunctionHandler, type FunctionOptions, type FunctionRecords, type FunctionSource, HttpProblem, type IssueTokenOptions, type JobContext, type JobDefinition, type JobOptions, type JobsRuntime, type JwtOptions, type LinkedAccount, type ListParams, type LocalFilesOptions, type MemberInput, MissingCredentials, ModelError, type ModelSettings, type NewOrganization, type NewUser, type OperatorProvider, type OrganizationRole, type OrganizationsOptions, type PipelineHook, type PlatformSignIn, type PluginOption, type PluginRef, type ProjectConfig, type ProtectedFunctionOptions, type PublicFunctionContext, type PublicFunctionEnv, type PublicFunctionHandler, type PublicFunctionOptions, type QueryOptions, type QueryResult, type ReasoningDetail, type ReasoningEffort, type RecordKeyInput, type RecordPage, type ResourceChange, type ResourceSource, type RoleDefinition, type RolesInput, type RunContext, type RuntimeOptions, type ScanGuardMode, type ScheduledDelete, type Session, type SessionOrganization, type SignInPolicy, type SignInRule, type SignatureOptions, type SourceOption, type SourceRef, type StaffAccessRule, type StaffSignIn, type StandardSchema, type StandardWebhookOptions, type StartJobsOptions, type StoredMember, type StoredObject, type StoredOrganization, type StoredUser, type SubscriptionDefinition, type SubscriptionEvent, type SubscriptionOptions, type TenantValue, type ToolCall, type ToolContext, type ToolRecord, type ToolRecords, type ToolSpec, type TrustedApp, type TurnOptions, type User, type UserCreatedHook, type ViewSource, type WebhooksOptions, type WidgetOptions, type WriteEvent, type WriteOperation, addAppRoleToOwners, addMember, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaMigration, badRequest, betterAuthAuthenticator, changesMigration, checkAuthSchema, checkChanges, checkJobsSchema, checkSourceNames, checkWebhooksSchema, coalesce, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createFileCleanup, createOrganization, createUser, defaultSignInPolicy, defineConfig, defineFunction, defineJob, defineTool, deleteUser, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, functionApp, hasUsers, imageSize, isJobDefinition, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, listOrganizations, listUsers, localFileStore, localFiles, maxTokenTtlSeconds, maxWidgetPropsBytes, mergeConfig, migrationCommand, newWebhookSecret, normalizeRoles, notFound, parseColumns, parseRoles, pluginOptions, pluginsFor, pluginsVariable, processor, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readWriteQueryTool, recordOf, removeMember, requestApproval, requireEnv, resourceChange, roleChoices, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, seesAssistant, seesRuntime, setMemberRoles, setUserBanned, setUserRole, signInPolicyMethods, sourceOptions, sourcesFor, sourcesVariable, startErrorReporting, startJobs, storageCleanupSchedule, streamChatCompletion, subscription, unauthorized, verifySignature, verifyStandardWebhook, webauthnPath, webhooks, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps };
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { assistantRoles, assistantSettings, assistantVariables, seesAssistant } from "./assistant/assistant-settings.js";
2
2
  import { conversationKey, createConversations } from "./assistant/conversations.js";
3
3
  import { fileConversationStore } from "./assistant/file-conversation-store.js";
4
- import { HttpProblem, badRequest, forbidden, notFound, unauthorized } from "./problem.js";
4
+ import { HttpProblem, MissingCredentials, badRequest, forbidden, notFound, unauthorized } from "./problem.js";
5
5
  import { assistantProtocolRoutes } from "./assistant/protocol-routes.js";
6
6
  import { runReadOnlyQuery, runReadWriteQuery } from "./assistant/run-query.js";
7
7
  import { defineTool } from "./assistant/tools.js";
@@ -56,4 +56,4 @@ import { createFileCleanup } from "./files/file-cleanup.js";
56
56
  import { aspectRatio, fileSize, fileType, imageSize, processor } from "./files/processors.js";
57
57
  import { startJobs } from "./jobs/runtime.js";
58
58
  import { jobsDrainApp } from "./jobs/drain-route.js";
59
- export { FatalJobError, HttpProblem, ModelError, addAppRoleToOwners, addMember, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaMigration, badRequest, betterAuthAuthenticator, changesMigration, checkAuthSchema, checkChanges, checkJobsSchema, checkSourceNames, checkWebhooksSchema, coalesce, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createFileCleanup, createOrganization, createUser, defaultSignInPolicy, defineConfig, defineFunction, defineJob, defineTool, deleteUser, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, functionApp, hasUsers, imageSize, isJobDefinition, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, listOrganizations, listUsers, localFileStore, localFiles, maxTokenTtlSeconds, maxWidgetPropsBytes, mergeConfig, migrationCommand, newWebhookSecret, normalizeRoles, notFound, parseColumns, parseRoles, pluginOptions, pluginsFor, pluginsVariable, processor, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readWriteQueryTool, recordOf, removeMember, requestApproval, requireEnv, resourceChange, roleChoices, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, seesAssistant, seesRuntime, setMemberRoles, setUserBanned, setUserRole, signInPolicyMethods, sourceOptions, sourcesFor, sourcesVariable, startErrorReporting, startJobs, storageCleanupSchedule, streamChatCompletion, subscription, unauthorized, verifySignature, verifyStandardWebhook, webauthnPath, webhooks, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps };
59
+ export { FatalJobError, HttpProblem, MissingCredentials, ModelError, addAppRoleToOwners, addMember, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaMigration, badRequest, betterAuthAuthenticator, changesMigration, checkAuthSchema, checkChanges, checkJobsSchema, checkSourceNames, checkWebhooksSchema, coalesce, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createFileCleanup, createOrganization, createUser, defaultSignInPolicy, defineConfig, defineFunction, defineJob, defineTool, deleteUser, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, functionApp, hasUsers, imageSize, isJobDefinition, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, listOrganizations, listUsers, localFileStore, localFiles, maxTokenTtlSeconds, maxWidgetPropsBytes, mergeConfig, migrationCommand, newWebhookSecret, normalizeRoles, notFound, parseColumns, parseRoles, pluginOptions, pluginsFor, pluginsVariable, processor, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readWriteQueryTool, recordOf, removeMember, requestApproval, requireEnv, resourceChange, roleChoices, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, seesAssistant, seesRuntime, setMemberRoles, setUserBanned, setUserRole, signInPolicyMethods, sourceOptions, sourcesFor, sourcesVariable, startErrorReporting, startJobs, storageCleanupSchedule, streamChatCompletion, subscription, unauthorized, verifySignature, verifyStandardWebhook, webauthnPath, webhooks, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps };
package/dist/problem.d.ts CHANGED
@@ -12,6 +12,14 @@ export declare class HttpProblem extends Error {
12
12
  }
13
13
  export declare const badRequest: (slug: string, detail: string, extras?: ProblemExtras) => HttpProblem;
14
14
  export declare const unauthorized: (detail: string) => HttpProblem;
15
+ /**
16
+ * The 401 of a request that carries no credential at all, as opposed to an invalid one: with an `anonymous` role, the
17
+ * resources API serves such a request as an anonymous caller instead. An authenticator of the app's own throws it to
18
+ * let that happen.
19
+ */
20
+ export declare class MissingCredentials extends HttpProblem {
21
+ constructor(detail: string);
22
+ }
15
23
  export declare const forbidden: (slug: string, detail: string) => HttpProblem;
16
24
  export declare const notFound: (detail: string) => HttpProblem;
17
25
  //#endregion
package/dist/problem.js CHANGED
@@ -35,9 +35,19 @@ const problemResponse = (problem, instance) => new Response(JSON.stringify({
35
35
  });
36
36
  const badRequest = (slug, detail, extras) => new HttpProblem(400, slug, "Bad Request", detail, extras);
37
37
  const unauthorized = (detail) => new HttpProblem(401, "unauthenticated", "Unauthorized", detail, {}, { "www-authenticate": "Bearer" });
38
+ /**
39
+ * The 401 of a request that carries no credential at all, as opposed to an invalid one: with an `anonymous` role, the
40
+ * resources API serves such a request as an anonymous caller instead. An authenticator of the app's own throws it to
41
+ * let that happen.
42
+ */
43
+ var MissingCredentials = class extends HttpProblem {
44
+ constructor(detail) {
45
+ super(401, "unauthenticated", "Unauthorized", detail, {}, { "www-authenticate": "Bearer" });
46
+ }
47
+ };
38
48
  const forbidden = (slug, detail) => new HttpProblem(403, slug, "Forbidden", detail);
39
49
  const notFound = (detail) => new HttpProblem(404, "not-found", "Not Found", detail);
40
50
  const preconditionFailed = (detail) => new HttpProblem(412, "precondition-failed", "Precondition Failed", detail);
41
51
  const preconditionRequired = (detail) => new HttpProblem(428, "precondition-required", "Precondition Required", detail);
42
52
  //#endregion
43
- export { HttpProblem, badRequest, forbidden, notFound, preconditionFailed, preconditionRequired, problemResponse, problemType, unauthorized };
53
+ export { HttpProblem, MissingCredentials, badRequest, forbidden, notFound, preconditionFailed, preconditionRequired, problemResponse, problemType, unauthorized };
@@ -1,4 +1,4 @@
1
- import { forbidden, notFound } from "./problem.js";
1
+ import { forbidden, notFound, unauthorized } from "./problem.js";
2
2
  import { checkedFilter } from "./filter-input.js";
3
3
  import { tenantScope } from "./tenant.js";
4
4
  import { checkFilter, resolveAccess, restrictModel } from "@protobase/schema";
@@ -28,10 +28,14 @@ const requestAccess = async (deps, entry, session) => {
28
28
  };
29
29
  };
30
30
  const hasNoAccess = (resolved) => !Object.values(resolved.operations).some(Boolean);
31
- /** 403 when the operation is denied; a resource the user can do nothing with does not exist for them. */
31
+ /**
32
+ * 403 when the operation is denied, 401 for a request without a token; a resource the user can do nothing with does not
33
+ * exist for them.
34
+ */
32
35
  const requireOperation = (access, operation) => {
33
36
  if (access.resolved.operations[operation]) return;
34
37
  if (hasNoAccess(access.resolved)) throw notFound(`There is no resource "${access.entry.name}"`);
38
+ if (access.session.anonymous) throw unauthorized(`Send a bearer token to ${operation} ${access.entry.name}`);
35
39
  throw forbidden("access-denied", `You may not ${operation} ${access.entry.name}`);
36
40
  };
37
41
  /**
package/dist/types.d.ts CHANGED
@@ -13,10 +13,12 @@ export type User = {
13
13
  export type SessionOrganization = {
14
14
  id: TenantValue;
15
15
  };
16
+ /** `anonymous` marks a request without a token, served as the project's `anonymous` role. */
16
17
  export type Session = {
17
18
  user: User;
18
19
  tenant?: TenantValue;
19
20
  organization?: SessionOrganization;
21
+ anonymous?: true;
20
22
  };
21
23
  /** Resolves the caller of a request, or throws a 401 problem (see `unauthorized`). */
22
24
  export type Authenticator = (request: Request) => Promise<Session> | Session;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/server",
3
- "version": "0.12.7",
3
+ "version": "0.12.9",
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": {
@@ -30,9 +30,9 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@better-auth/passkey": "^1.7.7",
33
- "@protobase/layout": "0.12.7",
34
- "@protobase/query": "0.12.7",
35
- "@protobase/schema": "0.12.7",
33
+ "@protobase/layout": "0.12.9",
34
+ "@protobase/query": "0.12.9",
35
+ "@protobase/schema": "0.12.9",
36
36
  "@scalar/hono-api-reference": "^0.12.10",
37
37
  "@sentry/core": "^11.7.0",
38
38
  "@simplewebauthn/server": "^13.3.1",