@protobase/server 0.66.6 → 0.66.8

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 (45) hide show
  1. package/dist/assistant/delete-record-tool.d.ts +1 -1
  2. package/dist/assistant/delete-record-tool.js +11 -11
  3. package/dist/better-auth/auth-mail/auth-emails.d.ts +2 -0
  4. package/dist/better-auth/auth-mail/auth-emails.js +3 -1
  5. package/dist/better-auth/auth-mail/confirmation-code-email.d.ts +1 -0
  6. package/dist/better-auth/auth-mail/confirmation-code-email.js +32 -0
  7. package/dist/better-auth/auth-mail/password-changed-email.d.ts +1 -0
  8. package/dist/better-auth/auth-mail/password-changed-email.js +32 -0
  9. package/dist/better-auth/auth-settings.d.ts +108 -2
  10. package/dist/better-auth/confirmation-code-plugin.d.ts +9 -0
  11. package/dist/better-auth/confirmation-code-plugin.js +95 -0
  12. package/dist/better-auth/confirmation-codes.js +54 -0
  13. package/dist/better-auth/create-auth.d.ts +119 -3
  14. package/dist/better-auth/create-auth.js +34 -7
  15. package/dist/better-auth/mcp/revoke-grants.js +7 -4
  16. package/dist/better-auth/mobile-requests.js +23 -3
  17. package/dist/better-auth/password-change.js +68 -0
  18. package/dist/better-auth/password-checks.js +69 -0
  19. package/dist/better-auth/password-reset.js +2 -1
  20. package/dist/better-auth/recent-confirmation.js +58 -0
  21. package/dist/better-auth/sign-in-mail.js +8 -5
  22. package/dist/better-auth/sign-up/sign-up-code.js +4 -3
  23. package/dist/better-auth/sign-up/sign-up-plugin.js +3 -2
  24. package/dist/better-auth/staff-checks.js +2 -0
  25. package/dist/better-auth/strong-sign-ins.js +2 -2
  26. package/dist/built-in-resources.d.ts +89 -0
  27. package/dist/built-in-resources.js +31 -0
  28. package/dist/changes/capture-schema.js +3 -1
  29. package/dist/comments/comments-sql.d.ts +4 -1
  30. package/dist/comments/comments-sql.js +16 -7
  31. package/dist/comments/record-comments.js +1 -1
  32. package/dist/config-exports.d.ts +3 -1
  33. package/dist/config-exports.js +3 -1
  34. package/dist/create-admin.js +9 -16
  35. package/dist/gotrue/user-routes.js +6 -4
  36. package/dist/has-indexes.js +9 -0
  37. package/dist/history/history-sql.js +13 -4
  38. package/dist/internal.js +1 -1
  39. package/dist/mcp/mcp-input-schemas.js +24 -0
  40. package/dist/mcp/mcp-server.js +3 -9
  41. package/dist/package.js +1 -1
  42. package/dist/reveal-service.js +1 -1
  43. package/dist/webhooks/table/schema-sql.js +30 -6
  44. package/dist/webhooks/table/schema.js +10 -4
  45. package/package.json +4 -4
@@ -1,6 +1,6 @@
1
1
  import { AssistantTool, ToolContext } from "./tools.js";
2
2
  import { WriteRecordToolsInput } from "./write-tool-limits.js";
3
3
  //#region src/assistant/delete-record-tool.d.ts
4
- /** Deletes one record by its key and ETag once the user approves it, under the caller's access rules. */
4
+ /** Deletes one record by its key, and its ETag when given, once the user approves it, under the caller's access rules. */
5
5
  export declare const deleteRecordTool: (input?: WriteRecordToolsInput) => AssistantTool<ToolContext>;
6
6
  //#endregion
@@ -21,12 +21,15 @@ const deleteKeyOf = (value) => {
21
21
  if (key === void 0) return void 0;
22
22
  return (Array.isArray(key) ? key : [key]).every(keyPartOk) ? key : void 0;
23
23
  };
24
- const etagOf = (value) => typeof value === "string" && /^"[\x21\x23-\x2b\x2d-\x7e]+"$/.test(value) ? value : void 0;
25
- const usage = "Pass only the resource, key and etag as { \"resource\": \"<name>\", \"key\": \"<key>\", \"etag\": \"<the etag get_record gave>\" }; nothing was deleted.";
26
- /** Deletes one record by its key and ETag once the user approves it, under the caller's access rules. */
24
+ const etagOf = (value) => {
25
+ if (value === void 0 || value === "*") return { etag: value };
26
+ return typeof value === "string" && /^"[\x21\x23-\x2b\x2d-\x7e]+"$/.test(value) ? { etag: value } : false;
27
+ };
28
+ const usage = "Pass only the resource, key and, optionally, etag as { \"resource\": \"<name>\", \"key\": \"<key>\", \"etag\": \"<the etag get_record gave>\" }; nothing was deleted.";
29
+ /** Deletes one record by its key, and its ETag when given, once the user approves it, under the caller's access rules. */
27
30
  const deleteRecordTool = (input = {}) => defineTool({
28
31
  name: "delete_record",
29
- description: `Deletes one record of a resource by its key. Pass the etag get_record gave: it fails when someone changed the record since. A resource that soft deletes keeps the record, deleted; any other loses it for good. The user approves each delete in their client first; nothing is deleted without it.${onlyNote(input)}`,
32
+ description: `Deletes one record of a resource by its key. Pass the etag get_record gave to fail when someone changed the record since; without one, or with *, it deletes the record whatever its version. A resource that soft deletes keeps the record, deleted; any other loses it for good. The user approves each delete in their client first; nothing is deleted without it.${onlyNote(input)}`,
30
33
  parameters: {
31
34
  type: "object",
32
35
  properties: {
@@ -37,11 +40,7 @@ const deleteRecordTool = (input = {}) => defineTool({
37
40
  description: "The etag get_record gave, to fail when the record changed since."
38
41
  }
39
42
  },
40
- required: [
41
- "resource",
42
- "key",
43
- "etag"
44
- ],
43
+ required: ["resource", "key"],
45
44
  additionalProperties: false
46
45
  },
47
46
  annotations: {
@@ -52,12 +51,13 @@ const deleteRecordTool = (input = {}) => defineTool({
52
51
  run: async (args, context) => {
53
52
  const resource = resourceOf(args);
54
53
  const key = deleteKeyOf(args.key);
55
- const etag = etagOf(args.etag);
56
- if (!resource || key === void 0 || !etag || Object.keys(args).some((name) => !parameterNames.includes(name))) return usage;
54
+ const given = etagOf(args.etag);
55
+ if (!resource || key === void 0 || !given || Object.keys(args).some((name) => !parameterNames.includes(name))) return usage;
57
56
  const refusal = outside(input, resource);
58
57
  if (refusal) return refusal;
59
58
  const softDelete = await context.records.softDeleteField(resource).then((field) => ({ field }), refused);
60
59
  if (typeof softDelete === "string") return softDelete;
60
+ const { etag } = given;
61
61
  const before = await recordBefore(context, resource, key, etag);
62
62
  if (typeof before === "string") return before;
63
63
  const { text, described } = await approvalTexts(context, resource);
@@ -1,7 +1,9 @@
1
1
  import "./account-exists-email.js";
2
2
  import "./change-email-code-email.js";
3
+ import "./confirmation-code-email.js";
3
4
  import "./invitation-email.js";
4
5
  import "./no-account-email.js";
6
+ import "./password-changed-email.js";
5
7
  import "./reset-password-email.js";
6
8
  import "./sign-in-code-email.js";
7
9
  import "./sign-up-code-email.js";
@@ -1,10 +1,12 @@
1
1
  import { accountExistsEmail } from "./account-exists-email.js";
2
2
  import { changeEmailCodeEmail } from "./change-email-code-email.js";
3
+ import { confirmationCodeEmail } from "./confirmation-code-email.js";
3
4
  import { invitationEmail } from "./invitation-email.js";
4
5
  import { noAccountEmail } from "./no-account-email.js";
6
+ import { passwordChangedEmail } from "./password-changed-email.js";
5
7
  import { resetPasswordEmail } from "./reset-password-email.js";
6
8
  import { signInCodeEmail } from "./sign-in-code-email.js";
7
9
  import { signUpCodeEmail } from "./sign-up-code-email.js";
8
10
  import { staffSignInEmail } from "./staff-sign-in-email.js";
9
11
  import { twoFactorCodeEmail } from "./two-factor-code-email.js";
10
- export { accountExistsEmail, changeEmailCodeEmail, invitationEmail, noAccountEmail, resetPasswordEmail, signInCodeEmail, signUpCodeEmail, staffSignInEmail, twoFactorCodeEmail };
12
+ export { accountExistsEmail, changeEmailCodeEmail, confirmationCodeEmail, invitationEmail, noAccountEmail, passwordChangedEmail, 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 { EmailCode, EmailLayout, EmailNote } from "../../mail/email-layout.js";
3
+ import { Text } from "react-email";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ //#region src/better-auth/auth-mail/confirmation-code-email.tsx
6
+ const ConfirmationCodeEmail = ({ app, code, lifetime }) => /* @__PURE__ */ jsxs(EmailLayout, {
7
+ app,
8
+ preview: `Your code to confirm it's you in ${app.name}`,
9
+ heading: "Confirm it's you",
10
+ children: [
11
+ /* @__PURE__ */ jsxs(Text, { children: [
12
+ "You are signed in to ",
13
+ app.name,
14
+ " and asked to confirm it's you. Use this code there:"
15
+ ] }),
16
+ /* @__PURE__ */ jsx(EmailCode, { code }),
17
+ /* @__PURE__ */ jsxs(Text, { children: [
18
+ "It works for ",
19
+ lifetime,
20
+ ", once, and only where you asked for it. It does not sign you in."
21
+ ] }),
22
+ /* @__PURE__ */ jsx(EmailNote, { children: "If you did not ask for it, someone may be signed in to your account: sign out of your other devices on your account page." })
23
+ ]
24
+ });
25
+ /** The email with a code that confirms it's them inside a signed-in session, for an account without a passkey or a second step. */
26
+ const confirmationCodeEmail = defineEmail({
27
+ name: "confirmation-code",
28
+ subject: ({ app, code }) => `${code} confirms it's you in ${app.name}`,
29
+ component: ConfirmationCodeEmail
30
+ });
31
+ //#endregion
32
+ export { confirmationCodeEmail };
@@ -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/password-changed-email.tsx
7
+ const PasswordChangedEmail = ({ app }) => /* @__PURE__ */ jsxs(EmailLayout, {
8
+ app,
9
+ preview: `The password of your ${app.name} account was changed`,
10
+ heading: "Your password was changed",
11
+ children: [
12
+ /* @__PURE__ */ jsxs(Text, { children: [
13
+ "The password of your account with ",
14
+ app.name,
15
+ " was just changed."
16
+ ] }),
17
+ /* @__PURE__ */ jsx(Text, { children: "If this was not you, choose a new password at once with \"Forgot password?\" on the sign-in page:" }),
18
+ /* @__PURE__ */ jsx(EmailButton, {
19
+ href: new URL(signInPath({ step: "forgot-password" }), app.url).href,
20
+ children: "Choose a new password"
21
+ }),
22
+ /* @__PURE__ */ jsx(EmailNote, { children: "If this was you, ignore this email." })
23
+ ]
24
+ });
25
+ /** The email to an account whose password changed: what happened, and how to take the account back if it was not them. */
26
+ const passwordChangedEmail = defineEmail({
27
+ name: "password-changed",
28
+ subject: ({ app }) => `Your ${app.name} password was changed`,
29
+ component: PasswordChangedEmail
30
+ });
31
+ //#endregion
32
+ export { passwordChangedEmail };
@@ -146,6 +146,7 @@ export declare const buildAppAuth: (settings: AuthSettings, input: AppAuthInput)
146
146
  };
147
147
  };
148
148
  session: {
149
+ freshAge: number;
149
150
  fields: {
150
151
  expiresAt: string;
151
152
  createdAt: string;
@@ -1009,8 +1010,8 @@ export declare const buildAppAuth: (settings: AuthSettings, input: AppAuthInput)
1009
1010
  $Infer: {
1010
1011
  body: {
1011
1012
  permissions: {
1012
- readonly user?: ("create" | "update" | "delete" | "list" | "set-role" | "ban" | "impersonate" | "set-password" | "set-email" | "get" | "impersonate-admins")[] | undefined;
1013
- readonly session?: ("delete" | "list" | "revoke")[] | undefined;
1013
+ readonly user?: ("list" | "create" | "update" | "delete" | "set-role" | "ban" | "impersonate" | "set-password" | "set-email" | "get" | "impersonate-admins")[] | undefined;
1014
+ readonly session?: ("list" | "delete" | "revoke")[] | undefined;
1014
1015
  };
1015
1016
  } & {
1016
1017
  userId?: string | undefined;
@@ -2570,6 +2571,14 @@ export declare const buildAppAuth: (settings: AuthSettings, input: AppAuthInput)
2570
2571
  handler: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<void>>;
2571
2572
  }[];
2572
2573
  };
2574
+ } | {
2575
+ id: "protobase-recent-confirmation";
2576
+ hooks: {
2577
+ before: {
2578
+ matcher: (context: import("better-auth").HookEndpointContext) => boolean;
2579
+ handler: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<void>>;
2580
+ }[];
2581
+ };
2573
2582
  } | {
2574
2583
  id: "email-otp";
2575
2584
  version: string;
@@ -3110,6 +3119,69 @@ export declare const buildAppAuth: (settings: AuthSettings, input: AppAuthInput)
3110
3119
  handler: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<void>>;
3111
3120
  }[];
3112
3121
  };
3122
+ } | {
3123
+ id: "protobase-confirmation-code";
3124
+ endpoints: {
3125
+ sendConfirmationCode: import("better-call").StrictEndpoint<string, {
3126
+ method: "POST";
3127
+ use: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<{
3128
+ session: {
3129
+ session: Record<string, any> & {
3130
+ id: string;
3131
+ createdAt: Date;
3132
+ updatedAt: Date;
3133
+ userId: string;
3134
+ expiresAt: Date;
3135
+ token: string;
3136
+ ipAddress?: string | null | undefined;
3137
+ userAgent?: string | null | undefined;
3138
+ };
3139
+ user: Record<string, any> & {
3140
+ id: string;
3141
+ createdAt: Date;
3142
+ updatedAt: Date;
3143
+ email: string;
3144
+ emailVerified: boolean;
3145
+ name: string;
3146
+ image?: string | null | undefined;
3147
+ };
3148
+ };
3149
+ }>>[];
3150
+ }, {
3151
+ status: boolean;
3152
+ }>;
3153
+ verifyConfirmationCode: import("better-call").StrictEndpoint<string, {
3154
+ method: "POST";
3155
+ use: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<{
3156
+ session: {
3157
+ session: Record<string, any> & {
3158
+ id: string;
3159
+ createdAt: Date;
3160
+ updatedAt: Date;
3161
+ userId: string;
3162
+ expiresAt: Date;
3163
+ token: string;
3164
+ ipAddress?: string | null | undefined;
3165
+ userAgent?: string | null | undefined;
3166
+ };
3167
+ user: Record<string, any> & {
3168
+ id: string;
3169
+ createdAt: Date;
3170
+ updatedAt: Date;
3171
+ email: string;
3172
+ emailVerified: boolean;
3173
+ name: string;
3174
+ image?: string | null | undefined;
3175
+ };
3176
+ };
3177
+ }>>[];
3178
+ body: import("zod").ZodObject<{
3179
+ code: import("zod").ZodString;
3180
+ }, import("zod/v4/core").$strip>;
3181
+ }, {
3182
+ status: boolean;
3183
+ }>;
3184
+ };
3113
3185
  } | {
3114
3186
  id: "protobase-sign-up";
3115
3187
  endpoints: {
@@ -4463,6 +4535,14 @@ export declare const buildAppAuth: (settings: AuthSettings, input: AppAuthInput)
4463
4535
  handler: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<void>>;
4464
4536
  }[];
4465
4537
  };
4538
+ } | {
4539
+ id: "protobase-password-checks";
4540
+ hooks: {
4541
+ before: {
4542
+ matcher: (ctx: import("better-auth").HookEndpointContext) => boolean;
4543
+ handler: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<void>>;
4544
+ }[];
4545
+ };
4466
4546
  } | {
4467
4547
  id: "protobase-set-password";
4468
4548
  endpoints: {
@@ -4498,6 +4578,32 @@ export declare const buildAppAuth: (settings: AuthSettings, input: AppAuthInput)
4498
4578
  status: boolean;
4499
4579
  }>;
4500
4580
  };
4581
+ } | {
4582
+ id: "protobase-password-change";
4583
+ endpoints: {
4584
+ passwordChanged: import("better-call").StrictEndpoint<string, {
4585
+ method: "POST";
4586
+ body: import("zod").ZodObject<{
4587
+ email: import("zod").ZodString;
4588
+ }, import("zod/v4/core").$strip>;
4589
+ }, {
4590
+ status: boolean;
4591
+ }>;
4592
+ };
4593
+ hooks: {
4594
+ before: {
4595
+ matcher: (ctx: import("better-auth").HookEndpointContext) => boolean;
4596
+ handler: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<{
4597
+ context: {
4598
+ body: any;
4599
+ };
4600
+ } | undefined>>;
4601
+ }[];
4602
+ after: {
4603
+ matcher: (ctx: import("better-auth").HookEndpointContext) => boolean;
4604
+ handler: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<void>>;
4605
+ }[];
4606
+ };
4501
4607
  } | {
4502
4608
  id: "protobase-leaked-passwords";
4503
4609
  hooks: {
@@ -0,0 +1,9 @@
1
+ import "./auth-mail/send-auth-email.js";
2
+ import "zod";
3
+ //#region src/better-auth/confirmation-code-plugin.d.ts
4
+ /** The endpoints of the emailed confirmation code, as Better Auth's `ctx.path` names them. */
5
+ export declare const confirmationCodePaths: {
6
+ send: string;
7
+ verify: string;
8
+ };
9
+ //#endregion
@@ -0,0 +1,95 @@
1
+ import { sendAuthEmail } from "./auth-mail/send-auth-email.js";
2
+ import { codeLifetime } from "./sign-in-mail.js";
3
+ import { saveStrongSignIn } from "./strong-sign-ins.js";
4
+ import { checkConfirmationCode, dropConfirmationCode, newConfirmationCode, saveConfirmationCode } from "./confirmation-codes.js";
5
+ import * as z$1 from "zod";
6
+ import { APIError, createAuthEndpoint, sessionMiddleware } from "better-auth/api";
7
+ //#region src/better-auth/confirmation-code-plugin.ts
8
+ /** The endpoints of the emailed confirmation code, as Better Auth's `ctx.path` names them. */
9
+ const confirmationCodePaths = {
10
+ send: "/account/confirmation-code/send",
11
+ verify: "/account/confirmation-code/verify"
12
+ };
13
+ const notOffered = {
14
+ code: "CONFIRMATION_CODE_NOT_OFFERED",
15
+ message: "Confirm it's you with your passkey or your second step instead."
16
+ };
17
+ const refusals = {
18
+ wrong: {
19
+ status: "BAD_REQUEST",
20
+ code: "INVALID_CODE",
21
+ message: "The code is not right."
22
+ },
23
+ "too-many": {
24
+ status: "FORBIDDEN",
25
+ code: "TOO_MANY_ATTEMPTS",
26
+ message: "Too many wrong codes. Ask for a new one."
27
+ },
28
+ expired: {
29
+ status: "BAD_REQUEST",
30
+ code: "OTP_EXPIRED",
31
+ message: "The code has expired. Ask for a new one."
32
+ }
33
+ };
34
+ const isOffered = async (context, { user }) => !user.twoFactorEnabled && await context.adapter.count({
35
+ model: "passkey",
36
+ where: [{
37
+ field: "userId",
38
+ value: user.id
39
+ }]
40
+ }) === 0;
41
+ /**
42
+ * An emailed code that confirms it's them inside a session someone is signed in with, for an account without a passkey
43
+ * or a second step, which has nothing stronger to confirm with: a right code notes a strong sign-in for that session
44
+ * (`saveStrongSignIn`), as a passkey or a second step does. The code is its own kind, never a sign-in code; it belongs
45
+ * to the session and user that asked for it, a new one ends the one before, and it stops working after a few wrong
46
+ * ones (`confirmationCodeTries`). When the email cannot be sent, the request fails and no code is kept. The endpoints
47
+ * read the session cookie only, so an MCP client or an app's token cannot ask for one; staff sessions are refused by
48
+ * the staff plugin.
49
+ */
50
+ const confirmationCodePlugin = (mail) => ({
51
+ id: "protobase-confirmation-code",
52
+ endpoints: {
53
+ /** `POST /account/confirmation-code/send`: emails the signed-in person a new confirmation code. */
54
+ sendConfirmationCode: createAuthEndpoint(confirmationCodePaths.send, {
55
+ method: "POST",
56
+ use: [sessionMiddleware]
57
+ }, async (ctx) => {
58
+ const signed = ctx.context.session;
59
+ if (!await isOffered(ctx.context, signed)) throw new APIError("FORBIDDEN", notOffered);
60
+ const holder = {
61
+ userId: signed.user.id,
62
+ sessionId: signed.session.id
63
+ };
64
+ await dropConfirmationCode(ctx.context.internalAdapter, holder.sessionId);
65
+ const code = newConfirmationCode();
66
+ await sendAuthEmail(mail, "confirmationCodeEmail", signed.user.email, {
67
+ code,
68
+ lifetime: codeLifetime
69
+ }, ctx.context.baseURL);
70
+ await saveConfirmationCode(ctx.context.internalAdapter, holder, code);
71
+ return ctx.json({ status: true });
72
+ }),
73
+ /** `POST /account/confirmation-code/verify` with `{ code }`: confirms the session with the code it was emailed. */
74
+ verifyConfirmationCode: createAuthEndpoint(confirmationCodePaths.verify, {
75
+ method: "POST",
76
+ use: [sessionMiddleware],
77
+ body: z$1.object({ code: z$1.string() })
78
+ }, async (ctx) => {
79
+ const signed = ctx.context.session;
80
+ if (!await isOffered(ctx.context, signed)) throw new APIError("FORBIDDEN", notOffered);
81
+ const checked = await checkConfirmationCode(ctx.context.internalAdapter, {
82
+ userId: signed.user.id,
83
+ sessionId: signed.session.id
84
+ }, ctx.body.code.trim());
85
+ if (checked !== "confirmed") {
86
+ const { status, ...refusal } = refusals[checked];
87
+ throw new APIError(status, refusal);
88
+ }
89
+ await saveStrongSignIn(ctx.context.internalAdapter, signed.session);
90
+ return ctx.json({ status: true });
91
+ })
92
+ }
93
+ });
94
+ //#endregion
95
+ export { confirmationCodePaths, confirmationCodePlugin };
@@ -0,0 +1,54 @@
1
+ import { emailCodeMinutes } from "@protobase/schema/internal";
2
+ import { createHash, timingSafeEqual } from "node:crypto";
3
+ import { generateRandomString } from "better-auth/crypto";
4
+ const identifierOf = (sessionId) => `protobase-confirmation-code-${sessionId}`;
5
+ const hashOf = (code) => createHash("sha256").update(code).digest("hex");
6
+ const sameHash = (a, b) => a.length === b.length && timingSafeEqual(Buffer.from(a), Buffer.from(b));
7
+ /** Ends the session's confirmation code, if it has one, so only a newer one works. */
8
+ const dropConfirmationCode = (internal, sessionId) => internal.deleteVerificationByIdentifier(identifierOf(sessionId));
9
+ /** A new six-digit code, as the email shows it. */
10
+ const newConfirmationCode = () => generateRandomString(6, "0-9");
11
+ /** Keeps `code` as the session's confirmation code for `emailCodeMinutes`, in place of any earlier one. */
12
+ const saveConfirmationCode = async (internal, holder, code, now = /* @__PURE__ */ new Date()) => {
13
+ const identifier = identifierOf(holder.sessionId);
14
+ await internal.deleteVerificationByIdentifier(identifier);
15
+ const value = {
16
+ userId: holder.userId,
17
+ hash: hashOf(code),
18
+ wrong: 0
19
+ };
20
+ await internal.createVerificationValue({
21
+ identifier,
22
+ value: JSON.stringify(value),
23
+ expiresAt: new Date(now.getTime() + emailCodeMinutes * 6e4)
24
+ });
25
+ };
26
+ /**
27
+ * Checks `code` against the session's confirmation code: a code of another session or user is none. A right code is
28
+ * used up; a wrong one counts, and the code stops working at `confirmationCodeTries` wrong ones.
29
+ */
30
+ const checkConfirmationCode = async (internal, holder, code, now = /* @__PURE__ */ new Date()) => {
31
+ const identifier = identifierOf(holder.sessionId);
32
+ const found = await internal.findVerificationValue(identifier);
33
+ if (!found || new Date(found.expiresAt) <= now) return "expired";
34
+ const stored = JSON.parse(found.value);
35
+ if (stored.userId !== holder.userId) return "expired";
36
+ if (sameHash(stored.hash, hashOf(code))) {
37
+ await internal.deleteVerificationByIdentifier(identifier);
38
+ return "confirmed";
39
+ }
40
+ await internal.deleteVerificationByIdentifier(identifier);
41
+ const wrong = stored.wrong + 1;
42
+ if (wrong >= 3) return "too-many";
43
+ await internal.createVerificationValue({
44
+ identifier,
45
+ value: JSON.stringify({
46
+ ...stored,
47
+ wrong
48
+ }),
49
+ expiresAt: new Date(found.expiresAt)
50
+ });
51
+ return "wrong";
52
+ };
53
+ //#endregion
54
+ export { checkConfirmationCode, dropConfirmationCode, newConfirmationCode, saveConfirmationCode };
@@ -22,6 +22,7 @@ import { SsoOptions } from "./saml-sign-in.js";
22
22
  import { samlPlugin } from "./saml-plugin.js";
23
23
  import { signUpPaths } from "./sign-up/sign-up-plugin.js";
24
24
  import { PasswordHash } from "./password-hashes.js";
25
+ import { confirmationCodePaths } from "./confirmation-code-plugin.js";
25
26
  import { BetterAuthOptions } from "better-auth";
26
27
  import { JwtOptions } from "better-auth/plugins";
27
28
  //#region src/better-auth/create-auth.d.ts
@@ -278,6 +279,7 @@ export declare const createAuth: (options: CreateAuthOptions, plugins?: AuthPlug
278
279
  };
279
280
  };
280
281
  session: {
282
+ freshAge: number;
281
283
  fields: {
282
284
  expiresAt: string;
283
285
  createdAt: string;
@@ -1141,8 +1143,8 @@ export declare const createAuth: (options: CreateAuthOptions, plugins?: AuthPlug
1141
1143
  $Infer: {
1142
1144
  body: {
1143
1145
  permissions: {
1144
- readonly user?: ("create" | "update" | "delete" | "list" | "set-role" | "ban" | "impersonate" | "set-password" | "set-email" | "get" | "impersonate-admins")[] | undefined;
1145
- readonly session?: ("delete" | "list" | "revoke")[] | undefined;
1146
+ readonly user?: ("list" | "create" | "update" | "delete" | "set-role" | "ban" | "impersonate" | "set-password" | "set-email" | "get" | "impersonate-admins")[] | undefined;
1147
+ readonly session?: ("list" | "delete" | "revoke")[] | undefined;
1146
1148
  };
1147
1149
  } & {
1148
1150
  userId?: string | undefined;
@@ -2702,6 +2704,14 @@ export declare const createAuth: (options: CreateAuthOptions, plugins?: AuthPlug
2702
2704
  handler: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<void>>;
2703
2705
  }[];
2704
2706
  };
2707
+ } | {
2708
+ id: "protobase-recent-confirmation";
2709
+ hooks: {
2710
+ before: {
2711
+ matcher: (context: import("better-auth").HookEndpointContext) => boolean;
2712
+ handler: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<void>>;
2713
+ }[];
2714
+ };
2705
2715
  } | {
2706
2716
  id: "email-otp";
2707
2717
  version: string;
@@ -3242,6 +3252,69 @@ export declare const createAuth: (options: CreateAuthOptions, plugins?: AuthPlug
3242
3252
  handler: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<void>>;
3243
3253
  }[];
3244
3254
  };
3255
+ } | {
3256
+ id: "protobase-confirmation-code";
3257
+ endpoints: {
3258
+ sendConfirmationCode: import("better-call").StrictEndpoint<string, {
3259
+ method: "POST";
3260
+ use: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<{
3261
+ session: {
3262
+ session: Record<string, any> & {
3263
+ id: string;
3264
+ createdAt: Date;
3265
+ updatedAt: Date;
3266
+ userId: string;
3267
+ expiresAt: Date;
3268
+ token: string;
3269
+ ipAddress?: string | null | undefined;
3270
+ userAgent?: string | null | undefined;
3271
+ };
3272
+ user: Record<string, any> & {
3273
+ id: string;
3274
+ createdAt: Date;
3275
+ updatedAt: Date;
3276
+ email: string;
3277
+ emailVerified: boolean;
3278
+ name: string;
3279
+ image?: string | null | undefined;
3280
+ };
3281
+ };
3282
+ }>>[];
3283
+ }, {
3284
+ status: boolean;
3285
+ }>;
3286
+ verifyConfirmationCode: import("better-call").StrictEndpoint<string, {
3287
+ method: "POST";
3288
+ use: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<{
3289
+ session: {
3290
+ session: Record<string, any> & {
3291
+ id: string;
3292
+ createdAt: Date;
3293
+ updatedAt: Date;
3294
+ userId: string;
3295
+ expiresAt: Date;
3296
+ token: string;
3297
+ ipAddress?: string | null | undefined;
3298
+ userAgent?: string | null | undefined;
3299
+ };
3300
+ user: Record<string, any> & {
3301
+ id: string;
3302
+ createdAt: Date;
3303
+ updatedAt: Date;
3304
+ email: string;
3305
+ emailVerified: boolean;
3306
+ name: string;
3307
+ image?: string | null | undefined;
3308
+ };
3309
+ };
3310
+ }>>[];
3311
+ body: import("zod").ZodObject<{
3312
+ code: import("zod").ZodString;
3313
+ }, import("zod/v4/core").$strip>;
3314
+ }, {
3315
+ status: boolean;
3316
+ }>;
3317
+ };
3245
3318
  } | {
3246
3319
  id: "protobase-sign-up";
3247
3320
  endpoints: {
@@ -4591,6 +4664,14 @@ export declare const createAuth: (options: CreateAuthOptions, plugins?: AuthPlug
4591
4664
  handler: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<void>>;
4592
4665
  }[];
4593
4666
  };
4667
+ } | {
4668
+ id: "protobase-password-checks";
4669
+ hooks: {
4670
+ before: {
4671
+ matcher: (ctx: import("better-auth").HookEndpointContext) => boolean;
4672
+ handler: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<void>>;
4673
+ }[];
4674
+ };
4594
4675
  } | {
4595
4676
  id: "protobase-set-password";
4596
4677
  endpoints: {
@@ -4626,6 +4707,32 @@ export declare const createAuth: (options: CreateAuthOptions, plugins?: AuthPlug
4626
4707
  status: boolean;
4627
4708
  }>;
4628
4709
  };
4710
+ } | {
4711
+ id: "protobase-password-change";
4712
+ endpoints: {
4713
+ passwordChanged: import("better-call").StrictEndpoint<string, {
4714
+ method: "POST";
4715
+ body: import("zod").ZodObject<{
4716
+ email: import("zod").ZodString;
4717
+ }, import("zod/v4/core").$strip>;
4718
+ }, {
4719
+ status: boolean;
4720
+ }>;
4721
+ };
4722
+ hooks: {
4723
+ before: {
4724
+ matcher: (ctx: import("better-auth").HookEndpointContext) => boolean;
4725
+ handler: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<{
4726
+ context: {
4727
+ body: any;
4728
+ };
4729
+ } | undefined>>;
4730
+ }[];
4731
+ after: {
4732
+ matcher: (ctx: import("better-auth").HookEndpointContext) => boolean;
4733
+ handler: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<void>>;
4734
+ }[];
4735
+ };
4629
4736
  } | {
4630
4737
  id: "protobase-leaked-passwords";
4631
4738
  hooks: {
@@ -8897,6 +9004,14 @@ export declare const createAuth: (options: CreateAuthOptions, plugins?: AuthPlug
8897
9004
  window: number;
8898
9005
  max: number;
8899
9006
  };
9007
+ [confirmationCodePaths.send]: {
9008
+ window: number;
9009
+ max: number;
9010
+ };
9011
+ [confirmationCodePaths.verify]: {
9012
+ window: number;
9013
+ max: number;
9014
+ };
8900
9015
  '/sign-in/email': {
8901
9016
  window: number;
8902
9017
  max: number;
@@ -8968,7 +9083,8 @@ export declare const createAuth: (options: CreateAuthOptions, plugins?: AuthPlug
8968
9083
  }> & {
8969
9084
  /**
8970
9085
  * Answers a request from the app's iOS or Android build: Better Auth also trusts the Capacitor origin the request
8971
- * names. Only `createAdmin` with a `mobile` section uses it; `handler` and `api` never trust a Capacitor origin.
9086
+ * names. Only `createAdmin` with a `mobile` section uses it; `handler` and `api` never trust a Capacitor origin. A
9087
+ * Capacitor origin's POST without a JSON body is refused before it is granted, as only a page sends one.
8972
9088
  */
8973
9089
  handleMobile: (request: Request) => Promise<Response>;
8974
9090
  /**