@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.
- package/dist/assistant/delete-record-tool.d.ts +1 -1
- package/dist/assistant/delete-record-tool.js +11 -11
- package/dist/better-auth/auth-mail/auth-emails.d.ts +2 -0
- package/dist/better-auth/auth-mail/auth-emails.js +3 -1
- package/dist/better-auth/auth-mail/confirmation-code-email.d.ts +1 -0
- package/dist/better-auth/auth-mail/confirmation-code-email.js +32 -0
- package/dist/better-auth/auth-mail/password-changed-email.d.ts +1 -0
- package/dist/better-auth/auth-mail/password-changed-email.js +32 -0
- package/dist/better-auth/auth-settings.d.ts +108 -2
- package/dist/better-auth/confirmation-code-plugin.d.ts +9 -0
- package/dist/better-auth/confirmation-code-plugin.js +95 -0
- package/dist/better-auth/confirmation-codes.js +54 -0
- package/dist/better-auth/create-auth.d.ts +119 -3
- package/dist/better-auth/create-auth.js +34 -7
- package/dist/better-auth/mcp/revoke-grants.js +7 -4
- package/dist/better-auth/mobile-requests.js +23 -3
- package/dist/better-auth/password-change.js +68 -0
- package/dist/better-auth/password-checks.js +69 -0
- package/dist/better-auth/password-reset.js +2 -1
- package/dist/better-auth/recent-confirmation.js +58 -0
- package/dist/better-auth/sign-in-mail.js +8 -5
- package/dist/better-auth/sign-up/sign-up-code.js +4 -3
- package/dist/better-auth/sign-up/sign-up-plugin.js +3 -2
- package/dist/better-auth/staff-checks.js +2 -0
- package/dist/better-auth/strong-sign-ins.js +2 -2
- package/dist/built-in-resources.d.ts +89 -0
- package/dist/built-in-resources.js +31 -0
- package/dist/changes/capture-schema.js +3 -1
- package/dist/comments/comments-sql.d.ts +4 -1
- package/dist/comments/comments-sql.js +16 -7
- package/dist/comments/record-comments.js +1 -1
- package/dist/config-exports.d.ts +3 -1
- package/dist/config-exports.js +3 -1
- package/dist/create-admin.js +9 -16
- package/dist/gotrue/user-routes.js +6 -4
- package/dist/has-indexes.js +9 -0
- package/dist/history/history-sql.js +13 -4
- package/dist/internal.js +1 -1
- package/dist/mcp/mcp-input-schemas.js +24 -0
- package/dist/mcp/mcp-server.js +3 -9
- package/dist/package.js +1 -1
- package/dist/reveal-service.js +1 -1
- package/dist/webhooks/table/schema-sql.js +30 -6
- package/dist/webhooks/table/schema.js +10 -4
- 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) =>
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
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
|
|
56
|
-
if (!resource || key === void 0 || !
|
|
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?: ("
|
|
1013
|
-
readonly session?: ("
|
|
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?: ("
|
|
1145
|
-
readonly session?: ("
|
|
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
|
/**
|