@protobase/server 0.66.8 → 0.66.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/better-auth/auth-settings.d.ts +39 -0
- package/dist/better-auth/confirmation-code-plugin.js +2 -7
- package/dist/better-auth/create-auth.d.ts +43 -2
- package/dist/better-auth/create-auth.js +6 -0
- package/dist/better-auth/password-checks.js +2 -1
- package/dist/better-auth/password-confirmation.js +63 -0
- package/dist/better-auth/recent-confirmation.js +19 -16
- package/dist/better-auth/staff-checks.js +1 -0
- package/dist/better-auth/strong-factors.js +14 -0
- package/dist/package.js +1 -1
- package/package.json +4 -4
|
@@ -2579,6 +2579,41 @@ export declare const buildAppAuth: (settings: AuthSettings, input: AppAuthInput)
|
|
|
2579
2579
|
handler: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<void>>;
|
|
2580
2580
|
}[];
|
|
2581
2581
|
};
|
|
2582
|
+
} | {
|
|
2583
|
+
id: "protobase-password-confirmation";
|
|
2584
|
+
endpoints: {
|
|
2585
|
+
confirmPassword: import("better-call").StrictEndpoint<"/account/confirm-password", {
|
|
2586
|
+
method: "POST";
|
|
2587
|
+
use: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<{
|
|
2588
|
+
session: {
|
|
2589
|
+
session: Record<string, any> & {
|
|
2590
|
+
id: string;
|
|
2591
|
+
createdAt: Date;
|
|
2592
|
+
updatedAt: Date;
|
|
2593
|
+
userId: string;
|
|
2594
|
+
expiresAt: Date;
|
|
2595
|
+
token: string;
|
|
2596
|
+
ipAddress?: string | null | undefined;
|
|
2597
|
+
userAgent?: string | null | undefined;
|
|
2598
|
+
};
|
|
2599
|
+
user: Record<string, any> & {
|
|
2600
|
+
id: string;
|
|
2601
|
+
createdAt: Date;
|
|
2602
|
+
updatedAt: Date;
|
|
2603
|
+
email: string;
|
|
2604
|
+
emailVerified: boolean;
|
|
2605
|
+
name: string;
|
|
2606
|
+
image?: string | null | undefined;
|
|
2607
|
+
};
|
|
2608
|
+
};
|
|
2609
|
+
}>>[];
|
|
2610
|
+
body: import("zod").ZodObject<{
|
|
2611
|
+
password: import("zod").ZodString;
|
|
2612
|
+
}, import("zod/v4/core").$strip>;
|
|
2613
|
+
}, {
|
|
2614
|
+
status: boolean;
|
|
2615
|
+
}>;
|
|
2616
|
+
};
|
|
2582
2617
|
} | {
|
|
2583
2618
|
id: "email-otp";
|
|
2584
2619
|
version: string;
|
|
@@ -8893,6 +8928,10 @@ export declare const buildAppAuth: (settings: AuthSettings, input: AppAuthInput)
|
|
|
8893
8928
|
window: number;
|
|
8894
8929
|
max: number;
|
|
8895
8930
|
};
|
|
8931
|
+
"/account/confirm-password": {
|
|
8932
|
+
window: number;
|
|
8933
|
+
max: number;
|
|
8934
|
+
};
|
|
8896
8935
|
};
|
|
8897
8936
|
};
|
|
8898
8937
|
advanced: {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { sendAuthEmail } from "./auth-mail/send-auth-email.js";
|
|
2
2
|
import { codeLifetime } from "./sign-in-mail.js";
|
|
3
3
|
import { saveStrongSignIn } from "./strong-sign-ins.js";
|
|
4
|
+
import { hasPasskeyOrSecondStep } from "./strong-factors.js";
|
|
4
5
|
import { checkConfirmationCode, dropConfirmationCode, newConfirmationCode, saveConfirmationCode } from "./confirmation-codes.js";
|
|
5
6
|
import * as z$1 from "zod";
|
|
6
7
|
import { APIError, createAuthEndpoint, sessionMiddleware } from "better-auth/api";
|
|
@@ -31,13 +32,7 @@ const refusals = {
|
|
|
31
32
|
message: "The code has expired. Ask for a new one."
|
|
32
33
|
}
|
|
33
34
|
};
|
|
34
|
-
const isOffered = async (context, { user }) => !
|
|
35
|
-
model: "passkey",
|
|
36
|
-
where: [{
|
|
37
|
-
field: "userId",
|
|
38
|
-
value: user.id
|
|
39
|
-
}]
|
|
40
|
-
}) === 0;
|
|
35
|
+
const isOffered = async (context, { user }) => !await hasPasskeyOrSecondStep(context.adapter, user);
|
|
41
36
|
/**
|
|
42
37
|
* An emailed code that confirms it's them inside a session someone is signed in with, for an account without a passkey
|
|
43
38
|
* or a second step, which has nothing stronger to confirm with: a right code notes a strong sign-in for that session
|
|
@@ -1666,7 +1666,7 @@ export declare const createAuth: (options: CreateAuthOptions, plugins?: AuthPlug
|
|
|
1666
1666
|
email: string;
|
|
1667
1667
|
emailVerified: boolean;
|
|
1668
1668
|
name: string;
|
|
1669
|
-
image
|
|
1669
|
+
image? /** Public origin of the API, for example `https://admin.example.com`. Issuer and audience of the tokens; new passkeys are made for its host unless `passkeyDomain` names another. */ : string | null | undefined;
|
|
1670
1670
|
};
|
|
1671
1671
|
};
|
|
1672
1672
|
}>>[];
|
|
@@ -2411,7 +2411,9 @@ export declare const createAuth: (options: CreateAuthOptions, plugins?: AuthPlug
|
|
|
2411
2411
|
email: string;
|
|
2412
2412
|
emailVerified: boolean;
|
|
2413
2413
|
name: string;
|
|
2414
|
-
image
|
|
2414
|
+
image?
|
|
2415
|
+
/** The RP ID of new passkeys, from `resolvePasskeyDomain`; default the host of `baseURL`. */
|
|
2416
|
+
/** The RP ID of new passkeys, from `resolvePasskeyDomain`; default the host of `baseURL`. */ : string | null | undefined;
|
|
2415
2417
|
};
|
|
2416
2418
|
};
|
|
2417
2419
|
}>>[];
|
|
@@ -2712,6 +2714,41 @@ export declare const createAuth: (options: CreateAuthOptions, plugins?: AuthPlug
|
|
|
2712
2714
|
handler: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<void>>;
|
|
2713
2715
|
}[];
|
|
2714
2716
|
};
|
|
2717
|
+
} | {
|
|
2718
|
+
id: "protobase-password-confirmation";
|
|
2719
|
+
endpoints: {
|
|
2720
|
+
confirmPassword: import("better-call").StrictEndpoint<"/account/confirm-password", {
|
|
2721
|
+
method: "POST";
|
|
2722
|
+
use: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<{
|
|
2723
|
+
session: {
|
|
2724
|
+
session: Record<string, any> & {
|
|
2725
|
+
id: string;
|
|
2726
|
+
createdAt: Date;
|
|
2727
|
+
updatedAt: Date;
|
|
2728
|
+
userId: string;
|
|
2729
|
+
expiresAt: Date;
|
|
2730
|
+
token: string;
|
|
2731
|
+
ipAddress?: string | null | undefined;
|
|
2732
|
+
userAgent?: string | null | undefined;
|
|
2733
|
+
};
|
|
2734
|
+
user: Record<string, any> & {
|
|
2735
|
+
id: string;
|
|
2736
|
+
createdAt: Date;
|
|
2737
|
+
updatedAt: Date;
|
|
2738
|
+
email: string;
|
|
2739
|
+
emailVerified: boolean;
|
|
2740
|
+
name: string;
|
|
2741
|
+
image?: string | null | undefined;
|
|
2742
|
+
};
|
|
2743
|
+
};
|
|
2744
|
+
}>>[];
|
|
2745
|
+
body: import("zod").ZodObject<{
|
|
2746
|
+
password: import("zod").ZodString;
|
|
2747
|
+
}, import("zod/v4/core").$strip>;
|
|
2748
|
+
}, {
|
|
2749
|
+
status: boolean;
|
|
2750
|
+
}>;
|
|
2751
|
+
};
|
|
2715
2752
|
} | {
|
|
2716
2753
|
id: "email-otp";
|
|
2717
2754
|
version: string;
|
|
@@ -9028,6 +9065,10 @@ export declare const createAuth: (options: CreateAuthOptions, plugins?: AuthPlug
|
|
|
9028
9065
|
window: number;
|
|
9029
9066
|
max: number;
|
|
9030
9067
|
};
|
|
9068
|
+
"/account/confirm-password": {
|
|
9069
|
+
window: number;
|
|
9070
|
+
max: number;
|
|
9071
|
+
};
|
|
9031
9072
|
};
|
|
9032
9073
|
};
|
|
9033
9074
|
advanced: {
|
|
@@ -50,6 +50,7 @@ import { confirmedOnCreate, confirmedOnUpdate, emailConfirmedField } from "./ema
|
|
|
50
50
|
import { rehashPasswordsPlugin, verifyAcceptedHash } from "./password-hashes.js";
|
|
51
51
|
import { staffSignInPlugin } from "./staff-plugin.js";
|
|
52
52
|
import { strongSignInsPlugin } from "./strong-sign-ins.js";
|
|
53
|
+
import { passwordConfirmationPath, passwordConfirmationPlugin } from "./password-confirmation.js";
|
|
53
54
|
import { recentConfirmationPlugin } from "./recent-confirmation.js";
|
|
54
55
|
import { confirmationCodePaths, confirmationCodePlugin } from "./confirmation-code-plugin.js";
|
|
55
56
|
import { tokenClaims } from "./token-claims.js";
|
|
@@ -225,6 +226,7 @@ const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, defini
|
|
|
225
226
|
}),
|
|
226
227
|
strongSignInsPlugin(),
|
|
227
228
|
recentConfirmationPlugin(),
|
|
229
|
+
passwordConfirmationPlugin(),
|
|
228
230
|
...mail ? [
|
|
229
231
|
emailOTP(emailCodeOptions(mail)),
|
|
230
232
|
noAccountMailPlugin(mail),
|
|
@@ -361,6 +363,10 @@ const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, defini
|
|
|
361
363
|
[confirmationCodePaths.verify]: {
|
|
362
364
|
window: 60,
|
|
363
365
|
max: 5
|
|
366
|
+
},
|
|
367
|
+
[passwordConfirmationPath]: {
|
|
368
|
+
window: 60,
|
|
369
|
+
max: 5
|
|
364
370
|
}
|
|
365
371
|
}
|
|
366
372
|
},
|
|
@@ -7,7 +7,8 @@ const checkedFields = {
|
|
|
7
7
|
"/two-factor/enable": "password",
|
|
8
8
|
"/two-factor/disable": "password",
|
|
9
9
|
"/two-factor/get-totp-uri": "password",
|
|
10
|
-
"/two-factor/generate-backup-codes": "password"
|
|
10
|
+
"/two-factor/generate-backup-codes": "password",
|
|
11
|
+
"/account/confirm-password": "password"
|
|
11
12
|
};
|
|
12
13
|
/**
|
|
13
14
|
* Counts the checks of each account in a fixed window that opens at its first check: `take` answers how many seconds
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { hasPasskeyOrSecondStep } from "./strong-factors.js";
|
|
2
|
+
import { confirmationMinutes } from "@protobase/schema/internal";
|
|
3
|
+
import * as z$1 from "zod";
|
|
4
|
+
import { APIError, createAuthEndpoint, sessionMiddleware } from "better-auth/api";
|
|
5
|
+
//#region src/better-auth/password-confirmation.ts
|
|
6
|
+
/** The endpoint that confirms it's them with the account's password, as Better Auth's `ctx.path` names it. */
|
|
7
|
+
const passwordConfirmationPath = "/account/confirm-password";
|
|
8
|
+
const identifierOf = (sessionId) => `protobase-password-confirmation-${sessionId}`;
|
|
9
|
+
/** Notes that the session re-entered the account's password now; the note lasts `confirmationMinutes`. */
|
|
10
|
+
const savePasswordConfirmation = async (internal, sessionId, now = /* @__PURE__ */ new Date()) => {
|
|
11
|
+
const identifier = identifierOf(sessionId);
|
|
12
|
+
await internal.deleteVerificationByIdentifier(identifier);
|
|
13
|
+
await internal.createVerificationValue({
|
|
14
|
+
identifier,
|
|
15
|
+
value: now.toISOString(),
|
|
16
|
+
expiresAt: new Date(now.getTime() + confirmationMinutes * 6e4)
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
/** Whether the session re-entered the account's password within `confirmationMinutes`. */
|
|
20
|
+
const isPasswordConfirmed = async (internal, sessionId, now = /* @__PURE__ */ new Date()) => {
|
|
21
|
+
const found = await internal.findVerificationValue(identifierOf(sessionId));
|
|
22
|
+
return Boolean(found) && new Date(found.expiresAt) > now;
|
|
23
|
+
};
|
|
24
|
+
const notOffered = {
|
|
25
|
+
code: "PASSWORD_CONFIRMATION_NOT_OFFERED",
|
|
26
|
+
message: "Confirm it's you with your passkey or your second step instead."
|
|
27
|
+
};
|
|
28
|
+
const noPassword = {
|
|
29
|
+
code: "PASSWORD_CONFIRMATION_NOT_OFFERED",
|
|
30
|
+
message: "This account has no password: confirm it's you with an emailed code instead."
|
|
31
|
+
};
|
|
32
|
+
const wrongPassword = {
|
|
33
|
+
code: "INVALID_PASSWORD",
|
|
34
|
+
message: "The password is not right."
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* `POST /account/confirm-password` with `{ password }`: an account with a password and neither a passkey nor a second
|
|
38
|
+
* step confirms it's them with it, for adding its first passkey (`recentConfirmationPlugin`). Unlike an emailed code,
|
|
39
|
+
* it is no strong sign-in: a reveal still asks for one. Each try counts toward the password checks of the account
|
|
40
|
+
* (`passwordChecksPlugin`); staff sessions are refused by the staff plugin.
|
|
41
|
+
*/
|
|
42
|
+
const passwordConfirmationPlugin = () => ({
|
|
43
|
+
id: "protobase-password-confirmation",
|
|
44
|
+
endpoints: { confirmPassword: createAuthEndpoint(passwordConfirmationPath, {
|
|
45
|
+
method: "POST",
|
|
46
|
+
use: [sessionMiddleware],
|
|
47
|
+
body: z$1.object({ password: z$1.string() })
|
|
48
|
+
}, async (ctx) => {
|
|
49
|
+
const signed = ctx.context.session;
|
|
50
|
+
if (await hasPasskeyOrSecondStep(ctx.context.adapter, signed.user)) throw new APIError("FORBIDDEN", notOffered);
|
|
51
|
+
const hash = (await ctx.context.internalAdapter.findCredentialAccount(signed.user.id))?.password;
|
|
52
|
+
if (!hash) throw new APIError("FORBIDDEN", noPassword);
|
|
53
|
+
if (ctx.body.password.length > ctx.context.password.config.maxPasswordLength) throw new APIError("BAD_REQUEST", wrongPassword);
|
|
54
|
+
if (!await ctx.context.password.verify({
|
|
55
|
+
hash,
|
|
56
|
+
password: ctx.body.password
|
|
57
|
+
})) throw new APIError("BAD_REQUEST", wrongPassword);
|
|
58
|
+
await savePasswordConfirmation(ctx.context.internalAdapter, signed.session.id);
|
|
59
|
+
return ctx.json({ status: true });
|
|
60
|
+
}) }
|
|
61
|
+
});
|
|
62
|
+
//#endregion
|
|
63
|
+
export { isPasswordConfirmed, passwordConfirmationPath, passwordConfirmationPlugin, savePasswordConfirmation };
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { isStaffSession } from "./staff-checks.js";
|
|
2
2
|
import { strongSignInAt } from "./strong-sign-ins.js";
|
|
3
|
+
import { hasPasskeyOrSecondStep } from "./strong-factors.js";
|
|
4
|
+
import { isPasswordConfirmed } from "./password-confirmation.js";
|
|
3
5
|
import { confirmationMinutes } from "@protobase/schema/internal";
|
|
4
6
|
import { APIError, createAuthMiddleware, getSessionFromCtx } from "better-auth/api";
|
|
5
7
|
//#region src/better-auth/recent-confirmation.ts
|
|
@@ -11,22 +13,19 @@ const confirmationRequired = {
|
|
|
11
13
|
message: `Confirm it's you first: this change needs a passkey, a second step or an emailed code in the last ${confirmationMinutes} minutes.`
|
|
12
14
|
};
|
|
13
15
|
const passkeyPaths = ["/passkey/generate-register-options", "/passkey/verify-registration"];
|
|
16
|
+
const twoFactorPath = "/two-factor/enable";
|
|
14
17
|
const guardedPaths = [
|
|
15
18
|
...passkeyPaths,
|
|
19
|
+
twoFactorPath,
|
|
16
20
|
"/unlink-account",
|
|
17
21
|
"/delete-user"
|
|
18
22
|
];
|
|
19
|
-
const
|
|
20
|
-
if (path === "/delete-user") return
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
return await context.
|
|
24
|
-
|
|
25
|
-
where: [{
|
|
26
|
-
field: "userId",
|
|
27
|
-
value: user.id
|
|
28
|
-
}]
|
|
29
|
-
}) > 0;
|
|
23
|
+
const confirmationAsked = async (context, path, body, { user }) => {
|
|
24
|
+
if (path === "/delete-user") return body?.password ? "nothing" : "confirm";
|
|
25
|
+
if (path === "/unlink-account") return "confirm";
|
|
26
|
+
const first = !await hasPasskeyOrSecondStep(context.adapter, user);
|
|
27
|
+
if (path === twoFactorPath) return first && !(await context.internalAdapter.findCredentialAccount(user.id))?.password ? "confirm" : "nothing";
|
|
28
|
+
return first ? "confirm-or-password" : "confirm";
|
|
30
29
|
};
|
|
31
30
|
/** Whether the session signed in within a day, or confirmed it's them within `confirmationMinutes`. */
|
|
32
31
|
const isRecentlyConfirmed = async (context, session, now = /* @__PURE__ */ new Date()) => {
|
|
@@ -36,10 +35,12 @@ const isRecentlyConfirmed = async (context, session, now = /* @__PURE__ */ new D
|
|
|
36
35
|
};
|
|
37
36
|
/**
|
|
38
37
|
* Replaces Better Auth's fresh-session check, which `createAuth` turns off (`freshAge: 0`), and which could only be met
|
|
39
|
-
* by signing out and in again. A session of any age
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
38
|
+
* by signing out and in again. A session of any age lists its devices. Adding a passkey, turning on a second step
|
|
39
|
+
* without a password, unlinking a sign-in provider and deleting the account without a password ask a session older
|
|
40
|
+
* than a day to confirm it's them in place first (`strongSignInsPlugin` notes it), else `403 STRONG_SIGN_IN_REQUIRED`.
|
|
41
|
+
* An account with neither a passkey nor a second step confirms its first passkey with its password too, which counts
|
|
42
|
+
* for that change only, never as a strong sign-in. Staff sessions are left to the staff plugin, which refuses these
|
|
43
|
+
* changes.
|
|
43
44
|
*/
|
|
44
45
|
const recentConfirmationPlugin = () => ({
|
|
45
46
|
id: "protobase-recent-confirmation",
|
|
@@ -48,8 +49,10 @@ const recentConfirmationPlugin = () => ({
|
|
|
48
49
|
handler: createAuthMiddleware(async (ctx) => {
|
|
49
50
|
const signedIn = await getSessionFromCtx(ctx);
|
|
50
51
|
if (!signedIn || isStaffSession(signedIn.session)) return;
|
|
51
|
-
|
|
52
|
+
const asked = await confirmationAsked(ctx.context, ctx.path, ctx.body, signedIn);
|
|
53
|
+
if (asked === "nothing") return;
|
|
52
54
|
if (await isRecentlyConfirmed(ctx.context, signedIn.session)) return;
|
|
55
|
+
if (asked === "confirm-or-password" && await isPasswordConfirmed(ctx.context.internalAdapter, signedIn.session.id)) return;
|
|
53
56
|
throw new APIError("FORBIDDEN", confirmationRequired);
|
|
54
57
|
})
|
|
55
58
|
}] }
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region src/better-auth/strong-factors.ts
|
|
2
|
+
/**
|
|
3
|
+
* Whether the account has a passkey or a second step, which confirm it's them; one with neither confirms a first one
|
|
4
|
+
* with its password or an emailed code instead.
|
|
5
|
+
*/
|
|
6
|
+
const hasPasskeyOrSecondStep = async (adapter, user) => Boolean(user.twoFactorEnabled) || await adapter.count({
|
|
7
|
+
model: "passkey",
|
|
8
|
+
where: [{
|
|
9
|
+
field: "userId",
|
|
10
|
+
value: user.id
|
|
11
|
+
}]
|
|
12
|
+
}) > 0;
|
|
13
|
+
//#endregion
|
|
14
|
+
export { hasPasskeyOrSecondStep };
|
package/dist/package.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/server",
|
|
3
|
-
"version": "0.66.
|
|
3
|
+
"version": "0.66.10",
|
|
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": {
|
|
@@ -43,9 +43,9 @@
|
|
|
43
43
|
"@better-auth/sso": "^1.7.12",
|
|
44
44
|
"@livekit/protocol": "1.53.1",
|
|
45
45
|
"@modelcontextprotocol/server": "^2.3.1",
|
|
46
|
-
"@protobase/layout": "0.66.
|
|
47
|
-
"@protobase/query": "0.66.
|
|
48
|
-
"@protobase/schema": "0.66.
|
|
46
|
+
"@protobase/layout": "0.66.10",
|
|
47
|
+
"@protobase/query": "0.66.10",
|
|
48
|
+
"@protobase/schema": "0.66.10",
|
|
49
49
|
"@react-email/render": "^2.1.0",
|
|
50
50
|
"@react-pdf/primitives": "^4.4.0",
|
|
51
51
|
"@react-pdf/renderer": "^4.9.0",
|