@protobase/server 0.45.12 → 0.45.14
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/authenticator.js +13 -7
- package/dist/better-auth/create-auth.d.ts +21 -0
- package/dist/better-auth/create-auth.js +13 -1
- package/dist/better-auth/token-standing.js +21 -12
- package/dist/better-auth/user-deletion.d.ts +4 -0
- package/dist/better-auth/user-deletion.js +50 -0
- package/dist/better-auth/users.d.ts +1 -1
- package/dist/better-auth/users.js +3 -1
- package/dist/create-admin.d.ts +4 -1
- package/dist/create-admin.js +9 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.js +7 -1
- package/dist/notifications/caller.js +16 -0
- package/dist/notifications/device-input.js +80 -0
- package/dist/notifications/device-store-of.d.ts +13 -0
- package/dist/notifications/device-store-of.js +13 -0
- package/dist/notifications/device-store.d.ts +25 -0
- package/dist/notifications/device-store.js +22 -0
- package/dist/notifications/device.d.ts +30 -0
- package/dist/notifications/device.js +5 -0
- package/dist/notifications/options.d.ts +20 -0
- package/dist/notifications/options.js +43 -0
- package/dist/notifications/paths.d.ts +6 -0
- package/dist/notifications/paths.js +7 -0
- package/dist/notifications/plugin.d.ts +10 -0
- package/dist/notifications/plugin.js +13 -0
- package/dist/notifications/postgres-store.d.ts +15 -0
- package/dist/notifications/postgres-store.js +110 -0
- package/dist/notifications/push-hosts.js +18 -0
- package/dist/notifications/routes.d.ts +6 -0
- package/dist/notifications/routes.js +57 -0
- package/dist/notifications/schema-sql.js +40 -0
- package/dist/notifications/schema.d.ts +17 -0
- package/dist/notifications/schema.js +59 -0
- package/dist/package.js +1 -1
- package/dist/project-config.d.ts +3 -0
- package/dist/project-config.js +3 -1
- package/dist/types.d.ts +9 -0
- package/package.json +4 -4
|
@@ -44,8 +44,12 @@ const betterAuthAuthenticator = (options) => {
|
|
|
44
44
|
});
|
|
45
45
|
const verify = async (request) => {
|
|
46
46
|
const { payload, session } = await sessions(request);
|
|
47
|
-
if (auth)
|
|
48
|
-
|
|
47
|
+
if (!auth) return session;
|
|
48
|
+
const signIn = await standing(await auth.$context, payload, session);
|
|
49
|
+
return signIn ? {
|
|
50
|
+
...session,
|
|
51
|
+
signIn
|
|
52
|
+
} : session;
|
|
49
53
|
};
|
|
50
54
|
if (!organizations) return verify;
|
|
51
55
|
return async (request) => {
|
|
@@ -65,11 +69,13 @@ const betterAuthAuthenticator = (options) => {
|
|
|
65
69
|
const dpopReplayStore = () => replayStore ??= options.auth ? options.auth.$context.then(({ internalAdapter }) => createDpopReplayStore(internalAdapter)) : Promise.resolve(createInMemoryDpopReplayStore());
|
|
66
70
|
const ownStanding = (context, payload, session) => checkTokenStanding(context, tokenClaims(payload, session.user.roles, organizations ? session.tenant : void 0), organizations);
|
|
67
71
|
const own = sessionOf({ audience: options.audience ?? issuer }, ownStanding);
|
|
68
|
-
const grantStanding = (context, payload, session) =>
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
const grantStanding = async (context, payload, session) => {
|
|
73
|
+
await checkMcpGrant(context.adapter, {
|
|
74
|
+
userId: payload.sub,
|
|
75
|
+
clientId: session.client?.id,
|
|
76
|
+
issuedAt: payload.iat
|
|
77
|
+
});
|
|
78
|
+
};
|
|
73
79
|
return Object.assign(withTrustedApps(own, issuer, options.apps ?? []), { forAudience: (audience) => sessionOf({
|
|
74
80
|
audience,
|
|
75
81
|
typ: "at+jwt",
|
|
@@ -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 { UserDeletionListener } from "./user-deletion.js";
|
|
4
5
|
import "./auth-base-path.js";
|
|
5
6
|
import { Mailer } from "../mail/smtp-mailer.js";
|
|
6
7
|
import { OperatorProvider } from "./operator-provider.js";
|
|
@@ -232,6 +233,18 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
232
233
|
};
|
|
233
234
|
};
|
|
234
235
|
plugins: ({
|
|
236
|
+
id: "protobase-user-deletion";
|
|
237
|
+
hooks: {
|
|
238
|
+
before: {
|
|
239
|
+
matcher: (context: import("better-auth").HookEndpointContext) => boolean;
|
|
240
|
+
handler: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<void>>;
|
|
241
|
+
}[];
|
|
242
|
+
after: {
|
|
243
|
+
matcher: (context: import("better-auth").HookEndpointContext) => boolean;
|
|
244
|
+
handler: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<void>>;
|
|
245
|
+
}[];
|
|
246
|
+
};
|
|
247
|
+
} | {
|
|
235
248
|
id: "admin";
|
|
236
249
|
version: string;
|
|
237
250
|
init(): {
|
|
@@ -8717,6 +8730,14 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
8717
8730
|
* names. Only `createAdmin` with a `mobile` section uses it; `handler` and `api` never trust a Capacitor origin.
|
|
8718
8731
|
*/
|
|
8719
8732
|
handleMobile: (request: Request) => Promise<Response>;
|
|
8733
|
+
/**
|
|
8734
|
+
* What runs when a user is deleted: `set` a named listener that removes what the app keeps of the user elsewhere,
|
|
8735
|
+
* such as their devices; `run` is for a deletion outside Better Auth's endpoints (`deleteUser`).
|
|
8736
|
+
*/
|
|
8737
|
+
userDeletion: {
|
|
8738
|
+
set: (name: string, listener: UserDeletionListener) => undefined;
|
|
8739
|
+
run: (userId: string) => Promise<void>;
|
|
8740
|
+
};
|
|
8720
8741
|
/** Issuer and audience of the tokens: `baseURL`, on every origin. */
|
|
8721
8742
|
issuer: string;
|
|
8722
8743
|
/** Every origin the app is served on, `baseURL`'s first. */
|
|
@@ -12,6 +12,7 @@ import { platformMailer } from "../mail/platform-mailer.js";
|
|
|
12
12
|
import { readOperatorSettings, resolveOperator } from "./operator-provider.js";
|
|
13
13
|
import { defaultInvitationUrl } from "./invitations/invitation-link-hook.js";
|
|
14
14
|
import { lastAdminGuardPlugin } from "./last-admin-guard.js";
|
|
15
|
+
import { userDeletion } from "./user-deletion.js";
|
|
15
16
|
import { revokeMcpGrants } from "./mcp/revoke-grants.js";
|
|
16
17
|
import { coreNames, pluginNames } from "./snake-case-names.js";
|
|
17
18
|
import { lastOrganizationField, sessionOrganizationHooks } from "./organizations/session-organization.js";
|
|
@@ -62,7 +63,7 @@ const organizationPaths = ["/organization/set-active", "/organization/accept-inv
|
|
|
62
63
|
* The options `createAuth` gives Better Auth, with the roles, default role, mailer, operator provider and platform
|
|
63
64
|
* sign-in provider already resolved.
|
|
64
65
|
*/
|
|
65
|
-
const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, definitions, defaultRole, appName = defaultAppName, mailer, signUpRole, operator, platformSignIn, saml = [], organizations, mcp, context = noContext, signInPolicy = defaultSignInPolicy, mobile }) => {
|
|
66
|
+
const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, definitions, defaultRole, appName = defaultAppName, mailer, signUpRole, operator, platformSignIn, saml = [], organizations, mcp, context = noContext, signInPolicy = defaultSignInPolicy, mobile, deletion }) => {
|
|
66
67
|
const secure = options.baseURL.startsWith("https://");
|
|
67
68
|
const signIn = platformSignInFor(platformSignIn, Object.keys(options.socialProviders ?? {}));
|
|
68
69
|
const hosts = passkeyHosts([options.baseURL, ...origins]);
|
|
@@ -182,6 +183,7 @@ const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, defini
|
|
|
182
183
|
}),
|
|
183
184
|
rolesPlugin({ definitions }),
|
|
184
185
|
lastAdminGuardPlugin(),
|
|
186
|
+
...deletion ? [deletion.plugin] : [],
|
|
185
187
|
...organizations ? [organizationPlugin({
|
|
186
188
|
resolved: organizations,
|
|
187
189
|
secret: options.secret,
|
|
@@ -314,8 +316,10 @@ const createAuth = (options) => {
|
|
|
314
316
|
let instance;
|
|
315
317
|
const context = () => instance?.$context ?? Promise.reject(/* @__PURE__ */ new Error("The auth instance is not made yet"));
|
|
316
318
|
const mobile = mobileRequests();
|
|
319
|
+
const deletion = userDeletion();
|
|
317
320
|
const authOptions = betterAuthOptions(options, {
|
|
318
321
|
mobile,
|
|
322
|
+
deletion,
|
|
319
323
|
origins,
|
|
320
324
|
passkeyDomain,
|
|
321
325
|
roles,
|
|
@@ -341,6 +345,14 @@ const createAuth = (options) => {
|
|
|
341
345
|
* names. Only `createAdmin` with a `mobile` section uses it; `handler` and `api` never trust a Capacitor origin.
|
|
342
346
|
*/
|
|
343
347
|
handleMobile: (request) => auth.handler(mobile.grant(request)),
|
|
348
|
+
/**
|
|
349
|
+
* What runs when a user is deleted: `set` a named listener that removes what the app keeps of the user elsewhere,
|
|
350
|
+
* such as their devices; `run` is for a deletion outside Better Auth's endpoints (`deleteUser`).
|
|
351
|
+
*/
|
|
352
|
+
userDeletion: {
|
|
353
|
+
set: deletion.set,
|
|
354
|
+
run: deletion.run
|
|
355
|
+
},
|
|
344
356
|
/** Issuer and audience of the tokens: `baseURL`, on every origin. */
|
|
345
357
|
issuer: options.baseURL,
|
|
346
358
|
/** Every origin the app is served on, `baseURL`'s first. */
|
|
@@ -3,7 +3,16 @@ import { noGlobalRole } from "./organizations/role-definitions.js";
|
|
|
3
3
|
import { parseRoles } from "./parse-roles.js";
|
|
4
4
|
import { effectiveRoles, globalRoles } from "./organizations/app-roles.js";
|
|
5
5
|
import { findMember, findOrganization } from "./organizations/store.js";
|
|
6
|
+
import { isStaffSession } from "./staff-checks.js";
|
|
6
7
|
//#region src/better-auth/token-standing.ts
|
|
8
|
+
/**
|
|
9
|
+
* The sign-in a session row stands for: staff started it when the admin plugin's `impersonatedBy` names someone. A row
|
|
10
|
+
* read without that field gives `undefined`, so a caller who cannot be told apart from staff is never taken for the user.
|
|
11
|
+
*/
|
|
12
|
+
const signInOf = (session, id) => "impersonatedBy" in session && session.impersonatedBy !== void 0 ? {
|
|
13
|
+
session: id,
|
|
14
|
+
staff: isStaffSession(session)
|
|
15
|
+
} : void 0;
|
|
7
16
|
const tokenClaims = (payload, roles, tenant) => ({
|
|
8
17
|
userId: payload.sub,
|
|
9
18
|
...typeof payload.sid === "string" && { sessionId: payload.sid },
|
|
@@ -26,20 +35,19 @@ const heldWhere = async ({ adapter }, user, organizations, organizationId) => {
|
|
|
26
35
|
/**
|
|
27
36
|
* Whether a token still stands, read from the store on every request: its user exists and is not banned, its session
|
|
28
37
|
* is not signed out or expired, and each role it carries is one the user still holds there. A token that fails is a
|
|
29
|
-
* 401, so the client asks its session for a new one, which carries the roles as they are now or is refused.
|
|
38
|
+
* 401, so the client asks its session for a new one, which carries the roles as they are now or is refused. Answers the
|
|
39
|
+
* sign-in the token's session stands for (`signInOf`), `undefined` for a token without one.
|
|
30
40
|
*/
|
|
31
41
|
const checkTokenStanding = async (context, claims, organizations, now = /* @__PURE__ */ new Date()) => {
|
|
32
42
|
const { adapter } = context;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (!session || session.userId !== claims.userId || session.expiresAt <= now) throw revoked();
|
|
42
|
-
}
|
|
43
|
+
const session = claims.sessionId === void 0 ? void 0 : await adapter.findOne({
|
|
44
|
+
model: "session",
|
|
45
|
+
where: [{
|
|
46
|
+
field: "id",
|
|
47
|
+
value: claims.sessionId
|
|
48
|
+
}]
|
|
49
|
+
});
|
|
50
|
+
if (claims.sessionId !== void 0 && (!session || session.userId !== claims.userId || session.expiresAt <= now)) throw revoked();
|
|
43
51
|
const user = await adapter.findOne({
|
|
44
52
|
model: "user",
|
|
45
53
|
where: [{
|
|
@@ -50,6 +58,7 @@ const checkTokenStanding = async (context, claims, organizations, now = /* @__PU
|
|
|
50
58
|
if (!user || bannedNow(user, now)) throw revoked();
|
|
51
59
|
const held = await heldWhere(context, user, organizations, claims.organizationId);
|
|
52
60
|
if (!held || claims.roles.some((role) => !held.includes(role))) throw revoked();
|
|
61
|
+
return session ? signInOf(session, claims.sessionId) : void 0;
|
|
53
62
|
};
|
|
54
63
|
//#endregion
|
|
55
|
-
export { checkTokenStanding, tokenClaims };
|
|
64
|
+
export { checkTokenStanding, signInOf, tokenClaims };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import "./organizations/role-definitions.js";
|
|
2
|
+
import { parseRoles } from "./parse-roles.js";
|
|
3
|
+
import { createAuthMiddleware, getSessionFromCtx, isAPIError } from "better-auth/api";
|
|
4
|
+
//#region src/better-auth/user-deletion.ts
|
|
5
|
+
const removePath = "/admin/remove-user";
|
|
6
|
+
/**
|
|
7
|
+
* What runs when a user is deleted, by an admin (`/admin/remove-user`) or by `deleteUser` (the CLI): each listener
|
|
8
|
+
* before anything is deleted, so one that throws stops the deletion with the user, their sessions and accounts in
|
|
9
|
+
* place, and once more after the user is gone, for what was added in between.
|
|
10
|
+
*/
|
|
11
|
+
const userDeletion = () => {
|
|
12
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
13
|
+
const run = async (userId) => {
|
|
14
|
+
for (const listener of listeners.values()) await listener(userId);
|
|
15
|
+
};
|
|
16
|
+
const allowedRemoval = async (ctx) => {
|
|
17
|
+
const userId = ctx.body?.userId;
|
|
18
|
+
if (typeof userId !== "string" || userId === "") return void 0;
|
|
19
|
+
const session = await getSessionFromCtx(ctx);
|
|
20
|
+
if (!session || !parseRoles(session.user.role).includes("admin") || session.user.id === userId) return void 0;
|
|
21
|
+
return await ctx.context.internalAdapter.findUserById(userId) ? userId : void 0;
|
|
22
|
+
};
|
|
23
|
+
return {
|
|
24
|
+
plugin: {
|
|
25
|
+
id: "protobase-user-deletion",
|
|
26
|
+
hooks: {
|
|
27
|
+
before: [{
|
|
28
|
+
matcher: (context) => context.path === removePath,
|
|
29
|
+
handler: createAuthMiddleware(async (ctx) => {
|
|
30
|
+
const userId = await allowedRemoval(ctx);
|
|
31
|
+
if (userId !== void 0) await run(userId);
|
|
32
|
+
})
|
|
33
|
+
}],
|
|
34
|
+
after: [{
|
|
35
|
+
matcher: (context) => context.path === removePath,
|
|
36
|
+
handler: createAuthMiddleware(async (ctx) => {
|
|
37
|
+
const userId = ctx.body?.userId;
|
|
38
|
+
if (isAPIError(ctx.context.returned) || typeof userId !== "string" || userId === "") return;
|
|
39
|
+
await run(userId);
|
|
40
|
+
})
|
|
41
|
+
}]
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
run,
|
|
45
|
+
/** Sets the listener of `name`, in place of the one it had: an app built again does not add another. */
|
|
46
|
+
set: (name, listener) => void listeners.set(name, listener)
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
//#endregion
|
|
50
|
+
export { userDeletion };
|
|
@@ -42,7 +42,7 @@ export declare const createUser: (auth: AdminAuth, input: NewUser) => Promise<{
|
|
|
42
42
|
role: string;
|
|
43
43
|
}>;
|
|
44
44
|
export declare const listUsers: (auth: AdminAuth) => Promise<StoredUser[]>;
|
|
45
|
-
/** Deletes the user with their sessions and accounts; refuses to delete the last active admin. */
|
|
45
|
+
/** Deletes the user with their sessions and accounts, and what `auth.userDeletion` removes; refuses to delete the last active admin. */
|
|
46
46
|
export declare const deleteUser: (auth: AdminAuth, email: string) => Promise<void>;
|
|
47
47
|
/** Sets the role: one of the configured roles, or several separated by commas; refuses to demote the last active admin. */
|
|
48
48
|
export declare const setUserRole: (auth: AdminAuth, input: {
|
|
@@ -80,13 +80,15 @@ const listUsers = async (auth) => (await allUsers(auth)).map((user) => ({
|
|
|
80
80
|
banned: Boolean(user.banned),
|
|
81
81
|
createdAt: new Date(user.createdAt).toISOString()
|
|
82
82
|
}));
|
|
83
|
-
/** Deletes the user with their sessions and accounts; refuses to delete the last active admin. */
|
|
83
|
+
/** Deletes the user with their sessions and accounts, and what `auth.userDeletion` removes; refuses to delete the last active admin. */
|
|
84
84
|
const deleteUser = async (auth, email) => {
|
|
85
85
|
const user = await findUser(auth, email);
|
|
86
86
|
await refuseLastAdmin(auth, user, "delete");
|
|
87
87
|
const { internalAdapter } = await auth.$context;
|
|
88
|
+
await auth.userDeletion.run(user.id);
|
|
88
89
|
await internalAdapter.deleteUserSessions(user.id);
|
|
89
90
|
await internalAdapter.deleteUser(user.id);
|
|
91
|
+
await auth.userDeletion.run(user.id);
|
|
90
92
|
};
|
|
91
93
|
/** Sets the role: one of the configured roles, or several separated by commas; refuses to demote the last active admin. */
|
|
92
94
|
const setUserRole = async (auth, input) => {
|
package/dist/create-admin.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { Admin, Authenticator, PageSource, ViewSource } from "./types.js";
|
|
|
8
8
|
import { CommentUsers } from "./comments/comment-users.js";
|
|
9
9
|
import { HistoryOptions } from "./history/options.js";
|
|
10
10
|
import { WebhooksOptions } from "./webhooks/table/options.js";
|
|
11
|
+
import { NotificationsOptions } from "./notifications/options.js";
|
|
11
12
|
import { SubscriptionDefinition } from "./webhooks/subscription.js";
|
|
12
13
|
import { FilesOptions } from "./files/options.js";
|
|
13
14
|
import { Difficulty } from "./forms/challenge.js";
|
|
@@ -46,6 +47,8 @@ export type CreateAdminInput = {
|
|
|
46
47
|
subscriptions?: SubscriptionDefinition[];
|
|
47
48
|
/** Webhook subscriptions made in the admin app, from `extends: [webhooks()]` (see `WebhooksOptions`). */
|
|
48
49
|
webhooks?: WebhooksOptions;
|
|
50
|
+
/** Each user's devices and the endpoints that register them, from `extends: [notifications()]` (see `NotificationsOptions`). */
|
|
51
|
+
notifications?: NotificationsOptions;
|
|
49
52
|
/**
|
|
50
53
|
* The app's iOS and Android build (see `MobileConfig`): the API answers its Capacitor origins through CORS, and sign-in
|
|
51
54
|
* at `/api/auth` trusts the Capacitor origin a request names, without CORS, as the app calls it through native HTTP.
|
|
@@ -85,5 +88,5 @@ export type CreateAdminInput = {
|
|
|
85
88
|
upgradeWebSocket?: UpgradeWebSocket;
|
|
86
89
|
};
|
|
87
90
|
/** The admin REST API as a Hono app. It uses only Web APIs, so it runs on Node, Workers and anywhere else Hono does. */
|
|
88
|
-
export declare const createAdmin: ({ resources: appResources, views: appViews, pages, userMenu, bottomBars, db, authenticate, auth, functions, files: filesOptions, events, history: historyOptions, jobs, subscriptions, webhooks: webhooksOptions, mobile, app: identity, icons, bundlePlugins, fieldTypes, options, formsDifficulty, commentUsers: givenCommentUsers, verifyToken, upgradeWebSocket }: CreateAdminInput) => Admin;
|
|
91
|
+
export declare const createAdmin: ({ resources: appResources, views: appViews, pages, userMenu, bottomBars, db, authenticate, auth, functions, files: filesOptions, events, history: historyOptions, jobs, subscriptions, webhooks: webhooksOptions, notifications, mobile, app: identity, icons, bundlePlugins, fieldTypes, options, formsDifficulty, commentUsers: givenCommentUsers, verifyToken, upgradeWebSocket }: CreateAdminInput) => Admin;
|
|
89
92
|
//#endregion
|
package/dist/create-admin.js
CHANGED
|
@@ -51,6 +51,9 @@ import { toApiFunction } from "./functions/define-function.js";
|
|
|
51
51
|
import { createWebhooks } from "./webhooks/webhooks.js";
|
|
52
52
|
import { webhookTable } from "./webhooks/table/webhook-table.js";
|
|
53
53
|
import { subscriptionUrlCheck, subscriptionWriteCheck } from "./webhooks/table/validate-hook.js";
|
|
54
|
+
import { deviceStore, removeDevicesWithUsers } from "./notifications/device-store-of.js";
|
|
55
|
+
import { devicesPath } from "./notifications/paths.js";
|
|
56
|
+
import { deviceRoutes } from "./notifications/routes.js";
|
|
54
57
|
import { visitorsBrowse, withAnonymous } from "./anonymous.js";
|
|
55
58
|
import { checkBottomBars, checkShell } from "./check-shell.js";
|
|
56
59
|
import { checkOwnerFields } from "./check-owners.js";
|
|
@@ -94,7 +97,7 @@ const declaredRoles = (auth, roles) => {
|
|
|
94
97
|
return names.length > 0 ? [...new Set(names)] : void 0;
|
|
95
98
|
};
|
|
96
99
|
/** The admin REST API as a Hono app. It uses only Web APIs, so it runs on Node, Workers and anywhere else Hono does. */
|
|
97
|
-
const createAdmin = ({ resources: appResources, views: appViews = [], pages = [], userMenu, bottomBars = [], db, authenticate, auth, functions = {}, files: filesOptions, events, history: historyOptions, jobs = [], subscriptions = [], webhooks: webhooksOptions, mobile, app: identity, icons, bundlePlugins = [], fieldTypes = {}, options = {}, formsDifficulty, commentUsers: givenCommentUsers, verifyToken, upgradeWebSocket }) => {
|
|
100
|
+
const createAdmin = ({ resources: appResources, views: appViews = [], pages = [], userMenu, bottomBars = [], db, authenticate, auth, functions = {}, files: filesOptions, events, history: historyOptions, jobs = [], subscriptions = [], webhooks: webhooksOptions, notifications, mobile, app: identity, icons, bundlePlugins = [], fieldTypes = {}, options = {}, formsDifficulty, commentUsers: givenCommentUsers, verifyToken, upgradeWebSocket }) => {
|
|
98
101
|
const basePath = options.basePath ?? "/api/v1";
|
|
99
102
|
const appModels = appResources.map((resource) => resource.toModel());
|
|
100
103
|
const withComments = appModels.some((model) => model.comments);
|
|
@@ -361,6 +364,11 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
361
364
|
}));
|
|
362
365
|
app.route("/", batchRoute(deps, authenticate, meta));
|
|
363
366
|
app.route("/", fileRoutes(deps, authenticate));
|
|
367
|
+
if (notifications) {
|
|
368
|
+
const devices = deviceStore(db, notifications);
|
|
369
|
+
if (auth) removeDevicesWithUsers(auth, devices);
|
|
370
|
+
app.route(devicesPath(systemPath), guarded((routes) => void routes.route("/", deviceRoutes(devices))));
|
|
371
|
+
}
|
|
364
372
|
if (forms) app.route(`${systemPath}/forms`, formRoutes(deps, forms));
|
|
365
373
|
const feed = tableFeed({
|
|
366
374
|
db,
|
package/dist/index.d.ts
CHANGED
|
@@ -18,6 +18,9 @@ import { Admin, AuditEvent, AuditQueue, Authenticator, PipelineHook, ScanGuardMo
|
|
|
18
18
|
import { CommentUsers } from "./comments/comment-users.js";
|
|
19
19
|
import { HistoryOptions } from "./history/options.js";
|
|
20
20
|
import { WebhooksOptions } from "./webhooks/table/options.js";
|
|
21
|
+
import { ApnsEnvironment, Device, DeviceKind, NewDevice } from "./notifications/device.js";
|
|
22
|
+
import { DeviceStore } from "./notifications/device-store.js";
|
|
23
|
+
import { DeviceColumn, NotificationsOptions } from "./notifications/options.js";
|
|
21
24
|
import { EventData, RunContext, SubscriptionDefinition, SubscriptionEvent, SubscriptionOptions, subscription } from "./webhooks/subscription.js";
|
|
22
25
|
import { Sharp, renderVariant, thumbHash } from "./files/images.js";
|
|
23
26
|
import { FileProvider, FileStore, StoredObject } from "./files/store.js";
|
|
@@ -86,6 +89,12 @@ import { checkWebhooksSchema, webhooksMigration } from "./webhooks/table/schema.
|
|
|
86
89
|
import { checkComments, commentsMigration } from "./comments/comments-sql.js";
|
|
87
90
|
import { CheckAiInput, checkAi } from "./ai/check-ai.js";
|
|
88
91
|
import { CommentItem } from "./comments/comment-item.js";
|
|
92
|
+
import { notifications } from "./notifications/plugin.js";
|
|
93
|
+
import { checkNotificationsSchema, notificationsMigration } from "./notifications/schema.js";
|
|
94
|
+
import { deviceStore, removeDevicesWithUsers } from "./notifications/device-store-of.js";
|
|
95
|
+
import { DeviceStoreError, postgresDeviceStore } from "./notifications/postgres-store.js";
|
|
96
|
+
import { deviceIdParam, devicesPath } from "./notifications/paths.js";
|
|
97
|
+
import { maxDevicesPerUser } from "./notifications/routes.js";
|
|
89
98
|
import { MemberInput, NewOrganization, addAppRoleToOwners, addMember, createOrganization, listOrganizations, removeMember, setMemberRoles } from "./better-auth/organizations/host.js";
|
|
90
99
|
import { AssistantBackend, assistantProtocolRoutes } from "./assistant/protocol-routes.js";
|
|
91
100
|
import { fileConversationStore } from "./assistant/file-conversation-store.js";
|
|
@@ -137,4 +146,4 @@ import { checkJobsSchema, jobsSchema, jobsSchemaMigration } from "./jobs/schema.
|
|
|
137
146
|
import { MobileRelease, ReleaseFile, ServedReleaseFile, composeRelease, releaseManifest } from "./mobile-release/release.js";
|
|
138
147
|
import { MobileReleaseAppInput, OfferedRelease, mobileReleaseApp } from "./mobile-release/release-app.js";
|
|
139
148
|
import { mobileSigningKeyVariable, readSigningKey } from "./mobile-release/signing-key.js";
|
|
140
|
-
export { type Admin, type AdminAuth, type AdminOptions, type ApiFunction, type ApiKeys, type ApiKeysOptions, 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 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 HistoryOptions, HttpProblem, type IssueTokenOptions, type JobContext, type JobDefinition, type JobOptions, type JobsRuntime, 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 NewOrganization, type NewUser, 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 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 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 User, type UserCreatedHook, type ViewSource, 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, checkQueryRunner, checkSourceNames, checkViewQueries, checkWebhooksSchema, coalesce, commentsMigration, composeRelease, computedMigration, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createEncryptionTools, createFileCleanup, createOrganization, createRecordTool, createUser, defaultAppName, defaultSignInPolicy, defineConfig, defineEmail, defineFunction, defineJob, defineTool, deleteRecordTool, deleteUser, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, getRecordTool, goTruePath, handleDroppedConnections, hasUsers, 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, maxTokenTtlSeconds, maxWidgetPropsBytes, mcpGate, mcpHandler, mcpRoutes, mcpServer, mcpToolContext, mcpTools, mergeConfig, migrationCommand, mobileReleaseApp, mobileSigningKeyVariable, newWebhookSecret, normalizeRoles, notFound, oauthWellKnownPaths, objectKey, parseColumns, parseRoles, passwordLength, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, pngIcons, poolWaitMs, processor, queryRunnerMigration, readDbConnections, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readRecordTools, readSigningKey, readWriteQueryTool, readableResources, realtimeLimits, recordOf, releaseManifest, 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, variantPath, verifySignature, verifyStandardWebhook, waitUntil, wantsLongQueries, webManifestPath, webauthnPath, webhooks, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps, writeRecordTools };
|
|
149
|
+
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 HistoryOptions, HttpProblem, type IssueTokenOptions, type JobContext, type JobDefinition, type JobOptions, type JobsRuntime, 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 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 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 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 User, type UserCreatedHook, type ViewSource, 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, defaultSignInPolicy, defineConfig, defineEmail, defineFunction, defineJob, defineTool, deleteRecordTool, deleteUser, deviceIdParam, deviceStore, devicesPath, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, getRecordTool, goTruePath, handleDroppedConnections, hasUsers, 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, maxTokenTtlSeconds, maxWidgetPropsBytes, mcpGate, mcpHandler, mcpRoutes, mcpServer, mcpToolContext, mcpTools, mergeConfig, migrationCommand, mobileReleaseApp, mobileSigningKeyVariable, newWebhookSecret, normalizeRoles, notFound, notifications, notificationsMigration, oauthWellKnownPaths, objectKey, parseColumns, parseRoles, passwordLength, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, pngIcons, poolWaitMs, postgresDeviceStore, processor, queryRunnerMigration, readDbConnections, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readRecordTools, readSigningKey, readWriteQueryTool, readableResources, realtimeLimits, recordOf, releaseManifest, removeDevicesWithUsers, 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, variantPath, verifySignature, verifyStandardWebhook, waitUntil, wantsLongQueries, webManifestPath, webauthnPath, webhooks, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps, writeRecordTools };
|
package/dist/index.js
CHANGED
|
@@ -56,6 +56,10 @@ import { recordOf, resourceChange } from "./changes/resource-change.js";
|
|
|
56
56
|
import { defineFunction } from "./functions/define-function.js";
|
|
57
57
|
import { newWebhookSecret } from "./webhooks/standard-webhooks.js";
|
|
58
58
|
import { subscription } from "./webhooks/subscription.js";
|
|
59
|
+
import { DeviceStoreError, postgresDeviceStore } from "./notifications/postgres-store.js";
|
|
60
|
+
import { deviceStore, removeDevicesWithUsers } from "./notifications/device-store-of.js";
|
|
61
|
+
import { deviceIdParam, devicesPath } from "./notifications/paths.js";
|
|
62
|
+
import { maxDevicesPerUser } from "./notifications/routes.js";
|
|
59
63
|
import { errorReportingSettings, errorReportingVariable } from "./error-reporting/error-reporting-settings.js";
|
|
60
64
|
import { storageCleanupSchedule } from "./files/cleanup-schedule.js";
|
|
61
65
|
import { limitedFileStore, storeUsage } from "./files/limits.js";
|
|
@@ -86,6 +90,8 @@ import { checkComputed, computedMigration } from "./computed/computed-schema.js"
|
|
|
86
90
|
import { webhooks } from "./webhooks/table/plugin.js";
|
|
87
91
|
import { checkWebhooksSchema, webhooksMigration } from "./webhooks/table/schema.js";
|
|
88
92
|
import { checkAi } from "./ai/check-ai.js";
|
|
93
|
+
import { notifications } from "./notifications/plugin.js";
|
|
94
|
+
import { checkNotificationsSchema, notificationsMigration } from "./notifications/schema.js";
|
|
89
95
|
import { maxWidgetPropsBytes, widget } from "./assistant/widget.js";
|
|
90
96
|
import { checkQueryRunner, queryRunnerMigration } from "./assistant/query-runner/query-runner.js";
|
|
91
97
|
import { startErrorReporting } from "./error-reporting/error-reporter.js";
|
|
@@ -104,4 +110,4 @@ import { jobsDrainApp } from "./jobs/drain-route.js";
|
|
|
104
110
|
import { composeRelease, releaseManifest } from "./mobile-release/release.js";
|
|
105
111
|
import { mobileReleaseApp } from "./mobile-release/release-app.js";
|
|
106
112
|
import { mobileSigningKeyVariable, readSigningKey } from "./mobile-release/signing-key.js";
|
|
107
|
-
export { ConnectionsBusy, FatalJobError, HttpProblem, MissingCredentials, ModelError, 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, checkQueryRunner, checkSourceNames, checkViewQueries, checkWebhooksSchema, coalesce, commentsMigration, composeRelease, computedMigration, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createEncryptionTools, createFileCleanup, createOrganization, createRecordTool, createUser, defaultAppName, defaultSignInPolicy, defineConfig, defineEmail, defineFunction, defineJob, defineTool, deleteRecordTool, deleteUser, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, getRecordTool, goTruePath, handleDroppedConnections, hasUsers, 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, maxTokenTtlSeconds, maxWidgetPropsBytes, mcpGate, mcpHandler, mcpRoutes, mcpServer, mcpToolContext, mcpTools, mergeConfig, migrationCommand, mobileReleaseApp, mobileSigningKeyVariable, newWebhookSecret, normalizeRoles, notFound, oauthWellKnownPaths, objectKey, parseColumns, parseRoles, passwordLength, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, pngIcons, poolWaitMs, processor, queryRunnerMigration, readDbConnections, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readRecordTools, readSigningKey, readWriteQueryTool, readableResources, realtimeLimits, recordOf, releaseManifest, 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, variantPath, verifySignature, verifyStandardWebhook, waitUntil, wantsLongQueries, webManifestPath, webauthnPath, webhooks, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps, writeRecordTools };
|
|
113
|
+
export { ConnectionsBusy, DeviceStoreError, FatalJobError, HttpProblem, MissingCredentials, ModelError, 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, defaultSignInPolicy, defineConfig, defineEmail, defineFunction, defineJob, defineTool, deleteRecordTool, deleteUser, deviceIdParam, deviceStore, devicesPath, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, getRecordTool, goTruePath, handleDroppedConnections, hasUsers, 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, maxTokenTtlSeconds, maxWidgetPropsBytes, mcpGate, mcpHandler, mcpRoutes, mcpServer, mcpToolContext, mcpTools, mergeConfig, migrationCommand, mobileReleaseApp, mobileSigningKeyVariable, newWebhookSecret, normalizeRoles, notFound, notifications, notificationsMigration, oauthWellKnownPaths, objectKey, parseColumns, parseRoles, passwordLength, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, pngIcons, poolWaitMs, postgresDeviceStore, processor, queryRunnerMigration, readDbConnections, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readRecordTools, readSigningKey, readWriteQueryTool, readableResources, realtimeLimits, recordOf, releaseManifest, removeDevicesWithUsers, 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, variantPath, verifySignature, verifyStandardWebhook, waitUntil, wantsLongQueries, webManifestPath, webauthnPath, webhooks, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps, writeRecordTools };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { forbidden } from "../problem.js";
|
|
2
|
+
//#region src/notifications/caller.ts
|
|
3
|
+
/**
|
|
4
|
+
* The id of the user whose devices a request may register and remove: only someone signed in as themselves. Refused:
|
|
5
|
+
* visitors, outside apps acting for the user (an MCP client), and every session whose authenticator does not mark it as
|
|
6
|
+
* the user's own sign-in (`Session.signIn`), staff signed in as the user included.
|
|
7
|
+
*/
|
|
8
|
+
const ownDeviceUser = (session) => {
|
|
9
|
+
const refuse = () => forbidden("own-sign-in-only", "Only someone signed in as themselves can change their devices");
|
|
10
|
+
if (session.anonymous || session.client || session.signIn?.staff !== false) throw refuse();
|
|
11
|
+
const userId = String(session.user.id);
|
|
12
|
+
if (userId === "" || session.user.id === void 0 || session.user.id === null) throw refuse();
|
|
13
|
+
return userId;
|
|
14
|
+
};
|
|
15
|
+
//#endregion
|
|
16
|
+
export { ownDeviceUser };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { badRequest } from "../problem.js";
|
|
2
|
+
import { apnsEnvironments } from "./device.js";
|
|
3
|
+
import { canonicalEndpoint, isPushEndpoint } from "./push-hosts.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
//#region src/notifications/device-input.ts
|
|
6
|
+
const text = (max) => z.string().min(1).max(max).regex(/^[^\u0000-\u001f\u007f]*$/);
|
|
7
|
+
const base64url = (bytes) => z.string().max(200).regex(/^[A-Za-z0-9_-]+$/).refine((value) => Buffer.from(value, "base64url").length === bytes);
|
|
8
|
+
const appId = z.string().regex(/^[A-Za-z0-9._-]{1,155}$/);
|
|
9
|
+
const userAgent = text(512).optional();
|
|
10
|
+
const web = z.strictObject({
|
|
11
|
+
kind: z.literal("web"),
|
|
12
|
+
endpoint: text(2048).refine(isPushEndpoint),
|
|
13
|
+
keys: z.strictObject({
|
|
14
|
+
p256dh: base64url(65),
|
|
15
|
+
auth: base64url(16)
|
|
16
|
+
}),
|
|
17
|
+
userAgent
|
|
18
|
+
});
|
|
19
|
+
const apns = z.strictObject({
|
|
20
|
+
kind: z.literal("apns"),
|
|
21
|
+
token: z.string().regex(/^[0-9a-f]{64,200}$/),
|
|
22
|
+
environment: z.enum(apnsEnvironments),
|
|
23
|
+
appId,
|
|
24
|
+
userAgent
|
|
25
|
+
});
|
|
26
|
+
const fcm = z.strictObject({
|
|
27
|
+
kind: z.literal("fcm"),
|
|
28
|
+
token: z.string().regex(/^[A-Za-z0-9_:-]{1,4096}$/),
|
|
29
|
+
appId: appId.optional(),
|
|
30
|
+
userAgent
|
|
31
|
+
});
|
|
32
|
+
const registration = z.discriminatedUnion("kind", [
|
|
33
|
+
web,
|
|
34
|
+
apns,
|
|
35
|
+
fcm
|
|
36
|
+
]);
|
|
37
|
+
/** The largest register body, in bytes. */
|
|
38
|
+
const maxDeviceBodyBytes = 8192;
|
|
39
|
+
/**
|
|
40
|
+
* The device a register request's JSON body describes, or a 400 that names the first field at fault and never its
|
|
41
|
+
* value: one kind's fields only, each within its format and length, and nothing else, such as a user.
|
|
42
|
+
*/
|
|
43
|
+
const deviceInput = (body) => {
|
|
44
|
+
const parsed = registration.safeParse(body);
|
|
45
|
+
if (!parsed.success) {
|
|
46
|
+
const field = parsed.error.issues[0].path.join(".") || "body";
|
|
47
|
+
throw badRequest("invalid-device", `The device is invalid: ${field}`);
|
|
48
|
+
}
|
|
49
|
+
const device = parsed.data;
|
|
50
|
+
const base = { userAgent: device.userAgent ?? null };
|
|
51
|
+
if (device.kind === "web") return {
|
|
52
|
+
...base,
|
|
53
|
+
kind: "web",
|
|
54
|
+
token: canonicalEndpoint(device.endpoint),
|
|
55
|
+
p256dh: device.keys.p256dh,
|
|
56
|
+
auth: device.keys.auth,
|
|
57
|
+
environment: null,
|
|
58
|
+
appId: null
|
|
59
|
+
};
|
|
60
|
+
if (device.kind === "apns") return {
|
|
61
|
+
...base,
|
|
62
|
+
kind: "apns",
|
|
63
|
+
token: device.token,
|
|
64
|
+
p256dh: null,
|
|
65
|
+
auth: null,
|
|
66
|
+
environment: device.environment,
|
|
67
|
+
appId: device.appId
|
|
68
|
+
};
|
|
69
|
+
return {
|
|
70
|
+
...base,
|
|
71
|
+
kind: "fcm",
|
|
72
|
+
token: device.token,
|
|
73
|
+
p256dh: null,
|
|
74
|
+
auth: null,
|
|
75
|
+
environment: null,
|
|
76
|
+
appId: device.appId ?? null
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
//#endregion
|
|
80
|
+
export { deviceInput, maxDeviceBodyBytes };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { AdminAuth } from "../better-auth/create-auth.js";
|
|
2
|
+
import { DeviceStore } from "./device-store.js";
|
|
3
|
+
import { NotificationsOptions } from "./options.js";
|
|
4
|
+
import { Db } from "@protobase/query";
|
|
5
|
+
//#region src/notifications/device-store-of.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* The devices' store `notifications()` configures: the app's own `store`, or the devices table in `db`. An app that
|
|
8
|
+
* deletes users without Protobase's auth calls its `removeAllForUser` as it deletes one.
|
|
9
|
+
*/
|
|
10
|
+
export declare const deviceStore: (db: Db, options?: NotificationsOptions) => DeviceStore;
|
|
11
|
+
/** Has `auth` remove a user's devices from `store` when it deletes the user, before and once more after. */
|
|
12
|
+
export declare const removeDevicesWithUsers: (auth: AdminAuth, store: DeviceStore) => void;
|
|
13
|
+
//#endregion
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { postgresDeviceStore } from "./postgres-store.js";
|
|
2
|
+
//#region src/notifications/device-store-of.ts
|
|
3
|
+
/**
|
|
4
|
+
* The devices' store `notifications()` configures: the app's own `store`, or the devices table in `db`. An app that
|
|
5
|
+
* deletes users without Protobase's auth calls its `removeAllForUser` as it deletes one.
|
|
6
|
+
*/
|
|
7
|
+
const deviceStore = (db, options = {}) => options.store ?? postgresDeviceStore(db, options);
|
|
8
|
+
/** Has `auth` remove a user's devices from `store` when it deletes the user, before and once more after. */
|
|
9
|
+
const removeDevicesWithUsers = (auth, store) => {
|
|
10
|
+
auth.userDeletion.set("notifications", (userId) => store.removeAllForUser(userId));
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
export { deviceStore, removeDevicesWithUsers };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Device, NewDevice } from "./device.js";
|
|
2
|
+
//#region src/notifications/device-store.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Where each user's devices are kept: the Postgres default (`postgresDeviceStore`), or a store of the app's own passed
|
|
5
|
+
* as `notifications({ store })`. Plain data in and out, so any store can implement it. Every method throws on an empty
|
|
6
|
+
* user id, id or token, and none touches every device.
|
|
7
|
+
*/
|
|
8
|
+
export type DeviceStore = {
|
|
9
|
+
/**
|
|
10
|
+
* Registers the device for `userId`, or refreshes it when the user has it already. A token another user holds moves
|
|
11
|
+
* to `userId` as a new device: a new id, its fields as given, created and last seen now.
|
|
12
|
+
*/
|
|
13
|
+
register: (userId: string, device: NewDevice) => Promise<Device>;
|
|
14
|
+
/** Removes the device with this id when it is `userId`'s; another user's is left as it is. */
|
|
15
|
+
remove: (userId: string, id: string) => Promise<void>;
|
|
16
|
+
/** The user's devices. */
|
|
17
|
+
listForUser: (userId: string) => Promise<Device[]>;
|
|
18
|
+
/** The devices with these ids, whoever holds them: for senders, never for ids a client sends. */
|
|
19
|
+
listByIds: (ids: readonly string[]) => Promise<Device[]>;
|
|
20
|
+
/** Removes the devices whose tokens a push service reports gone, whoever holds them: for senders only. */
|
|
21
|
+
removeGone: (tokens: readonly string[]) => Promise<void>;
|
|
22
|
+
/** Removes every device of the user, as when the user is deleted. */
|
|
23
|
+
removeAllForUser: (userId: string) => Promise<void>;
|
|
24
|
+
};
|
|
25
|
+
//#endregion
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//#region src/notifications/device-store.ts
|
|
2
|
+
const methods = [
|
|
3
|
+
"register",
|
|
4
|
+
"remove",
|
|
5
|
+
"listForUser",
|
|
6
|
+
"listByIds",
|
|
7
|
+
"removeGone",
|
|
8
|
+
"removeAllForUser"
|
|
9
|
+
];
|
|
10
|
+
/** The store, checked to have every method; a missing one is an error naming it. */
|
|
11
|
+
const checkDeviceStore = (store) => {
|
|
12
|
+
const missing = methods.find((method) => typeof store[method] !== "function");
|
|
13
|
+
if (missing !== void 0) throw new Error(`notifications({ store }): the device store has no ${missing} method`);
|
|
14
|
+
return store;
|
|
15
|
+
};
|
|
16
|
+
/** The value, refused when empty: no store call may be read as "every device". */
|
|
17
|
+
const required = (name, value) => {
|
|
18
|
+
if (typeof value !== "string" || value === "") throw new Error(`The device store needs a ${name}`);
|
|
19
|
+
return value;
|
|
20
|
+
};
|
|
21
|
+
//#endregion
|
|
22
|
+
export { checkDeviceStore, required };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
//#region src/notifications/device.d.ts
|
|
2
|
+
/** How a device receives push: a browser's Web Push subscription, an iOS app's APNs token or an Android app's FCM token. */
|
|
3
|
+
export declare const deviceKinds: readonly ["web", "apns", "fcm"];
|
|
4
|
+
export type DeviceKind = (typeof deviceKinds)[number];
|
|
5
|
+
/** The APNs environment an iOS build's token belongs to. */
|
|
6
|
+
export declare const apnsEnvironments: readonly ["sandbox", "production"];
|
|
7
|
+
export type ApnsEnvironment = (typeof apnsEnvironments)[number];
|
|
8
|
+
/**
|
|
9
|
+
* A device as a signed-in user registers it. `token` is unique among all devices: the APNs or FCM token, or for `web`
|
|
10
|
+
* the subscription's endpoint URL. A field that does not apply to the kind is `null`: the Web Push keys (`p256dh`,
|
|
11
|
+
* `auth`) for web only, the APNs `environment` for apns only, the `appId` (the iOS bundle id or Android package) for
|
|
12
|
+
* apns and fcm.
|
|
13
|
+
*/
|
|
14
|
+
export type NewDevice = {
|
|
15
|
+
kind: DeviceKind;
|
|
16
|
+
token: string;
|
|
17
|
+
p256dh: string | null;
|
|
18
|
+
auth: string | null;
|
|
19
|
+
environment: ApnsEnvironment | null;
|
|
20
|
+
appId: string | null;
|
|
21
|
+
userAgent: string | null;
|
|
22
|
+
};
|
|
23
|
+
/** A stored device: whose it is, its own id, when it was registered by that user and when last seen. */
|
|
24
|
+
export type Device = NewDevice & {
|
|
25
|
+
id: string;
|
|
26
|
+
userId: string;
|
|
27
|
+
createdAt: Date;
|
|
28
|
+
lastSeenAt: Date;
|
|
29
|
+
};
|
|
30
|
+
//#endregion
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { DeviceStore } from "./device-store.js";
|
|
2
|
+
//#region src/notifications/options.d.ts
|
|
3
|
+
/** The columns of the devices table, by the name Protobase gives them. */
|
|
4
|
+
export declare const deviceColumns: readonly ["id", "user_id", "kind", "token", "p256dh", "auth", "environment", "app_id", "user_agent", "created_at", "last_seen_at"];
|
|
5
|
+
export type DeviceColumn = (typeof deviceColumns)[number];
|
|
6
|
+
/** Notifications, turned on with `extends: [notifications()]`. */
|
|
7
|
+
export type NotificationsOptions = {
|
|
8
|
+
/** The bell in the top bar; default true. `false` keeps only the backend. */
|
|
9
|
+
ui?: boolean;
|
|
10
|
+
/** The schema of the built-in tables; default `notifications`. */
|
|
11
|
+
schema?: string;
|
|
12
|
+
/** The devices table: its name, default `devices`, and its columns named otherwise than Protobase names them. */
|
|
13
|
+
devices?: {
|
|
14
|
+
table?: string;
|
|
15
|
+
columns?: Partial<Record<DeviceColumn, string>>;
|
|
16
|
+
};
|
|
17
|
+
/** Keeps the devices somewhere else than the devices table, which is then not needed. */
|
|
18
|
+
store?: DeviceStore;
|
|
19
|
+
};
|
|
20
|
+
//#endregion
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { checkDeviceStore } from "./device-store.js";
|
|
2
|
+
//#region src/notifications/options.ts
|
|
3
|
+
/** The columns of the devices table, by the name Protobase gives them. */
|
|
4
|
+
const deviceColumns = [
|
|
5
|
+
"id",
|
|
6
|
+
"user_id",
|
|
7
|
+
"kind",
|
|
8
|
+
"token",
|
|
9
|
+
"p256dh",
|
|
10
|
+
"auth",
|
|
11
|
+
"environment",
|
|
12
|
+
"app_id",
|
|
13
|
+
"user_agent",
|
|
14
|
+
"created_at",
|
|
15
|
+
"last_seen_at"
|
|
16
|
+
];
|
|
17
|
+
const namePattern = /^[a-z_][a-z0-9_]*$/;
|
|
18
|
+
const maxNameBytes = 63;
|
|
19
|
+
const checkedName = (option, name) => {
|
|
20
|
+
if (typeof name !== "string" || !namePattern.test(name) || Buffer.byteLength(name) > maxNameBytes) throw new Error(`notifications ${option} is not a valid name: use lowercase letters, digits and "_", at most ${maxNameBytes} characters`);
|
|
21
|
+
return name;
|
|
22
|
+
};
|
|
23
|
+
/** The built-in tables' names, checked: a bad or unknown name is an error naming its option. */
|
|
24
|
+
const notificationsNames = (options = {}) => {
|
|
25
|
+
const given = options.devices?.columns ?? {};
|
|
26
|
+
const unknown = Object.keys(given).find((name) => !deviceColumns.includes(name));
|
|
27
|
+
if (unknown !== void 0) throw new Error(`notifications devices.columns names "${unknown}", which is not a column of the devices table: ${deviceColumns.join(", ")}`);
|
|
28
|
+
const columns = Object.fromEntries(deviceColumns.map((column) => [column, checkedName(`devices.columns.${column}`, given[column] ?? column)]));
|
|
29
|
+
return {
|
|
30
|
+
schema: checkedName("schema", options.schema ?? "notifications"),
|
|
31
|
+
table: checkedName("devices.table", options.devices?.table ?? "devices"),
|
|
32
|
+
columns
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
/** The options, checked as `notifications()` takes them: the names, and a given store's methods. */
|
|
36
|
+
const checkNotificationsOptions = (options) => {
|
|
37
|
+
if (options.ui !== void 0 && typeof options.ui !== "boolean") throw new Error("notifications ui is true or false");
|
|
38
|
+
notificationsNames(options);
|
|
39
|
+
if (options.store !== void 0) checkDeviceStore(options.store);
|
|
40
|
+
return options;
|
|
41
|
+
};
|
|
42
|
+
//#endregion
|
|
43
|
+
export { checkNotificationsOptions, deviceColumns, notificationsNames };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
//#region src/notifications/paths.d.ts
|
|
2
|
+
/** The name of the device id in a device's path. */
|
|
3
|
+
export declare const deviceIdParam = "id";
|
|
4
|
+
/** The path of the caller's devices under the API's system path (`/api`), or of one of them by its id. */
|
|
5
|
+
export declare const devicesPath: (systemPath: string, id?: string) => string;
|
|
6
|
+
//#endregion
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
//#region src/notifications/paths.ts
|
|
2
|
+
/** The name of the device id in a device's path. */
|
|
3
|
+
const deviceIdParam = "id";
|
|
4
|
+
/** The path of the caller's devices under the API's system path (`/api`), or of one of them by its id. */
|
|
5
|
+
const devicesPath = (systemPath, id) => `${systemPath}/notifications/devices${id === void 0 ? "" : `/${encodeURIComponent(id)}`}`;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { deviceIdParam, devicesPath };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { NotificationsOptions } from "./options.js";
|
|
2
|
+
import { ProjectConfig } from "../project-config.js";
|
|
3
|
+
//#region src/notifications/plugin.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Notifications: `extends: [notifications()]` in `protobase.config.ts`. It keeps each user's devices, in the
|
|
6
|
+
* `notifications.devices` table that `protobase migration` writes or in the `store` given, and serves the endpoints
|
|
7
|
+
* through which a signed-in user registers and removes their own (`devicesPath`).
|
|
8
|
+
*/
|
|
9
|
+
export declare const notifications: (options?: NotificationsOptions) => ProjectConfig;
|
|
10
|
+
//#endregion
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { checkNotificationsOptions } from "./options.js";
|
|
2
|
+
//#region src/notifications/plugin.ts
|
|
3
|
+
/**
|
|
4
|
+
* Notifications: `extends: [notifications()]` in `protobase.config.ts`. It keeps each user's devices, in the
|
|
5
|
+
* `notifications.devices` table that `protobase migration` writes or in the `store` given, and serves the endpoints
|
|
6
|
+
* through which a signed-in user registers and removes their own (`devicesPath`).
|
|
7
|
+
*/
|
|
8
|
+
const notifications = (options = {}) => ({
|
|
9
|
+
name: "protobase-notifications",
|
|
10
|
+
notifications: checkNotificationsOptions(options)
|
|
11
|
+
});
|
|
12
|
+
//#endregion
|
|
13
|
+
export { notifications };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { DeviceStore } from "./device-store.js";
|
|
2
|
+
import { NotificationsOptions } from "./options.js";
|
|
3
|
+
import { Db } from "@protobase/query";
|
|
4
|
+
//#region src/notifications/postgres-store.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* A database error of the device store, holding its SQLSTATE alone: the driver's own carries the failing row in its
|
|
7
|
+
* detail, tokens and keys included, which must never reach a log.
|
|
8
|
+
*/
|
|
9
|
+
export declare class DeviceStoreError extends Error {
|
|
10
|
+
readonly sqlState: string;
|
|
11
|
+
constructor(sqlState: string);
|
|
12
|
+
}
|
|
13
|
+
/** The default device store: the devices table `protobase migration` writes, under the names the options give. */
|
|
14
|
+
export declare const postgresDeviceStore: (db: Db, options?: NotificationsOptions) => DeviceStore;
|
|
15
|
+
//#endregion
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { isDatabaseError } from "../error-handler.js";
|
|
2
|
+
import { required } from "./device-store.js";
|
|
3
|
+
import { deviceColumns, notificationsNames } from "./options.js";
|
|
4
|
+
import { sql } from "kysely";
|
|
5
|
+
//#region src/notifications/postgres-store.ts
|
|
6
|
+
/**
|
|
7
|
+
* A database error of the device store, holding its SQLSTATE alone: the driver's own carries the failing row in its
|
|
8
|
+
* detail, tokens and keys included, which must never reach a log.
|
|
9
|
+
*/
|
|
10
|
+
var DeviceStoreError = class extends Error {
|
|
11
|
+
sqlState;
|
|
12
|
+
constructor(sqlState) {
|
|
13
|
+
super(`The device store's query failed (SQLSTATE ${sqlState})`);
|
|
14
|
+
this.sqlState = sqlState;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
18
|
+
const deviceOf = (row) => ({
|
|
19
|
+
id: String(row.id),
|
|
20
|
+
userId: String(row.user_id),
|
|
21
|
+
kind: row.kind,
|
|
22
|
+
token: row.token,
|
|
23
|
+
p256dh: row.p256dh,
|
|
24
|
+
auth: row.auth,
|
|
25
|
+
environment: row.environment,
|
|
26
|
+
appId: row.app_id,
|
|
27
|
+
userAgent: row.user_agent,
|
|
28
|
+
createdAt: new Date(row.created_at),
|
|
29
|
+
lastSeenAt: new Date(row.last_seen_at)
|
|
30
|
+
});
|
|
31
|
+
/** The default device store: the devices table `protobase migration` writes, under the names the options give. */
|
|
32
|
+
const postgresDeviceStore = (db, options = {}) => {
|
|
33
|
+
const { schema, table, columns } = notificationsNames(options);
|
|
34
|
+
const devices = sql.id(schema, table);
|
|
35
|
+
const column = (name) => sql.id(columns[name]);
|
|
36
|
+
const selected = sql.join(deviceColumns.map((name) => sql`${column(name)} as ${sql.id(name)}`));
|
|
37
|
+
const execute = (query) => query.execute(db).catch((error) => {
|
|
38
|
+
if (isDatabaseError(error)) throw new DeviceStoreError(error.code);
|
|
39
|
+
throw error;
|
|
40
|
+
});
|
|
41
|
+
const rows = async (query) => (await execute(query)).rows.map(deviceOf);
|
|
42
|
+
return {
|
|
43
|
+
register: async (userId, device) => {
|
|
44
|
+
required("user id", userId);
|
|
45
|
+
required("token", device.token);
|
|
46
|
+
const now = /* @__PURE__ */ new Date();
|
|
47
|
+
const values = {
|
|
48
|
+
id: crypto.randomUUID(),
|
|
49
|
+
user_id: userId,
|
|
50
|
+
kind: device.kind,
|
|
51
|
+
token: device.token,
|
|
52
|
+
p256dh: device.p256dh,
|
|
53
|
+
auth: device.auth,
|
|
54
|
+
environment: device.environment,
|
|
55
|
+
app_id: device.appId,
|
|
56
|
+
user_agent: device.userAgent,
|
|
57
|
+
created_at: now,
|
|
58
|
+
last_seen_at: now
|
|
59
|
+
};
|
|
60
|
+
const names = sql.join(deviceColumns.map(column));
|
|
61
|
+
const given = sql.join(deviceColumns.map((name) => sql`${values[name]}`));
|
|
62
|
+
const sameUser = sql`${sql.ref(`d.${columns.user_id}`)} = excluded.${column("user_id")}`;
|
|
63
|
+
const keep = (name) => sql`${column(name)} = case when ${sameUser} then ${sql.ref(`d.${columns[name]}`)} else excluded.${column(name)} end`;
|
|
64
|
+
const replaced = [
|
|
65
|
+
"user_id",
|
|
66
|
+
"kind",
|
|
67
|
+
"p256dh",
|
|
68
|
+
"auth",
|
|
69
|
+
"environment",
|
|
70
|
+
"app_id",
|
|
71
|
+
"user_agent",
|
|
72
|
+
"last_seen_at"
|
|
73
|
+
].map((name) => sql`${column(name)} = excluded.${column(name)}`);
|
|
74
|
+
const [stored] = await rows(sql`insert into ${devices} as d (${names}) values (${given})
|
|
75
|
+
on conflict (${column("token")}) do update set ${sql.join([
|
|
76
|
+
keep("id"),
|
|
77
|
+
keep("created_at"),
|
|
78
|
+
...replaced
|
|
79
|
+
])}
|
|
80
|
+
returning ${selected}`);
|
|
81
|
+
return stored;
|
|
82
|
+
},
|
|
83
|
+
remove: async (userId, id) => {
|
|
84
|
+
required("user id", userId);
|
|
85
|
+
required("device id", id);
|
|
86
|
+
if (!uuidPattern.test(id)) return;
|
|
87
|
+
await execute(sql`delete from ${devices} where ${column("user_id")} = ${userId} and ${column("id")} = ${id}`);
|
|
88
|
+
},
|
|
89
|
+
listForUser: async (userId) => {
|
|
90
|
+
required("user id", userId);
|
|
91
|
+
return rows(sql`select ${selected} from ${devices} where ${column("user_id")} = ${userId} order by ${column("last_seen_at")} desc`);
|
|
92
|
+
},
|
|
93
|
+
listByIds: async (ids) => {
|
|
94
|
+
const valid = ids.map((id) => required("device id", id)).filter((id) => uuidPattern.test(id));
|
|
95
|
+
if (valid.length === 0) return [];
|
|
96
|
+
return rows(sql`select ${selected} from ${devices} where ${column("id")} = any(${valid}::uuid[])`);
|
|
97
|
+
},
|
|
98
|
+
removeGone: async (tokens) => {
|
|
99
|
+
const given = tokens.map((token) => required("token", token));
|
|
100
|
+
if (given.length === 0) return;
|
|
101
|
+
await execute(sql`delete from ${devices} where ${column("token")} = any(${given}::text[])`);
|
|
102
|
+
},
|
|
103
|
+
removeAllForUser: async (userId) => {
|
|
104
|
+
required("user id", userId);
|
|
105
|
+
await execute(sql`delete from ${devices} where ${column("user_id")} = ${userId}`);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
//#endregion
|
|
110
|
+
export { DeviceStoreError, postgresDeviceStore };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/notifications/push-hosts.ts
|
|
2
|
+
const exactHosts = [
|
|
3
|
+
"fcm.googleapis.com",
|
|
4
|
+
"updates.push.services.mozilla.com",
|
|
5
|
+
"web.push.apple.com"
|
|
6
|
+
];
|
|
7
|
+
const hostSuffixes = [".notify.windows.com"];
|
|
8
|
+
/** Whether `endpoint`, as a browser's subscription gives it, is an https URL on a known push service, with no userinfo, port or fragment. */
|
|
9
|
+
const isPushEndpoint = (endpoint) => {
|
|
10
|
+
if (!URL.canParse(endpoint) || endpoint.includes("#")) return false;
|
|
11
|
+
const url = new URL(endpoint);
|
|
12
|
+
if (url.protocol !== "https:" || url.username !== "" || url.password !== "" || url.port !== "") return false;
|
|
13
|
+
return exactHosts.includes(url.hostname) || hostSuffixes.some((suffix) => url.hostname.endsWith(suffix) && url.hostname.length > suffix.length);
|
|
14
|
+
};
|
|
15
|
+
/** The endpoint as one spelling, so two spellings of one subscription are one device. */
|
|
16
|
+
const canonicalEndpoint = (endpoint) => new URL(endpoint).href;
|
|
17
|
+
//#endregion
|
|
18
|
+
export { canonicalEndpoint, isPushEndpoint };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { HttpProblem } from "../problem.js";
|
|
2
|
+
import "./paths.js";
|
|
3
|
+
import { ownDeviceUser } from "./caller.js";
|
|
4
|
+
import { deviceInput, maxDeviceBodyBytes } from "./device-input.js";
|
|
5
|
+
import { Hono } from "hono";
|
|
6
|
+
import { bodyLimit } from "hono/body-limit";
|
|
7
|
+
//#region src/notifications/routes.ts
|
|
8
|
+
/** The most devices one user keeps: registering one more drops their least recently seen. */
|
|
9
|
+
const maxDevicesPerUser = 20;
|
|
10
|
+
const ownSignIn = async (c, next) => {
|
|
11
|
+
c.set("deviceUser", ownDeviceUser(c.get("session")));
|
|
12
|
+
await next();
|
|
13
|
+
};
|
|
14
|
+
const jsonOnly = async (c, next) => {
|
|
15
|
+
if (c.req.header("content-type")?.split(";")[0]?.trim().toLowerCase() !== "application/json") throw new HttpProblem(415, "json-only", "Unsupported Media Type", "Send the device as application/json");
|
|
16
|
+
await next();
|
|
17
|
+
};
|
|
18
|
+
const limited = bodyLimit({
|
|
19
|
+
maxSize: maxDeviceBodyBytes,
|
|
20
|
+
onError: () => {
|
|
21
|
+
throw new HttpProblem(413, "too-large", "Payload Too Large", `The device is larger than ${maxDeviceBodyBytes} bytes`);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
const answer = (device) => ({
|
|
25
|
+
id: device.id,
|
|
26
|
+
kind: device.kind,
|
|
27
|
+
createdAt: device.createdAt.toISOString(),
|
|
28
|
+
lastSeenAt: device.lastSeenAt.toISOString()
|
|
29
|
+
});
|
|
30
|
+
const dropOldest = async (store, userId, kept) => {
|
|
31
|
+
const extra = (await store.listForUser(userId)).filter((device) => device.id !== kept.id).sort((a, b) => b.lastSeenAt.getTime() - a.lastSeenAt.getTime()).slice(19);
|
|
32
|
+
for (const device of extra) await store.remove(userId, device.id);
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* The caller's own devices, mounted at `devicesPath` behind the API's authenticator: `POST` registers or refreshes the
|
|
36
|
+
* device the request comes from, `DELETE` with its id removes it. Tokens travel in the body only, never the URL.
|
|
37
|
+
*/
|
|
38
|
+
const deviceRoutes = (store) => {
|
|
39
|
+
const app = new Hono();
|
|
40
|
+
app.post("/", ownSignIn, jsonOnly, limited, async (c) => {
|
|
41
|
+
const userId = c.get("deviceUser");
|
|
42
|
+
const device = deviceInput(await c.req.json());
|
|
43
|
+
const stored = await store.register(userId, device);
|
|
44
|
+
if (stored.userId !== userId || stored.token !== device.token) throw new Error("The device store answered register with another device");
|
|
45
|
+
await dropOldest(store, userId, stored);
|
|
46
|
+
return c.json(answer(stored));
|
|
47
|
+
});
|
|
48
|
+
app.delete(`/:id`, ownSignIn, async (c) => {
|
|
49
|
+
const userId = c.get("deviceUser");
|
|
50
|
+
const id = c.req.param("id");
|
|
51
|
+
if ((await store.listForUser(userId)).some((device) => device.id === id)) await store.remove(userId, id);
|
|
52
|
+
return c.body(null, 204);
|
|
53
|
+
});
|
|
54
|
+
return app;
|
|
55
|
+
};
|
|
56
|
+
//#endregion
|
|
57
|
+
export { deviceRoutes, maxDevicesPerUser };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { quoteIdent } from "../changes/capture-sql.js";
|
|
2
|
+
//#region src/notifications/schema-sql.ts
|
|
3
|
+
/** A table by its schema, when one is named, and its name, quoted. */
|
|
4
|
+
const qualified = ({ schema, table }) => schema ? `${quoteIdent(schema)}.${quoteIdent(table)}` : quoteIdent(table);
|
|
5
|
+
/**
|
|
6
|
+
* The devices table: the token unique, so registering one moves it; with auth, its user column points at auth's users
|
|
7
|
+
* table and goes with the user. A field that does not apply to the kind must be NULL, and nothing has a default.
|
|
8
|
+
*/
|
|
9
|
+
const devicesTableSql = ({ schema, table, columns }, user) => {
|
|
10
|
+
const c = Object.fromEntries(Object.entries(columns).map(([name, value]) => [name, quoteIdent(value)]));
|
|
11
|
+
const devices = qualified({
|
|
12
|
+
schema,
|
|
13
|
+
table
|
|
14
|
+
});
|
|
15
|
+
return [
|
|
16
|
+
`create schema if not exists ${quoteIdent(schema)}`,
|
|
17
|
+
`create table ${devices} (
|
|
18
|
+
${c.id} uuid primary key,
|
|
19
|
+
${c.user_id} ${user.type} not null${user.references ? ` references ${user.references} on delete cascade` : ""},
|
|
20
|
+
${c.kind} text not null check (${c.kind} in ('web', 'apns', 'fcm')),
|
|
21
|
+
${c.token} text not null,
|
|
22
|
+
${c.p256dh} text,
|
|
23
|
+
${c.auth} text,
|
|
24
|
+
${c.environment} text check (${c.environment} in ('sandbox', 'production')),
|
|
25
|
+
${c.app_id} text,
|
|
26
|
+
${c.user_agent} text,
|
|
27
|
+
${c.created_at} timestamptz not null,
|
|
28
|
+
${c.last_seen_at} timestamptz not null,
|
|
29
|
+
check (
|
|
30
|
+
(${c.kind} = 'web' and ${c.p256dh} is not null and ${c.auth} is not null and ${c.environment} is null and ${c.app_id} is null)
|
|
31
|
+
or (${c.kind} = 'apns' and ${c.p256dh} is null and ${c.auth} is null and ${c.environment} is not null and ${c.app_id} is not null)
|
|
32
|
+
or (${c.kind} = 'fcm' and ${c.p256dh} is null and ${c.auth} is null and ${c.environment} is null)
|
|
33
|
+
)
|
|
34
|
+
)`,
|
|
35
|
+
`create unique index on ${devices} (${c.token})`,
|
|
36
|
+
`create index on ${devices} (${c.user_id})`
|
|
37
|
+
];
|
|
38
|
+
};
|
|
39
|
+
//#endregion
|
|
40
|
+
export { devicesTableSql, qualified };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { AdminAuth } from "../better-auth/create-auth.js";
|
|
2
|
+
import { NotificationsOptions } from "./options.js";
|
|
3
|
+
import { Db } from "@protobase/query";
|
|
4
|
+
//#region src/notifications/schema.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* The SQL that adds the devices table `notifications()` keeps devices in, under the names its options give; with
|
|
7
|
+
* `auth`, its user column points at auth's users table. `undefined` when the table exists, and with a store of the
|
|
8
|
+
* app's own, which needs none.
|
|
9
|
+
*/
|
|
10
|
+
export declare const notificationsMigration: (db: Db, options?: NotificationsOptions, auth?: AdminAuth) => Promise<string | undefined>;
|
|
11
|
+
/**
|
|
12
|
+
* Refuses to start while the devices table lacks what `notifications()` needs, naming it: the table, each column under
|
|
13
|
+
* its configured name, a unique index on the token alone, which moving a device relies on, and with `auth` a foreign key
|
|
14
|
+
* from the user column that deletes a user's devices with the user. Nothing to check with a store of the app's own.
|
|
15
|
+
*/
|
|
16
|
+
export declare const checkNotificationsSchema: (db: Db, options?: NotificationsOptions, auth?: AdminAuth) => Promise<void>;
|
|
17
|
+
//#endregion
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { migrationCommand } from "../changes/capture-schema.js";
|
|
2
|
+
import { notificationsNames } from "./options.js";
|
|
3
|
+
import { storeSchema } from "../better-auth/auth-tables.js";
|
|
4
|
+
import { devicesTableSql, qualified } from "./schema-sql.js";
|
|
5
|
+
import { sql } from "kysely";
|
|
6
|
+
//#region src/notifications/schema.ts
|
|
7
|
+
/** Auth's users table, as the store names it: devices point at it. */
|
|
8
|
+
const authUsersTable = (auth) => ({
|
|
9
|
+
schema: storeSchema(auth),
|
|
10
|
+
table: auth.options.user?.modelName ?? "user"
|
|
11
|
+
});
|
|
12
|
+
const regclass = async (db, name) => (await sql`select to_regclass(${name})::oid as oid`.execute(db)).rows[0].oid;
|
|
13
|
+
const userIdType = async (db, users) => {
|
|
14
|
+
const oid = await regclass(db, qualified(users));
|
|
15
|
+
if (oid === null) throw new Error(`notifications(): auth's users table ${qualified(users)} is not in the app's database, so devices cannot point at it`);
|
|
16
|
+
const { rows } = await sql`select format_type(atttypid, atttypmod) as type from pg_attribute where attrelid = ${oid} and attname = 'id'`.execute(db);
|
|
17
|
+
return rows[0].type;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* The SQL that adds the devices table `notifications()` keeps devices in, under the names its options give; with
|
|
21
|
+
* `auth`, its user column points at auth's users table. `undefined` when the table exists, and with a store of the
|
|
22
|
+
* app's own, which needs none.
|
|
23
|
+
*/
|
|
24
|
+
const notificationsMigration = async (db, options = {}, auth) => {
|
|
25
|
+
if (options.store) return void 0;
|
|
26
|
+
const names = notificationsNames(options);
|
|
27
|
+
if (await regclass(db, qualified(names)) !== null) return void 0;
|
|
28
|
+
const users = auth && authUsersTable(auth);
|
|
29
|
+
const user = users ? {
|
|
30
|
+
type: await userIdType(db, users),
|
|
31
|
+
references: `${qualified(users)} (id)`
|
|
32
|
+
} : { type: "text" };
|
|
33
|
+
return `${devicesTableSql(names, user).join(";\n\n")};`;
|
|
34
|
+
};
|
|
35
|
+
const columnsOf = async (db, oid) => (await sql`select attname as name, attnum as num from pg_attribute where attrelid = ${oid} and attnum > 0 and not attisdropped`.execute(db)).rows;
|
|
36
|
+
const uniqueOn = async (db, oid, num) => (await sql`select exists (select from pg_index where indrelid = ${oid} and indisunique and indpred is null and indexprs is null and indnatts = 1 and indkey[0] = ${num}) as found`.execute(db)).rows[0].found;
|
|
37
|
+
const cascadesFrom = async (db, oid, num, users) => (await sql`select exists (select from pg_constraint where conrelid = ${oid} and contype = 'f' and confdeltype = 'c'
|
|
38
|
+
and conkey = array[${num}]::int2[] and confrelid = to_regclass(${qualified(users)})) as found`.execute(db)).rows[0].found;
|
|
39
|
+
/**
|
|
40
|
+
* Refuses to start while the devices table lacks what `notifications()` needs, naming it: the table, each column under
|
|
41
|
+
* its configured name, a unique index on the token alone, which moving a device relies on, and with `auth` a foreign key
|
|
42
|
+
* from the user column that deletes a user's devices with the user. Nothing to check with a store of the app's own.
|
|
43
|
+
*/
|
|
44
|
+
const checkNotificationsSchema = async (db, options = {}, auth) => {
|
|
45
|
+
if (options.store) return;
|
|
46
|
+
const names = notificationsNames(options);
|
|
47
|
+
const devices = qualified(names);
|
|
48
|
+
const oid = await regclass(db, devices);
|
|
49
|
+
if (oid === null) throw new Error(`The devices table ${devices} of notifications() is missing. Write it as a migration with \`${migrationCommand}\`, apply it with the project's migrations, then start again.`);
|
|
50
|
+
const found = await columnsOf(db, oid);
|
|
51
|
+
const numOf = (column) => found.find((row) => row.name === names.columns[column])?.num;
|
|
52
|
+
const missing = Object.keys(names.columns).find((column) => numOf(column) === void 0);
|
|
53
|
+
if (missing !== void 0) throw new Error(`The devices table ${devices} has no column "${names.columns[missing]}": add it, or name the table's own with notifications({ devices: { columns: { ${missing}: ... } } })`);
|
|
54
|
+
if (!await uniqueOn(db, oid, numOf("token"))) throw new Error(`The devices table ${devices} needs a unique index on its token column "${names.columns.token}" alone, which moving a device to the user who registers it relies on`);
|
|
55
|
+
const users = auth && authUsersTable(auth);
|
|
56
|
+
if (users && !await cascadesFrom(db, oid, numOf("user_id"), users)) throw new Error(`The devices table ${devices} needs a foreign key from "${names.columns.user_id}" to ${qualified(users)} with on delete cascade, so a deleted user's devices go with them`);
|
|
57
|
+
};
|
|
58
|
+
//#endregion
|
|
59
|
+
export { authUsersTable, checkNotificationsSchema, notificationsMigration };
|
package/dist/package.js
CHANGED
package/dist/project-config.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { EventsOptions } from "./changes/events-options.js";
|
|
|
5
5
|
import { Authenticator } from "./types.js";
|
|
6
6
|
import { HistoryOptions } from "./history/options.js";
|
|
7
7
|
import { WebhooksOptions } from "./webhooks/table/options.js";
|
|
8
|
+
import { NotificationsOptions } from "./notifications/options.js";
|
|
8
9
|
import { FilesOptions } from "./files/options.js";
|
|
9
10
|
import { FunctionSource } from "./functions/define-function.js";
|
|
10
11
|
import { AdminOptions } from "./admin-options.js";
|
|
@@ -32,6 +33,8 @@ export type ProjectConfig = {
|
|
|
32
33
|
history?: HistoryOptions;
|
|
33
34
|
/** Webhook subscriptions made in the admin app; set by `extends: [webhooks()]` (see `WebhooksOptions`). */
|
|
34
35
|
webhooks?: WebhooksOptions;
|
|
36
|
+
/** Notifications and each user's devices; set by `extends: [notifications()]` (see `NotificationsOptions`). */
|
|
37
|
+
notifications?: NotificationsOptions;
|
|
35
38
|
/** The module of the app's jobs, e.g. `import * as jobs from './config/jobs'`: every `defineJob` it exports. */
|
|
36
39
|
jobs?: Record<string, unknown>;
|
|
37
40
|
/** The app's iOS and Android build (see `MobileConfig`): `protobase mobile` needs it, and the API answers the app's origins. */
|
package/dist/project-config.js
CHANGED
|
@@ -143,13 +143,14 @@ const mergedOptions = (named) => {
|
|
|
143
143
|
*/
|
|
144
144
|
const mergeConfig = (...configs) => {
|
|
145
145
|
const named = configs.flatMap((config, index) => flatten(config, config.name ?? `config ${index + 1}`));
|
|
146
|
-
const { db, authenticate, auth, events, history, webhooks, mobile, app } = lastSet(named.map(({ config }) => ({
|
|
146
|
+
const { db, authenticate, auth, events, history, webhooks, notifications, mobile, app } = lastSet(named.map(({ config }) => ({
|
|
147
147
|
db: config.db,
|
|
148
148
|
authenticate: config.authenticate,
|
|
149
149
|
auth: config.auth,
|
|
150
150
|
events: config.events,
|
|
151
151
|
history: config.history,
|
|
152
152
|
webhooks: config.webhooks,
|
|
153
|
+
notifications: config.notifications,
|
|
153
154
|
mobile: config.mobile,
|
|
154
155
|
app: config.app
|
|
155
156
|
})));
|
|
@@ -170,6 +171,7 @@ const mergeConfig = (...configs) => {
|
|
|
170
171
|
...events && { events },
|
|
171
172
|
...history && { history },
|
|
172
173
|
...webhooks && { webhooks },
|
|
174
|
+
...notifications && { notifications },
|
|
173
175
|
...jobs && { jobs },
|
|
174
176
|
...mobile && { mobile },
|
|
175
177
|
...app && { app },
|
package/dist/types.d.ts
CHANGED
|
@@ -24,6 +24,15 @@ export type Session = {
|
|
|
24
24
|
id: string;
|
|
25
25
|
scopes: string[];
|
|
26
26
|
};
|
|
27
|
+
/**
|
|
28
|
+
* The user's own sign-in this request comes with: its session id, when it has one, and whether staff started it as
|
|
29
|
+
* the user. An authenticator sets it only when it knows; without it, what only the user may do, such as registering
|
|
30
|
+
* their devices for notifications, is refused.
|
|
31
|
+
*/
|
|
32
|
+
signIn?: {
|
|
33
|
+
session?: string;
|
|
34
|
+
staff: boolean;
|
|
35
|
+
};
|
|
27
36
|
};
|
|
28
37
|
/**
|
|
29
38
|
* Resolves the caller of a request, or throws a 401 problem (see `unauthorized`). `userIdType` declares the type of
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/server",
|
|
3
|
-
"version": "0.45.
|
|
3
|
+
"version": "0.45.14",
|
|
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.14",
|
|
40
|
+
"@protobase/query": "0.45.14",
|
|
41
|
+
"@protobase/schema": "0.45.14",
|
|
42
42
|
"@react-email/render": "^2.1.0",
|
|
43
43
|
"@scalar/hono-api-reference": "^0.12.10",
|
|
44
44
|
"@sentry/core": "^11.7.0",
|