@protobase/server 0.28.2 → 0.28.4

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.
@@ -0,0 +1,29 @@
1
+ import { defaultSignInPolicy, parseSignInPolicy, signInPolicyProblem } from "./sign-in-policy.js";
2
+ //#region src/better-auth/app-sign-in-policy.ts
3
+ const firstAdmin = {
4
+ password: true,
5
+ passkeys: 0,
6
+ twoFactor: false,
7
+ authenticatorApp: false,
8
+ socialSignIn: false
9
+ };
10
+ /**
11
+ * The policy an app applies until an admin saves one: `defaultSignInPolicy` with the methods `given` names changed.
12
+ * It is checked as an admin's save is, with mail on: without mail the policy applies as a saved one does (see
13
+ * `effectiveSignInPolicy`), so a missing mail setting turns passwords back on rather than stopping the app.
14
+ */
15
+ const appSignInPolicy = (given = {}) => {
16
+ const policy = parseSignInPolicy({
17
+ ...defaultSignInPolicy,
18
+ ...given
19
+ });
20
+ if (!policy) throw new Error("signInPolicy: choose one of the offered rules for every sign-in method (see signInPolicyMethods)");
21
+ const problem = signInPolicyProblem(policy, {
22
+ mail: true,
23
+ admin: firstAdmin
24
+ });
25
+ if (problem) throw new Error(`signInPolicy: ${problem}`);
26
+ return policy;
27
+ };
28
+ //#endregion
29
+ export { appSignInPolicy };
@@ -1,4 +1,3 @@
1
- import { SignInPolicy } from "./sign-in-policy.js";
2
1
  import { StaffSignIn } from "./staff-log.js";
3
2
  import { Mailer } from "../mail/smtp-mailer.js";
4
3
  import { OperatorProvider } from "./operator-provider.js";
@@ -6,6 +5,7 @@ import { RoleDefinitions, RolesInput } from "./organizations/role-definitions.js
6
5
  import { OrganizationRole } from "./organizations/store.js";
7
6
  import { OrganizationsOptions, ResolvedOrganizations } from "./organizations/options.js";
8
7
  import { PlatformSignIn } from "./platform-sign-in.js";
8
+ import { SignInPolicy } from "./sign-in-policy.js";
9
9
  import { SsoOptions } from "./saml-sign-in.js";
10
10
  import { signUpPaths } from "./sign-up/sign-up-plugin.js";
11
11
  import { PasswordHash } from "./password-hashes.js";
@@ -63,6 +63,12 @@ export type CreateAuthOptions = {
63
63
  signUp?: {
64
64
  role: string;
65
65
  };
66
+ /**
67
+ * The sign-in policy that applies until an admin saves one on the Sign-in policy page, and when the saved one no
68
+ * longer reads: the methods it names, for example `{ password: 'forbidden' }` for emailed codes and passkeys only,
69
+ * with everything else allowed. Checked at startup as an admin's save is. Default everything allowed.
70
+ */
71
+ signInPolicy?: Partial<SignInPolicy>;
66
72
  /** Sign-in attempts allowed per minute and client, default 5; also the sign-up requests allowed. */
67
73
  signInPerMinute?: number;
68
74
  /**
@@ -339,7 +345,7 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
339
345
  expiresAt: Date;
340
346
  token: string;
341
347
  ipAddress?: string | null | undefined;
342
- userAgent?: string | null | undefined;
348
+ userAgent? /** Signs cookies and encrypts the JWT private keys. Generate with `openssl rand -base64 32`. */ : string | null | undefined;
343
349
  };
344
350
  };
345
351
  }>>[];
@@ -1295,7 +1301,7 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
1295
1301
  email: string;
1296
1302
  emailVerified: boolean;
1297
1303
  name: string;
1298
- image? /** Signs cookies and encrypts the JWT private keys. Generate with `openssl rand -base64 32`. */ : string | null | undefined;
1304
+ image?: string | null | undefined;
1299
1305
  };
1300
1306
  };
1301
1307
  }>>[];
@@ -4415,7 +4421,7 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
4415
4421
  twoFactor: boolean;
4416
4422
  authenticatorApp: boolean;
4417
4423
  };
4418
- missing: ("twoFactor" | "passkey")[];
4424
+ missing: ("passkey" | "twoFactor")[];
4419
4425
  }>;
4420
4426
  };
4421
4427
  hooks: {
@@ -5964,6 +5970,8 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
5964
5970
  /** New users get their ids from `generateUserId` instead of Better Auth's random text. */
5965
5971
  generatesUserIds: boolean;
5966
5972
  mail: boolean;
5973
+ /** The policy until an admin saves one. */
5974
+ signInPolicy: SignInPolicy;
5967
5975
  passwordReset: boolean;
5968
5976
  /** Open sign-up is on and offered: the app asks for it, and can mail the codes. */
5969
5977
  signUp: boolean;
@@ -1,6 +1,7 @@
1
1
  import { appOrigins, multiHostBaseURL, readOrigins, resolveOrigins } from "./origins.js";
2
2
  import { appNameOver, defaultAppName } from "../app-name.js";
3
3
  import { goTrueIssuer, goTrueKeyPair } from "../gotrue/keys.js";
4
+ import { defaultSignInPolicy } from "./sign-in-policy.js";
4
5
  import { roleDefinitions } from "./organizations/role-definitions.js";
5
6
  import { narrowClaims } from "./roles/active-role.js";
6
7
  import { signInPolicyPlugin } from "./policy-plugin.js";
@@ -20,6 +21,7 @@ import { passkeyPlugin } from "./passkey-plugin.js";
20
21
  import { passwordResetOptions } from "./password-reset.js";
21
22
  import { rolesPlugin } from "./roles/roles-plugin.js";
22
23
  import { platformSignInFor, platformSignInProvider, readPlatformSignIn, resolvePlatformSignIn } from "./platform-sign-in.js";
24
+ import { appSignInPolicy } from "./app-sign-in-policy.js";
23
25
  import { emailInDomains, resolveSamlProviders, samlDisabledPaths, samlGuardPlugin, samlPlugin } from "./saml-sign-in.js";
24
26
  import { emailCodeOptions, twoFactorCodeOptions, unusedEmailCodePaths } from "./sign-in-mail.js";
25
27
  import { signUpPaths, signUpPlugin } from "./sign-up/sign-up-plugin.js";
@@ -55,7 +57,7 @@ const organizationPaths = ["/organization/set-active", "/organization/accept-inv
55
57
  * The options `createAuth` gives Better Auth, with the roles, default role, mailer, operator provider and platform
56
58
  * sign-in provider already resolved.
57
59
  */
58
- const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, definitions, defaultRole, appName = defaultAppName, mailer, signUpRole, operator, platformSignIn, saml = [], organizations, context = noContext }) => {
60
+ const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, definitions, defaultRole, appName = defaultAppName, mailer, signUpRole, operator, platformSignIn, saml = [], organizations, context = noContext, signInPolicy = defaultSignInPolicy }) => {
59
61
  const secure = options.baseURL.startsWith("https://");
60
62
  const signIn = platformSignInFor(platformSignIn, Object.keys(options.socialProviders ?? {}));
61
63
  const hosts = passkeyHosts([options.baseURL, ...origins]);
@@ -69,6 +71,7 @@ const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, defini
69
71
  const passwordHashes = options.passwordHashes ?? [];
70
72
  const policy = {
71
73
  mail: Boolean(mailer),
74
+ defaultPolicy: signInPolicy,
72
75
  socialProviders: [...Object.keys(options.socialProviders ?? {}), ...samlIds],
73
76
  ...operator && { operator: operator.name },
74
77
  ...signIn && { platformSignIn: {
@@ -153,7 +156,8 @@ const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, defini
153
156
  ...mail ? [emailOTP(emailCodeOptions(mail))] : [],
154
157
  ...mail && signUpRole ? [signUpPlugin({
155
158
  role: signUpRole,
156
- mail
159
+ mail,
160
+ defaultPolicy: signInPolicy
157
161
  })] : [],
158
162
  ...signIn ? [genericOAuth({ config: [platformSignInProvider(signIn)] })] : [],
159
163
  ...saml.length > 0 ? [samlPlugin(saml, options.baseURL), samlGuardPlugin(samlIds)] : [],
@@ -165,7 +169,8 @@ const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, defini
165
169
  }),
166
170
  staffSignInPlugin({
167
171
  ...operator && { operator },
168
- ...mail && { mail }
172
+ ...mail && { mail },
173
+ defaultPolicy: signInPolicy
169
174
  }),
170
175
  rolesPlugin({ definitions }),
171
176
  lastAdminGuardPlugin(),
@@ -180,6 +185,7 @@ const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, defini
180
185
  })] : [],
181
186
  usersPlugin({
182
187
  definitions,
188
+ defaultPolicy: signInPolicy,
183
189
  ...mail && { mail },
184
190
  invitationUrl: organizations?.invitationUrl ?? defaultInvitationUrl(options.baseURL),
185
191
  invitationSeconds: organizations?.invitationExpiresIn ?? 604800,
@@ -273,6 +279,7 @@ const createAuth = (options) => {
273
279
  const roles = definitions.names;
274
280
  if (options.defaultRole !== void 0 && !roles.includes(options.defaultRole)) throw new Error(`defaultRole "${options.defaultRole}" is not one of the roles: ${roles.join(", ")}`);
275
281
  const signUpRole = options.signUp && checkSignUpRole(options.signUp.role, roles);
282
+ const signInPolicy = appSignInPolicy(options.signInPolicy);
276
283
  const others = roles.filter((role) => role !== "admin");
277
284
  const defaultRole = options.defaultRole ?? (options.organizations ? "user" : others.length === 1 ? others[0] : void 0);
278
285
  const appName = appNameOver(globalThis.process?.env ?? {}, options.appName);
@@ -300,6 +307,7 @@ const createAuth = (options) => {
300
307
  operator,
301
308
  platformSignIn,
302
309
  saml,
310
+ signInPolicy,
303
311
  ...organizations && {
304
312
  organizations,
305
313
  context
@@ -320,6 +328,8 @@ const createAuth = (options) => {
320
328
  /** New users get their ids from `generateUserId` instead of Better Auth's random text. */
321
329
  generatesUserIds: Boolean(options.generateUserId),
322
330
  mail: Boolean(mailer),
331
+ /** The policy until an admin saves one. */
332
+ signInPolicy,
323
333
  passwordReset: Boolean(mailer),
324
334
  /** Open sign-up is on and offered: the app asks for it, and can mail the codes. */
325
335
  signUp: Boolean(signUpRole && mailer),
@@ -10,7 +10,7 @@ import { setSessionCookie } from "better-auth/cookies";
10
10
  */
11
11
  const createInvitedAccount = async (ctx, input) => {
12
12
  const { internalAdapter } = ctx.context;
13
- checkNewAccountPassword(effectiveSignInPolicy((await readSignInPolicy(ctx.context.adapter)).policy, { mail: input.mail }), input.password);
13
+ checkNewAccountPassword(effectiveSignInPolicy((await readSignInPolicy(ctx.context.adapter, input.defaultPolicy)).policy, { mail: input.mail }), input.password);
14
14
  const user = await internalAdapter.createUser({
15
15
  email: input.email.toLowerCase(),
16
16
  name: input.name,
@@ -14,7 +14,7 @@ const tokenQuery = z$1.object({ token: z$1.string().min(1) });
14
14
  * vouched for, and the link in the mail proves the person reads it. Accepting an organization's invitation signed in is
15
15
  * the organization plugin's.
16
16
  */
17
- const invitationEndpoints = ({ mail, organizations }) => ({
17
+ const invitationEndpoints = ({ mail, defaultPolicy, organizations }) => ({
18
18
  /**
19
19
  * `GET /invitation?token=`: who invites, the address and roles, until when, and whether the address has an account;
20
20
  * for an invitation to the app also the app's name, and for an organization's the organization and the role there.
@@ -75,7 +75,8 @@ const invitationEndpoints = ({ mail, organizations }) => ({
75
75
  email,
76
76
  name: ctx.body.name,
77
77
  ...ctx.body.password !== void 0 && { password: ctx.body.password },
78
- mail
78
+ mail,
79
+ defaultPolicy
79
80
  };
80
81
  if (found.kind === "app") {
81
82
  const user = await createInvitedAccount(ctx, {
@@ -31,14 +31,14 @@ const accountMethods = async (context, user, socialProviders) => {
31
31
  };
32
32
  };
33
33
  const refuse = (status, refusal) => new APIError(status, refusal);
34
- const currentPolicy = async (context, mail) => effectiveSignInPolicy((await readSignInPolicy(context.adapter)).policy, { mail });
34
+ const currentPolicy = async (context, { mail, defaultPolicy }) => effectiveSignInPolicy((await readSignInPolicy(context.adapter, defaultPolicy)).policy, { mail });
35
35
  const providersUnder = (policy, { socialProviders, platformSignIn }) => [...socialProviders, ...platformSignIn && policy.platformSignIn === "allowed" ? [platformSignIn.provider] : []];
36
36
  /**
37
37
  * Why `user` gets no API token yet, or `undefined` when they may have one: the policy requires a method, such as
38
38
  * two-factor authentication, that their account has not set up. `read` is the effective policy, when read already.
39
39
  */
40
40
  const tokenRefusal = async (context, user, options, read) => {
41
- const policy = read ?? await currentPolicy(context, options.mail);
41
+ const policy = read ?? await currentPolicy(context, options);
42
42
  if (policy.twoFactor !== "required" && policy.passkey !== "required") return void 0;
43
43
  const account = await accountMethods(context, user, providersUnder(policy, options));
44
44
  return missingRequiredMethods(policy, account).length > 0 ? setupRequired : void 0;
@@ -49,10 +49,10 @@ const tokenRefusal = async (context, user, options, read) => {
49
49
  * someone without what it requires gets a session, so they can set it up, but no API token (`403 SIGN_IN_SETUP_REQUIRED`).
50
50
  */
51
51
  const signInPolicyPlugin = (options) => {
52
- const { mail, operator, platformSignIn } = options;
52
+ const { mail, defaultPolicy, operator, platformSignIn } = options;
53
53
  const policyAdmin = adminOnly("Only an admin can change how people sign in.");
54
54
  const policyAnswer = async (context) => {
55
- const stored = await readSignInPolicy(context.adapter);
55
+ const stored = await readSignInPolicy(context.adapter, defaultPolicy);
56
56
  const savedBy = stored.savedBy && (await context.internalAdapter.findUserById(stored.savedBy))?.email;
57
57
  return {
58
58
  policy: stored.policy,
@@ -105,7 +105,7 @@ const signInPolicyPlugin = (options) => {
105
105
  method: "GET",
106
106
  use: [sessionMiddleware]
107
107
  }, async (ctx) => {
108
- const policy = await currentPolicy(ctx.context, mail);
108
+ const policy = await currentPolicy(ctx.context, options);
109
109
  const { socialSignIn: _, ...account } = await accountMethods(ctx.context, ctx.context.session.user, providersUnder(policy, options));
110
110
  return ctx.json({
111
111
  policy,
@@ -123,7 +123,7 @@ const signInPolicyPlugin = (options) => {
123
123
  code: "SIGN_IN_CODES_ONLY",
124
124
  message: "Only sign-in codes can be emailed."
125
125
  });
126
- const policy = await currentPolicy(ctx.context, mail);
126
+ const policy = await currentPolicy(ctx.context, options);
127
127
  const refusal = policyRefusal(policy, {
128
128
  path,
129
129
  body: ctx.body,
@@ -1,4 +1,4 @@
1
- import { defaultSignInPolicy, parseSignInPolicy } from "./sign-in-policy.js";
1
+ import { parseSignInPolicy } from "./sign-in-policy.js";
2
2
  //#region src/better-auth/policy-store.ts
3
3
  /** Better Auth's model for the sign-in policy: one row per save, the newest applies, the older ones are its history. */
4
4
  const signInPolicyModel = "signInPolicy";
@@ -47,8 +47,8 @@ const signInPolicySchema = { [signInPolicyModel]: {
47
47
  }
48
48
  }
49
49
  } };
50
- /** The newest saved policy, or the default when none was saved. A row that no longer parses counts as none. */
51
- const readSignInPolicy = async (adapter) => {
50
+ /** The newest saved policy, or the app's default (see `appSignInPolicy`) when none was saved. A row that no longer parses counts as none. */
51
+ const readSignInPolicy = async (adapter, appDefault) => {
52
52
  const [row] = await adapter.findMany({
53
53
  model: signInPolicyModel,
54
54
  sortBy: {
@@ -58,7 +58,7 @@ const readSignInPolicy = async (adapter) => {
58
58
  limit: 1
59
59
  });
60
60
  const policy = row && parseSignInPolicy(row);
61
- if (!policy) return { policy: defaultSignInPolicy };
61
+ if (!policy) return { policy: appDefault };
62
62
  return {
63
63
  policy,
64
64
  savedAt: new Date(row.createdAt).toISOString(),
@@ -27,7 +27,7 @@ const noAdminYet = () => new APIError("FORBIDDEN", {
27
27
  * has an account or not; one that has gets a mail saying so, with a link to sign in, instead of a code. While the store
28
28
  * has no user, nobody signs up: the first admin is made on the host.
29
29
  */
30
- const signUpPlugin = ({ role, mail }) => ({
30
+ const signUpPlugin = ({ role, mail, defaultPolicy }) => ({
31
31
  id: "protobase-sign-up",
32
32
  endpoints: {
33
33
  /** `POST /sign-up/send-code` with `{ email }`: mails a code for making an account with the address. */
@@ -78,7 +78,7 @@ const signUpPlugin = ({ role, mail }) => ({
78
78
  const { ticket, name, password } = ctx.body;
79
79
  const { internalAdapter, adapter } = ctx.context;
80
80
  if (await adapter.count({ model: "user" }) === 0) throw noAdminYet();
81
- checkNewAccountPassword(effectiveSignInPolicy((await readSignInPolicy(adapter)).policy, { mail: true }), password);
81
+ checkNewAccountPassword(effectiveSignInPolicy((await readSignInPolicy(adapter, defaultPolicy)).policy, { mail: true }), password);
82
82
  const email = await consumeSignUpTicket(ctx.context, ticket);
83
83
  const passwordHash = password === void 0 ? void 0 : await ctx.context.password.hash(password);
84
84
  const user = await createSignedUpAccount(ctx.context, {
@@ -11,12 +11,13 @@ import { deleteSessionCookie } from "better-auth/cookies";
11
11
  * staff sign-in is logged for the app's admins. The plugin is there without an operator provider too, so its table
12
12
  * exists from the first auth migration.
13
13
  */
14
- const staffSignInPlugin = ({ operator, mail }) => ({
14
+ const staffSignInPlugin = ({ operator, mail, defaultPolicy }) => ({
15
15
  id: "protobase-staff-sign-in",
16
16
  schema: staffSignInSchema,
17
17
  endpoints: {
18
18
  ...operator && staffSignInEndpoints({
19
19
  operator,
20
+ defaultPolicy,
20
21
  ...mail && { mail }
21
22
  }),
22
23
  /** `GET /staff/session`: for a staff session, who started it, as whom, why, and until when; otherwise `{ staff: null }`. */
@@ -35,7 +35,7 @@ const withError = (url, code) => {
35
35
  * permission and sign-in and starts a session as the person, of Better Auth's admin plugin (`impersonatedBy` names
36
36
  * them). The session is short and never extended, and the person's two-factor and passkey requirements do not stop it.
37
37
  */
38
- const staffSignInEndpoints = ({ operator, mail }) => {
38
+ const staffSignInEndpoints = ({ operator, mail, defaultPolicy }) => {
39
39
  const clients = /* @__PURE__ */ new Map();
40
40
  const clientOf = (context) => {
41
41
  const callback = `${context.baseURL}/staff/callback`;
@@ -52,7 +52,7 @@ const staffSignInEndpoints = ({ operator, mail }) => {
52
52
  now: Math.floor(Date.now() / 1e3)
53
53
  });
54
54
  if (refusal) return refusal;
55
- const policy = effectiveSignInPolicy((await readSignInPolicy(ctx.context.adapter)).policy, { mail: Boolean(mail) });
55
+ const policy = effectiveSignInPolicy((await readSignInPolicy(ctx.context.adapter, defaultPolicy)).policy, { mail: Boolean(mail) });
56
56
  const off = policyRefusal(policy, { path: staffSignInPath });
57
57
  if (off) return off;
58
58
  const found = await ctx.context.internalAdapter.findUserByEmail(started.email.toLowerCase());
@@ -22,7 +22,7 @@ const statusRoute = (auth, { anonymous = false, plugins = [], locale } = {}) =>
22
22
  const app = new Hono();
23
23
  app.get("/status", async (c) => {
24
24
  const context = await auth.$context;
25
- const policy = effectiveSignInPolicy((await readSignInPolicy(context.adapter)).policy, { mail: auth.mail });
25
+ const policy = effectiveSignInPolicy((await readSignInPolicy(context.adapter, auth.signInPolicy)).policy, { mail: auth.mail });
26
26
  const signInMethods = [
27
27
  ...policy.password === "allowed" ? ["password"] : [],
28
28
  ...policy.emailCode === "allowed" ? ["emailCode"] : [],
@@ -13,6 +13,7 @@ const usersPlugin = (options) => ({
13
13
  ...userEndpoints(options),
14
14
  ...invitationEndpoints({
15
15
  mail: Boolean(options.mail),
16
+ defaultPolicy: options.defaultPolicy,
16
17
  ...options.organizations && { organizations: options.organizations }
17
18
  })
18
19
  },
@@ -8,10 +8,13 @@ export type LinkedAccount = {
8
8
  providerId: string;
9
9
  accountId: string;
10
10
  };
11
- /** `metadata` lands in the user metadata column, which `createAuth`'s `users.metadata` names; it needs one. */
11
+ /**
12
+ * Without `password` the user signs in by emailed code, passkey or a sign-in provider only. `metadata` lands in the
13
+ * user metadata column, which `createAuth`'s `users.metadata` names; it needs one.
14
+ */
12
15
  export type NewUser = {
13
16
  email: string;
14
- password: string;
17
+ password?: string;
15
18
  name?: string;
16
19
  role?: string;
17
20
  accounts?: LinkedAccount[];
@@ -35,7 +35,7 @@ const createUser = async (auth, input) => {
35
35
  await checkAccounts(auth, accounts);
36
36
  const { user } = await auth.api.createUser({ body: {
37
37
  email: input.email,
38
- password: input.password,
38
+ ...input.password !== void 0 && { password: input.password },
39
39
  name: input.name ?? input.email.split("@")[0],
40
40
  role,
41
41
  ...input.metadata && { data: { metadata: input.metadata } }
@@ -4,6 +4,7 @@ import { unauthorized } from "./problem.js";
4
4
  import { fillHook } from "./ai/fill-hook.js";
5
5
  import { createErrorHandler } from "./error-handler.js";
6
6
  import { isCaptured } from "./changes/capture-sql.js";
7
+ import { wakeDispatch } from "./changes/actor.js";
7
8
  import { assistantRecords } from "./assistant/records.js";
8
9
  import { fillJob } from "./ai/fill-job.js";
9
10
  import { assistantSettings } from "./assistant/assistant-settings.js";
@@ -271,7 +272,8 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
271
272
  authenticate,
272
273
  db,
273
274
  records: (session) => callerRecords(deps, session),
274
- onError
275
+ onError,
276
+ wake: () => wakeDispatch(deps)
275
277
  }));
276
278
  app.route("/", batchRoute(deps, authenticate, meta));
277
279
  app.route("/", fileRoutes(deps, authenticate));
@@ -16,9 +16,10 @@ const requireRoles = (name, session, roles) => {
16
16
  /**
17
17
  * Every function at `<basePath>/<name>` and below, any method. A protected function authenticates the request first, so
18
18
  * a call without a valid token is the authenticator's 401 problem; a public one gets the caller only when it asks. Each
19
- * call runs as background work, so calls that hold the database for long leave a connection to page requests.
19
+ * call runs as background work, so calls that hold the database for long leave a connection to page requests. Once a
20
+ * call ends, failed or not, `wake` runs, so what it wrote reaches open pages without waiting for the dispatcher's poll.
20
21
  */
21
- const functionRoutes = ({ basePath, functions, authenticate, db, records, onError }) => {
22
+ const functionRoutes = ({ basePath, functions, authenticate, db, records, onError, wake }) => {
22
23
  const app = new Hono().basePath(basePath);
23
24
  app.onError(onError);
24
25
  const callerOf = async (request) => {
@@ -28,11 +29,12 @@ const functionRoutes = ({ basePath, functions, authenticate, db, records, onErro
28
29
  records: records(session)
29
30
  };
30
31
  };
32
+ const call = (work) => work().finally(wake);
31
33
  for (const [name, fn] of Object.entries(functions)) {
32
34
  const prefix = `${basePath}/${name}`;
33
35
  const { options } = fn;
34
36
  if (options.cors) app.use(`/${name}/*`, cors(options.cors));
35
- app.all(`/${name}/*`, (c) => runInBackground(async () => {
37
+ app.all(`/${name}/*`, (c) => runInBackground(() => call(async () => {
36
38
  const request = relativeRequest(c.req.raw, prefix);
37
39
  if (options.public) {
38
40
  let caller;
@@ -51,7 +53,7 @@ const functionRoutes = ({ basePath, functions, authenticate, db, records, onErro
51
53
  caller: async () => caller
52
54
  };
53
55
  return invokeFunction(name, fn, request, context);
54
- }));
56
+ })));
55
57
  }
56
58
  app.all("/*", (c) => {
57
59
  throw notFound(`There is no function at ${c.req.path}`);
@@ -41,7 +41,7 @@ const emailCodeRoutes = ({ auth, context, sessionOf }) => {
41
41
  const checkMetadata = (data) => {
42
42
  if (Object.keys(data).length > 0 && !auth.userMetadata) throw new GoTrueError(400, "validation_failed", "This app keeps no user metadata: leave data out");
43
43
  };
44
- const signUpPolicy = async () => effectiveSignInPolicy((await readSignInPolicy((await context()).adapter)).policy, { mail: true });
44
+ const signUpPolicy = async () => effectiveSignInPolicy((await readSignInPolicy((await context()).adapter, auth.signInPolicy)).policy, { mail: true });
45
45
  const sendSignUpCode = (request, email) => callBetterAuth(auth, request, "/sign-up/send-code", { email });
46
46
  const sendSignInCode = (request, email) => callBetterAuth(auth, request, "/email-otp/send-verification-otp", {
47
47
  email,
@@ -69,7 +69,7 @@ const userRoutes = ({ auth, context }) => {
69
69
  if (email !== void 0 && email.trim().toLowerCase() !== user.email) throw new GoTrueError(422, "email_change_not_allowed", "Changing the email address is not offered");
70
70
  if (data !== void 0 && !auth.userMetadata) throw new GoTrueError(400, "validation_failed", "This app keeps no user metadata: leave data out");
71
71
  if (password !== void 0) {
72
- checkNewAccountPassword(effectiveSignInPolicy((await readSignInPolicy(ctx.adapter)).policy, { mail: auth.mail }), password);
72
+ checkNewAccountPassword(effectiveSignInPolicy((await readSignInPolicy(ctx.adapter, auth.signInPolicy)).policy, { mail: auth.mail }), password);
73
73
  const hash = await ctx.password.hash(password);
74
74
  if (await ctx.internalAdapter.findCredentialAccount(user.id)) await ctx.internalAdapter.updatePassword(user.id, hash);
75
75
  else await ctx.internalAdapter.linkAccount({
package/dist/index.d.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import { MobileConfig } from "./mobile-config.js";
2
2
  import { AppIdentity } from "./app-identity.js";
3
3
  import { AppIcons, PngIcon, pngIcons } from "./web-app/app-icons.js";
4
- import { SignInPolicy, SignInRule, StaffAccessRule, defaultSignInPolicy, signInPolicyMethods } from "./better-auth/sign-in-policy.js";
5
4
  import { StaffSignIn } from "./better-auth/staff-log.js";
6
5
  import { MailMessage, Mailer } from "./mail/smtp-mailer.js";
7
6
  import { OperatorProvider, readOperatorSettings } from "./better-auth/operator-provider.js";
@@ -9,6 +8,7 @@ import { RoleDefinition, RolesInput } from "./better-auth/organizations/role-def
9
8
  import { OrganizationRole, StoredMember, StoredOrganization } from "./better-auth/organizations/store.js";
10
9
  import { OrganizationsOptions } from "./better-auth/organizations/options.js";
11
10
  import { PlatformSignIn, readPlatformSignIn } from "./better-auth/platform-sign-in.js";
11
+ import { SignInPolicy, SignInRule, StaffAccessRule, defaultSignInPolicy, signInPolicyMethods } from "./better-auth/sign-in-policy.js";
12
12
  import { EmailComponent, EmailOptions, EmailTemplate, RenderedEmail, defineEmail } from "./mail/define-email.js";
13
13
  import { AdminAuth, CreateAuthOptions, UserCreatedHook, createAuth, normalizeRoles } from "./better-auth/create-auth.js";
14
14
  import { EventsOptions } from "./changes/events-options.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/server",
3
- "version": "0.28.2",
3
+ "version": "0.28.4",
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": {
@@ -32,9 +32,9 @@
32
32
  "dependencies": {
33
33
  "@better-auth/passkey": "^1.7.7",
34
34
  "@better-auth/sso": "^1.7.7",
35
- "@protobase/layout": "0.28.2",
36
- "@protobase/query": "0.28.2",
37
- "@protobase/schema": "0.28.2",
35
+ "@protobase/layout": "0.28.4",
36
+ "@protobase/query": "0.28.4",
37
+ "@protobase/schema": "0.28.4",
38
38
  "@react-email/render": "^2.1.0",
39
39
  "@scalar/hono-api-reference": "^0.12.10",
40
40
  "@sentry/core": "^11.7.0",