@protobase/server 0.59.2 → 0.59.3

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.
@@ -1,5 +1,5 @@
1
- import { ddl, ddlText } from "./ddl.js";
2
1
  import { storeTable } from "./store-names.js";
2
+ import { ddl, ddlText } from "./ddl.js";
3
3
  import { sql } from "kysely";
4
4
  //#region src/better-auth/store/migrations-table.ts
5
5
  /** The table, in the store's schema. */
@@ -10,6 +10,7 @@ import { HistoryOptions } from "./history/options.js";
10
10
  import { WebhooksOptions } from "./webhooks/table/options.js";
11
11
  import { NotificationsOptions } from "./notifications/options.js";
12
12
  import { CallsOptions } from "./calls/calls-options.js";
13
+ import { StatusOptions } from "./status/options.js";
13
14
  import { SubscriptionDefinition } from "./webhooks/subscription.js";
14
15
  import { FilesOptions } from "./files/options.js";
15
16
  import { Difficulty } from "./forms/challenge.js";
@@ -55,6 +56,8 @@ export type CreateAdminInput = {
55
56
  realtime?: RealtimeOptions;
56
57
  /** Each user's devices and the endpoints that register them, from `notifications: {}` (see `NotificationsOptions`). */
57
58
  notifications?: NotificationsOptions;
59
+ /** People's own status, from `status: {}` (see `StatusOptions`): it needs `auth` with organizations. */
60
+ status?: StatusOptions;
58
61
  /** The media server behind the calls functions admit members to with `calls.join` (see `CallsOptions`). */
59
62
  calls?: CallsOptions;
60
63
  /**
@@ -96,5 +99,5 @@ export type CreateAdminInput = {
96
99
  upgradeWebSocket?: UpgradeWebSocket;
97
100
  };
98
101
  /** The admin REST API as a Hono app. It uses only Web APIs, so it runs on Node, Workers and anywhere else Hono does. */
99
- export declare const createAdmin: ({ resources: appResources, views: appViews, pages, userMenu, bottomBars, db, authenticate, auth, functions, secret, files: filesOptions, events, history: historyOptions, jobs, subscriptions, webhooks: webhooksOptions, realtime, notifications, calls, mobile, app: identity, icons, bundlePlugins, fieldTypes, options, formsDifficulty, commentUsers: givenCommentUsers, verifyToken, upgradeWebSocket }: CreateAdminInput) => Admin;
102
+ export declare const createAdmin: ({ resources: appResources, views: appViews, pages, userMenu, bottomBars, db, authenticate, auth, functions, secret, files: filesOptions, events, history: historyOptions, jobs, subscriptions, webhooks: webhooksOptions, realtime, notifications, status, calls, mobile, app: identity, icons, bundlePlugins, fieldTypes, options, formsDifficulty, commentUsers: givenCommentUsers, verifyToken, upgradeWebSocket }: CreateAdminInput) => Admin;
100
103
  //#endregion
@@ -66,6 +66,10 @@ import { historyRoutes } from "./notifications/history-routes.js";
66
66
  import { deviceRoutes } from "./notifications/routes.js";
67
67
  import { notificationSenders, webPushKey } from "./notifications/senders-of.js";
68
68
  import { pushJob } from "./notifications/push-job.js";
69
+ import { onDoNotDisturb } from "./status/do-not-disturb.js";
70
+ import { checkStatusAuth, statusNames } from "./status/options.js";
71
+ import { statusRoutes } from "./status/routes.js";
72
+ import { storeSchema } from "./better-auth/store/store-names.js";
69
73
  import { liveSessions } from "./better-auth/live-sessions.js";
70
74
  import { checkBottomBars, checkShell } from "./check-shell.js";
71
75
  import { checkOwnerFields } from "./check-owners.js";
@@ -106,13 +110,14 @@ import { createScanGuard } from "./scan-guard.js";
106
110
  import { failedJobs, failedJobsView } from "./jobs/failed-jobs.js";
107
111
  import { Hono } from "hono";
108
112
  import { accountPage, accountPageName, checkPages, settingsResources, withSettingsFields } from "@protobase/layout";
113
+ import { statusPath } from "@protobase/schema/internal";
109
114
  //#region src/create-admin.ts
110
115
  const declaredRoles = (auth, roles) => {
111
116
  const names = [...auth?.roles ?? [], ...Object.keys(roles?.definitions ?? {})];
112
117
  return names.length > 0 ? [...new Set(names)] : void 0;
113
118
  };
114
119
  /** The admin REST API as a Hono app. It uses only Web APIs, so it runs on Node, Workers and anywhere else Hono does. */
115
- const createAdmin = ({ resources: appResources, views: appViews = [], pages = [], userMenu, bottomBars = [], db, authenticate, auth, functions = {}, secret, files: filesOptions, events, history: historyOptions, jobs = [], subscriptions = [], webhooks: webhooksOptions, realtime, notifications, calls, mobile, app: identity, icons, bundlePlugins = [], fieldTypes = {}, options = {}, formsDifficulty, commentUsers: givenCommentUsers, verifyToken, upgradeWebSocket }) => {
120
+ const createAdmin = ({ resources: appResources, views: appViews = [], pages = [], userMenu, bottomBars = [], db, authenticate, auth, functions = {}, secret, files: filesOptions, events, history: historyOptions, jobs = [], subscriptions = [], webhooks: webhooksOptions, realtime, notifications, status, calls, mobile, app: identity, icons, bundlePlugins = [], fieldTypes = {}, options = {}, formsDifficulty, commentUsers: givenCommentUsers, verifyToken, upgradeWebSocket }) => {
116
121
  const basePath = options.basePath ?? "/api/v1";
117
122
  const appModels = appResources.map((resource) => resource.toModel());
118
123
  const withComments = appModels.some((model) => model.comments);
@@ -173,6 +178,14 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
173
178
  const errorReporting = errorReportingSettings(options.errorReporting, globalThis.process?.env ?? {});
174
179
  const maps = mapSettings(options.maps, globalThis.process?.env ?? {});
175
180
  const senders = notifications ? notificationSenders(notifications) : [];
181
+ if (status) checkStatusAuth(auth);
182
+ const statusTables = status && auth && {
183
+ statuses: statusNames(status),
184
+ members: {
185
+ schema: storeSchema(auth),
186
+ table: "member"
187
+ }
188
+ };
176
189
  const pushKey = webPushKey(senders);
177
190
  const locale = localeSetting(options.locale);
178
191
  const plugins = pluginOptions(options.plugins, globalThis.process?.env ?? {}, bundlePlugins);
@@ -231,6 +244,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
231
244
  ...maps && { maps },
232
245
  ...notifications && notifications.ui !== false && { bell: true },
233
246
  ...pushKey && { webPushKey: pushKey },
247
+ ...statusTables && { status: true },
234
248
  ...locale && { locale },
235
249
  plugins,
236
250
  sources,
@@ -444,6 +458,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
444
458
  if (auth) removeHistoryWithUsers(auth, history);
445
459
  app.route(historyPath(systemPath), guarded((routes) => void routes.route("/", historyRoutes(history, auth?.origins ?? []))));
446
460
  }
461
+ if (statusTables) app.route(statusPath(systemPath), guarded((routes) => void routes.route("/", statusRoutes(db, statusTables))));
447
462
  if (forms) app.route(`${systemPath}/forms`, formRoutes(deps, forms));
448
463
  const feed = tableFeed({
449
464
  db,
@@ -514,7 +529,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
514
529
  ...withHistory && days !== void 0 ? [pruneHistoryJob(changes, days)] : [],
515
530
  ...mentions ? [mentions] : [],
516
531
  ...fill ? [fill] : [],
517
- ...notifications ? [pushJob(notifications, senders, auth && liveSessions(auth))] : []
532
+ ...notifications ? [pushJob(notifications, senders, auth && liveSessions(auth), statusTables ? (jobDb, userIds) => onDoNotDisturb(jobDb, statusTables.statuses, userIds) : void 0)] : []
518
533
  ]
519
534
  });
520
535
  };
package/dist/deps.d.ts CHANGED
@@ -53,6 +53,8 @@ export type Deps = {
53
53
  maps?: MapSettings;
54
54
  /** Whether the shell draws the notification bell: notifications are on and their `ui` is not false. */
55
55
  bell?: boolean;
56
+ /** Whether people set their own status (`status: {}`), which `/meta` names to everyone. */
57
+ status?: boolean;
56
58
  /** The VAPID public key browsers subscribe for Web Push with, when notifications have a web sender, which `/meta` names to everyone. */
57
59
  webPushKey?: string;
58
60
  /** The language the admin app's screens are in for everyone, which `/meta` names to everyone. */
package/dist/index.d.ts CHANGED
@@ -22,6 +22,7 @@ import { HistoryStore, maxHistoryPage, maxUnreadCount } from "./notifications/hi
22
22
  import { PushAnswer, PushSender } from "./notifications/sender.js";
23
23
  import { DeviceColumn, HistoryColumn, NotificationsOptions } from "./notifications/options.js";
24
24
  import { CallBackend, CallsOptions, LiveKitBackend } from "./calls/calls-options.js";
25
+ import { StatusOptions, statusNames } from "./status/options.js";
25
26
  import { EventData, RunContext, SubscriptionDefinition, SubscriptionEvent, SubscriptionOptions, subscription } from "./webhooks/subscription.js";
26
27
  import { Sharp, renderVariant, thumbHash } from "./files/images.js";
27
28
  import { FileProvider, FileStore, StoredObject } from "./files/store.js";
@@ -118,6 +119,8 @@ import { sendNotification } from "./notifications/send-notification.js";
118
119
  import { VapidSettings, readVapidSettings, vapidPrivateKeyVariable, vapidPublicKeyVariable, vapidSubjectVariable } from "./notifications/web-push/vapid-settings.js";
119
120
  import { WebPushOptions, webPushSender } from "./notifications/web-push/web-push-sender.js";
120
121
  import { generateVapidKeys } from "./notifications/web-push/keys.js";
122
+ import { checkStatusSchema, statusMigration } from "./status/schema.js";
123
+ import { onDoNotDisturb } from "./status/do-not-disturb.js";
121
124
  import { MemberInput, NewOrganization, addAppRoleToOwners, addMember, createOrganization, listOrganizations, removeMember, setMemberRoles } from "./better-auth/organizations/host.js";
122
125
  import { AssistantBackend, assistantProtocolRoutes } from "./assistant/protocol-routes.js";
123
126
  import { fileConversationStore } from "./assistant/file-conversation-store.js";
@@ -176,4 +179,4 @@ import { DrainResult, JobsRuntime, StartJobsOptions, startJobs } from "./jobs/ru
176
179
  import { jobsDrainApp } from "./jobs/drain-route.js";
177
180
  import { checkJobsSchema, jobsSchema, jobsSchemaMigration } from "./jobs/schema.js";
178
181
  import { AdminAuth, CreateAuthOptions, UserCreatedHook, normalizeRoles } from "./better-auth/create-auth.js";
179
- export { type Admin, type AdminAuth, type AdminOptions, type AggregateParams, type ApiFunction, type ApiKeys, type ApiKeysOptions, type ApnsEnvironment, type AppIcons, type AppIdentity, type AppPoolConfig, type ApplyOptions, type ApplyResult, type ApprovalAnswer, type ApprovalRequest, type ArgumentsPayload, type AssistantBackend, type AssistantOptions, type AssistantSettings, type AssistantTool, type AuditEvent, type AuditQueue, type AuthHooks, type AuthSettings, type AuthenticateSettings, type Authenticator, type BaseToolContext, type BatchOperation, type BatchResult, type BetterAuthAuthenticator, type BetterAuthAuthenticatorOptions, type CallBackend, type CallMember, type Caller, type CallerOptions, type CallsAccess, type CallsOptions, type CapturedRow, type CascadingKey, type ChannelBroadcast, type ChannelSender, type ChatMessage, type ChatTextPart, type CheckAiInput, type CleanupEntry, type CleanupReport, type CleanupSchedule, type CoalescedChange, type CommentItem, type CommentToolsInput, type CommentUsers, type Completion, type CompletionOptions, ConnectionsBusy, type Conversation, type ConversationRecord, type ConversationStore, type ConversationsOptions, type CorsOptions, type CorsOrigins, type CreateAdminInput, type CreateAuthOptions, type DecryptField, type Device, type DeviceColumn, type DeviceKind, type DeviceStore, DeviceStoreError, type DrainResult, type EmailComponent, type EmailOptions, type EmailTemplate, type EncryptionStatus, type EncryptionTools, type EncryptionToolsInput, type EnqueueJob, type EnqueueOptions, type ErrorReporter, type ErrorReportingOptions, type ErrorReportingSettings, type EventData, type EventsOptions, FatalJobError, type FieldApplied, type FieldDecrypter, type FieldReader, type FieldStatus, type FileCleanupInput, type FileLimits, type FileObject, type FileProvider, type FileProviderSettings, 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 HistoryColumn, type HistoryOptions, type HistoryPosition, type HistoryStore, HistoryStoreError, type HookContext, HttpProblem, type IssueTokenOptions, type JobContext, type JobDeclaration, type JobDefinition, type JobName, type JobOptions, type JobPayload, type JobRun, type JobsRuntime, type JsonObject, type JsonValue, type JwtOptions, type JwtVerifyOptions, type KeyFunctionContext, type LinkedAccount, type ListParams, type LiveKitBackend, type LocalFilesOptions, type MailMessage, type Mailer, type MapLayer, type MapOptions, type MapSettings, type McpApprovals, type McpGateInput, type McpHandlerInput, type McpRoutesInput, type McpServerInput, type McpToolsInput, type McpWrites, type MemberInput, type Membership, MissingCredentials, type MobileConfig, ModelError, type ModelSettings, type NewDevice, type NewNotification, type NewOrganization, type NewUser, type NotificationRecipients, NotificationSendError, type NotificationsOptions, type OperatorProvider, type OrganizationRole, type OrganizationsOptions, type PgModule, type PipelineHook, type PlatformSignIn, type PluginOption, type PluginRef, type PngIcon, type ProtectedFunctionOptions, type PublicFunctionContext, type PublicFunctionEnv, type PublicFunctionHandler, type PublicFunctionOptions, type PushAnswer, type PushMessage, PushMessageError, type PushSender, type QueryOptions, type QueryResult, type ReadableResources, type RealtimeOptions, type RealtimePrune, type ReasoningDetail, type ReasoningEffort, type RebindOption, type RecordGroups, type RecordKey, 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 SendNotification, 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 StoredNotification, type StoredObject, type StoredOrganization, type StoredUser, type StrayValues, type SubscriptionDefinition, type SubscriptionEvent, type SubscriptionOptions, type TenantValue, type ToolAnnotations, type ToolCall, type ToolComment, type ToolCommentPage, type ToolComments, type ToolContext, type ToolRecord, type ToolRecords, type ToolSpec, type TrustedApp, type TurnOptions, type UnreadableEntry, type Urgency, type User, type UserCreatedHook, type VapidSettings, type ViewSource, type WebPushFields, type WebPushOptions, type WebhooksOptions, type WidgetOptions, type WriteEvent, type WriteOperation, type WriteRecordToolsInput, addAppRoleToOwners, addCommentTool, addMember, apiKeyCaller, apiKeySettings, apiKeyVariables, appNameOver, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaDrift, authSchemaMigration, authStoreTables, backgroundSettled, badRequest, batchWriteTool, beforeSignInPluginsVariable, betterAuthTokens, capSocketMessages, changesMigration, channelSendsMigration, checkAi, checkAuthSchema, checkChanges, checkChannelSends, checkComments, checkComputed, checkJobsSchema, checkNotificationsSchema, checkQueryRunner, checkRealtimeRelay, checkResourcesOutsideAuthStore, checkSourceNames, checkViewQueries, checkWebhooksSchema, coalesce, commentTools, commentsMigration, computedMigration, configExports, consoleAuditQueue, conversationKey, countRecordsTool, createAdmin, createConversations, createEncryptionTools, createFileCleanup, createOrganization, createRecordTool, createUser, defaultAppName, defaultHistoryPage, defaultSignInPolicy, defineEmail, defineFunction, defineJob, defineTool, deleteRecordTool, deleteUser, describeResourceTool, deviceIdParam, deviceStore, devicesPath, enqueueJob, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, generateVapidKeys, getRecordTool, goTruePath, handleDroppedConnections, hasUsers, historyIdParam, historyIdPattern, historyLimits, historyPath, historyStore, honorSocketMessageCaps, imageSize, isBackgroundWork, isCaptured, isJobDefinition, isOAuthWellKnownPath, isSchedule, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, jwtVerifier, limitedFileStore, listCommentsTool, listOrganizations, listRecordsTool, listResourcesTool, listUsers, localFileStore, longQueryTimeoutMs, mapSettings, mapStyleUrl, mapStyleVariable, maxDevicesPerUser, maxHistoryPage, maxMessageBytes, maxRecipients, maxTokenTtlSeconds, maxTtlSeconds, maxUnreadCount, maxWidgetPropsBytes, mcpApprovals, mcpGate, mcpHandler, mcpIcons, mcpInstructions, mcpRoutes, mcpServer, mcpToolContext, mcpTools, migrationCommand, newWebhookSecret, normalizeRoles, notFound, notificationSenders, notificationsMigration, oauthWellKnownPaths, objectKey, parseColumns, parseRoles, passwordLength, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, pngIcons, poolWaitMs, postgresDeviceStore, postgresHistoryStore, processor, pushMessage, queryRunnerMigration, readDbConnections, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readRecordTools, readVapidSettings, readWriteQueryTool, readableResources, realtimeLimits, realtimeRelayMigration, recordOf, refusalOf, refused, relayed, removeDevicesWithUsers, removeHistoryWithUsers, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runInBackground, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, seesAssistant, seesRuntime, sendNotification, sessionOf, setMemberRoles, setUserBanned, setUserRole, shownAppName, signInPolicyMethods, socketMessageCapOf, sourceOptions, sourcesFor, sourcesVariable, sqlSends, startErrorReporting, startJobs, storageCleanupSchedule, storageProvider, storeFile, storeUsage, streamChatCompletion, subscription, supabasePath, thumbHash, unauthorized, updateRecordTool, urgencies, vapidPrivateKeyVariable, vapidPublicKeyVariable, vapidSubjectVariable, variantPath, verifySignature, verifyStandardWebhook, waitUntil, wantsLongQueries, webManifestPath, webPushSender, webauthnPath, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps, writeRecordTools };
182
+ export { type Admin, type AdminAuth, type AdminOptions, type AggregateParams, type ApiFunction, type ApiKeys, type ApiKeysOptions, type ApnsEnvironment, type AppIcons, type AppIdentity, type AppPoolConfig, type ApplyOptions, type ApplyResult, type ApprovalAnswer, type ApprovalRequest, type ArgumentsPayload, type AssistantBackend, type AssistantOptions, type AssistantSettings, type AssistantTool, type AuditEvent, type AuditQueue, type AuthHooks, type AuthSettings, type AuthenticateSettings, type Authenticator, type BaseToolContext, type BatchOperation, type BatchResult, type BetterAuthAuthenticator, type BetterAuthAuthenticatorOptions, type CallBackend, type CallMember, type Caller, type CallerOptions, type CallsAccess, type CallsOptions, type CapturedRow, type CascadingKey, type ChannelBroadcast, type ChannelSender, type ChatMessage, type ChatTextPart, type CheckAiInput, type CleanupEntry, type CleanupReport, type CleanupSchedule, type CoalescedChange, type CommentItem, type CommentToolsInput, type CommentUsers, type Completion, type CompletionOptions, ConnectionsBusy, type Conversation, type ConversationRecord, type ConversationStore, type ConversationsOptions, type CorsOptions, type CorsOrigins, type CreateAdminInput, type CreateAuthOptions, type DecryptField, type Device, type DeviceColumn, type DeviceKind, type DeviceStore, DeviceStoreError, type DrainResult, type EmailComponent, type EmailOptions, type EmailTemplate, type EncryptionStatus, type EncryptionTools, type EncryptionToolsInput, type EnqueueJob, type EnqueueOptions, type ErrorReporter, type ErrorReportingOptions, type ErrorReportingSettings, type EventData, type EventsOptions, FatalJobError, type FieldApplied, type FieldDecrypter, type FieldReader, type FieldStatus, type FileCleanupInput, type FileLimits, type FileObject, type FileProvider, type FileProviderSettings, 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 HistoryColumn, type HistoryOptions, type HistoryPosition, type HistoryStore, HistoryStoreError, type HookContext, HttpProblem, type IssueTokenOptions, type JobContext, type JobDeclaration, type JobDefinition, type JobName, type JobOptions, type JobPayload, type JobRun, type JobsRuntime, type JsonObject, type JsonValue, type JwtOptions, type JwtVerifyOptions, type KeyFunctionContext, type LinkedAccount, type ListParams, type LiveKitBackend, type LocalFilesOptions, type MailMessage, type Mailer, type MapLayer, type MapOptions, type MapSettings, type McpApprovals, type McpGateInput, type McpHandlerInput, type McpRoutesInput, type McpServerInput, type McpToolsInput, type McpWrites, type MemberInput, type Membership, MissingCredentials, type MobileConfig, ModelError, type ModelSettings, type NewDevice, type NewNotification, type NewOrganization, type NewUser, type NotificationRecipients, NotificationSendError, type NotificationsOptions, type OperatorProvider, type OrganizationRole, type OrganizationsOptions, type PgModule, type PipelineHook, type PlatformSignIn, type PluginOption, type PluginRef, type PngIcon, type ProtectedFunctionOptions, type PublicFunctionContext, type PublicFunctionEnv, type PublicFunctionHandler, type PublicFunctionOptions, type PushAnswer, type PushMessage, PushMessageError, type PushSender, type QueryOptions, type QueryResult, type ReadableResources, type RealtimeOptions, type RealtimePrune, type ReasoningDetail, type ReasoningEffort, type RebindOption, type RecordGroups, type RecordKey, 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 SendNotification, 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 StatusOptions, type StoreFileInput, type StoredMember, type StoredNotification, type StoredObject, type StoredOrganization, type StoredUser, type StrayValues, type SubscriptionDefinition, type SubscriptionEvent, type SubscriptionOptions, type TenantValue, type ToolAnnotations, type ToolCall, type ToolComment, type ToolCommentPage, type ToolComments, type ToolContext, type ToolRecord, type ToolRecords, type ToolSpec, type TrustedApp, type TurnOptions, type UnreadableEntry, type Urgency, type User, type UserCreatedHook, type VapidSettings, type ViewSource, type WebPushFields, type WebPushOptions, type WebhooksOptions, type WidgetOptions, type WriteEvent, type WriteOperation, type WriteRecordToolsInput, addAppRoleToOwners, addCommentTool, addMember, apiKeyCaller, apiKeySettings, apiKeyVariables, appNameOver, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaDrift, authSchemaMigration, authStoreTables, backgroundSettled, badRequest, batchWriteTool, beforeSignInPluginsVariable, betterAuthTokens, capSocketMessages, changesMigration, channelSendsMigration, checkAi, checkAuthSchema, checkChanges, checkChannelSends, checkComments, checkComputed, checkJobsSchema, checkNotificationsSchema, checkQueryRunner, checkRealtimeRelay, checkResourcesOutsideAuthStore, checkSourceNames, checkStatusSchema, checkViewQueries, checkWebhooksSchema, coalesce, commentTools, commentsMigration, computedMigration, configExports, consoleAuditQueue, conversationKey, countRecordsTool, createAdmin, createConversations, createEncryptionTools, createFileCleanup, createOrganization, createRecordTool, createUser, defaultAppName, defaultHistoryPage, defaultSignInPolicy, defineEmail, defineFunction, defineJob, defineTool, deleteRecordTool, deleteUser, describeResourceTool, deviceIdParam, deviceStore, devicesPath, enqueueJob, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, generateVapidKeys, getRecordTool, goTruePath, handleDroppedConnections, hasUsers, historyIdParam, historyIdPattern, historyLimits, historyPath, historyStore, honorSocketMessageCaps, imageSize, isBackgroundWork, isCaptured, isJobDefinition, isOAuthWellKnownPath, isSchedule, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, jwtVerifier, limitedFileStore, listCommentsTool, listOrganizations, listRecordsTool, listResourcesTool, listUsers, localFileStore, longQueryTimeoutMs, mapSettings, mapStyleUrl, mapStyleVariable, maxDevicesPerUser, maxHistoryPage, maxMessageBytes, maxRecipients, maxTokenTtlSeconds, maxTtlSeconds, maxUnreadCount, maxWidgetPropsBytes, mcpApprovals, mcpGate, mcpHandler, mcpIcons, mcpInstructions, mcpRoutes, mcpServer, mcpToolContext, mcpTools, migrationCommand, newWebhookSecret, normalizeRoles, notFound, notificationSenders, notificationsMigration, oauthWellKnownPaths, objectKey, onDoNotDisturb, parseColumns, parseRoles, passwordLength, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, pngIcons, poolWaitMs, postgresDeviceStore, postgresHistoryStore, processor, pushMessage, queryRunnerMigration, readDbConnections, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readRecordTools, readVapidSettings, readWriteQueryTool, readableResources, realtimeLimits, realtimeRelayMigration, recordOf, refusalOf, refused, relayed, removeDevicesWithUsers, removeHistoryWithUsers, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runInBackground, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, seesAssistant, seesRuntime, sendNotification, sessionOf, setMemberRoles, setUserBanned, setUserRole, shownAppName, signInPolicyMethods, socketMessageCapOf, sourceOptions, sourcesFor, sourcesVariable, sqlSends, startErrorReporting, startJobs, statusMigration, statusNames, storageCleanupSchedule, storageProvider, storeFile, storeUsage, streamChatCompletion, subscription, supabasePath, thumbHash, unauthorized, updateRecordTool, urgencies, vapidPrivateKeyVariable, vapidPublicKeyVariable, vapidSubjectVariable, variantPath, verifySignature, verifyStandardWebhook, waitUntil, wantsLongQueries, webManifestPath, webPushSender, webauthnPath, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps, writeRecordTools };
package/dist/index.js CHANGED
@@ -78,6 +78,8 @@ import { readVapidSettings, vapidPrivateKeyVariable, vapidPublicKeyVariable, vap
78
78
  import { webPushSender } from "./notifications/web-push/web-push-sender.js";
79
79
  import { notificationSenders } from "./notifications/senders-of.js";
80
80
  import { NotificationSendError, maxMessageBytes, maxRecipients } from "./notifications/send-input.js";
81
+ import { onDoNotDisturb } from "./status/do-not-disturb.js";
82
+ import { statusNames } from "./status/options.js";
81
83
  import { errorReportingSettings, errorReportingVariable } from "./error-reporting/error-reporting-settings.js";
82
84
  import { storageCleanupSchedule } from "./files/cleanup-schedule.js";
83
85
  import { limitedFileStore, storeUsage } from "./files/limits.js";
@@ -113,6 +115,7 @@ import { channelSendsMigration, checkChannelSends } from "./channels/sends-schem
113
115
  import { checkAi } from "./ai/check-ai.js";
114
116
  import { checkNotificationsSchema, notificationsMigration } from "./notifications/schema.js";
115
117
  import { sendNotification } from "./notifications/send-notification.js";
118
+ import { checkStatusSchema, statusMigration } from "./status/schema.js";
116
119
  import { maxWidgetPropsBytes, widget } from "./assistant/widget.js";
117
120
  import { checkQueryRunner, queryRunnerMigration } from "./assistant/query-runner/query-runner.js";
118
121
  import { startErrorReporting } from "./error-reporting/error-reporter.js";
@@ -128,4 +131,4 @@ import { enqueueJob } from "./jobs/declared-job.js";
128
131
  import { defineEmail } from "./mail/define-email.js";
129
132
  import { startJobs } from "./jobs/runtime.js";
130
133
  import { jobsDrainApp } from "./jobs/drain-route.js";
131
- export { ConnectionsBusy, DeviceStoreError, FatalJobError, HistoryStoreError, HttpProblem, MissingCredentials, ModelError, NotificationSendError, PushMessageError, addAppRoleToOwners, addCommentTool, addMember, apiKeyCaller, apiKeySettings, apiKeyVariables, appNameOver, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaDrift, authSchemaMigration, authStoreTables, backgroundSettled, badRequest, batchWriteTool, beforeSignInPluginsVariable, betterAuthTokens, capSocketMessages, changesMigration, channelSendsMigration, checkAi, checkAuthSchema, checkChanges, checkChannelSends, checkComments, checkComputed, checkJobsSchema, checkNotificationsSchema, checkQueryRunner, checkRealtimeRelay, checkResourcesOutsideAuthStore, checkSourceNames, checkViewQueries, checkWebhooksSchema, coalesce, commentTools, commentsMigration, computedMigration, configExports, consoleAuditQueue, conversationKey, countRecordsTool, createAdmin, createConversations, createEncryptionTools, createFileCleanup, createOrganization, createRecordTool, createUser, defaultAppName, defaultHistoryPage, defaultSignInPolicy, defineEmail, defineFunction, defineJob, defineTool, deleteRecordTool, deleteUser, describeResourceTool, deviceIdParam, deviceStore, devicesPath, enqueueJob, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, generateVapidKeys, getRecordTool, goTruePath, handleDroppedConnections, hasUsers, historyIdParam, historyIdPattern, historyLimits, historyPath, historyStore, honorSocketMessageCaps, imageSize, isBackgroundWork, isCaptured, isJobDefinition, isOAuthWellKnownPath, isSchedule, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, jwtVerifier, limitedFileStore, listCommentsTool, listOrganizations, listRecordsTool, listResourcesTool, listUsers, localFileStore, longQueryTimeoutMs, mapSettings, mapStyleUrl, mapStyleVariable, maxDevicesPerUser, maxHistoryPage, maxMessageBytes, maxRecipients, maxTokenTtlSeconds, maxTtlSeconds, maxUnreadCount, maxWidgetPropsBytes, mcpApprovals, mcpGate, mcpHandler, mcpIcons, mcpInstructions, mcpRoutes, mcpServer, mcpToolContext, mcpTools, migrationCommand, newWebhookSecret, normalizeRoles, notFound, notificationSenders, notificationsMigration, oauthWellKnownPaths, objectKey, parseColumns, parseRoles, passwordLength, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, pngIcons, poolWaitMs, postgresDeviceStore, postgresHistoryStore, processor, pushMessage, queryRunnerMigration, readDbConnections, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readRecordTools, readVapidSettings, readWriteQueryTool, readableResources, realtimeLimits, realtimeRelayMigration, recordOf, refusalOf, refused, relayed, removeDevicesWithUsers, removeHistoryWithUsers, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runInBackground, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, seesAssistant, seesRuntime, sendNotification, sessionOf, setMemberRoles, setUserBanned, setUserRole, shownAppName, signInPolicyMethods, socketMessageCapOf, sourceOptions, sourcesFor, sourcesVariable, sqlSends, startErrorReporting, startJobs, storageCleanupSchedule, storageProvider, storeFile, storeUsage, streamChatCompletion, subscription, supabasePath, thumbHash, unauthorized, updateRecordTool, urgencies, vapidPrivateKeyVariable, vapidPublicKeyVariable, vapidSubjectVariable, variantPath, verifySignature, verifyStandardWebhook, waitUntil, wantsLongQueries, webManifestPath, webPushSender, webauthnPath, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps, writeRecordTools };
134
+ export { ConnectionsBusy, DeviceStoreError, FatalJobError, HistoryStoreError, HttpProblem, MissingCredentials, ModelError, NotificationSendError, PushMessageError, addAppRoleToOwners, addCommentTool, addMember, apiKeyCaller, apiKeySettings, apiKeyVariables, appNameOver, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaDrift, authSchemaMigration, authStoreTables, backgroundSettled, badRequest, batchWriteTool, beforeSignInPluginsVariable, betterAuthTokens, capSocketMessages, changesMigration, channelSendsMigration, checkAi, checkAuthSchema, checkChanges, checkChannelSends, checkComments, checkComputed, checkJobsSchema, checkNotificationsSchema, checkQueryRunner, checkRealtimeRelay, checkResourcesOutsideAuthStore, checkSourceNames, checkStatusSchema, checkViewQueries, checkWebhooksSchema, coalesce, commentTools, commentsMigration, computedMigration, configExports, consoleAuditQueue, conversationKey, countRecordsTool, createAdmin, createConversations, createEncryptionTools, createFileCleanup, createOrganization, createRecordTool, createUser, defaultAppName, defaultHistoryPage, defaultSignInPolicy, defineEmail, defineFunction, defineJob, defineTool, deleteRecordTool, deleteUser, describeResourceTool, deviceIdParam, deviceStore, devicesPath, enqueueJob, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, generateVapidKeys, getRecordTool, goTruePath, handleDroppedConnections, hasUsers, historyIdParam, historyIdPattern, historyLimits, historyPath, historyStore, honorSocketMessageCaps, imageSize, isBackgroundWork, isCaptured, isJobDefinition, isOAuthWellKnownPath, isSchedule, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, jwtVerifier, limitedFileStore, listCommentsTool, listOrganizations, listRecordsTool, listResourcesTool, listUsers, localFileStore, longQueryTimeoutMs, mapSettings, mapStyleUrl, mapStyleVariable, maxDevicesPerUser, maxHistoryPage, maxMessageBytes, maxRecipients, maxTokenTtlSeconds, maxTtlSeconds, maxUnreadCount, maxWidgetPropsBytes, mcpApprovals, mcpGate, mcpHandler, mcpIcons, mcpInstructions, mcpRoutes, mcpServer, mcpToolContext, mcpTools, migrationCommand, newWebhookSecret, normalizeRoles, notFound, notificationSenders, notificationsMigration, oauthWellKnownPaths, objectKey, onDoNotDisturb, parseColumns, parseRoles, passwordLength, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, pngIcons, poolWaitMs, postgresDeviceStore, postgresHistoryStore, processor, pushMessage, queryRunnerMigration, readDbConnections, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readRecordTools, readVapidSettings, readWriteQueryTool, readableResources, realtimeLimits, realtimeRelayMigration, recordOf, refusalOf, refused, relayed, removeDevicesWithUsers, removeHistoryWithUsers, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runInBackground, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, seesAssistant, seesRuntime, sendNotification, sessionOf, setMemberRoles, setUserBanned, setUserRole, shownAppName, signInPolicyMethods, socketMessageCapOf, sourceOptions, sourcesFor, sourcesVariable, sqlSends, startErrorReporting, startJobs, statusMigration, statusNames, storageCleanupSchedule, storageProvider, storeFile, storeUsage, streamChatCompletion, subscription, supabasePath, thumbHash, unauthorized, updateRecordTool, urgencies, vapidPrivateKeyVariable, vapidPublicKeyVariable, vapidSubjectVariable, variantPath, verifySignature, verifyStandardWebhook, waitUntil, wantsLongQueries, webManifestPath, webPushSender, webauthnPath, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps, writeRecordTools };
@@ -1,5 +1,6 @@
1
1
  import { checkNotificationsOptions } from "./notifications/options.js";
2
2
  import { checkCallsOptions } from "./calls/calls-options.js";
3
+ import { checkStatusOptions } from "./status/options.js";
3
4
  import { isSubscription } from "./webhooks/subscription.js";
4
5
  import { appPool, longQueryPool } from "./app-pool.js";
5
6
  import { AppAuthInput, appAuthOptions, buildAppAuth } from "./better-auth/auth-settings.js";
@@ -11,4 +12,4 @@ import { fileProviderOf } from "./files/provider-settings.js";
11
12
  import { declaredJob } from "./jobs/declared-job.js";
12
13
  import { AuthPlugins, buildAuth, createAuth, loadAuthPlugins } from "./better-auth/create-auth.js";
13
14
  import { webhooksModule } from "./webhooks/table/webhooks-module.js";
14
- export { type AppAuthInput, type AuthPlugins, appAuthOptions, appPool, betterAuthAuthenticator, buildAppAuth, buildAuth, buildAuthenticator, checkCallsOptions, checkNotificationsOptions, createAuth, declaredJob, fileProviderOf, isSubscription, loadAuthPlugins, localFiles, longQueryPool, s3Files, webhooksModule };
15
+ export { type AppAuthInput, type AuthPlugins, appAuthOptions, appPool, betterAuthAuthenticator, buildAppAuth, buildAuth, buildAuthenticator, checkCallsOptions, checkNotificationsOptions, checkStatusOptions, createAuth, declaredJob, fileProviderOf, isSubscription, loadAuthPlugins, localFiles, longQueryPool, s3Files, webhooksModule };
package/dist/internal.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { buildAuth, createAuth, loadAuthPlugins } from "./better-auth/create-auth.js";
2
2
  import { isSubscription } from "./webhooks/subscription.js";
3
3
  import { checkNotificationsOptions } from "./notifications/options.js";
4
+ import { checkStatusOptions } from "./status/options.js";
4
5
  import { localFiles } from "./files/local-files.js";
5
6
  import { s3Files } from "./files/s3-files.js";
6
7
  import { appPool, longQueryPool } from "./app-pool.js";
@@ -11,4 +12,4 @@ import { appAuthOptions, buildAppAuth } from "./better-auth/auth-settings.js";
11
12
  import { buildAuthenticator } from "./better-auth/authenticate-settings.js";
12
13
  import { webhooksModule } from "./webhooks/table/webhooks-module.js";
13
14
  import { checkCallsOptions } from "./calls/calls-options.js";
14
- export { appAuthOptions, appPool, betterAuthAuthenticator, buildAppAuth, buildAuth, buildAuthenticator, checkCallsOptions, checkNotificationsOptions, createAuth, declaredJob, fileProviderOf, isSubscription, loadAuthPlugins, localFiles, longQueryPool, s3Files, webhooksModule };
15
+ export { appAuthOptions, appPool, betterAuthAuthenticator, buildAppAuth, buildAuth, buildAuthenticator, checkCallsOptions, checkNotificationsOptions, checkStatusOptions, createAuth, declaredJob, fileProviderOf, isSubscription, loadAuthPlugins, localFiles, longQueryPool, s3Files, webhooksModule };
package/dist/meta.d.ts CHANGED
@@ -59,6 +59,8 @@ export type CallerMeta = {
59
59
  bell?: true;
60
60
  webPushKey?: string;
61
61
  };
62
+ /** With `status: {}`: people set their own status, and see the statuses of those they share an organization with. */
63
+ status?: true;
62
64
  /** The language the admin app's screens are in, when the app sets one for everyone. */
63
65
  locale?: Locale;
64
66
  /** The hosts besides the app's own whose images the admin app draws with the page's referrer, when there are any. */
package/dist/meta.js CHANGED
@@ -97,6 +97,7 @@ const createMeta = (deps, appName, appIcon) => {
97
97
  ...deps.bell && { bell: true },
98
98
  ...deps.webPushKey && { webPushKey: deps.webPushKey }
99
99
  } },
100
+ ...deps.status && { status: true },
100
101
  ...deps.locale && { locale: deps.locale },
101
102
  ...deps.files && deps.files.trustedHosts.length > 0 && { files: { trustedHosts: deps.files.trustedHosts } },
102
103
  ...plugins.length > 0 && { plugins },
@@ -5,8 +5,8 @@ import { forbidden } from "../problem.js";
5
5
  * Refused: visitors, outside apps acting for the user (an MCP client), and every session whose authenticator does not
6
6
  * mark it as the user's own sign-in (`Session.signIn`), staff signed in as the user included.
7
7
  */
8
- const ownSignInUser = (session) => {
9
- const refuse = () => forbidden("own-sign-in-only", "Only someone signed in as themselves can reach their devices and notifications");
8
+ const ownSignInUser = (session, what = "reach their devices and notifications") => {
9
+ const refuse = () => forbidden("own-sign-in-only", `Only someone signed in as themselves can ${what}`);
10
10
  if (session.anonymous || session.client || session.signIn?.staff !== false) throw refuse();
11
11
  const userId = String(session.user.id);
12
12
  if (userId === "" || session.user.id === void 0 || session.user.id === null) throw refuse();
@@ -16,7 +16,8 @@ const listed = (counts) => [...counts].map(([method, count]) => `${count} in ${m
16
16
  * One try of a send, resuming from `given`: for a user send, each user's history entry when the send is for the inbox
17
17
  * (once, as `record` marks it), then the user's devices as the store lists them now; for a device send, the devices
18
18
  * with those ids now. A device the store gives that is not the user's, or not one asked for, gets nothing. With
19
- * `liveSessions`, a device whose session no longer stands is removed instead of pushed to. Each kind's
19
+ * `liveSessions`, a device whose session no longer stands is removed instead of pushed to; with `quiet`, a device of a
20
+ * user on do not disturb gets nothing, now or on a later try, though the history keeps the entry. Each kind's
20
21
  * devices go to its sender in one batch; a kind without a sender is skipped. An answer of `gone` removes the device,
21
22
  * `retry` leaves it to the next try once and gives up on it the second time, and any other is done. A sender that
22
23
  * throws leaves its batch to the next try. The try then throws, counting what is left, so the job retries it, or
@@ -70,8 +71,19 @@ const deliver = async ({ to, message }, given, steps) => {
70
71
  }
71
72
  return pending.filter(stands);
72
73
  };
74
+ const undisturbed = async (devices) => {
75
+ const { quiet } = steps;
76
+ const pending = devices.filter((device) => !done.has(device.id));
77
+ if (!quiet || pending.length === 0) return pending;
78
+ const held = await quiet([...new Set(pending.map((device) => device.userId))]);
79
+ const kept = pending.filter((device) => !held.has(device.userId));
80
+ if (kept.length === pending.length) return kept;
81
+ for (const device of pending.filter((device) => held.has(device.userId))) markDone(device.id);
82
+ await steps.save(progress);
83
+ return kept;
84
+ };
73
85
  const push = async (given) => {
74
- const devices = await standing(given);
86
+ const devices = await undisturbed(await standing(given));
75
87
  for (const device of devices) if (!steps.senders.some((sender) => sender.kinds.includes(device.kind))) markDone(device.id);
76
88
  for (const sender of steps.senders) {
77
89
  const batch = devices.filter((device) => sender.kinds.includes(device.kind) && !done.has(device.id));
@@ -26,11 +26,12 @@ const recordOnce = (db, options, job) => async (userId, entry, progress) => {
26
26
  };
27
27
  /**
28
28
  * The job that delivers what `sendNotification` sends, through `senders` (guarded, see `notificationSenders`) and the
29
- * stores `options` configure; with `liveSessions`, as with Protobase's auth, only to devices whose session stands. A
29
+ * stores `options` configure; with `liveSessions`, as with Protobase's auth, only to devices whose session stands, and
30
+ * with `quiet`, as with `status`, to no device of a user on do not disturb. A
30
31
  * try resumes where the last one stopped (see `deliver`); once delivered, the job keeps no message. A few tries only: a
31
32
  * device that keeps asking for a retry is given up on the second time.
32
33
  */
33
- const pushJob = (options, senders, liveSessions) => defineJob({
34
+ const pushJob = (options, senders, liveSessions, quiet) => defineJob({
34
35
  name: pushJobName,
35
36
  payload: pushPayload,
36
37
  maxAttempts: 5,
@@ -44,7 +45,8 @@ const pushJob = (options, senders, liveSessions) => defineJob({
44
45
  save: (progress) => saveJobProgress(db, job, progress),
45
46
  hold: () => holdJob(db, job),
46
47
  complete: () => replaceJobData(db, job, { complete: true }),
47
- ...liveSessions && { liveSessions }
48
+ ...liveSessions && { liveSessions },
49
+ ...quiet && { quiet: (userIds) => quiet(db, userIds) }
48
50
  });
49
51
  }
50
52
  });
@@ -1,7 +1,7 @@
1
1
  import { migrationCommand } from "../changes/capture-schema.js";
2
2
  import { historyNames, notificationsNames } from "./options.js";
3
- import { storeSchema } from "../better-auth/store/store-names.js";
4
3
  import { deviceSessionColumnSql, devicesTableSql, qualified } from "./schema-sql.js";
4
+ import { storeSchema } from "../better-auth/store/store-names.js";
5
5
  import { historyTableSql } from "./history-sql.js";
6
6
  import { sql } from "kysely";
7
7
  //#region src/notifications/schema.ts
@@ -11,9 +11,12 @@ const authUsersTable = (auth) => ({
11
11
  table: auth.options.user?.modelName ?? "user"
12
12
  });
13
13
  const regclass = async (db, name) => (await sql`select to_regclass(${name})::oid as oid`.execute(db)).rows[0].oid;
14
- const userIdType = async (db, users) => {
14
+ const userIdType = async (db, users, of = {
15
+ feature: "notifications",
16
+ rows: "devices"
17
+ }) => {
15
18
  const oid = await regclass(db, qualified(users));
16
- if (oid === null) throw new Error(`notifications: auth's users table ${qualified(users)} is not in the app's database, so devices cannot point at it`);
19
+ if (oid === null) throw new Error(`${of.feature}: auth's users table ${qualified(users)} is not in the app's database, so ${of.rows} cannot point at it`);
17
20
  const { rows } = await sql`select format_type(atttypid, atttypmod) as type from pg_attribute where attrelid = ${oid} and attname = 'id'`.execute(db);
18
21
  return rows[0].type;
19
22
  };
@@ -89,4 +92,4 @@ const checkNotificationsSchema = async (db, options = {}, auth) => {
89
92
  await checkHistoryTable(db, options, users);
90
93
  };
91
94
  //#endregion
92
- export { authUsersTable, checkNotificationsSchema, notificationsMigration };
95
+ export { authUsersTable, checkNotificationsSchema, notificationsMigration, regclass, userIdType };
package/dist/package.js CHANGED
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "0.59.2";
2
+ var version = "0.59.3";
3
3
  //#endregion
4
4
  export { version };
@@ -0,0 +1,9 @@
1
+ import { StatusNames } from "./options.js";
2
+ import { Db } from "@protobase/query";
3
+ //#region src/status/do-not-disturb.d.ts
4
+ /**
5
+ * Which of these people are on do not disturb now: their status says so and its clear time, if any, is still to come.
6
+ * The one check of everything that would alert someone, such as a push, so each holds it alike.
7
+ */
8
+ export declare const onDoNotDisturb: (db: Db, names: StatusNames, userIds: readonly string[]) => Promise<Set<string>>;
9
+ //#endregion
@@ -0,0 +1,15 @@
1
+ import { qualified } from "../notifications/schema-sql.js";
2
+ import { sql } from "kysely";
3
+ //#region src/status/do-not-disturb.ts
4
+ /**
5
+ * Which of these people are on do not disturb now: their status says so and its clear time, if any, is still to come.
6
+ * The one check of everything that would alert someone, such as a push, so each holds it alike.
7
+ */
8
+ const onDoNotDisturb = async (db, names, userIds) => {
9
+ if (userIds.length === 0) return /* @__PURE__ */ new Set();
10
+ const { rows } = await sql`select user_id from ${sql.raw(qualified(names))}
11
+ where user_id in (${sql.join([...new Set(userIds)])}) and availability = 'do_not_disturb' and (clears_at is null or clears_at > now())`.execute(db);
12
+ return new Set(rows.map((row) => String(row.user_id)));
13
+ };
14
+ //#endregion
15
+ export { onDoNotDisturb };
@@ -0,0 +1,15 @@
1
+ //#region src/status/options.d.ts
2
+ /** People's own status, turned on with `status: {}` in `protobase.config.ts`; it needs auth with organizations. */
3
+ export type StatusOptions = {
4
+ /** The schema of the statuses table; default `status`. */
5
+ schema?: string;
6
+ };
7
+ /** The statuses table's names, checked: a bad schema name is an error naming the option. */
8
+ export declare const statusNames: (options?: StatusOptions) => {
9
+ schema: string;
10
+ table: string;
11
+ };
12
+ export type StatusNames = ReturnType<typeof statusNames>;
13
+ /** The options, checked as `status` takes them. */
14
+ export declare const checkStatusOptions: (options: StatusOptions) => StatusOptions;
15
+ //#endregion
@@ -0,0 +1,25 @@
1
+ //#region src/status/options.ts
2
+ const namePattern = /^[a-z_][a-z0-9_]*$/;
3
+ const maxNameBytes = 63;
4
+ /** The statuses table's names, checked: a bad schema name is an error naming the option. */
5
+ const statusNames = (options = {}) => {
6
+ const schema = options.schema ?? "status";
7
+ if (typeof schema !== "string" || !namePattern.test(schema) || Buffer.byteLength(schema) > maxNameBytes) throw new Error(`status schema is not a valid name: use lowercase letters, digits and "_", at most ${maxNameBytes} characters`);
8
+ return {
9
+ schema,
10
+ table: "statuses"
11
+ };
12
+ };
13
+ /** The options, checked as `status` takes them. */
14
+ const checkStatusOptions = (options) => {
15
+ const unknown = Object.keys(options).find((key) => key !== "schema");
16
+ if (unknown !== void 0) throw new Error(`status takes no option "${unknown}": only schema`);
17
+ statusNames(options);
18
+ return options;
19
+ };
20
+ /** Status shows people to the members of their organizations, so an app without organizations cannot turn it on. */
21
+ const checkStatusAuth = (auth) => {
22
+ if (!auth?.organizations) throw new Error("status: {} shows people to the members of their organizations: turn organizations on in the auth config, or remove status");
23
+ };
24
+ //#endregion
25
+ export { checkStatusAuth, checkStatusOptions, statusNames };
@@ -0,0 +1,38 @@
1
+ import { ownSignInUser } from "../notifications/caller.js";
2
+ import { jsonBody, jsonOnly, limitedBody } from "../notifications/history-request.js";
3
+ import { statusInput } from "./status-input.js";
4
+ import { clearStatus, ownStatus, setStatus, visibleStatuses } from "./status-store.js";
5
+ import { Hono } from "hono";
6
+ //#region src/status/routes.ts
7
+ const noStore = { "Cache-Control": "no-store" };
8
+ const viewerOf = (session) => session.anonymous || session.client ? void 0 : String(session.user.id);
9
+ /**
10
+ * The status endpoints, mounted at `statusPath` behind the API's authenticator: `GET /` the caller's own status,
11
+ * `PUT /` sets it, `DELETE /` clears it, and `GET /people` the statuses of everyone who shares an organization with
12
+ * the caller. No request names whose status it sets: the person comes from the session alone, and only someone
13
+ * signed in as themselves sets theirs, never staff signed in as them (see `ownSignInUser`).
14
+ */
15
+ const statusRoutes = (db, tables) => {
16
+ const app = new Hono();
17
+ app.get("/", async (c) => {
18
+ const viewer = viewerOf(c.get("session"));
19
+ const status = viewer === void 0 ? void 0 : await ownStatus(db, tables, viewer);
20
+ return c.json({ status: status ?? null }, 200, noStore);
21
+ });
22
+ app.put("/", jsonOnly, limitedBody, async (c) => {
23
+ const userId = ownSignInUser(c.get("session"), "set their status");
24
+ await setStatus(db, tables, userId, statusInput(await jsonBody(c)));
25
+ return c.json({ status: await ownStatus(db, tables, userId) ?? null }, 200, noStore);
26
+ });
27
+ app.delete("/", async (c) => {
28
+ await clearStatus(db, tables, ownSignInUser(c.get("session"), "set their status"));
29
+ return c.body(null, 204);
30
+ });
31
+ app.get("/people", async (c) => {
32
+ const viewer = viewerOf(c.get("session"));
33
+ return c.json({ statuses: viewer === void 0 ? [] : await visibleStatuses(db, tables, viewer) }, 200, noStore);
34
+ });
35
+ return app;
36
+ };
37
+ //#endregion
38
+ export { statusRoutes };
@@ -0,0 +1,19 @@
1
+ import { quoteIdent } from "../changes/capture-sql.js";
2
+ import { qualified } from "../notifications/schema-sql.js";
3
+ //#region src/status/schema-sql.ts
4
+ /**
5
+ * The statuses table: one row per person who set a status, which goes with the user. A custom text or a clear time
6
+ * not set is NULL, and nothing has a default. Clearing a status deletes its row.
7
+ */
8
+ const statusesTableSql = ({ schema, table }, user) => [`create schema if not exists ${quoteIdent(schema)}`, `create table ${qualified({
9
+ schema,
10
+ table
11
+ })} (
12
+ user_id ${user.type} primary key references ${user.references} on delete cascade,
13
+ availability text not null check (availability in ('automatic', 'busy', 'do_not_disturb')),
14
+ text text check (char_length(text) between 1 and 100),
15
+ clears_at timestamptz,
16
+ updated_at timestamptz not null
17
+ )`];
18
+ //#endregion
19
+ export { statusesTableSql };
@@ -0,0 +1,12 @@
1
+ import { StatusOptions } from "./options.js";
2
+ import { AdminAuth } from "../better-auth/create-auth.js";
3
+ import { Db } from "@protobase/query";
4
+ //#region src/status/schema.d.ts
5
+ /**
6
+ * The SQL that adds the statuses table `status` keeps people's statuses in, its user column pointing at auth's users
7
+ * table; `undefined` when it exists. An app without organizations is refused (see `checkStatusAuth`).
8
+ */
9
+ export declare const statusMigration: (db: Db, options: StatusOptions, auth: AdminAuth | undefined) => Promise<string | undefined>;
10
+ /** Refuses to start while the statuses table is missing, or the app has no organizations. */
11
+ export declare const checkStatusSchema: (db: Db, options: StatusOptions, auth: AdminAuth | undefined) => Promise<void>;
12
+ //#endregion
@@ -0,0 +1,33 @@
1
+ import { migrationCommand } from "../changes/capture-schema.js";
2
+ import { qualified } from "../notifications/schema-sql.js";
3
+ import { checkStatusAuth, statusNames } from "./options.js";
4
+ import { authUsersTable, regclass, userIdType } from "../notifications/schema.js";
5
+ import { statusesTableSql } from "./schema-sql.js";
6
+ //#region src/status/schema.ts
7
+ /**
8
+ * The SQL that adds the statuses table `status` keeps people's statuses in, its user column pointing at auth's users
9
+ * table; `undefined` when it exists. An app without organizations is refused (see `checkStatusAuth`).
10
+ */
11
+ const statusMigration = async (db, options, auth) => {
12
+ checkStatusAuth(auth);
13
+ const names = statusNames(options);
14
+ if (await regclass(db, qualified(names)) !== null) return;
15
+ const users = authUsersTable(auth);
16
+ const type = await userIdType(db, users, {
17
+ feature: "status",
18
+ rows: "statuses"
19
+ });
20
+ return `${statusesTableSql(names, {
21
+ type,
22
+ references: `${qualified(users)} (id)`
23
+ }).join(";\n\n")};`;
24
+ };
25
+ /** Refuses to start while the statuses table is missing, or the app has no organizations. */
26
+ const checkStatusSchema = async (db, options, auth) => {
27
+ checkStatusAuth(auth);
28
+ const table = qualified(statusNames(options));
29
+ if (await regclass(db, table) !== null) return;
30
+ throw new Error(`The statuses table ${table} of status is missing. Write it as a migration with \`${migrationCommand}\`, apply it with the project's migrations, then start again.`);
31
+ };
32
+ //#endregion
33
+ export { checkStatusSchema, statusMigration };
@@ -0,0 +1,33 @@
1
+ import { badRequest } from "../problem.js";
2
+ import { availabilities, statusTextProblem } from "@protobase/schema/internal";
3
+ import { z } from "zod";
4
+ //#region src/status/status-input.ts
5
+ const body = z.strictObject({
6
+ availability: z.enum(availabilities),
7
+ text: z.string().nullable(),
8
+ clearsAt: z.iso.datetime({ offset: true }).nullable()
9
+ });
10
+ /**
11
+ * The status a request sets, checked: an availability, a one-line text of at most `maxStatusText` characters, kept
12
+ * trimmed, an empty one as none, and a clear time to come or `null` for never. Anything else, a user named in it
13
+ * too, is a 400 that says what is wrong.
14
+ */
15
+ const statusInput = (given, now = /* @__PURE__ */ new Date()) => {
16
+ const parsed = body.safeParse(given);
17
+ if (!parsed.success) {
18
+ const issue = parsed.error.issues[0];
19
+ throw badRequest("invalid-status", `Send { availability, text, clearsAt }: ${issue.path.join(".") || "the body"} ${issue.message.toLowerCase()}`);
20
+ }
21
+ const { availability, clearsAt } = parsed.data;
22
+ const text = parsed.data.text?.trim() || null;
23
+ const problem = text === null ? void 0 : statusTextProblem(text);
24
+ if (problem) throw badRequest("invalid-status-text", problem);
25
+ if (clearsAt !== null && Date.parse(clearsAt) <= now.getTime()) throw badRequest("invalid-clear-time", "A status clears at a time still to come, or never");
26
+ return {
27
+ availability,
28
+ text,
29
+ clearsAt: clearsAt === null ? null : new Date(clearsAt).toISOString()
30
+ };
31
+ };
32
+ //#endregion
33
+ export { statusInput };
@@ -0,0 +1,39 @@
1
+ import { qualified } from "../notifications/schema-sql.js";
2
+ import { sql } from "kysely";
3
+ //#region src/status/status-store.ts
4
+ const statusOf = (row) => ({
5
+ userId: String(row.user_id),
6
+ availability: row.availability,
7
+ text: row.text,
8
+ clearsAt: row.clears_at?.toISOString() ?? null
9
+ });
10
+ const holds = sql`(clears_at is null or clears_at > now())`;
11
+ /** The person's own status, as it holds now; `undefined` when they have none. */
12
+ const ownStatus = async (db, tables, userId) => {
13
+ const statuses = sql.raw(qualified(tables.statuses));
14
+ const { rows } = await sql`select user_id, availability, text, clears_at from ${statuses} where user_id = ${userId} and ${holds}`.execute(db);
15
+ return rows[0] && statusOf(rows[0]);
16
+ };
17
+ /** The statuses that hold now of everyone who shares an organization with `viewerId`, their own included when they have one. */
18
+ const visibleStatuses = async (db, tables, viewerId) => {
19
+ const statuses = sql.raw(qualified(tables.statuses));
20
+ const members = sql.raw(qualified(tables.members));
21
+ const { rows } = await sql`select s.user_id, s.availability, s.text, s.clears_at from ${statuses} s
22
+ where ${holds} and exists (select from ${members} own join ${members} other on other.organization_id = own.organization_id
23
+ where own.user_id = ${viewerId} and other.user_id = s.user_id)
24
+ order by s.user_id`.execute(db);
25
+ return rows.map(statusOf);
26
+ };
27
+ /** Sets the person's status, replacing the one they had; one that says nothing, automatic without a text, clears it. */
28
+ const setStatus = async (db, tables, userId, input) => {
29
+ if (input.availability === "automatic" && input.text === null) return clearStatus(db, tables, userId);
30
+ const statuses = sql.raw(qualified(tables.statuses));
31
+ await sql`insert into ${statuses} (user_id, availability, text, clears_at, updated_at) values (${userId}, ${input.availability}, ${input.text}, ${input.clearsAt}, now())
32
+ on conflict (user_id) do update set availability = excluded.availability, text = excluded.text, clears_at = excluded.clears_at, updated_at = excluded.updated_at`.execute(db);
33
+ };
34
+ /** Clears the person's status, whether or not they had one. */
35
+ const clearStatus = async (db, tables, userId) => {
36
+ await sql`delete from ${sql.raw(qualified(tables.statuses))} where user_id = ${userId}`.execute(db);
37
+ };
38
+ //#endregion
39
+ export { clearStatus, ownStatus, setStatus, visibleStatuses };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/server",
3
- "version": "0.59.2",
3
+ "version": "0.59.3",
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": {
@@ -42,9 +42,9 @@
42
42
  "@better-auth/sso": "^1.7.12",
43
43
  "@livekit/protocol": "1.53.1",
44
44
  "@modelcontextprotocol/server": "^2.3.1",
45
- "@protobase/layout": "0.59.2",
46
- "@protobase/query": "0.59.2",
47
- "@protobase/schema": "0.59.2",
45
+ "@protobase/layout": "0.59.3",
46
+ "@protobase/query": "0.59.3",
47
+ "@protobase/schema": "0.59.3",
48
48
  "@react-email/render": "^2.1.0",
49
49
  "@scalar/hono-api-reference": "^0.12.10",
50
50
  "@sentry/core": "^11.7.0",