@protobase/server 0.45.18 → 0.45.20
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/better-auth/create-auth.d.ts +9 -20
- package/dist/better-auth/create-auth.js +9 -1
- package/dist/better-auth/end-replaced-session.d.ts +17 -0
- package/dist/better-auth/end-replaced-session.js +15 -0
- package/dist/better-auth/live-sessions.js +24 -0
- package/dist/create-admin.js +6 -3
- 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/caller.js +15 -1
- package/dist/notifications/deliver.js +133 -0
- package/dist/notifications/device-input.js +17 -6
- package/dist/notifications/device-store.d.ts +5 -3
- package/dist/notifications/device.d.ts +3 -1
- package/dist/notifications/guarded-sender.js +13 -4
- package/dist/notifications/history.js +2 -1
- package/dist/notifications/options.d.ts +1 -1
- package/dist/notifications/options.js +1 -0
- package/dist/notifications/paths.d.ts +3 -5
- package/dist/notifications/paths.js +1 -4
- package/dist/notifications/postgres-store.js +4 -0
- package/dist/notifications/push-job-name.js +5 -0
- package/dist/notifications/push-job.js +52 -0
- package/dist/notifications/push-payload.js +60 -0
- package/dist/notifications/routes.js +22 -10
- package/dist/notifications/schema-sql.js +7 -1
- package/dist/notifications/schema.d.ts +2 -1
- package/dist/notifications/schema.js +19 -9
- 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 +9 -5
- package/dist/package.js +1 -1
- package/package.json +4 -4
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { StaffSignIn } from "./staff-log.js";
|
|
2
2
|
import { RoleDefinitions, RolesInput } from "./organizations/role-definitions.js";
|
|
3
3
|
import { ConnectedApp } from "./mcp/connected-apps.js";
|
|
4
|
+
import { ReplacingContext } from "./end-replaced-session.js";
|
|
4
5
|
import { UserDeletionListener } from "./user-deletion.js";
|
|
5
6
|
import "./auth-base-path.js";
|
|
6
7
|
import { Mailer } from "../mail/smtp-mailer.js";
|
|
@@ -3752,7 +3753,7 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
3752
3753
|
userId: string;
|
|
3753
3754
|
expiresAt: Date;
|
|
3754
3755
|
token: string;
|
|
3755
|
-
ipAddress
|
|
3756
|
+
ipAddress?: string | null | undefined;
|
|
3756
3757
|
userAgent?: string | null | undefined;
|
|
3757
3758
|
};
|
|
3758
3759
|
user: Record<string, any> & {
|
|
@@ -8590,25 +8591,6 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
8590
8591
|
})[];
|
|
8591
8592
|
disabledPaths: string[];
|
|
8592
8593
|
databaseHooks: {
|
|
8593
|
-
session?: {
|
|
8594
|
-
create: {
|
|
8595
|
-
before: (session: {
|
|
8596
|
-
userId: string;
|
|
8597
|
-
activeOrganizationId?: string | null;
|
|
8598
|
-
} & Record<string, unknown>) => Promise<{
|
|
8599
|
-
data: {
|
|
8600
|
-
activeOrganizationId: string;
|
|
8601
|
-
userId: string;
|
|
8602
|
-
};
|
|
8603
|
-
} | undefined>;
|
|
8604
|
-
};
|
|
8605
|
-
update: {
|
|
8606
|
-
after: (session: {
|
|
8607
|
-
userId: string;
|
|
8608
|
-
activeOrganizationId?: string | null;
|
|
8609
|
-
}) => Promise<void>;
|
|
8610
|
-
};
|
|
8611
|
-
} | undefined;
|
|
8612
8594
|
user: {
|
|
8613
8595
|
create: {
|
|
8614
8596
|
after?: ((user: import("better-auth").User & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<void>) | undefined;
|
|
@@ -8654,6 +8636,13 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
8654
8636
|
}>;
|
|
8655
8637
|
};
|
|
8656
8638
|
};
|
|
8639
|
+
session: {
|
|
8640
|
+
create: {
|
|
8641
|
+
after: (session: {
|
|
8642
|
+
token: string;
|
|
8643
|
+
}, ctx: ReplacingContext | null) => Promise<void>;
|
|
8644
|
+
};
|
|
8645
|
+
};
|
|
8657
8646
|
};
|
|
8658
8647
|
rateLimit: {
|
|
8659
8648
|
enabled: true;
|
|
@@ -5,6 +5,7 @@ import { defaultSignInPolicy } from "./sign-in-policy.js";
|
|
|
5
5
|
import { roleDefinitions } from "./organizations/role-definitions.js";
|
|
6
6
|
import { signInPolicyPlugin } from "./policy-plugin.js";
|
|
7
7
|
import { goTrueSessionsPlugin } from "../gotrue/sessions-plugin.js";
|
|
8
|
+
import { endReplacedSession } from "./end-replaced-session.js";
|
|
8
9
|
import { authBasePath } from "./auth-base-path.js";
|
|
9
10
|
import { mobileRequests } from "./mobile-requests.js";
|
|
10
11
|
import { passwordLength } from "./new-account-password.js";
|
|
@@ -43,6 +44,13 @@ import { betterAuth } from "better-auth";
|
|
|
43
44
|
import { admin, emailOTP, genericOAuth, jwt } from "better-auth/plugins";
|
|
44
45
|
import { adminAc, userAc } from "better-auth/plugins/admin/access";
|
|
45
46
|
//#region src/better-auth/create-auth.ts
|
|
47
|
+
const withEndedReplaced = (hooks) => ({
|
|
48
|
+
...hooks,
|
|
49
|
+
create: {
|
|
50
|
+
...hooks.create,
|
|
51
|
+
after: endReplacedSession
|
|
52
|
+
}
|
|
53
|
+
});
|
|
46
54
|
const platformOperator = () => readOperatorSettings(globalThis.process?.env ?? {});
|
|
47
55
|
const platformSignInSettings = () => readPlatformSignIn(globalThis.process?.env ?? {});
|
|
48
56
|
const platformOrigins = () => readOrigins(globalThis.process?.env ?? {});
|
|
@@ -244,7 +252,7 @@ const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, defini
|
|
|
244
252
|
},
|
|
245
253
|
update: { before: async (user) => ({ data: confirmedOnUpdate(user, /* @__PURE__ */ new Date()) }) }
|
|
246
254
|
},
|
|
247
|
-
|
|
255
|
+
session: withEndedReplaced(organizations ? sessionOrganizationHooks(context) : {})
|
|
248
256
|
},
|
|
249
257
|
rateLimit: {
|
|
250
258
|
enabled: true,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region src/better-auth/end-replaced-session.d.ts
|
|
2
|
+
/** The part of Better Auth's endpoint context that ending a replaced session reads. */
|
|
3
|
+
export type ReplacingContext = {
|
|
4
|
+
getSignedCookie: (name: string, secret: string) => Promise<string | null | false>;
|
|
5
|
+
context: {
|
|
6
|
+
authCookies: {
|
|
7
|
+
sessionToken: {
|
|
8
|
+
name: string;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
secret: string;
|
|
12
|
+
internalAdapter: {
|
|
13
|
+
deleteSession: (token: string) => Promise<void>;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
//#endregion
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/better-auth/end-replaced-session.ts
|
|
2
|
+
/**
|
|
3
|
+
* Better Auth's `databaseHooks.session.create.after`: a sign-in in a browser ends the session that browser's cookie
|
|
4
|
+
* held, whoever it was for, as the new session's cookie replaces it. Without this the old session would stand until it
|
|
5
|
+
* expired, and with it the devices registered in it, which then kept getting their user's push in someone else's
|
|
6
|
+
* browser. A staff sign-in as someone ends it too: nothing restores the earlier session when it stops.
|
|
7
|
+
*/
|
|
8
|
+
const endReplacedSession = async (session, ctx) => {
|
|
9
|
+
if (!ctx) return;
|
|
10
|
+
const held = await ctx.getSignedCookie(ctx.context.authCookies.sessionToken.name, ctx.context.secret);
|
|
11
|
+
if (!held || held === session.token) return;
|
|
12
|
+
await ctx.context.internalAdapter.deleteSession(held);
|
|
13
|
+
};
|
|
14
|
+
//#endregion
|
|
15
|
+
export { endReplacedSession };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//#region src/better-auth/live-sessions.ts
|
|
2
|
+
/**
|
|
3
|
+
* Which of the sessions with these ids still stand, read from the store: each live one's id with its user's id. A
|
|
4
|
+
* session signed out, revoked, ended by a ban or a newer sign-in in the same browser, or expired is not among them.
|
|
5
|
+
* Reads by the session's id, never its token.
|
|
6
|
+
*/
|
|
7
|
+
const liveSessionsIn = async (context, ids, now = /* @__PURE__ */ new Date()) => {
|
|
8
|
+
const live = /* @__PURE__ */ new Map();
|
|
9
|
+
if (ids.length === 0) return live;
|
|
10
|
+
const rows = await context.adapter.findMany({
|
|
11
|
+
model: "session",
|
|
12
|
+
where: [{
|
|
13
|
+
field: "id",
|
|
14
|
+
operator: "in",
|
|
15
|
+
value: [...ids]
|
|
16
|
+
}]
|
|
17
|
+
});
|
|
18
|
+
for (const row of rows) if (ids.includes(row.id) && new Date(row.expiresAt) > now) live.set(row.id, String(row.userId));
|
|
19
|
+
return live;
|
|
20
|
+
};
|
|
21
|
+
/** `liveSessionsIn` on the store of this Better Auth instance. */
|
|
22
|
+
const liveSessions = (auth) => async (ids) => liveSessionsIn(await auth.$context, ids);
|
|
23
|
+
//#endregion
|
|
24
|
+
export { liveSessions, liveSessionsIn };
|
package/dist/create-admin.js
CHANGED
|
@@ -57,6 +57,8 @@ 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";
|
|
61
|
+
import { liveSessions } from "./better-auth/live-sessions.js";
|
|
60
62
|
import { visitorsBrowse, withAnonymous } from "./anonymous.js";
|
|
61
63
|
import { checkBottomBars, checkShell } from "./check-shell.js";
|
|
62
64
|
import { checkOwnerFields } from "./check-owners.js";
|
|
@@ -105,7 +107,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
105
107
|
const appModels = appResources.map((resource) => resource.toModel());
|
|
106
108
|
const withComments = appModels.some((model) => model.comments);
|
|
107
109
|
const withAiFields = hasAiFields(appModels);
|
|
108
|
-
const withJobs = jobs.length > 0 || appModels.some(isCaptured) || withComments || withAiFields;
|
|
110
|
+
const withJobs = jobs.length > 0 || appModels.some(isCaptured) || withComments || withAiFields || notifications !== void 0;
|
|
109
111
|
const withHistory = appModels.some((model) => model.history);
|
|
110
112
|
const changes = changesSchema(events);
|
|
111
113
|
const days = historyDays(historyOptions);
|
|
@@ -373,7 +375,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
373
375
|
if (notifications) {
|
|
374
376
|
const devices = deviceStore(db, notifications);
|
|
375
377
|
if (auth) removeDevicesWithUsers(auth, devices);
|
|
376
|
-
app.route(devicesPath(systemPath), guarded((routes) => void routes.route("/", deviceRoutes(devices))));
|
|
378
|
+
app.route(devicesPath(systemPath), guarded((routes) => void routes.route("/", deviceRoutes(devices, { sessionRequired: Boolean(auth) }))));
|
|
377
379
|
const history = historyStore(db, notifications);
|
|
378
380
|
if (auth) removeHistoryWithUsers(auth, history);
|
|
379
381
|
app.route(historyPath(systemPath), guarded((routes) => void routes.route("/", historyRoutes(history, auth?.origins ?? []))));
|
|
@@ -435,7 +437,8 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
435
437
|
...webhooks.jobs,
|
|
436
438
|
...withHistory && days !== void 0 ? [pruneHistoryJob(changes, days)] : [],
|
|
437
439
|
...mentions ? [mentions] : [],
|
|
438
|
-
...fill ? [fill] : []
|
|
440
|
+
...fill ? [fill] : [],
|
|
441
|
+
...notifications ? [pushJob(notifications, senders, auth && liveSessions(auth))] : []
|
|
439
442
|
] });
|
|
440
443
|
};
|
|
441
444
|
//#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,
|
|
@@ -12,5 +12,19 @@ const ownSignInUser = (session) => {
|
|
|
12
12
|
if (userId === "" || session.user.id === void 0 || session.user.id === null) throw refuse();
|
|
13
13
|
return userId;
|
|
14
14
|
};
|
|
15
|
+
/**
|
|
16
|
+
* The caller of a device request: `ownSignInUser`'s user, and the id of the session they signed in with, when their
|
|
17
|
+
* authenticator names one. With `sessionRequired`, as with Protobase's auth, whose devices get push only while their
|
|
18
|
+
* session stands, a caller without one is refused too.
|
|
19
|
+
*/
|
|
20
|
+
const ownSignInDevice = (session, sessionRequired) => {
|
|
21
|
+
const userId = ownSignInUser(session);
|
|
22
|
+
const sessionId = session.signIn?.session;
|
|
23
|
+
if (sessionRequired && (typeof sessionId !== "string" || sessionId === "")) throw forbidden("own-sign-in-only", "Only someone signed in as themselves can reach their devices and notifications");
|
|
24
|
+
return {
|
|
25
|
+
userId,
|
|
26
|
+
sessionId: typeof sessionId === "string" && sessionId !== "" ? sessionId : null
|
|
27
|
+
};
|
|
28
|
+
};
|
|
15
29
|
//#endregion
|
|
16
|
-
export { ownSignInUser };
|
|
30
|
+
export { ownSignInDevice, ownSignInUser };
|
|
@@ -0,0 +1,133 @@
|
|
|
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. With
|
|
19
|
+
* `liveSessions`, a device whose session no longer stands is removed instead of pushed to. Each kind's
|
|
20
|
+
* devices go to its sender in one batch; a kind without a sender is skipped. An answer of `gone` removes the device,
|
|
21
|
+
* `retry` leaves it to the next try once and gives up on it the second time, and any other is done. A sender that
|
|
22
|
+
* throws leaves its batch to the next try. The try then throws, counting what is left, so the job retries it, or
|
|
23
|
+
* completes the send.
|
|
24
|
+
*/
|
|
25
|
+
const deliver = async ({ to, message }, given, steps) => {
|
|
26
|
+
const progress = {
|
|
27
|
+
recorded: [...given.recorded],
|
|
28
|
+
done: [...given.done],
|
|
29
|
+
retried: [...given.retried]
|
|
30
|
+
};
|
|
31
|
+
const recorded = new Set(progress.recorded);
|
|
32
|
+
const done = new Set(progress.done);
|
|
33
|
+
const retried = new Set(progress.retried);
|
|
34
|
+
const left = tally();
|
|
35
|
+
const markDone = (id) => {
|
|
36
|
+
if (done.has(id)) return;
|
|
37
|
+
done.add(id);
|
|
38
|
+
progress.done.push(id);
|
|
39
|
+
};
|
|
40
|
+
if (steps.senders.length === 0 && !("userIds" in to && to.inbox)) return steps.complete();
|
|
41
|
+
const answered = async (sender, batch) => {
|
|
42
|
+
try {
|
|
43
|
+
return await sender.send(message, batch);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (error instanceof PushSenderError) return void 0;
|
|
46
|
+
if (!(error instanceof PushMessageError)) throw error;
|
|
47
|
+
left.refusedField = error.field;
|
|
48
|
+
return batch.map((device) => ({
|
|
49
|
+
id: device.id,
|
|
50
|
+
result: "refused"
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const settle = async (device, answer) => {
|
|
55
|
+
if (answer.result === "gone") await storeCall("removeGone", () => steps.devices.removeGone([device.token]));
|
|
56
|
+
if (answer.result !== "retry" || retried.has(device.id)) return markDone(device.id);
|
|
57
|
+
retried.add(device.id);
|
|
58
|
+
progress.retried.push(device.id);
|
|
59
|
+
left.retried += 1;
|
|
60
|
+
};
|
|
61
|
+
const standing = async (devices) => {
|
|
62
|
+
const { liveSessions } = steps;
|
|
63
|
+
if (!liveSessions) return devices;
|
|
64
|
+
const pending = devices.filter((device) => !done.has(device.id));
|
|
65
|
+
const live = await liveSessions([...new Set(pending.flatMap((device) => device.sessionId === null ? [] : [device.sessionId]))]);
|
|
66
|
+
const stands = (device) => device.sessionId !== null && live.get(device.sessionId) === device.userId;
|
|
67
|
+
for (const device of pending.filter((device) => !stands(device))) {
|
|
68
|
+
await storeCall("remove", () => steps.devices.remove(device.userId, device.id));
|
|
69
|
+
markDone(device.id);
|
|
70
|
+
}
|
|
71
|
+
return pending.filter(stands);
|
|
72
|
+
};
|
|
73
|
+
const push = async (given) => {
|
|
74
|
+
const devices = await standing(given);
|
|
75
|
+
for (const device of devices) if (!steps.senders.some((sender) => sender.kinds.includes(device.kind))) markDone(device.id);
|
|
76
|
+
for (const sender of steps.senders) {
|
|
77
|
+
const batch = devices.filter((device) => sender.kinds.includes(device.kind) && !done.has(device.id));
|
|
78
|
+
if (batch.length === 0) continue;
|
|
79
|
+
await steps.hold();
|
|
80
|
+
const answers = await answered(sender, batch);
|
|
81
|
+
if (answers === void 0) left.retried += batch.length;
|
|
82
|
+
const byId = new Map((answers ?? []).map((answer) => [answer.id, answer]));
|
|
83
|
+
for (const device of answers ? batch : []) await settle(device, byId.get(device.id) ?? {
|
|
84
|
+
id: device.id,
|
|
85
|
+
result: "retry"
|
|
86
|
+
});
|
|
87
|
+
await steps.save(progress);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const forRecipient = async (run) => {
|
|
91
|
+
try {
|
|
92
|
+
await run();
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if (!(error instanceof NotificationStoreError)) throw error;
|
|
95
|
+
counted(left.storeFailures, error.method);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
if ("userIds" in to) for (const userId of to.userIds) await forRecipient(async () => {
|
|
99
|
+
if (to.inbox && !recorded.has(userId)) {
|
|
100
|
+
const next = {
|
|
101
|
+
...progress,
|
|
102
|
+
recorded: [...progress.recorded, userId]
|
|
103
|
+
};
|
|
104
|
+
await storeCall("record", () => steps.record(userId, historyEntry(message), next));
|
|
105
|
+
recorded.add(userId);
|
|
106
|
+
progress.recorded.push(userId);
|
|
107
|
+
}
|
|
108
|
+
const devices = await storeCall("listForUser", () => steps.devices.listForUser(userId));
|
|
109
|
+
const own = devices.filter((device) => device.userId === userId);
|
|
110
|
+
if (own.length < devices.length) counted(left.strays, "listForUser", devices.length - own.length);
|
|
111
|
+
await push(own);
|
|
112
|
+
});
|
|
113
|
+
else {
|
|
114
|
+
const wanted = new Set(to.deviceIds);
|
|
115
|
+
const ids = to.deviceIds.filter((id) => !done.has(id));
|
|
116
|
+
if (ids.length > 0) await forRecipient(async () => {
|
|
117
|
+
const devices = await storeCall("listByIds", () => steps.devices.listByIds(ids));
|
|
118
|
+
const asked = devices.filter((device) => wanted.has(device.id));
|
|
119
|
+
if (asked.length < devices.length) counted(left.strays, "listByIds", devices.length - asked.length);
|
|
120
|
+
await push(asked);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
if (left.refusedField !== void 0) throw new FatalJobError(`notifications.send: a sender refused the message: ${left.refusedField}`);
|
|
124
|
+
const strays = left.strays.size > 0 ? `; devices the store gave that were not the recipients', which got nothing: ${listed(left.strays)}` : "";
|
|
125
|
+
if (left.retried > 0 || left.storeFailures.size > 0) {
|
|
126
|
+
const failures = left.storeFailures.size > 0 ? `; store failures: ${listed(left.storeFailures)}` : "";
|
|
127
|
+
throw new Error(`notifications.send: ${left.retried} deliveries are retried${failures}${strays}`);
|
|
128
|
+
}
|
|
129
|
+
await steps.complete();
|
|
130
|
+
if (strays) throw new FatalJobError(`notifications.send: delivered${strays}`);
|
|
131
|
+
};
|
|
132
|
+
//#endregion
|
|
133
|
+
export { deliver };
|
|
@@ -7,6 +7,7 @@ const text = (max) => z.string().min(1).max(max).regex(/^[^\u0000-\u001f\u007f]*
|
|
|
7
7
|
const base64url = (bytes) => z.string().max(200).regex(/^[A-Za-z0-9_-]+$/).refine((value) => Buffer.from(value, "base64url").length === bytes);
|
|
8
8
|
const appId = z.string().regex(/^[A-Za-z0-9._-]{1,155}$/);
|
|
9
9
|
const userAgent = text(512).optional();
|
|
10
|
+
const userId = text(255).optional();
|
|
10
11
|
const web = z.strictObject({
|
|
11
12
|
kind: z.literal("web"),
|
|
12
13
|
endpoint: text(2048).refine(isPushEndpoint),
|
|
@@ -14,20 +15,23 @@ const web = z.strictObject({
|
|
|
14
15
|
p256dh: base64url(65),
|
|
15
16
|
auth: base64url(16)
|
|
16
17
|
}),
|
|
17
|
-
userAgent
|
|
18
|
+
userAgent,
|
|
19
|
+
userId
|
|
18
20
|
});
|
|
19
21
|
const apns = z.strictObject({
|
|
20
22
|
kind: z.literal("apns"),
|
|
21
23
|
token: z.string().regex(/^[0-9a-f]{64,200}$/),
|
|
22
24
|
environment: z.enum(apnsEnvironments),
|
|
23
25
|
appId,
|
|
24
|
-
userAgent
|
|
26
|
+
userAgent,
|
|
27
|
+
userId
|
|
25
28
|
});
|
|
26
29
|
const fcm = z.strictObject({
|
|
27
30
|
kind: z.literal("fcm"),
|
|
28
31
|
token: z.string().regex(/^[A-Za-z0-9_:-]{1,4096}$/),
|
|
29
32
|
appId: appId.optional(),
|
|
30
|
-
userAgent
|
|
33
|
+
userAgent,
|
|
34
|
+
userId
|
|
31
35
|
});
|
|
32
36
|
const registration = z.discriminatedUnion("kind", [
|
|
33
37
|
web,
|
|
@@ -37,8 +41,9 @@ const registration = z.discriminatedUnion("kind", [
|
|
|
37
41
|
/** The largest register body, in bytes. */
|
|
38
42
|
const maxDeviceBodyBytes = 8192;
|
|
39
43
|
/**
|
|
40
|
-
* The device a register request's JSON body describes,
|
|
41
|
-
* value: one kind's fields only, each within its format and length, and
|
|
44
|
+
* The device a register request's JSON body describes, and the user it names when it names one, or a 400 that names
|
|
45
|
+
* the first field at fault and never its value: one kind's fields only, each within its format and length, and
|
|
46
|
+
* nothing else.
|
|
42
47
|
*/
|
|
43
48
|
const deviceInput = (body) => {
|
|
44
49
|
const parsed = registration.safeParse(body);
|
|
@@ -46,7 +51,13 @@ const deviceInput = (body) => {
|
|
|
46
51
|
const field = parsed.error.issues[0].path.join(".") || "body";
|
|
47
52
|
throw badRequest("invalid-device", `The device is invalid: ${field}`);
|
|
48
53
|
}
|
|
49
|
-
const
|
|
54
|
+
const { userId } = parsed.data;
|
|
55
|
+
return {
|
|
56
|
+
device: deviceOf(parsed.data),
|
|
57
|
+
...userId !== void 0 && { userId }
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
const deviceOf = (device) => {
|
|
50
61
|
const base = { userAgent: device.userAgent ?? null };
|
|
51
62
|
if (device.kind === "web") return {
|
|
52
63
|
...base,
|