@protobase/server 0.54.0 → 0.54.2

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.
@@ -2,6 +2,7 @@ import { UserColumn } from "./user-table.js";
2
2
  import { Mailer } from "../mail/smtp-mailer.js";
3
3
  import { SignInPolicy } from "./sign-in-policy.js";
4
4
  import { StaffSignIn } from "./staff-log.js";
5
+ import { Person } from "./organizations/people-endpoint.js";
5
6
  import { RoleDefinitions } from "./organizations/role-definitions.js";
6
7
  import { ConnectedApp } from "./mcp/connected-apps.js";
7
8
  import { ReplacingContext } from "./end-replaced-session.js";
@@ -5222,6 +5223,34 @@ export declare const buildAppAuth: (settings: AuthSettings, input: AppAuthInput)
5222
5223
  }> | {
5223
5224
  id: "protobase-organizations";
5224
5225
  endpoints: {
5226
+ peopleEndpoint: import("better-call").StrictEndpoint<"/organization/people", {
5227
+ method: "GET";
5228
+ use: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<{
5229
+ session: {
5230
+ session: Record<string, any> & {
5231
+ id: string;
5232
+ createdAt: Date;
5233
+ updatedAt: Date;
5234
+ userId: string;
5235
+ expiresAt: Date;
5236
+ token: string;
5237
+ ipAddress?: string | null | undefined;
5238
+ userAgent?: string | null | undefined;
5239
+ };
5240
+ user: Record<string, any> & {
5241
+ id: string;
5242
+ createdAt: Date;
5243
+ updatedAt: Date;
5244
+ email: string;
5245
+ emailVerified: boolean;
5246
+ name: string;
5247
+ image?: string | null | undefined;
5248
+ };
5249
+ };
5250
+ }>>[];
5251
+ }, {
5252
+ people: Person[];
5253
+ }>;
5225
5254
  setAppRoles: import("better-call").StrictEndpoint<"/organization/set-app-roles", {
5226
5255
  method: "POST";
5227
5256
  use: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<{
@@ -2,6 +2,7 @@ import { UserColumn, UserTableOptions } from "./user-table.js";
2
2
  import { Mailer } from "../mail/smtp-mailer.js";
3
3
  import { SignInPolicy } from "./sign-in-policy.js";
4
4
  import { StaffSignIn } from "./staff-log.js";
5
+ import { Person } from "./organizations/people-endpoint.js";
5
6
  import { RoleDefinitions, RolesInput } from "./organizations/role-definitions.js";
6
7
  import { ConnectedApp } from "./mcp/connected-apps.js";
7
8
  import { ReplacingContext } from "./end-replaced-session.js";
@@ -5341,6 +5342,34 @@ export declare const createAuth: (options: CreateAuthOptions, plugins?: AuthPlug
5341
5342
  }> | {
5342
5343
  id: "protobase-organizations";
5343
5344
  endpoints: {
5345
+ peopleEndpoint: import("better-call").StrictEndpoint<"/organization/people", {
5346
+ method: "GET";
5347
+ use: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<{
5348
+ session: {
5349
+ session: Record<string, any> & {
5350
+ id: string;
5351
+ createdAt: Date;
5352
+ updatedAt: Date;
5353
+ userId: string;
5354
+ expiresAt: Date;
5355
+ token: string;
5356
+ ipAddress?: string | null | undefined;
5357
+ userAgent?: string | null | undefined;
5358
+ };
5359
+ user: Record<string, any> & {
5360
+ id: string;
5361
+ createdAt: Date;
5362
+ updatedAt: Date;
5363
+ email: string;
5364
+ emailVerified: boolean;
5365
+ name: string;
5366
+ image?: string | null | undefined;
5367
+ };
5368
+ };
5369
+ }>>[];
5370
+ }, {
5371
+ people: Person[];
5372
+ }>;
5344
5373
  setAppRoles: import("better-call").StrictEndpoint<"/organization/set-app-roles", {
5345
5374
  method: "POST";
5346
5375
  use: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<{
@@ -0,0 +1,8 @@
1
+ //#region src/better-auth/organizations/people-endpoint.d.ts
2
+ /** A member as everyone in the organization may see them: their user id, name and photo, never their address. */
3
+ export type Person = {
4
+ id: string;
5
+ name: string;
6
+ image?: string;
7
+ };
8
+ //#endregion
@@ -0,0 +1,48 @@
1
+ import { everyRow, findMember, membersOf } from "./store.js";
2
+ import { APIError, createAuthEndpoint, sessionMiddleware } from "better-auth/api";
3
+ //#region src/better-auth/organizations/people-endpoint.ts
4
+ /** The people of these users, by name; one without a name is shown by nothing else, so is left out. */
5
+ const peopleOf = (users) => users.flatMap((user) => user.name ? [{
6
+ id: user.id,
7
+ name: user.name,
8
+ ...user.image && { image: user.image }
9
+ }] : []).sort((a, b) => a.name.localeCompare(b.name));
10
+ /**
11
+ * `GET /organization/people`: the members of the organization the session works in, for every member of it, as
12
+ * `{ people: Person[] }`, to show authors and offer mentions. The organization comes from the session alone, never
13
+ * from the request, and someone who is not a member of it, a global role included, is refused.
14
+ */
15
+ const peopleEndpoint = createAuthEndpoint("/organization/people", {
16
+ method: "GET",
17
+ use: [sessionMiddleware]
18
+ }, async (ctx) => {
19
+ const { adapter } = ctx.context;
20
+ const { user, session } = ctx.context.session;
21
+ const organizationId = session.activeOrganizationId;
22
+ const member = organizationId ? await findMember(adapter, {
23
+ organizationId,
24
+ userId: user.id
25
+ }) : void 0;
26
+ if (!organizationId || !member) throw new APIError("FORBIDDEN", {
27
+ code: "NOT_A_MEMBER",
28
+ message: "Only the members of the organization you work in can see its people."
29
+ });
30
+ const ids = (await membersOf(adapter, organizationId)).map((row) => row.userId);
31
+ const users = await adapter.findMany({
32
+ model: "user",
33
+ where: [{
34
+ field: "id",
35
+ operator: "in",
36
+ value: ids
37
+ }],
38
+ select: [
39
+ "id",
40
+ "name",
41
+ "image"
42
+ ],
43
+ limit: everyRow
44
+ });
45
+ return ctx.json({ people: peopleOf(users) });
46
+ });
47
+ //#endregion
48
+ export { peopleEndpoint, peopleOf };
@@ -1,17 +1,19 @@
1
1
  import { invitationLinkHook } from "../invitations/invitation-link-hook.js";
2
2
  import { invitationEndpoints } from "./invitation-endpoints.js";
3
3
  import { memberEndpoints } from "./member-endpoints.js";
4
+ import { peopleEndpoint } from "./people-endpoint.js";
4
5
  //#region src/better-auth/organizations/protobase-plugin.ts
5
6
  /**
6
7
  * Protobase's endpoints beside Better Auth's organization plugin: accepting invitations signed in, app roles, handing
7
- * ownership over, and finding organizations for people with a global role. Without a mailer, whoever invites gets the
8
- * link in the answer.
8
+ * ownership over, the people of the organization one works in for its members, and finding organizations for people
9
+ * with a global role. Without a mailer, whoever invites gets the link in the answer.
9
10
  */
10
11
  const protobaseOrganizationPlugin = ({ resolved, mail }) => ({
11
12
  id: "protobase-organizations",
12
13
  endpoints: {
13
14
  ...invitationEndpoints({ resolved }),
14
- ...memberEndpoints({ resolved })
15
+ ...memberEndpoints({ resolved }),
16
+ peopleEndpoint
15
17
  },
16
18
  hooks: { after: mail ? [] : [invitationLinkHook(["/organization/invite-member"], resolved.invitationUrl)] }
17
19
  });
@@ -16,7 +16,9 @@ import { apiCors, readCorsOrigins, withMobileOrigins } from "./api-cors.js";
16
16
  import { apiKeySettings } from "./api-keys/api-key-settings.js";
17
17
  import { apiKeyCaller } from "./api-keys/resolve-caller.js";
18
18
  import { jwtVerifier } from "./auth/jwt.js";
19
+ import { relayed } from "./realtime/relay/realtime-options.js";
19
20
  import { realtimeApp } from "./realtime/realtime-app.js";
21
+ import { startRealtimePrune } from "./realtime/realtime-prune.js";
20
22
  import { restRoutes } from "./rest/rest-routes.js";
21
23
  import { storageRoutes } from "./storage/storage-routes.js";
22
24
  import { shownAppName } from "./app-name.js";
@@ -458,13 +460,22 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
458
460
  log: (line) => console.log(line),
459
461
  ...table && { table }
460
462
  });
461
- return Object.assign(app, { jobs: [
462
- ...webhooks.jobs,
463
- ...withHistory && days !== void 0 ? [pruneHistoryJob(changes, days)] : [],
464
- ...mentions ? [mentions] : [],
465
- ...fill ? [fill] : [],
466
- ...notifications ? [pushJob(notifications, senders, auth && liveSessions(auth))] : []
467
- ] });
463
+ const prune = () => startRealtimePrune({
464
+ db,
465
+ statementTimeoutMs: deps.statementTimeoutMs,
466
+ relayed: relayed(realtime),
467
+ report
468
+ });
469
+ return Object.assign(app, {
470
+ startRealtimePrune: prune,
471
+ jobs: [
472
+ ...webhooks.jobs,
473
+ ...withHistory && days !== void 0 ? [pruneHistoryJob(changes, days)] : [],
474
+ ...mentions ? [mentions] : [],
475
+ ...fill ? [fill] : [],
476
+ ...notifications ? [pushJob(notifications, senders, auth && liveSessions(auth))] : []
477
+ ]
478
+ });
468
479
  };
469
480
  //#endregion
470
481
  export { createAdmin };
package/dist/index.d.ts CHANGED
@@ -10,6 +10,7 @@ import { HistoryPosition, NewNotification, StoredNotification, historyIdPattern,
10
10
  import { PushMessage, PushMessageError, Urgency, WebPushFields, maxTtlSeconds, pushMessage, urgencies } from "./notifications/message.js";
11
11
  import { NotificationRecipients, NotificationSendError, SendNotification, maxMessageBytes, maxRecipients } from "./notifications/send-input.js";
12
12
  import { EnqueueOptions, JobContext, JobDefinition, JobOptions } from "./jobs/job-types.js";
13
+ import { RealtimePrune } from "./realtime/realtime-prune.js";
13
14
  import { Admin, AuditEvent, AuditQueue, Authenticator, Membership, PipelineHook, ScanGuardMode, Session, SessionOrganization, TenantValue, User, ViewSource, WriteEvent, WriteOperation } from "./types.js";
14
15
  import { CommentUsers } from "./comments/comment-users.js";
15
16
  import { HistoryOptions } from "./history/options.js";
@@ -171,4 +172,4 @@ import { MobileRelease, ReleaseFile, ServedReleaseFile, composeRelease, releaseM
171
172
  import { MobileReleaseAppInput, OfferedRelease, mobileReleaseApp } from "./mobile-release/release-app.js";
172
173
  import { mobileSigningKeyVariable, readSigningKey } from "./mobile-release/signing-key.js";
173
174
  import { AdminAuth, CreateAuthOptions, UserCreatedHook, normalizeRoles } from "./better-auth/create-auth.js";
174
- 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 BatchOperation, type BatchResult, 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 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 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 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 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, type MobileRelease, type MobileReleaseAppInput, ModelError, type ModelSettings, type NewDevice, type NewNotification, type NewOrganization, type NewUser, type NotificationRecipients, NotificationSendError, type NotificationsOptions, type OfferedRelease, 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 ReasoningDetail, type ReasoningEffort, type RebindOption, type RecordGroups, type RecordKeyInput, type RecordPage, type ReleaseFile, 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 ServedReleaseFile, 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, checkAi, checkAuthSchema, checkChanges, checkComments, checkComputed, checkJobsSchema, checkNotificationsSchema, checkQueryRunner, checkRealtimeRelay, checkResourcesOutsideAuthStore, checkSourceNames, checkViewQueries, checkWebhooksSchema, coalesce, commentTools, commentsMigration, composeRelease, 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, mobileReleaseApp, mobileSigningKeyVariable, 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, readSigningKey, readVapidSettings, readWriteQueryTool, readableResources, realtimeLimits, realtimeRelayMigration, recordOf, relayed, releaseManifest, 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, 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 };
175
+ 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 BatchOperation, type BatchResult, 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 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 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 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 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, type MobileRelease, type MobileReleaseAppInput, ModelError, type ModelSettings, type NewDevice, type NewNotification, type NewOrganization, type NewUser, type NotificationRecipients, NotificationSendError, type NotificationsOptions, type OfferedRelease, 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 RecordKeyInput, type RecordPage, type ReleaseFile, 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 ServedReleaseFile, 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, checkAi, checkAuthSchema, checkChanges, checkComments, checkComputed, checkJobsSchema, checkNotificationsSchema, checkQueryRunner, checkRealtimeRelay, checkResourcesOutsideAuthStore, checkSourceNames, checkViewQueries, checkWebhooksSchema, coalesce, commentTools, commentsMigration, composeRelease, 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, mobileReleaseApp, mobileSigningKeyVariable, 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, readSigningKey, readVapidSettings, readWriteQueryTool, readableResources, realtimeLimits, realtimeRelayMigration, recordOf, relayed, releaseManifest, 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, 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 };
package/dist/package.js CHANGED
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "0.54.0";
2
+ var version = "0.54.2";
3
3
  //#endregion
4
4
  export { version };
@@ -0,0 +1,27 @@
1
+ import { writeTransaction } from "../transactions.js";
2
+ import { messagesTableExists } from "./database-broadcasts.js";
3
+ import { sql } from "kysely";
4
+ //#region src/realtime/broadcast-prune.ts
5
+ /** Broadcast rows deleted in one statement at most. */
6
+ const pruneBatchSize = 1e3;
7
+ /** Runs `deleteBatch` until it deletes fewer rows than `batchSize`, so a backlog drains one short statement at a time. */
8
+ const inBatches = async (batchSize, deleteBatch) => {
9
+ for (;;) if (await deleteBatch(batchSize) < batchSize) return;
10
+ };
11
+ const deleteOldBroadcasts = (db, statementTimeoutMs, limit) => writeTransaction(db, statementTimeoutMs, async (trx) => {
12
+ const { numAffectedRows } = await sql`delete from realtime.messages where id in (
13
+ select id from realtime.messages where extension = 'broadcast' and inserted_at < localtimestamp - interval '48 hours'
14
+ limit ${limit} for update skip locked
15
+ )`.execute(trx);
16
+ return Number(numAffectedRows ?? 0n);
17
+ });
18
+ /**
19
+ * Deletes the broadcast rows of `realtime.messages` more than 48 hours old, which no reader took because no socket was
20
+ * open, in batches of `batchSize`; a database without `realtime.messages` has none.
21
+ */
22
+ const pruneBroadcasts = async (db, statementTimeoutMs, batchSize = pruneBatchSize) => {
23
+ if (!await messagesTableExists(db)) return;
24
+ await inBatches(batchSize, (limit) => deleteOldBroadcasts(db, statementTimeoutMs, limit));
25
+ };
26
+ //#endregion
27
+ export { inBatches, pruneBatchSize, pruneBroadcasts };
@@ -32,7 +32,8 @@ const takeMessages = (db, statementTimeoutMs) => writeTransaction(db, statementT
32
32
  select id::text as id, topic, event, payload, private from taken order by inserted_at`.execute(trx);
33
33
  return rows;
34
34
  });
35
- const tableExists = async (db) => (await sql`select to_regclass('realtime.messages') is not null as found`.execute(db)).rows[0].found;
35
+ /** Whether the database has `realtime.messages`, which an app without Supabase's realtime schema lacks. */
36
+ const messagesTableExists = async (db) => (await sql`select to_regclass('realtime.messages') is not null as found`.execute(db)).rows[0].found;
36
37
  /**
37
38
  * Supabase's broadcast from the database: `realtime.send()` and `realtime.broadcast_changes()` insert a message into
38
39
  * `realtime.messages` in the caller's transaction, so only a committed one is seen, and while a socket of this process
@@ -45,7 +46,7 @@ const databaseBroadcasts = ({ db, statementTimeoutMs, broadcasts, intervalMs = 1
45
46
  let followers = 0;
46
47
  let running = false;
47
48
  const read = async () => {
48
- if (!await tableExists(db)) return;
49
+ if (!await messagesTableExists(db)) return;
49
50
  for (;;) {
50
51
  const rows = await takeMessages(db, statementTimeoutMs);
51
52
  for (const row of rows) {
@@ -80,4 +81,4 @@ follow: () => {
80
81
  } };
81
82
  };
82
83
  //#endregion
83
- export { databaseBroadcasts, publicationOfRow };
84
+ export { databaseBroadcasts, messagesTableExists, publicationOfRow };
@@ -0,0 +1,30 @@
1
+ //#region src/realtime/prune-schedule.ts
2
+ /** How often a serving process prunes the realtime tables. */
3
+ const realtimePruneIntervalMs = 6e5;
4
+ /**
5
+ * Runs each step once now, then every `intervalMs`, never two rounds at once, until `stop`, which waits for a round in
6
+ * progress. A failed step is reported and the steps after it still run; the next round tries it again.
7
+ */
8
+ const pruneSchedule = (steps, report, intervalMs = realtimePruneIntervalMs) => {
9
+ let stopped = false;
10
+ let running;
11
+ const round = async () => {
12
+ for (const step of steps) await step().catch(report);
13
+ };
14
+ const tick = () => {
15
+ if (stopped || running) return;
16
+ running = round().finally(() => {
17
+ running = void 0;
18
+ });
19
+ };
20
+ tick();
21
+ const timer = setInterval(tick, intervalMs);
22
+ timer.unref?.();
23
+ return { stop: async () => {
24
+ stopped = true;
25
+ clearInterval(timer);
26
+ await running;
27
+ } };
28
+ };
29
+ //#endregion
30
+ export { pruneSchedule, realtimePruneIntervalMs };
@@ -0,0 +1,20 @@
1
+ import { Db } from "@protobase/query";
2
+ //#region src/realtime/realtime-prune.d.ts
3
+ export type RealtimePruneOptions = {
4
+ db: Db;
5
+ statementTimeoutMs: number;
6
+ /** Whether the app's processes relay through `realtime_relay`, whose frames and counts are pruned too. */
7
+ relayed: boolean;
8
+ /** Where a failed prune goes; the next one tries again. */
9
+ report: (error: unknown) => void;
10
+ };
11
+ /**
12
+ * Prunes what the realtime tables keep whether or not a socket is open, as on an app that never opens one: the old
13
+ * broadcast rows of `realtime.messages` and, relayed, the relay's old frames and counts, on `pruneSchedule`. It runs
14
+ * only while the process does, so it never wakes a sleeping app.
15
+ */
16
+ export declare const startRealtimePrune: ({ db, statementTimeoutMs, relayed, report }: RealtimePruneOptions) => {
17
+ stop: () => Promise<void>;
18
+ };
19
+ export type RealtimePrune = ReturnType<typeof startRealtimePrune>;
20
+ //#endregion
@@ -0,0 +1,12 @@
1
+ import { pruneFrames } from "./relay/relay-frame-sql.js";
2
+ import { pruneBroadcasts } from "./broadcast-prune.js";
3
+ import { pruneSchedule } from "./prune-schedule.js";
4
+ //#region src/realtime/realtime-prune.ts
5
+ /**
6
+ * Prunes what the realtime tables keep whether or not a socket is open, as on an app that never opens one: the old
7
+ * broadcast rows of `realtime.messages` and, relayed, the relay's old frames and counts, on `pruneSchedule`. It runs
8
+ * only while the process does, so it never wakes a sleeping app.
9
+ */
10
+ const startRealtimePrune = ({ db, statementTimeoutMs, relayed, report }) => pruneSchedule([() => pruneBroadcasts(db, statementTimeoutMs), ...relayed ? [() => pruneFrames(db)] : []], report);
11
+ //#endregion
12
+ export { startRealtimePrune };
package/dist/types.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { JobDefinition } from "./jobs/job-types.js";
2
+ import { RealtimePrune } from "./realtime/realtime-prune.js";
2
3
  import { Hono } from "hono";
3
4
  import { PageModel } from "@protobase/layout";
4
5
  import { KeyType, ResourceModel, ViewModel } from "@protobase/schema";
@@ -135,8 +136,12 @@ export type ViewSource = {
135
136
  export type PageSource = {
136
137
  toModel(): PageModel;
137
138
  };
138
- /** The admin API, and the jobs it needs a worker to run: the delivery of resource events. */
139
+ /**
140
+ * The admin API, the jobs it needs a worker to run (the delivery of resource events), and the pruning of the realtime
141
+ * tables a serving process starts once it listens and stops as it closes.
142
+ */
139
143
  export type Admin = Hono<AdminEnv> & {
140
144
  jobs: JobDefinition[];
145
+ startRealtimePrune: () => RealtimePrune;
141
146
  };
142
147
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/server",
3
- "version": "0.54.0",
3
+ "version": "0.54.2",
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": {
@@ -40,9 +40,9 @@
40
40
  "@better-auth/passkey": "^1.7.12",
41
41
  "@better-auth/sso": "^1.7.12",
42
42
  "@modelcontextprotocol/server": "^2.3.1",
43
- "@protobase/layout": "0.54.0",
44
- "@protobase/query": "0.54.0",
45
- "@protobase/schema": "0.54.0",
43
+ "@protobase/layout": "0.54.2",
44
+ "@protobase/query": "0.54.2",
45
+ "@protobase/schema": "0.54.2",
46
46
  "@react-email/render": "^2.1.0",
47
47
  "@scalar/hono-api-reference": "^0.12.10",
48
48
  "@sentry/core": "^11.7.0",