@protobase/server 0.45.18 → 0.45.19
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.
- package/dist/create-admin.js +4 -2
- package/dist/index.d.ts +6 -4
- package/dist/index.js +3 -1
- package/dist/jobs/current.d.ts +2 -0
- package/dist/jobs/job-progress.js +30 -0
- package/dist/jobs/job-types.d.ts +5 -0
- package/dist/jobs/run-job.d.ts +1 -0
- package/dist/jobs/run-job.js +5 -3
- package/dist/jobs/runtime.js +7 -1
- package/dist/notifications/deliver.js +119 -0
- package/dist/notifications/guarded-sender.js +13 -4
- package/dist/notifications/history.js +2 -1
- package/dist/notifications/push-job-name.js +5 -0
- package/dist/notifications/push-job.js +50 -0
- package/dist/notifications/push-payload.js +60 -0
- package/dist/notifications/send-input.d.ts +28 -0
- package/dist/notifications/send-input.js +80 -0
- package/dist/notifications/send-notification.d.ts +11 -0
- package/dist/notifications/send-notification.js +19 -0
- package/dist/notifications/sender.d.ts +4 -3
- package/dist/notifications/store-call.js +32 -0
- package/dist/notifications/web-push/web-push-sender.d.ts +1 -1
- package/dist/notifications/web-push/web-push-sender.js +2 -2
- package/dist/package.js +1 -1
- package/package.json +4 -4
package/dist/create-admin.js
CHANGED
|
@@ -57,6 +57,7 @@ import { devicesPath, historyPath } from "./notifications/paths.js";
|
|
|
57
57
|
import { historyRoutes } from "./notifications/history-routes.js";
|
|
58
58
|
import { deviceRoutes } from "./notifications/routes.js";
|
|
59
59
|
import { notificationSenders, webPushKey } from "./notifications/senders-of.js";
|
|
60
|
+
import { pushJob } from "./notifications/push-job.js";
|
|
60
61
|
import { visitorsBrowse, withAnonymous } from "./anonymous.js";
|
|
61
62
|
import { checkBottomBars, checkShell } from "./check-shell.js";
|
|
62
63
|
import { checkOwnerFields } from "./check-owners.js";
|
|
@@ -105,7 +106,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
105
106
|
const appModels = appResources.map((resource) => resource.toModel());
|
|
106
107
|
const withComments = appModels.some((model) => model.comments);
|
|
107
108
|
const withAiFields = hasAiFields(appModels);
|
|
108
|
-
const withJobs = jobs.length > 0 || appModels.some(isCaptured) || withComments || withAiFields;
|
|
109
|
+
const withJobs = jobs.length > 0 || appModels.some(isCaptured) || withComments || withAiFields || notifications !== void 0;
|
|
109
110
|
const withHistory = appModels.some((model) => model.history);
|
|
110
111
|
const changes = changesSchema(events);
|
|
111
112
|
const days = historyDays(historyOptions);
|
|
@@ -435,7 +436,8 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
435
436
|
...webhooks.jobs,
|
|
436
437
|
...withHistory && days !== void 0 ? [pruneHistoryJob(changes, days)] : [],
|
|
437
438
|
...mentions ? [mentions] : [],
|
|
438
|
-
...fill ? [fill] : []
|
|
439
|
+
...fill ? [fill] : [],
|
|
440
|
+
...notifications ? [pushJob(notifications, senders)] : []
|
|
439
441
|
] });
|
|
440
442
|
};
|
|
441
443
|
//#endregion
|
package/dist/index.d.ts
CHANGED
|
@@ -13,6 +13,10 @@ import { EmailComponent, EmailOptions, EmailTemplate, RenderedEmail, defineEmail
|
|
|
13
13
|
import { AdminAuth, CreateAuthOptions, UserCreatedHook, createAuth, normalizeRoles } from "./better-auth/create-auth.js";
|
|
14
14
|
import { EventsOptions } from "./changes/events-options.js";
|
|
15
15
|
import { StandardSchema, readJson } from "./functions/read-json.js";
|
|
16
|
+
import { JsonObject, JsonValue } from "./notifications/json-value.js";
|
|
17
|
+
import { HistoryPosition, NewNotification, StoredNotification, historyIdPattern, historyLimits } from "./notifications/history.js";
|
|
18
|
+
import { PushMessage, PushMessageError, Urgency, WebPushFields, maxTtlSeconds, pushMessage, urgencies } from "./notifications/message.js";
|
|
19
|
+
import { NotificationRecipients, NotificationSendError, SendNotification, maxMessageBytes, maxRecipients } from "./notifications/send-input.js";
|
|
16
20
|
import { EnqueueOptions, JobContext, JobDefinition, JobOptions } from "./jobs/job-types.js";
|
|
17
21
|
import { Admin, AuditEvent, AuditQueue, Authenticator, PipelineHook, ScanGuardMode, Session, SessionOrganization, TenantValue, User, ViewSource, WriteEvent, WriteOperation } from "./types.js";
|
|
18
22
|
import { CommentUsers } from "./comments/comment-users.js";
|
|
@@ -20,10 +24,7 @@ import { HistoryOptions } from "./history/options.js";
|
|
|
20
24
|
import { WebhooksOptions } from "./webhooks/table/options.js";
|
|
21
25
|
import { ApnsEnvironment, Device, DeviceKind, NewDevice } from "./notifications/device.js";
|
|
22
26
|
import { DeviceStore } from "./notifications/device-store.js";
|
|
23
|
-
import { JsonObject, JsonValue } from "./notifications/json-value.js";
|
|
24
|
-
import { HistoryPosition, NewNotification, StoredNotification, historyIdPattern, historyLimits } from "./notifications/history.js";
|
|
25
27
|
import { HistoryStore, maxHistoryPage, maxUnreadCount } from "./notifications/history-store.js";
|
|
26
|
-
import { PushMessage, PushMessageError, Urgency, WebPushFields, maxTtlSeconds, pushMessage, urgencies } from "./notifications/message.js";
|
|
27
28
|
import { PushAnswer, PushSender } from "./notifications/sender.js";
|
|
28
29
|
import { DeviceColumn, HistoryColumn, NotificationsOptions } from "./notifications/options.js";
|
|
29
30
|
import { EventData, RunContext, SubscriptionDefinition, SubscriptionEvent, SubscriptionOptions, subscription } from "./webhooks/subscription.js";
|
|
@@ -104,6 +105,7 @@ import { historyStore, removeHistoryWithUsers } from "./notifications/history-st
|
|
|
104
105
|
import { HistoryStoreError, postgresHistoryStore } from "./notifications/postgres-history-store.js";
|
|
105
106
|
import { defaultHistoryPage } from "./notifications/history-routes.js";
|
|
106
107
|
import { notificationSenders } from "./notifications/senders-of.js";
|
|
108
|
+
import { sendNotification } from "./notifications/send-notification.js";
|
|
107
109
|
import { VapidSettings, readVapidSettings, vapidPrivateKeyVariable, vapidPublicKeyVariable, vapidSubjectVariable } from "./notifications/web-push/vapid-settings.js";
|
|
108
110
|
import { WebPushOptions, webPushSender } from "./notifications/web-push/web-push-sender.js";
|
|
109
111
|
import { generateVapidKeys } from "./notifications/web-push/keys.js";
|
|
@@ -158,4 +160,4 @@ import { checkJobsSchema, jobsSchema, jobsSchemaMigration } from "./jobs/schema.
|
|
|
158
160
|
import { MobileRelease, ReleaseFile, ServedReleaseFile, composeRelease, releaseManifest } from "./mobile-release/release.js";
|
|
159
161
|
import { MobileReleaseAppInput, OfferedRelease, mobileReleaseApp } from "./mobile-release/release-app.js";
|
|
160
162
|
import { mobileSigningKeyVariable, readSigningKey } from "./mobile-release/signing-key.js";
|
|
161
|
-
export { type Admin, type AdminAuth, type AdminOptions, type ApiFunction, type ApiKeys, type ApiKeysOptions, type ApnsEnvironment, type AppIcons, type AppIdentity, type AppPoolConfig, type ApplyOptions, type ApplyResult, type ApprovalAnswer, type ApprovalRequest, type AssistantBackend, type AssistantOptions, type AssistantSettings, type AssistantTool, type AuditEvent, type AuditQueue, 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 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 EnqueueOptions, type ErrorReporter, type ErrorReportingOptions, type ErrorReportingSettings, type EventData, type EventsOptions, FatalJobError, type FieldApplied, type FieldStatus, type FileCleanupInput, type FileLimits, type FileObject, type FileProvider, type FileRef, type FileStore, type FileVariantObject, type FilesOptions, type FunctionApp, type FunctionCaller, type FunctionContext, type FunctionEnv, type FunctionHandler, type FunctionOptions, type FunctionRecords, type FunctionSocket, type FunctionSource, type HistoryColumn, type HistoryOptions, type HistoryPosition, type HistoryStore, HistoryStoreError, HttpProblem, type IssueTokenOptions, type JobContext, type JobDefinition, type JobOptions, 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 McpGateInput, type McpHandlerInput, type McpRoutesInput, type McpServerInput, type McpToolsInput, type McpWrites, type MemberInput, MissingCredentials, type MobileConfig, type MobileRelease, type MobileReleaseAppInput, ModelError, type ModelSettings, type NewDevice, type NewNotification, type NewOrganization, type NewUser, type NotificationsOptions, type OfferedRelease, type OperatorProvider, type OrganizationRole, type OrganizationsOptions, type PgModule, type PipelineHook, type PlatformSignIn, type PluginOption, type PluginRef, type PngIcon, type ProjectConfig, type ProtectedFunctionOptions, type PublicFunctionContext, type PublicFunctionEnv, type PublicFunctionHandler, type PublicFunctionOptions, type PushAnswer, type PushMessage, PushMessageError, type PushSender, type QueryOptions, type QueryResult, type ReadableResources, type ReasoningDetail, type ReasoningEffort, type RebindOption, 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 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 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, addMember, apiKeyCaller, apiKeySettings, apiKeyVariables, appPool, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaMigration, authStoreTables, backgroundSettled, badRequest, batchWriteTool, beforeSignInPluginsVariable, betterAuthAuthenticator, betterAuthTokens, capSocketMessages, changesMigration, checkAi, checkAuthSchema, checkChanges, checkComments, checkComputed, checkJobsSchema, checkNotificationsSchema, checkQueryRunner, checkSourceNames, checkViewQueries, checkWebhooksSchema, coalesce, commentsMigration, composeRelease, computedMigration, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createEncryptionTools, createFileCleanup, createOrganization, createRecordTool, createUser, defaultAppName, defaultHistoryPage, defaultSignInPolicy, defineConfig, defineEmail, defineFunction, defineJob, defineTool, deleteRecordTool, deleteUser, deviceIdParam, deviceStore, devicesPath, 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, listOrganizations, listRecordsTool, listResourcesTool, listUsers, localFileStore, localFiles, longQueryPool, longQueryTimeoutMs, mapSettings, mapStyleUrl, mapStyleVariable, maxDevicesPerUser, maxHistoryPage, maxTokenTtlSeconds, maxTtlSeconds, maxUnreadCount, maxWidgetPropsBytes, mcpGate, mcpHandler, mcpRoutes, mcpServer, mcpToolContext, mcpTools, mergeConfig, migrationCommand, mobileReleaseApp, mobileSigningKeyVariable, newWebhookSecret, normalizeRoles, notFound, notificationSenders, notifications, 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, recordOf, releaseManifest, removeDevicesWithUsers, removeHistoryWithUsers, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runInBackground, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, s3Files, seesAssistant, seesRuntime, 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, webhooks, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps, writeRecordTools };
|
|
163
|
+
export { type Admin, type AdminAuth, type AdminOptions, type ApiFunction, type ApiKeys, type ApiKeysOptions, type ApnsEnvironment, type AppIcons, type AppIdentity, type AppPoolConfig, type ApplyOptions, type ApplyResult, type ApprovalAnswer, type ApprovalRequest, type AssistantBackend, type AssistantOptions, type AssistantSettings, type AssistantTool, type AuditEvent, type AuditQueue, 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 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 EnqueueOptions, type ErrorReporter, type ErrorReportingOptions, type ErrorReportingSettings, type EventData, type EventsOptions, FatalJobError, type FieldApplied, type FieldStatus, type FileCleanupInput, type FileLimits, type FileObject, type FileProvider, type FileRef, type FileStore, type FileVariantObject, type FilesOptions, type FunctionApp, type FunctionCaller, type FunctionContext, type FunctionEnv, type FunctionHandler, type FunctionOptions, type FunctionRecords, type FunctionSocket, type FunctionSource, type HistoryColumn, type HistoryOptions, type HistoryPosition, type HistoryStore, HistoryStoreError, HttpProblem, type IssueTokenOptions, type JobContext, type JobDefinition, type JobOptions, 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 McpGateInput, type McpHandlerInput, type McpRoutesInput, type McpServerInput, type McpToolsInput, type McpWrites, type MemberInput, 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 ProjectConfig, type ProtectedFunctionOptions, type PublicFunctionContext, type PublicFunctionEnv, type PublicFunctionHandler, type PublicFunctionOptions, type PushAnswer, type PushMessage, PushMessageError, type PushSender, type QueryOptions, type QueryResult, type ReadableResources, type ReasoningDetail, type ReasoningEffort, type RebindOption, 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 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, addMember, apiKeyCaller, apiKeySettings, apiKeyVariables, appPool, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaMigration, authStoreTables, backgroundSettled, badRequest, batchWriteTool, beforeSignInPluginsVariable, betterAuthAuthenticator, betterAuthTokens, capSocketMessages, changesMigration, checkAi, checkAuthSchema, checkChanges, checkComments, checkComputed, checkJobsSchema, checkNotificationsSchema, checkQueryRunner, checkSourceNames, checkViewQueries, checkWebhooksSchema, coalesce, commentsMigration, composeRelease, computedMigration, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createEncryptionTools, createFileCleanup, createOrganization, createRecordTool, createUser, defaultAppName, defaultHistoryPage, defaultSignInPolicy, defineConfig, defineEmail, defineFunction, defineJob, defineTool, deleteRecordTool, deleteUser, deviceIdParam, deviceStore, devicesPath, 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, listOrganizations, listRecordsTool, listResourcesTool, listUsers, localFileStore, localFiles, longQueryPool, longQueryTimeoutMs, mapSettings, mapStyleUrl, mapStyleVariable, maxDevicesPerUser, maxHistoryPage, maxMessageBytes, maxRecipients, maxTokenTtlSeconds, maxTtlSeconds, maxUnreadCount, maxWidgetPropsBytes, mcpGate, mcpHandler, mcpRoutes, mcpServer, mcpToolContext, mcpTools, mergeConfig, migrationCommand, mobileReleaseApp, mobileSigningKeyVariable, newWebhookSecret, normalizeRoles, notFound, notificationSenders, notifications, 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, recordOf, releaseManifest, removeDevicesWithUsers, removeHistoryWithUsers, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runInBackground, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, s3Files, 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, webhooks, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps, writeRecordTools };
|
package/dist/index.js
CHANGED
|
@@ -70,6 +70,7 @@ import { PushMessageError, maxTtlSeconds, pushMessage, urgencies } from "./notif
|
|
|
70
70
|
import { readVapidSettings, vapidPrivateKeyVariable, vapidPublicKeyVariable, vapidSubjectVariable } from "./notifications/web-push/vapid-settings.js";
|
|
71
71
|
import { webPushSender } from "./notifications/web-push/web-push-sender.js";
|
|
72
72
|
import { notificationSenders } from "./notifications/senders-of.js";
|
|
73
|
+
import { NotificationSendError, maxMessageBytes, maxRecipients } from "./notifications/send-input.js";
|
|
73
74
|
import { errorReportingSettings, errorReportingVariable } from "./error-reporting/error-reporting-settings.js";
|
|
74
75
|
import { storageCleanupSchedule } from "./files/cleanup-schedule.js";
|
|
75
76
|
import { limitedFileStore, storeUsage } from "./files/limits.js";
|
|
@@ -102,6 +103,7 @@ import { checkWebhooksSchema, webhooksMigration } from "./webhooks/table/schema.
|
|
|
102
103
|
import { checkAi } from "./ai/check-ai.js";
|
|
103
104
|
import { notifications } from "./notifications/plugin.js";
|
|
104
105
|
import { checkNotificationsSchema, notificationsMigration } from "./notifications/schema.js";
|
|
106
|
+
import { sendNotification } from "./notifications/send-notification.js";
|
|
105
107
|
import { maxWidgetPropsBytes, widget } from "./assistant/widget.js";
|
|
106
108
|
import { checkQueryRunner, queryRunnerMigration } from "./assistant/query-runner/query-runner.js";
|
|
107
109
|
import { startErrorReporting } from "./error-reporting/error-reporter.js";
|
|
@@ -120,4 +122,4 @@ import { jobsDrainApp } from "./jobs/drain-route.js";
|
|
|
120
122
|
import { composeRelease, releaseManifest } from "./mobile-release/release.js";
|
|
121
123
|
import { mobileReleaseApp } from "./mobile-release/release-app.js";
|
|
122
124
|
import { mobileSigningKeyVariable, readSigningKey } from "./mobile-release/signing-key.js";
|
|
123
|
-
export { ConnectionsBusy, DeviceStoreError, FatalJobError, HistoryStoreError, HttpProblem, MissingCredentials, ModelError, PushMessageError, addAppRoleToOwners, addMember, apiKeyCaller, apiKeySettings, apiKeyVariables, appPool, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaMigration, authStoreTables, backgroundSettled, badRequest, batchWriteTool, beforeSignInPluginsVariable, betterAuthAuthenticator, betterAuthTokens, capSocketMessages, changesMigration, checkAi, checkAuthSchema, checkChanges, checkComments, checkComputed, checkJobsSchema, checkNotificationsSchema, checkQueryRunner, checkSourceNames, checkViewQueries, checkWebhooksSchema, coalesce, commentsMigration, composeRelease, computedMigration, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createEncryptionTools, createFileCleanup, createOrganization, createRecordTool, createUser, defaultAppName, defaultHistoryPage, defaultSignInPolicy, defineConfig, defineEmail, defineFunction, defineJob, defineTool, deleteRecordTool, deleteUser, deviceIdParam, deviceStore, devicesPath, 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, listOrganizations, listRecordsTool, listResourcesTool, listUsers, localFileStore, localFiles, longQueryPool, longQueryTimeoutMs, mapSettings, mapStyleUrl, mapStyleVariable, maxDevicesPerUser, maxHistoryPage, maxTokenTtlSeconds, maxTtlSeconds, maxUnreadCount, maxWidgetPropsBytes, mcpGate, mcpHandler, mcpRoutes, mcpServer, mcpToolContext, mcpTools, mergeConfig, migrationCommand, mobileReleaseApp, mobileSigningKeyVariable, newWebhookSecret, normalizeRoles, notFound, notificationSenders, notifications, 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, recordOf, releaseManifest, removeDevicesWithUsers, removeHistoryWithUsers, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runInBackground, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, s3Files, seesAssistant, seesRuntime, 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, webhooks, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps, writeRecordTools };
|
|
125
|
+
export { ConnectionsBusy, DeviceStoreError, FatalJobError, HistoryStoreError, HttpProblem, MissingCredentials, ModelError, NotificationSendError, PushMessageError, addAppRoleToOwners, addMember, apiKeyCaller, apiKeySettings, apiKeyVariables, appPool, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaMigration, authStoreTables, backgroundSettled, badRequest, batchWriteTool, beforeSignInPluginsVariable, betterAuthAuthenticator, betterAuthTokens, capSocketMessages, changesMigration, checkAi, checkAuthSchema, checkChanges, checkComments, checkComputed, checkJobsSchema, checkNotificationsSchema, checkQueryRunner, checkSourceNames, checkViewQueries, checkWebhooksSchema, coalesce, commentsMigration, composeRelease, computedMigration, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createEncryptionTools, createFileCleanup, createOrganization, createRecordTool, createUser, defaultAppName, defaultHistoryPage, defaultSignInPolicy, defineConfig, defineEmail, defineFunction, defineJob, defineTool, deleteRecordTool, deleteUser, deviceIdParam, deviceStore, devicesPath, 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, listOrganizations, listRecordsTool, listResourcesTool, listUsers, localFileStore, localFiles, longQueryPool, longQueryTimeoutMs, mapSettings, mapStyleUrl, mapStyleVariable, maxDevicesPerUser, maxHistoryPage, maxMessageBytes, maxRecipients, maxTokenTtlSeconds, maxTtlSeconds, maxUnreadCount, maxWidgetPropsBytes, mcpGate, mcpHandler, mcpRoutes, mcpServer, mcpToolContext, mcpTools, mergeConfig, migrationCommand, mobileReleaseApp, mobileSigningKeyVariable, newWebhookSecret, normalizeRoles, notFound, notificationSenders, notifications, 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, recordOf, releaseManifest, removeDevicesWithUsers, removeHistoryWithUsers, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runInBackground, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, s3Files, 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, webhooks, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps, writeRecordTools };
|
package/dist/jobs/current.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { Db } from "@protobase/query";
|
|
|
4
4
|
/** What `defineJob(...).enqueue` reaches: the jobs runtime this process started. */
|
|
5
5
|
export type JobsEnqueuer = {
|
|
6
6
|
enqueue(definition: JobDefinition, db: Db, payload: unknown, options?: EnqueueOptions): Promise<string>;
|
|
7
|
+
/** The job of this name among the jobs this runtime took, or `undefined`. */
|
|
8
|
+
definition(name: string): JobDefinition | undefined;
|
|
7
9
|
/** Has this process's worker of the job look for due jobs now rather than at its next poll; nothing without one. */
|
|
8
10
|
wake(name: string): void;
|
|
9
11
|
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jobsSchema } from "./schema.js";
|
|
2
|
+
import { sql } from "kysely";
|
|
3
|
+
//#region src/jobs/job-progress.ts
|
|
4
|
+
/** Thrown when a try no longer holds its job: its time ran out and the job was failed, retried or taken by another try. */
|
|
5
|
+
var StaleJobError = class extends Error {
|
|
6
|
+
name = "StaleJobError";
|
|
7
|
+
constructor(name) {
|
|
8
|
+
super(`Job "${name}" is no longer this try's: its time ran out`);
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
const held = (job) => sql`name = ${job.name} and id = ${job.id}::uuid and state = 'active' and started_on > ${job.startedOn}::timestamptz - interval '1 millisecond' and started_on < ${job.startedOn}::timestamptz + interval '1 millisecond'`;
|
|
12
|
+
const table = sql.id(jobsSchema, "job");
|
|
13
|
+
/** Throws a `StaleJobError` unless this try still holds the job. */
|
|
14
|
+
const holdJob = async (db, job) => {
|
|
15
|
+
const { rows } = await sql`select 1 from ${table} where ${held(job)}`.execute(db);
|
|
16
|
+
if (rows.length === 0) throw new StaleJobError(job.name);
|
|
17
|
+
};
|
|
18
|
+
const updated = async (db, job, data) => {
|
|
19
|
+
if ((await sql`update ${table} set data = ${data} where ${held(job)}`.execute(db)).numAffectedRows !== 1n) throw new StaleJobError(job.name);
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Keeps `progress` in the job's payload under `progress`, where its next try finds it: pg-boss keeps a job's data
|
|
23
|
+
* when it retries it. Only while this try holds the job, or it throws a `StaleJobError`; in `db`'s transaction when it
|
|
24
|
+
* is one, so the work it marks commits with it.
|
|
25
|
+
*/
|
|
26
|
+
const saveJobProgress = (db, job, progress) => updated(db, job, sql`jsonb_set(data, '{progress}', ${JSON.stringify(progress)}::jsonb)`);
|
|
27
|
+
/** Replaces the job's payload with `data`, as when what it held is done with; only while this try holds the job. */
|
|
28
|
+
const replaceJobData = (db, job, data) => updated(db, job, sql`${JSON.stringify(data)}::jsonb`);
|
|
29
|
+
//#endregion
|
|
30
|
+
export { StaleJobError, holdJob, replaceJobData, saveJobProgress };
|
package/dist/jobs/job-types.d.ts
CHANGED
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
import { Mailer } from "../mail/smtp-mailer.js";
|
|
2
2
|
import { StandardSchema } from "../functions/read-json.js";
|
|
3
|
+
import { SendNotification } from "../notifications/send-input.js";
|
|
3
4
|
import { Db } from "@protobase/query";
|
|
4
5
|
//#region src/jobs/job-types.d.ts
|
|
5
6
|
/** What `run` gets beside the payload. */
|
|
6
7
|
export type JobContext = {
|
|
7
8
|
/** The database pool. The enqueuer's transaction has committed by the time a job runs. */
|
|
8
9
|
db: Db;
|
|
10
|
+
/** The job, and when this try took it: each try takes it at a new time. */
|
|
9
11
|
job: {
|
|
10
12
|
id: string;
|
|
11
13
|
name: string;
|
|
12
14
|
attempt: number;
|
|
13
15
|
maxAttempts: number;
|
|
16
|
+
startedOn: Date;
|
|
14
17
|
};
|
|
15
18
|
/** Aborted when the job's timeout passes, or when the worker shuts down and its grace period ends. */
|
|
16
19
|
signal: AbortSignal;
|
|
17
20
|
/** Sends mail through the app's SMTP server (`PROTOBASE_SMTP_URL`, `PROTOBASE_MAIL_FROM`); `undefined` when the app has no mail. */
|
|
18
21
|
mail?: Mailer;
|
|
22
|
+
/** Sends a notification as `sendNotification` does; `undefined` when the app has no `notifications()`. */
|
|
23
|
+
sendNotification?: SendNotification;
|
|
19
24
|
};
|
|
20
25
|
export type JobOptions<Payload> = {
|
|
21
26
|
/** Letters, digits, `_`, `-`, `.` and `/`; the job's queue in the `jobs` schema. */
|
package/dist/jobs/run-job.d.ts
CHANGED
package/dist/jobs/run-job.js
CHANGED
|
@@ -53,7 +53,7 @@ const namedTimeout = (definition, error) => {
|
|
|
53
53
|
* One try of one job: completed when `run` returns, failed (to be retried while attempts are left) when it throws, and
|
|
54
54
|
* failed for good on a `FatalJobError` or an invalid payload.
|
|
55
55
|
*/
|
|
56
|
-
const runJob = async (definition, job, db, mail) => {
|
|
56
|
+
const runJob = async (definition, job, db, { mail, sendNotification } = {}) => {
|
|
57
57
|
try {
|
|
58
58
|
const payload = await checkPayload(definition, job.data ?? void 0);
|
|
59
59
|
const context = {
|
|
@@ -62,10 +62,12 @@ const runJob = async (definition, job, db, mail) => {
|
|
|
62
62
|
id: job.id,
|
|
63
63
|
name: job.name,
|
|
64
64
|
attempt: job.retryCount + 1,
|
|
65
|
-
maxAttempts: job.retryLimit + 1
|
|
65
|
+
maxAttempts: job.retryLimit + 1,
|
|
66
|
+
startedOn: job.startedOn
|
|
66
67
|
},
|
|
67
68
|
signal: job.signal,
|
|
68
|
-
...mail && { mail }
|
|
69
|
+
...mail && { mail },
|
|
70
|
+
...sendNotification && { sendNotification }
|
|
69
71
|
};
|
|
70
72
|
await throughWriteFreeze(() => definition.options.run(payload, context), job.signal);
|
|
71
73
|
return {
|
package/dist/jobs/runtime.js
CHANGED
|
@@ -3,7 +3,9 @@ import { jobsSchema } from "./schema.js";
|
|
|
3
3
|
import "./schedule.js";
|
|
4
4
|
import { runJob, validatePayload } from "./run-job.js";
|
|
5
5
|
import { platformMailer } from "../mail/platform-mailer.js";
|
|
6
|
+
import "../notifications/push-job-name.js";
|
|
6
7
|
import { runInBackground } from "../background-work.js";
|
|
8
|
+
import { sendNotification } from "../notifications/send-notification.js";
|
|
7
9
|
import { busyDatabase } from "./busy-database.js";
|
|
8
10
|
import { jobsDatabase } from "./database.js";
|
|
9
11
|
import { queueOptions, queueSettingsDiffer, sendOptions, workOptions } from "./options.js";
|
|
@@ -56,7 +58,10 @@ const startJobs = async ({ db, jobs, work = true, pollSeconds = 2, shutdownSecon
|
|
|
56
58
|
const current = definitions.get(definition.name) ?? definition;
|
|
57
59
|
return runInBackground(async () => {
|
|
58
60
|
if (current.options.schedule !== void 0 && job.singletonKey === "protobase.schedule") await schedules.following(current, job.startAfter);
|
|
59
|
-
return [await runJob(current, job, db,
|
|
61
|
+
return [await runJob(current, job, db, {
|
|
62
|
+
mail,
|
|
63
|
+
...definitions.has("notifications.send") && { sendNotification }
|
|
64
|
+
})];
|
|
60
65
|
}, { longQueries: current.options.longQueries });
|
|
61
66
|
});
|
|
62
67
|
workers.set(definition.name, {
|
|
@@ -139,6 +144,7 @@ const startJobs = async ({ db, jobs, work = true, pollSeconds = 2, shutdownSecon
|
|
|
139
144
|
const runtime = {
|
|
140
145
|
works: work,
|
|
141
146
|
enqueue,
|
|
147
|
+
definition: (name) => definitions.get(name),
|
|
142
148
|
wake,
|
|
143
149
|
update,
|
|
144
150
|
drain,
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { FatalJobError } from "../jobs/run-job.js";
|
|
2
|
+
import { PushMessageError } from "./message.js";
|
|
3
|
+
import { PushSenderError } from "./guarded-sender.js";
|
|
4
|
+
import { historyEntry } from "./send-input.js";
|
|
5
|
+
import { NotificationStoreError, storeCall } from "./store-call.js";
|
|
6
|
+
//#region src/notifications/deliver.ts
|
|
7
|
+
const tally = () => ({
|
|
8
|
+
retried: 0,
|
|
9
|
+
storeFailures: /* @__PURE__ */ new Map(),
|
|
10
|
+
strays: /* @__PURE__ */ new Map(),
|
|
11
|
+
refusedField: void 0
|
|
12
|
+
});
|
|
13
|
+
const counted = (counts, method, count = 1) => counts.set(method, (counts.get(method) ?? 0) + count);
|
|
14
|
+
const listed = (counts) => [...counts].map(([method, count]) => `${count} in ${method}`).join(", ");
|
|
15
|
+
/**
|
|
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
|
+
* (once, as `record` marks it), then the user's devices as the store lists them now; for a device send, the devices
|
|
18
|
+
* with those ids now. A device the store gives that is not the user's, or not one asked for, gets nothing. Each kind's
|
|
19
|
+
* devices go to its sender in one batch; a kind without a sender is skipped. An answer of `gone` removes the device,
|
|
20
|
+
* `retry` leaves it to the next try once and gives up on it the second time, and any other is done. A sender that
|
|
21
|
+
* throws leaves its batch to the next try. The try then throws, counting what is left, so the job retries it, or
|
|
22
|
+
* completes the send.
|
|
23
|
+
*/
|
|
24
|
+
const deliver = async ({ to, message }, given, steps) => {
|
|
25
|
+
const progress = {
|
|
26
|
+
recorded: [...given.recorded],
|
|
27
|
+
done: [...given.done],
|
|
28
|
+
retried: [...given.retried]
|
|
29
|
+
};
|
|
30
|
+
const recorded = new Set(progress.recorded);
|
|
31
|
+
const done = new Set(progress.done);
|
|
32
|
+
const retried = new Set(progress.retried);
|
|
33
|
+
const left = tally();
|
|
34
|
+
const markDone = (id) => {
|
|
35
|
+
if (done.has(id)) return;
|
|
36
|
+
done.add(id);
|
|
37
|
+
progress.done.push(id);
|
|
38
|
+
};
|
|
39
|
+
if (steps.senders.length === 0 && !("userIds" in to && to.inbox)) return steps.complete();
|
|
40
|
+
const answered = async (sender, batch) => {
|
|
41
|
+
try {
|
|
42
|
+
return await sender.send(message, batch);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (error instanceof PushSenderError) return void 0;
|
|
45
|
+
if (!(error instanceof PushMessageError)) throw error;
|
|
46
|
+
left.refusedField = error.field;
|
|
47
|
+
return batch.map((device) => ({
|
|
48
|
+
id: device.id,
|
|
49
|
+
result: "refused"
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const settle = async (device, answer) => {
|
|
54
|
+
if (answer.result === "gone") await storeCall("removeGone", () => steps.devices.removeGone([device.token]));
|
|
55
|
+
if (answer.result !== "retry" || retried.has(device.id)) return markDone(device.id);
|
|
56
|
+
retried.add(device.id);
|
|
57
|
+
progress.retried.push(device.id);
|
|
58
|
+
left.retried += 1;
|
|
59
|
+
};
|
|
60
|
+
const push = async (devices) => {
|
|
61
|
+
for (const device of devices) if (!steps.senders.some((sender) => sender.kinds.includes(device.kind))) markDone(device.id);
|
|
62
|
+
for (const sender of steps.senders) {
|
|
63
|
+
const batch = devices.filter((device) => sender.kinds.includes(device.kind) && !done.has(device.id));
|
|
64
|
+
if (batch.length === 0) continue;
|
|
65
|
+
await steps.hold();
|
|
66
|
+
const answers = await answered(sender, batch);
|
|
67
|
+
if (answers === void 0) left.retried += batch.length;
|
|
68
|
+
const byId = new Map((answers ?? []).map((answer) => [answer.id, answer]));
|
|
69
|
+
for (const device of answers ? batch : []) await settle(device, byId.get(device.id) ?? {
|
|
70
|
+
id: device.id,
|
|
71
|
+
result: "retry"
|
|
72
|
+
});
|
|
73
|
+
await steps.save(progress);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
const forRecipient = async (run) => {
|
|
77
|
+
try {
|
|
78
|
+
await run();
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (!(error instanceof NotificationStoreError)) throw error;
|
|
81
|
+
counted(left.storeFailures, error.method);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
if ("userIds" in to) for (const userId of to.userIds) await forRecipient(async () => {
|
|
85
|
+
if (to.inbox && !recorded.has(userId)) {
|
|
86
|
+
const next = {
|
|
87
|
+
...progress,
|
|
88
|
+
recorded: [...progress.recorded, userId]
|
|
89
|
+
};
|
|
90
|
+
await storeCall("record", () => steps.record(userId, historyEntry(message), next));
|
|
91
|
+
recorded.add(userId);
|
|
92
|
+
progress.recorded.push(userId);
|
|
93
|
+
}
|
|
94
|
+
const devices = await storeCall("listForUser", () => steps.devices.listForUser(userId));
|
|
95
|
+
const own = devices.filter((device) => device.userId === userId);
|
|
96
|
+
if (own.length < devices.length) counted(left.strays, "listForUser", devices.length - own.length);
|
|
97
|
+
await push(own);
|
|
98
|
+
});
|
|
99
|
+
else {
|
|
100
|
+
const wanted = new Set(to.deviceIds);
|
|
101
|
+
const ids = to.deviceIds.filter((id) => !done.has(id));
|
|
102
|
+
if (ids.length > 0) await forRecipient(async () => {
|
|
103
|
+
const devices = await storeCall("listByIds", () => steps.devices.listByIds(ids));
|
|
104
|
+
const asked = devices.filter((device) => wanted.has(device.id));
|
|
105
|
+
if (asked.length < devices.length) counted(left.strays, "listByIds", devices.length - asked.length);
|
|
106
|
+
await push(asked);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
if (left.refusedField !== void 0) throw new FatalJobError(`notifications.send: a sender refused the message: ${left.refusedField}`);
|
|
110
|
+
const strays = left.strays.size > 0 ? `; devices the store gave that were not the recipients', which got nothing: ${listed(left.strays)}` : "";
|
|
111
|
+
if (left.retried > 0 || left.storeFailures.size > 0) {
|
|
112
|
+
const failures = left.storeFailures.size > 0 ? `; store failures: ${listed(left.storeFailures)}` : "";
|
|
113
|
+
throw new Error(`notifications.send: ${left.retried} deliveries are retried${failures}${strays}`);
|
|
114
|
+
}
|
|
115
|
+
await steps.complete();
|
|
116
|
+
if (strays) throw new FatalJobError(`notifications.send: delivered${strays}`);
|
|
117
|
+
};
|
|
118
|
+
//#endregion
|
|
119
|
+
export { deliver };
|
|
@@ -3,8 +3,17 @@ import { PushMessageError, pushMessage } from "./message.js";
|
|
|
3
3
|
const results = /* @__PURE__ */ new Set([
|
|
4
4
|
"ok",
|
|
5
5
|
"gone",
|
|
6
|
-
"retry"
|
|
6
|
+
"retry",
|
|
7
|
+
"refused"
|
|
7
8
|
]);
|
|
9
|
+
/** What a guarded sender throws for what its sender threw: it names the sender's index alone. */
|
|
10
|
+
var PushSenderError = class extends Error {
|
|
11
|
+
index;
|
|
12
|
+
constructor(index) {
|
|
13
|
+
super(`notifications sender ${index} failed`);
|
|
14
|
+
this.index = index;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
8
17
|
const deepFrozen = (value) => {
|
|
9
18
|
if (typeof value !== "object" || value === null) return value;
|
|
10
19
|
for (const item of Object.values(value)) deepFrozen(item);
|
|
@@ -34,7 +43,7 @@ const answersFor = (batch, given) => {
|
|
|
34
43
|
/**
|
|
35
44
|
* `sender` as the code that sends calls it, whether built in or the app's own: the message is checked as `pushMessage`
|
|
36
45
|
* checks it, and refused before the sender sees it; the sender gets a frozen copy of it and of the devices of its kinds
|
|
37
|
-
* only, and answers each of those once, as above. A sender that throws throws
|
|
46
|
+
* only, and answers each of those once, as above. A sender that throws throws a `PushSenderError` naming its index,
|
|
38
47
|
* never the sender's, which may carry an endpoint or a key, but for a `PushMessageError`, which names a field only.
|
|
39
48
|
*/
|
|
40
49
|
const guardedSender = (sender, index) => {
|
|
@@ -47,7 +56,7 @@ const guardedSender = (sender, index) => {
|
|
|
47
56
|
answers = await sender.send(message, batch);
|
|
48
57
|
} catch (error) {
|
|
49
58
|
if (error instanceof PushMessageError) throw error;
|
|
50
|
-
throw new
|
|
59
|
+
throw new PushSenderError(index);
|
|
51
60
|
}
|
|
52
61
|
return answersFor(batch, answers);
|
|
53
62
|
};
|
|
@@ -58,4 +67,4 @@ const guardedSender = (sender, index) => {
|
|
|
58
67
|
};
|
|
59
68
|
};
|
|
60
69
|
//#endregion
|
|
61
|
-
export { guardedSender };
|
|
70
|
+
export { PushSenderError, guardedSender };
|
|
@@ -12,6 +12,7 @@ const historyLimits = {
|
|
|
12
12
|
const noControls = /^[^\u0000-\u001f\u007f]*$/;
|
|
13
13
|
const noControlsButLines = /^[^\u0000-\u0008\u000b-\u001f\u007f]*$/;
|
|
14
14
|
const text = (value, max, pattern) => typeof value === "string" && value.length <= max && pattern.test(value);
|
|
15
|
+
/** The first field of the notification that is not within its format, or `undefined`. */
|
|
15
16
|
const notificationProblem = (notification) => {
|
|
16
17
|
if (typeof notification !== "object" || notification === null) return "notification";
|
|
17
18
|
if (!text(notification.title, historyLimits.title, noControls) || notification.title === "") return "title";
|
|
@@ -38,4 +39,4 @@ const checkedNotification = (notification) => {
|
|
|
38
39
|
};
|
|
39
40
|
};
|
|
40
41
|
//#endregion
|
|
41
|
-
export { checkedNotification, historyIdPattern, historyLimits };
|
|
42
|
+
export { checkedNotification, historyIdPattern, historyLimits, notificationProblem };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { defineJob } from "../jobs/define-job.js";
|
|
2
|
+
import { deviceStore } from "./device-store-of.js";
|
|
3
|
+
import { HistoryStoreError } from "./postgres-history-store.js";
|
|
4
|
+
import { historyStore } from "./history-store-of.js";
|
|
5
|
+
import { holdJob, replaceJobData, saveJobProgress } from "../jobs/job-progress.js";
|
|
6
|
+
import { deliver } from "./deliver.js";
|
|
7
|
+
import { noProgress, pushPayload } from "./push-payload.js";
|
|
8
|
+
import { pushJobName } from "./push-job-name.js";
|
|
9
|
+
//#region src/notifications/push-job.ts
|
|
10
|
+
const userGone = "23503";
|
|
11
|
+
/**
|
|
12
|
+
* Keeps the entry and the progress that marks it kept in one transaction, with the default store, so no try records
|
|
13
|
+
* it twice. An app's own store records outside it. When the user was deleted since the send, the entry is not kept
|
|
14
|
+
* and the user is marked all the same, after the transaction the failed insert ended.
|
|
15
|
+
*/
|
|
16
|
+
const recordOnce = (db, options, job) => async (userId, entry, progress) => {
|
|
17
|
+
try {
|
|
18
|
+
await db.transaction().execute(async (trx) => {
|
|
19
|
+
await historyStore(trx, options).record(userId, entry);
|
|
20
|
+
await saveJobProgress(trx, job, progress);
|
|
21
|
+
});
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (!(error instanceof HistoryStoreError && error.sqlState === userGone)) throw error;
|
|
24
|
+
await saveJobProgress(db, job, progress);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* The job that delivers what `sendNotification` sends, through `senders` (guarded, see `notificationSenders`) and the
|
|
29
|
+
* stores `options` configure. A try resumes where the last one stopped (see `deliver`); once delivered, the job keeps
|
|
30
|
+
* no message. A few tries only: a device that keeps asking for a retry is given up on the second time.
|
|
31
|
+
*/
|
|
32
|
+
const pushJob = (options, senders) => defineJob({
|
|
33
|
+
name: pushJobName,
|
|
34
|
+
payload: pushPayload,
|
|
35
|
+
maxAttempts: 5,
|
|
36
|
+
retryDelay: "1 minute",
|
|
37
|
+
run: async (payload, { db, job }) => {
|
|
38
|
+
if ("complete" in payload) return;
|
|
39
|
+
await deliver(payload, payload.progress ?? noProgress(), {
|
|
40
|
+
devices: deviceStore(db, options),
|
|
41
|
+
senders,
|
|
42
|
+
record: recordOnce(db, options, job),
|
|
43
|
+
save: (progress) => saveJobProgress(db, job, progress),
|
|
44
|
+
hold: () => holdJob(db, job),
|
|
45
|
+
complete: () => replaceJobData(db, job, { complete: true })
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
//#endregion
|
|
50
|
+
export { pushJob };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { NotificationSendError, checkedSend, isPlainRecord, maxRecipients } from "./send-input.js";
|
|
2
|
+
//#region src/notifications/push-payload.ts
|
|
3
|
+
const noProgress = () => ({
|
|
4
|
+
recorded: [],
|
|
5
|
+
done: [],
|
|
6
|
+
retried: []
|
|
7
|
+
});
|
|
8
|
+
const maxProgressIds = maxRecipients * 64;
|
|
9
|
+
const progressOf = (value) => {
|
|
10
|
+
if (!isPlainRecord(value)) return void 0;
|
|
11
|
+
const keys = Object.keys(value);
|
|
12
|
+
if (keys.length !== 3 || ![
|
|
13
|
+
"recorded",
|
|
14
|
+
"done",
|
|
15
|
+
"retried"
|
|
16
|
+
].every((key) => keys.includes(key))) return void 0;
|
|
17
|
+
if (![
|
|
18
|
+
value.recorded,
|
|
19
|
+
value.done,
|
|
20
|
+
value.retried
|
|
21
|
+
].every((list) => Array.isArray(list) && list.length <= maxProgressIds && list.every((id) => typeof id === "string"))) return void 0;
|
|
22
|
+
return {
|
|
23
|
+
recorded: [...value.recorded],
|
|
24
|
+
done: [...value.done],
|
|
25
|
+
retried: [...value.retried]
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
const invalid = (message) => ({ issues: [{ message }] });
|
|
29
|
+
/**
|
|
30
|
+
* The payload as the send job checks it when enqueued and before each try: the send checked again as `checkedSend`
|
|
31
|
+
* checks it, so a job queued around `sendNotification` is refused the same way and runs nothing.
|
|
32
|
+
*/
|
|
33
|
+
const validate = (value) => {
|
|
34
|
+
if (!isPlainRecord(value)) return invalid("The notification send is not an object");
|
|
35
|
+
if (value.complete === true && Object.keys(value).length === 1) return { value: { complete: true } };
|
|
36
|
+
if (Object.keys(value).some((key) => ![
|
|
37
|
+
"to",
|
|
38
|
+
"message",
|
|
39
|
+
"progress"
|
|
40
|
+
].includes(key))) return invalid("The notification send has a field it does not take");
|
|
41
|
+
const progress = value.progress === void 0 ? void 0 : progressOf(value.progress);
|
|
42
|
+
if (value.progress !== void 0 && progress === void 0) return invalid("The notification send has an invalid progress");
|
|
43
|
+
let send;
|
|
44
|
+
try {
|
|
45
|
+
send = checkedSend(value.to, value.message);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (!(error instanceof NotificationSendError)) throw error;
|
|
48
|
+
return invalid(error.message);
|
|
49
|
+
}
|
|
50
|
+
return { value: {
|
|
51
|
+
...send,
|
|
52
|
+
...progress && { progress }
|
|
53
|
+
} };
|
|
54
|
+
};
|
|
55
|
+
const pushPayload = { "~standard": {
|
|
56
|
+
version: 1,
|
|
57
|
+
validate
|
|
58
|
+
} };
|
|
59
|
+
//#endregion
|
|
60
|
+
export { noProgress, pushPayload };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { PushMessage } from "./message.js";
|
|
2
|
+
import { Db } from "@protobase/query";
|
|
3
|
+
//#region src/notifications/send-input.d.ts
|
|
4
|
+
/** The most users or devices one send reaches. */
|
|
5
|
+
export declare const maxRecipients = 1000;
|
|
6
|
+
/** The most bytes a checked message takes as JSON. */
|
|
7
|
+
export declare const maxMessageBytes = 16384;
|
|
8
|
+
/**
|
|
9
|
+
* Who a send reaches: users, saying whether it shows in their inbox, or devices by id, which it is only pushed to.
|
|
10
|
+
* `inbox` is required on a user send and refused on a device send.
|
|
11
|
+
*/
|
|
12
|
+
export type NotificationRecipients = {
|
|
13
|
+
userIds: readonly string[];
|
|
14
|
+
inbox: boolean;
|
|
15
|
+
deviceIds?: never;
|
|
16
|
+
} | {
|
|
17
|
+
deviceIds: readonly string[];
|
|
18
|
+
userIds?: never;
|
|
19
|
+
inbox?: never;
|
|
20
|
+
};
|
|
21
|
+
/** Sends a notification as `sendNotification` does. */
|
|
22
|
+
export type SendNotification = (db: Db, to: NotificationRecipients, message: PushMessage) => Promise<void>;
|
|
23
|
+
/** A send that is not one `checkedSend` accepts: `field` names the part at fault, never its value. */
|
|
24
|
+
export declare class NotificationSendError extends Error {
|
|
25
|
+
readonly field: string;
|
|
26
|
+
constructor(field: string);
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { notificationProblem } from "./history.js";
|
|
2
|
+
import { PushMessageError, pushMessage } from "./message.js";
|
|
3
|
+
//#region src/notifications/send-input.ts
|
|
4
|
+
/** The most users or devices one send reaches. */
|
|
5
|
+
const maxRecipients = 1e3;
|
|
6
|
+
/** The most bytes a checked message takes as JSON. */
|
|
7
|
+
const maxMessageBytes = 16384;
|
|
8
|
+
/** A send that is not one `checkedSend` accepts: `field` names the part at fault, never its value. */
|
|
9
|
+
var NotificationSendError = class extends Error {
|
|
10
|
+
field;
|
|
11
|
+
constructor(field) {
|
|
12
|
+
super(`The notification cannot be sent: its ${field} is invalid`);
|
|
13
|
+
this.field = field;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
const idPattern = /^[^\u0000-\u001f\u007f]{1,256}$/;
|
|
17
|
+
/** Whether `value` is an object literal, so its keys are all it holds. */
|
|
18
|
+
const isPlainRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && [Object.prototype, null].includes(Object.getPrototypeOf(value));
|
|
19
|
+
const checkedIds = (value, field) => {
|
|
20
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > 1e3) throw new NotificationSendError(field);
|
|
21
|
+
if (!value.every((id) => typeof id === "string" && idPattern.test(id))) throw new NotificationSendError(field);
|
|
22
|
+
return [...new Set(value)];
|
|
23
|
+
};
|
|
24
|
+
const checkedKeys = (to, allowed) => {
|
|
25
|
+
const unknown = Object.keys(to).find((key) => !allowed.includes(key));
|
|
26
|
+
if (unknown !== void 0) throw new NotificationSendError(`to.${unknown}`);
|
|
27
|
+
};
|
|
28
|
+
const checkedRecipients = (to) => {
|
|
29
|
+
if (!isPlainRecord(to)) throw new NotificationSendError("to");
|
|
30
|
+
if (Object.hasOwn(to, "userIds") && Object.hasOwn(to, "deviceIds")) throw new NotificationSendError("to");
|
|
31
|
+
if (Object.hasOwn(to, "deviceIds")) {
|
|
32
|
+
checkedKeys(to, ["deviceIds"]);
|
|
33
|
+
return { deviceIds: checkedIds(to.deviceIds, "to.deviceIds") };
|
|
34
|
+
}
|
|
35
|
+
if (!Object.hasOwn(to, "userIds")) throw new NotificationSendError("to");
|
|
36
|
+
checkedKeys(to, ["userIds", "inbox"]);
|
|
37
|
+
if (typeof to.inbox !== "boolean") throw new NotificationSendError("to.inbox");
|
|
38
|
+
return {
|
|
39
|
+
userIds: checkedIds(to.userIds, "to.userIds"),
|
|
40
|
+
inbox: to.inbox
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
const checkedMessage = (message) => {
|
|
44
|
+
let checked;
|
|
45
|
+
try {
|
|
46
|
+
checked = pushMessage(message);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (!(error instanceof PushMessageError)) throw error;
|
|
49
|
+
throw new NotificationSendError(error.field === "message" ? "message" : `message.${error.field}`);
|
|
50
|
+
}
|
|
51
|
+
if (Buffer.byteLength(JSON.stringify(checked)) > 16384) throw new NotificationSendError("message");
|
|
52
|
+
return checked;
|
|
53
|
+
};
|
|
54
|
+
/** The entry a user's history keeps of a checked message. */
|
|
55
|
+
const historyEntry = (message) => ({
|
|
56
|
+
title: message.title,
|
|
57
|
+
body: message.body ?? null,
|
|
58
|
+
url: message.url ?? null,
|
|
59
|
+
data: message.data ?? null
|
|
60
|
+
});
|
|
61
|
+
/**
|
|
62
|
+
* The send, checked, or a `NotificationSendError` naming the first field at fault: recipients of one kind, 1 to
|
|
63
|
+
* `maxRecipients` ids, a user send's `inbox` a boolean, and a message `pushMessage` accepts, its url an absolute `http`
|
|
64
|
+
* or `https` URL written as the parser writes it, of at most `maxMessageBytes`. For the inbox, the message must also be
|
|
65
|
+
* one the history keeps.
|
|
66
|
+
*/
|
|
67
|
+
const checkedSend = (to, message) => {
|
|
68
|
+
const recipients = checkedRecipients(to);
|
|
69
|
+
const checked = checkedMessage(message);
|
|
70
|
+
if ("userIds" in recipients && recipients.inbox) {
|
|
71
|
+
const problem = notificationProblem(historyEntry(checked));
|
|
72
|
+
if (problem !== void 0) throw new NotificationSendError(`message.${problem}`);
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
to: recipients,
|
|
76
|
+
message: checked
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
//#endregion
|
|
80
|
+
export { NotificationSendError, checkedSend, historyEntry, isPlainRecord, maxMessageBytes, maxRecipients };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { SendNotification } from "./send-input.js";
|
|
2
|
+
//#region src/notifications/send-notification.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Sends `message` to users or to devices as a background job, enqueued in `db`'s transaction when it is one, so it
|
|
5
|
+
* goes out only once that commits. A user send says whether it shows in the users' inbox: then each user's history
|
|
6
|
+
* keeps it, and either way it is pushed to their devices as they are when the job runs; a device send is only pushed.
|
|
7
|
+
* The send is checked before anything is enqueued (see `checkedSend`), and authorizes nothing: who gets what is the
|
|
8
|
+
* caller's to decide.
|
|
9
|
+
*/
|
|
10
|
+
export declare const sendNotification: SendNotification;
|
|
11
|
+
//#endregion
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { currentJobs } from "../jobs/current.js";
|
|
2
|
+
import { checkedSend } from "./send-input.js";
|
|
3
|
+
import { pushJobName } from "./push-job-name.js";
|
|
4
|
+
//#region src/notifications/send-notification.ts
|
|
5
|
+
/**
|
|
6
|
+
* Sends `message` to users or to devices as a background job, enqueued in `db`'s transaction when it is one, so it
|
|
7
|
+
* goes out only once that commits. A user send says whether it shows in the users' inbox: then each user's history
|
|
8
|
+
* keeps it, and either way it is pushed to their devices as they are when the job runs; a device send is only pushed.
|
|
9
|
+
* The send is checked before anything is enqueued (see `checkedSend`), and authorizes nothing: who gets what is the
|
|
10
|
+
* caller's to decide.
|
|
11
|
+
*/
|
|
12
|
+
const sendNotification = async (db, to, message) => {
|
|
13
|
+
const send = checkedSend(to, message);
|
|
14
|
+
const job = currentJobs().definition(pushJobName);
|
|
15
|
+
if (!job) throw new Error("Notifications are off: add extends: [notifications()] to protobase.config.ts to send them");
|
|
16
|
+
await job.enqueue(db, send);
|
|
17
|
+
};
|
|
18
|
+
//#endregion
|
|
19
|
+
export { sendNotification };
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import { Device, DeviceKind } from "./device.js";
|
|
2
1
|
import { PushMessage } from "./message.js";
|
|
2
|
+
import { Device, DeviceKind } from "./device.js";
|
|
3
3
|
//#region src/notifications/sender.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* What a sender answers for one device: `ok` when the push service took the message, `gone` when the device no longer
|
|
6
6
|
* exists there and should be removed, `retry` when it may take the message later, after `retryAfterSeconds` when the
|
|
7
|
-
* push service says
|
|
7
|
+
* push service says, and `refused` when the sender never delivers to the device as it is stored, such as one whose
|
|
8
|
+
* endpoint is on no known push service: it is neither retried nor removed.
|
|
8
9
|
*/
|
|
9
10
|
export type PushAnswer = {
|
|
10
11
|
id: string;
|
|
11
|
-
result: 'ok' | 'gone' | 'retry';
|
|
12
|
+
result: 'ok' | 'gone' | 'retry' | 'refused';
|
|
12
13
|
retryAfterSeconds?: number;
|
|
13
14
|
};
|
|
14
15
|
/**
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { StaleJobError } from "../jobs/job-progress.js";
|
|
2
|
+
//#region src/notifications/store-call.ts
|
|
3
|
+
/** A device or history store method that failed while sending: it names the method and at most a SQLSTATE. */
|
|
4
|
+
var NotificationStoreError = class extends Error {
|
|
5
|
+
method;
|
|
6
|
+
code;
|
|
7
|
+
constructor(method, code) {
|
|
8
|
+
super(`The notifications store's ${method} failed${code ? ` (${code})` : ""}`);
|
|
9
|
+
this.method = method;
|
|
10
|
+
this.code = code;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
const sqlStatePattern = /^[0-9A-Z]{5}$/;
|
|
14
|
+
const codeOf = (error) => {
|
|
15
|
+
const { sqlState, code } = typeof error === "object" && error !== null ? error : {};
|
|
16
|
+
return [sqlState, code].find((value) => typeof value === "string" && sqlStatePattern.test(value));
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Calls a store method for the send job. Whatever it throws becomes a `NotificationStoreError` with no cause: an app's
|
|
20
|
+
* own store may throw a driver error whose message or detail holds a token or key, and the job keeps its error. A try
|
|
21
|
+
* that no longer holds its job stops as it is.
|
|
22
|
+
*/
|
|
23
|
+
const storeCall = async (method, call) => {
|
|
24
|
+
try {
|
|
25
|
+
return await call();
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (error instanceof StaleJobError) throw error;
|
|
28
|
+
throw new NotificationStoreError(method, codeOf(error));
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
//#endregion
|
|
32
|
+
export { NotificationStoreError, storeCall };
|
|
@@ -9,7 +9,7 @@ export type WebPushOptions = PostOptions & {
|
|
|
9
9
|
* The built-in Web Push sender (RFC 8030, 8291, 8292): delivers to `web` devices, signed with the VAPID pair and
|
|
10
10
|
* encrypted for each device's keys. It checks the message as `pushMessage` does and throws before any request when it
|
|
11
11
|
* is not one. A device whose endpoint is not on a browser's push service, or whose keys are not a subscription's, is
|
|
12
|
-
* answered `
|
|
12
|
+
* answered `refused` with no request. It logs nothing, and its errors and answers carry no endpoint, key or token.
|
|
13
13
|
*/
|
|
14
14
|
export declare const webPushSender: (settings: VapidSettings, options?: WebPushOptions) => PushSender;
|
|
15
15
|
//#endregion
|
|
@@ -33,7 +33,7 @@ const headersOf = (message) => {
|
|
|
33
33
|
* The built-in Web Push sender (RFC 8030, 8291, 8292): delivers to `web` devices, signed with the VAPID pair and
|
|
34
34
|
* encrypted for each device's keys. It checks the message as `pushMessage` does and throws before any request when it
|
|
35
35
|
* is not one. A device whose endpoint is not on a browser's push service, or whose keys are not a subscription's, is
|
|
36
|
-
* answered `
|
|
36
|
+
* answered `refused` with no request. It logs nothing, and its errors and answers carry no endpoint, key or token.
|
|
37
37
|
*/
|
|
38
38
|
const webPushSender = (settings, options = {}) => {
|
|
39
39
|
const authorization = vapidAuthorizer(settings, options.now);
|
|
@@ -42,7 +42,7 @@ const webPushSender = (settings, options = {}) => {
|
|
|
42
42
|
const keys = keysOf(device);
|
|
43
43
|
if (!url || !keys) return {
|
|
44
44
|
id: device.id,
|
|
45
|
-
result: "
|
|
45
|
+
result: "refused"
|
|
46
46
|
};
|
|
47
47
|
const body = encryptPayload(plaintext, keys);
|
|
48
48
|
const answer = await postPush(url, {
|
package/dist/package.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/server",
|
|
3
|
-
"version": "0.45.
|
|
3
|
+
"version": "0.45.19",
|
|
4
4
|
"description": "The Protobase REST API as a Hono app, with Better Auth sign-in.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
"@better-auth/passkey": "^1.7.12",
|
|
37
37
|
"@better-auth/sso": "^1.7.12",
|
|
38
38
|
"@modelcontextprotocol/server": "^2.3.1",
|
|
39
|
-
"@protobase/layout": "0.45.
|
|
40
|
-
"@protobase/query": "0.45.
|
|
41
|
-
"@protobase/schema": "0.45.
|
|
39
|
+
"@protobase/layout": "0.45.19",
|
|
40
|
+
"@protobase/query": "0.45.19",
|
|
41
|
+
"@protobase/schema": "0.45.19",
|
|
42
42
|
"@react-email/render": "^2.1.0",
|
|
43
43
|
"@scalar/hono-api-reference": "^0.12.10",
|
|
44
44
|
"@sentry/core": "^11.7.0",
|