@protobase/server 0.45.13 → 0.45.15

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.
Files changed (44) hide show
  1. package/dist/better-auth/auth-mail/auth-emails.d.ts +1 -0
  2. package/dist/better-auth/auth-mail/auth-emails.js +2 -1
  3. package/dist/better-auth/auth-mail/no-account-email.d.ts +1 -0
  4. package/dist/better-auth/auth-mail/no-account-email.js +32 -0
  5. package/dist/better-auth/authenticator.js +13 -7
  6. package/dist/better-auth/create-auth.d.ts +30 -1
  7. package/dist/better-auth/create-auth.js +15 -2
  8. package/dist/better-auth/no-account-mail.js +24 -0
  9. package/dist/better-auth/token-standing.js +21 -12
  10. package/dist/better-auth/user-deletion.d.ts +4 -0
  11. package/dist/better-auth/user-deletion.js +50 -0
  12. package/dist/better-auth/users.d.ts +1 -1
  13. package/dist/better-auth/users.js +3 -1
  14. package/dist/create-admin.d.ts +4 -1
  15. package/dist/create-admin.js +9 -1
  16. package/dist/index.d.ts +10 -1
  17. package/dist/index.js +7 -1
  18. package/dist/notifications/caller.js +16 -0
  19. package/dist/notifications/device-input.js +80 -0
  20. package/dist/notifications/device-store-of.d.ts +13 -0
  21. package/dist/notifications/device-store-of.js +13 -0
  22. package/dist/notifications/device-store.d.ts +25 -0
  23. package/dist/notifications/device-store.js +22 -0
  24. package/dist/notifications/device.d.ts +30 -0
  25. package/dist/notifications/device.js +5 -0
  26. package/dist/notifications/options.d.ts +20 -0
  27. package/dist/notifications/options.js +43 -0
  28. package/dist/notifications/paths.d.ts +6 -0
  29. package/dist/notifications/paths.js +7 -0
  30. package/dist/notifications/plugin.d.ts +10 -0
  31. package/dist/notifications/plugin.js +13 -0
  32. package/dist/notifications/postgres-store.d.ts +15 -0
  33. package/dist/notifications/postgres-store.js +110 -0
  34. package/dist/notifications/push-hosts.js +18 -0
  35. package/dist/notifications/routes.d.ts +6 -0
  36. package/dist/notifications/routes.js +57 -0
  37. package/dist/notifications/schema-sql.js +40 -0
  38. package/dist/notifications/schema.d.ts +17 -0
  39. package/dist/notifications/schema.js +59 -0
  40. package/dist/package.js +1 -1
  41. package/dist/project-config.d.ts +3 -0
  42. package/dist/project-config.js +3 -1
  43. package/dist/types.d.ts +9 -0
  44. package/package.json +4 -4
@@ -1,5 +1,6 @@
1
1
  import "./account-exists-email.js";
2
2
  import "./invitation-email.js";
3
+ import "./no-account-email.js";
3
4
  import "./reset-password-email.js";
4
5
  import "./sign-in-code-email.js";
5
6
  import "./sign-up-code-email.js";
@@ -1,8 +1,9 @@
1
1
  import { accountExistsEmail } from "./account-exists-email.js";
2
2
  import { invitationEmail } from "./invitation-email.js";
3
+ import { noAccountEmail } from "./no-account-email.js";
3
4
  import { resetPasswordEmail } from "./reset-password-email.js";
4
5
  import { signInCodeEmail } from "./sign-in-code-email.js";
5
6
  import { signUpCodeEmail } from "./sign-up-code-email.js";
6
7
  import { staffSignInEmail } from "./staff-sign-in-email.js";
7
8
  import { twoFactorCodeEmail } from "./two-factor-code-email.js";
8
- export { accountExistsEmail, invitationEmail, resetPasswordEmail, signInCodeEmail, signUpCodeEmail, staffSignInEmail, twoFactorCodeEmail };
9
+ export { accountExistsEmail, invitationEmail, noAccountEmail, resetPasswordEmail, signInCodeEmail, signUpCodeEmail, staffSignInEmail, twoFactorCodeEmail };
@@ -0,0 +1 @@
1
+ import "../../index.js";
@@ -0,0 +1,32 @@
1
+ import { defineEmail } from "../../mail/define-email.js";
2
+ import { EmailButton, EmailLayout, EmailNote } from "../../mail/email-layout.js";
3
+ import { signInPath } from "@protobase/schema";
4
+ import { Text } from "react-email";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ //#region src/better-auth/auth-mail/no-account-email.tsx
7
+ const NoAccountEmail = ({ app }) => /* @__PURE__ */ jsxs(EmailLayout, {
8
+ app,
9
+ preview: `No ${app.name} account uses this address`,
10
+ heading: "No account for this address",
11
+ children: [
12
+ /* @__PURE__ */ jsxs(Text, { children: [
13
+ "Someone asked for a code to sign in to ",
14
+ app.name,
15
+ " with this address. No account uses it, so no code was sent."
16
+ ] }),
17
+ /* @__PURE__ */ jsx(Text, { children: "If you have an account, sign in again with the address you signed up with." }),
18
+ /* @__PURE__ */ jsxs(EmailButton, {
19
+ href: new URL(signInPath(), app.url).href,
20
+ children: ["Sign in to ", app.name]
21
+ }),
22
+ /* @__PURE__ */ jsx(EmailNote, { children: "If this was not you, ignore this email: nothing was changed." })
23
+ ]
24
+ });
25
+ /** The email to an address that asked for a sign-in code while no account uses it: no code, a hint to try another address. */
26
+ const noAccountEmail = defineEmail({
27
+ name: "no-account",
28
+ subject: ({ app }) => `No ${app.name} account uses this address`,
29
+ component: NoAccountEmail
30
+ });
31
+ //#endregion
32
+ export { noAccountEmail };
@@ -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) await standing(await auth.$context, payload, session);
48
- return session;
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) => checkMcpGrant(context.adapter, {
69
- userId: payload.sub,
70
- clientId: session.client?.id,
71
- issuedAt: payload.iat
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(): {
@@ -3105,6 +3118,14 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
3105
3118
  INVALID_OTP: import("better-auth").RawError<"INVALID_OTP">;
3106
3119
  TOO_MANY_ATTEMPTS: import("better-auth").RawError<"TOO_MANY_ATTEMPTS">;
3107
3120
  };
3121
+ } | {
3122
+ id: "protobase-no-account-mail";
3123
+ hooks: {
3124
+ after: {
3125
+ matcher: (ctx: import("better-auth").HookEndpointContext) => boolean;
3126
+ handler: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<void>>;
3127
+ }[];
3128
+ };
3108
3129
  } | {
3109
3130
  id: "protobase-sign-up";
3110
3131
  endpoints: {
@@ -3731,7 +3752,7 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
3731
3752
  userId: string;
3732
3753
  expiresAt: Date;
3733
3754
  token: string;
3734
- ipAddress?: string | null | undefined;
3755
+ ipAddress? /** The app keeps each user's metadata, in the column `users.metadata` names. */ : string | null | undefined;
3735
3756
  userAgent?: string | null | undefined;
3736
3757
  };
3737
3758
  user: Record<string, any> & {
@@ -8717,6 +8738,14 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
8717
8738
  * names. Only `createAdmin` with a `mobile` section uses it; `handler` and `api` never trust a Capacitor origin.
8718
8739
  */
8719
8740
  handleMobile: (request: Request) => Promise<Response>;
8741
+ /**
8742
+ * What runs when a user is deleted: `set` a named listener that removes what the app keeps of the user elsewhere,
8743
+ * such as their devices; `run` is for a deletion outside Better Auth's endpoints (`deleteUser`).
8744
+ */
8745
+ userDeletion: {
8746
+ set: (name: string, listener: UserDeletionListener) => undefined;
8747
+ run: (userId: string) => Promise<void>;
8748
+ };
8720
8749
  /** Issuer and audience of the tokens: `baseURL`, on every origin. */
8721
8750
  issuer: string;
8722
8751
  /** 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";
@@ -28,6 +29,7 @@ import { platformSignInFor, platformSignInProvider, readPlatformSignIn, resolveP
28
29
  import { appSignInPolicy } from "./app-sign-in-policy.js";
29
30
  import { assertNoEndpointKeyClashes } from "./endpoint-key-clashes.js";
30
31
  import { emailInDomains, resolveSamlProviders, samlDisabledPaths, samlGuardPlugin, samlPlugin } from "./saml-sign-in.js";
32
+ import { noAccountMailPlugin } from "./no-account-mail.js";
31
33
  import { emailCodeOptions, twoFactorCodeOptions, unusedEmailCodePaths } from "./sign-in-mail.js";
32
34
  import { signUpPaths, signUpPlugin } from "./sign-up/sign-up-plugin.js";
33
35
  import { confirmedOnCreate, confirmedOnUpdate, emailConfirmedField } from "./email-confirmed.js";
@@ -62,7 +64,7 @@ const organizationPaths = ["/organization/set-active", "/organization/accept-inv
62
64
  * The options `createAuth` gives Better Auth, with the roles, default role, mailer, operator provider and platform
63
65
  * sign-in provider already resolved.
64
66
  */
65
- const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, definitions, defaultRole, appName = defaultAppName, mailer, signUpRole, operator, platformSignIn, saml = [], organizations, mcp, context = noContext, signInPolicy = defaultSignInPolicy, mobile }) => {
67
+ const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, definitions, defaultRole, appName = defaultAppName, mailer, signUpRole, operator, platformSignIn, saml = [], organizations, mcp, context = noContext, signInPolicy = defaultSignInPolicy, mobile, deletion }) => {
66
68
  const secure = options.baseURL.startsWith("https://");
67
69
  const signIn = platformSignInFor(platformSignIn, Object.keys(options.socialProviders ?? {}));
68
70
  const hosts = passkeyHosts([options.baseURL, ...origins]);
@@ -161,7 +163,7 @@ const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, defini
161
163
  formerDomain: hosts[0],
162
164
  origins: appOrigins(options.baseURL, origins)
163
165
  }),
164
- ...mail ? [emailOTP(emailCodeOptions(mail))] : [],
166
+ ...mail ? [emailOTP(emailCodeOptions(mail)), noAccountMailPlugin(mail)] : [],
165
167
  ...mail && signUpRole ? [signUpPlugin({
166
168
  role: signUpRole,
167
169
  mail,
@@ -182,6 +184,7 @@ const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, defini
182
184
  }),
183
185
  rolesPlugin({ definitions }),
184
186
  lastAdminGuardPlugin(),
187
+ ...deletion ? [deletion.plugin] : [],
185
188
  ...organizations ? [organizationPlugin({
186
189
  resolved: organizations,
187
190
  secret: options.secret,
@@ -314,8 +317,10 @@ const createAuth = (options) => {
314
317
  let instance;
315
318
  const context = () => instance?.$context ?? Promise.reject(/* @__PURE__ */ new Error("The auth instance is not made yet"));
316
319
  const mobile = mobileRequests();
320
+ const deletion = userDeletion();
317
321
  const authOptions = betterAuthOptions(options, {
318
322
  mobile,
323
+ deletion,
319
324
  origins,
320
325
  passkeyDomain,
321
326
  roles,
@@ -341,6 +346,14 @@ const createAuth = (options) => {
341
346
  * names. Only `createAdmin` with a `mobile` section uses it; `handler` and `api` never trust a Capacitor origin.
342
347
  */
343
348
  handleMobile: (request) => auth.handler(mobile.grant(request)),
349
+ /**
350
+ * What runs when a user is deleted: `set` a named listener that removes what the app keeps of the user elsewhere,
351
+ * such as their devices; `run` is for a deletion outside Better Auth's endpoints (`deleteUser`).
352
+ */
353
+ userDeletion: {
354
+ set: deletion.set,
355
+ run: deletion.run
356
+ },
344
357
  /** Issuer and audience of the tokens: `baseURL`, on every origin. */
345
358
  issuer: options.baseURL,
346
359
  /** Every origin the app is served on, `baseURL`'s first. */
@@ -0,0 +1,24 @@
1
+ import { sendAuthEmail } from "./auth-mail/send-auth-email.js";
2
+ import { APIError, createAuthMiddleware } from "better-auth/api";
3
+ //#region src/better-auth/no-account-mail.ts
4
+ /**
5
+ * Mails an address that asked for a sign-in code while no account uses it, so its owner knows to try another address.
6
+ * The email OTP plugin answers such a request as it answers one for an account, and mails nothing; this hook sends the
7
+ * no-account email after it, so the answer still tells nobody which addresses have an account.
8
+ */
9
+ const noAccountMailPlugin = (mail) => ({
10
+ id: "protobase-no-account-mail",
11
+ hooks: { after: [{
12
+ matcher: (ctx) => ctx.path === "/email-otp/send-verification-otp",
13
+ handler: createAuthMiddleware(async (ctx) => {
14
+ if (ctx.context.returned instanceof APIError) return;
15
+ const { email, type } = ctx.body;
16
+ if (type !== "sign-in") return;
17
+ const address = email.toLowerCase();
18
+ if (await ctx.context.internalAdapter.findUserByEmail(address)) return;
19
+ await ctx.context.runInBackgroundOrAwait(sendAuthEmail(mail, "noAccountEmail", address, {}, ctx.context.baseURL));
20
+ })
21
+ }] }
22
+ });
23
+ //#endregion
24
+ export { noAccountMailPlugin };
@@ -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
- if (claims.sessionId !== void 0) {
34
- const session = await adapter.findOne({
35
- model: "session",
36
- where: [{
37
- field: "id",
38
- value: claims.sessionId
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,4 @@
1
+ //#region src/better-auth/user-deletion.d.ts
2
+ /** Removes what the app keeps of a user elsewhere than the auth store, such as their devices; it throws to stop the deletion. */
3
+ export type UserDeletionListener = (userId: string) => Promise<void>;
4
+ //#endregion
@@ -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) => {
@@ -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
@@ -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 };