@protobase/server 0.14.5 → 0.14.7
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/create-auth.d.ts +229 -26
- package/dist/better-auth/create-auth.js +28 -4
- package/dist/better-auth/new-account-password.js +25 -0
- package/dist/better-auth/organizations/invitation-endpoints.js +2 -15
- package/dist/better-auth/sign-up/sign-up-code.js +52 -0
- package/dist/better-auth/sign-up/sign-up-mail.js +29 -0
- package/dist/better-auth/sign-up/sign-up-plugin.d.ts +8 -0
- package/dist/better-auth/sign-up/sign-up-plugin.js +98 -0
- package/dist/better-auth/status-route.js +2 -1
- package/dist/changes/event-code-error.js +14 -0
- package/dist/changes/match-events.js +8 -1
- package/dist/webhooks/route-events.js +7 -1
- package/dist/webhooks/subscriber-view.js +9 -2
- package/dist/webhooks/webhooks.js +64 -22
- package/package.json +4 -4
|
@@ -6,6 +6,7 @@ import { RoleDefinitions, RolesInput } from "./organizations/role-definitions.js
|
|
|
6
6
|
import { OrganizationRole } from "./organizations/store.js";
|
|
7
7
|
import { OrganizationsOptions, ResolvedOrganizations } from "./organizations/options.js";
|
|
8
8
|
import { PlatformSignIn } from "./platform-sign-in.js";
|
|
9
|
+
import { signUpPaths } from "./sign-up/sign-up-plugin.js";
|
|
9
10
|
import "../index.js";
|
|
10
11
|
import { BetterAuthOptions } from "better-auth";
|
|
11
12
|
//#region src/better-auth/create-auth.d.ts
|
|
@@ -44,7 +45,16 @@ export type CreateAuthOptions = {
|
|
|
44
45
|
* creating a user without a role is an error that asks for one. With exactly one other role, that role is the default.
|
|
45
46
|
*/
|
|
46
47
|
defaultRole?: string;
|
|
47
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* Open sign-up: anyone makes their own account from the sign-in page, with `role`, after proving their address with
|
|
50
|
+
* an emailed code. Off by default, so accounts come only from the host, an admin, an invitation or a sign-in provider.
|
|
51
|
+
* Needs a mailer: without one it is not offered. `role` is one of `roles` and never `admin`; give it the least
|
|
52
|
+
* a stranger may hold. Not with `organizations`, whose people come from invitations.
|
|
53
|
+
*/
|
|
54
|
+
signUp?: {
|
|
55
|
+
role: string;
|
|
56
|
+
};
|
|
57
|
+
/** Sign-in attempts allowed per minute and client, default 5; also the sign-up requests allowed. */
|
|
48
58
|
signInPerMinute?: number;
|
|
49
59
|
/**
|
|
50
60
|
* Sends the password-reset email and the emailed sign-in and two-factor codes; without a mailer none of them is
|
|
@@ -87,9 +97,9 @@ type UserCreateHooks = NonNullable<NonNullable<NonNullable<BetterAuthOptions['da
|
|
|
87
97
|
export type UserCreatedHook = NonNullable<UserCreateHooks['after']>;
|
|
88
98
|
/**
|
|
89
99
|
* Better Auth for a single-tenant admin: sign-in with a password, an emailed code or a passkey, two-factor
|
|
90
|
-
* authentication, and the sign-in policy admins set; no public sign-up
|
|
91
|
-
* `createUser`, or by an admin through the admin plugin), roles from the admin plugin's `role`
|
|
92
|
-
* at `/api/auth/token` with the keys at `/api/auth/jwks`.
|
|
100
|
+
* authentication, and the sign-in policy admins set; no public sign-up unless `signUp` turns it on (every account is
|
|
101
|
+
* created on the host with `createUser`, or by an admin through the admin plugin), roles from the admin plugin's `role`
|
|
102
|
+
* column, 15 minute JWTs at `/api/auth/token` with the keys at `/api/auth/jwks`.
|
|
93
103
|
*/
|
|
94
104
|
/** The configured roles with `admin` first; `admin` is added when missing. */
|
|
95
105
|
export declare const normalizeRoles: (roles?: RolesInput) => string[];
|
|
@@ -733,7 +743,7 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
733
743
|
expiresAt: Date;
|
|
734
744
|
token: string;
|
|
735
745
|
ipAddress?: string | null | undefined;
|
|
736
|
-
userAgent
|
|
746
|
+
userAgent? /** Open sign-up is on and offered: the app asks for it, and can mail the codes. */ : string | null | undefined;
|
|
737
747
|
};
|
|
738
748
|
};
|
|
739
749
|
}>>[];
|
|
@@ -2192,7 +2202,7 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
2192
2202
|
userId: string;
|
|
2193
2203
|
expiresAt: Date;
|
|
2194
2204
|
token: string;
|
|
2195
|
-
ipAddress
|
|
2205
|
+
ipAddress?: string | null | undefined;
|
|
2196
2206
|
userAgent?: string | null | undefined;
|
|
2197
2207
|
};
|
|
2198
2208
|
user: Record<string, any> & {
|
|
@@ -2707,7 +2717,10 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
2707
2717
|
email: string;
|
|
2708
2718
|
emailVerified: boolean;
|
|
2709
2719
|
name: string;
|
|
2710
|
-
image
|
|
2720
|
+
image? /**
|
|
2721
|
+
* Turns on organizations: people work in one at a time, records of `.tenant` resources are scoped to it, and roles
|
|
2722
|
+
* are held globally (on the user) or per membership. Off by default: the app works as one organization.
|
|
2723
|
+
*/ : string | null | undefined;
|
|
2711
2724
|
} & Record<string, any>;
|
|
2712
2725
|
}>;
|
|
2713
2726
|
signInEmailOTP: import("better-auth").StrictEndpoint<"/sign-in/email-otp", {
|
|
@@ -3015,6 +3028,34 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
3015
3028
|
INVALID_OTP: import("better-auth").RawError<"INVALID_OTP">;
|
|
3016
3029
|
TOO_MANY_ATTEMPTS: import("better-auth").RawError<"TOO_MANY_ATTEMPTS">;
|
|
3017
3030
|
};
|
|
3031
|
+
} | {
|
|
3032
|
+
id: "protobase-sign-up";
|
|
3033
|
+
endpoints: {
|
|
3034
|
+
sendSignUpCode: import("better-auth").StrictEndpoint<string, {
|
|
3035
|
+
method: "POST";
|
|
3036
|
+
body: import("zod").ZodObject<{
|
|
3037
|
+
email: import("zod").ZodPipe<import("zod").ZodString, import("zod").ZodEmail>;
|
|
3038
|
+
}, import("zod/v4/core").$strip>;
|
|
3039
|
+
}, {
|
|
3040
|
+
sent: boolean;
|
|
3041
|
+
}>;
|
|
3042
|
+
createSignUpAccount: import("better-auth").StrictEndpoint<string, {
|
|
3043
|
+
method: "POST";
|
|
3044
|
+
body: import("zod").ZodObject<{
|
|
3045
|
+
email: import("zod").ZodPipe<import("zod").ZodString, import("zod").ZodEmail>;
|
|
3046
|
+
code: import("zod").ZodString;
|
|
3047
|
+
name: import("zod").ZodString;
|
|
3048
|
+
password: import("zod").ZodOptional<import("zod").ZodString>;
|
|
3049
|
+
}, import("zod/v4/core").$strip>;
|
|
3050
|
+
}, {
|
|
3051
|
+
user: {
|
|
3052
|
+
id: string;
|
|
3053
|
+
email: string;
|
|
3054
|
+
name: string;
|
|
3055
|
+
role: string;
|
|
3056
|
+
};
|
|
3057
|
+
}>;
|
|
3058
|
+
};
|
|
3018
3059
|
} | {
|
|
3019
3060
|
id: "generic-oauth";
|
|
3020
3061
|
version: string;
|
|
@@ -3100,7 +3141,15 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
3100
3141
|
email: string;
|
|
3101
3142
|
emailVerified: boolean;
|
|
3102
3143
|
name: string;
|
|
3103
|
-
image
|
|
3144
|
+
image?
|
|
3145
|
+
/**
|
|
3146
|
+
* The role of a user created without one. Optional: when unset, and more than one role besides `admin` exists,
|
|
3147
|
+
* creating a user without a role is an error that asks for one. With exactly one other role, that role is the default.
|
|
3148
|
+
*/
|
|
3149
|
+
/**
|
|
3150
|
+
* The role of a user created without one. Optional: when unset, and more than one role besides `admin` exists,
|
|
3151
|
+
* creating a user without a role is an error that asks for one. With exactly one other role, that role is the default.
|
|
3152
|
+
*/ : string | null | undefined;
|
|
3104
3153
|
};
|
|
3105
3154
|
};
|
|
3106
3155
|
}>>[];
|
|
@@ -3124,7 +3173,15 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
3124
3173
|
email: string;
|
|
3125
3174
|
emailVerified: boolean;
|
|
3126
3175
|
name: string;
|
|
3127
|
-
image
|
|
3176
|
+
image?
|
|
3177
|
+
/**
|
|
3178
|
+
* The role of a user created without one. Optional: when unset, and more than one role besides `admin` exists,
|
|
3179
|
+
* creating a user without a role is an error that asks for one. With exactly one other role, that role is the default.
|
|
3180
|
+
*/
|
|
3181
|
+
/**
|
|
3182
|
+
* The role of a user created without one. Optional: when unset, and more than one role besides `admin` exists,
|
|
3183
|
+
* creating a user without a role is an error that asks for one. With exactly one other role, that role is the default.
|
|
3184
|
+
*/ : string | null | undefined;
|
|
3128
3185
|
};
|
|
3129
3186
|
};
|
|
3130
3187
|
}>>[];
|
|
@@ -3147,7 +3204,15 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
3147
3204
|
email: string;
|
|
3148
3205
|
emailVerified: boolean;
|
|
3149
3206
|
name: string;
|
|
3150
|
-
image
|
|
3207
|
+
image?
|
|
3208
|
+
/**
|
|
3209
|
+
* The role of a user created without one. Optional: when unset, and more than one role besides `admin` exists,
|
|
3210
|
+
* creating a user without a role is an error that asks for one. With exactly one other role, that role is the default.
|
|
3211
|
+
*/
|
|
3212
|
+
/**
|
|
3213
|
+
* The role of a user created without one. Optional: when unset, and more than one role besides `admin` exists,
|
|
3214
|
+
* creating a user without a role is an error that asks for one. With exactly one other role, that role is the default.
|
|
3215
|
+
*/ : string | null | undefined;
|
|
3151
3216
|
};
|
|
3152
3217
|
} & {
|
|
3153
3218
|
session: {
|
|
@@ -3202,7 +3267,15 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
3202
3267
|
email: string;
|
|
3203
3268
|
emailVerified: boolean;
|
|
3204
3269
|
name: string;
|
|
3205
|
-
image
|
|
3270
|
+
image?
|
|
3271
|
+
/**
|
|
3272
|
+
* The role of a user created without one. Optional: when unset, and more than one role besides `admin` exists,
|
|
3273
|
+
* creating a user without a role is an error that asks for one. With exactly one other role, that role is the default.
|
|
3274
|
+
*/
|
|
3275
|
+
/**
|
|
3276
|
+
* The role of a user created without one. Optional: when unset, and more than one role besides `admin` exists,
|
|
3277
|
+
* creating a user without a role is an error that asks for one. With exactly one other role, that role is the default.
|
|
3278
|
+
*/ : string | null | undefined;
|
|
3206
3279
|
};
|
|
3207
3280
|
};
|
|
3208
3281
|
}>>[];
|
|
@@ -3226,7 +3299,15 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
3226
3299
|
email: string;
|
|
3227
3300
|
emailVerified: boolean;
|
|
3228
3301
|
name: string;
|
|
3229
|
-
image
|
|
3302
|
+
image?
|
|
3303
|
+
/**
|
|
3304
|
+
* The role of a user created without one. Optional: when unset, and more than one role besides `admin` exists,
|
|
3305
|
+
* creating a user without a role is an error that asks for one. With exactly one other role, that role is the default.
|
|
3306
|
+
*/
|
|
3307
|
+
/**
|
|
3308
|
+
* The role of a user created without one. Optional: when unset, and more than one role besides `admin` exists,
|
|
3309
|
+
* creating a user without a role is an error that asks for one. With exactly one other role, that role is the default.
|
|
3310
|
+
*/ : string | null | undefined;
|
|
3230
3311
|
};
|
|
3231
3312
|
};
|
|
3232
3313
|
}>>[];
|
|
@@ -3249,7 +3330,15 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
3249
3330
|
email: string;
|
|
3250
3331
|
emailVerified: boolean;
|
|
3251
3332
|
name: string;
|
|
3252
|
-
image
|
|
3333
|
+
image?
|
|
3334
|
+
/**
|
|
3335
|
+
* The role of a user created without one. Optional: when unset, and more than one role besides `admin` exists,
|
|
3336
|
+
* creating a user without a role is an error that asks for one. With exactly one other role, that role is the default.
|
|
3337
|
+
*/
|
|
3338
|
+
/**
|
|
3339
|
+
* The role of a user created without one. Optional: when unset, and more than one role besides `admin` exists,
|
|
3340
|
+
* creating a user without a role is an error that asks for one. With exactly one other role, that role is the default.
|
|
3341
|
+
*/ : string | null | undefined;
|
|
3253
3342
|
};
|
|
3254
3343
|
} & {
|
|
3255
3344
|
session: {
|
|
@@ -3304,7 +3393,15 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
3304
3393
|
email: string;
|
|
3305
3394
|
emailVerified: boolean;
|
|
3306
3395
|
name: string;
|
|
3307
|
-
image
|
|
3396
|
+
image?
|
|
3397
|
+
/**
|
|
3398
|
+
* The role of a user created without one. Optional: when unset, and more than one role besides `admin` exists,
|
|
3399
|
+
* creating a user without a role is an error that asks for one. With exactly one other role, that role is the default.
|
|
3400
|
+
*/
|
|
3401
|
+
/**
|
|
3402
|
+
* The role of a user created without one. Optional: when unset, and more than one role besides `admin` exists,
|
|
3403
|
+
* creating a user without a role is an error that asks for one. With exactly one other role, that role is the default.
|
|
3404
|
+
*/ : string | null | undefined;
|
|
3308
3405
|
};
|
|
3309
3406
|
};
|
|
3310
3407
|
}>>[];
|
|
@@ -3409,7 +3506,15 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
3409
3506
|
email: string;
|
|
3410
3507
|
emailVerified: boolean;
|
|
3411
3508
|
name: string;
|
|
3412
|
-
image
|
|
3509
|
+
image?
|
|
3510
|
+
/**
|
|
3511
|
+
* The role of a user created without one. Optional: when unset, and more than one role besides `admin` exists,
|
|
3512
|
+
* creating a user without a role is an error that asks for one. With exactly one other role, that role is the default.
|
|
3513
|
+
*/
|
|
3514
|
+
/**
|
|
3515
|
+
* The role of a user created without one. Optional: when unset, and more than one role besides `admin` exists,
|
|
3516
|
+
* creating a user without a role is an error that asks for one. With exactly one other role, that role is the default.
|
|
3517
|
+
*/ : string | null | undefined;
|
|
3413
3518
|
};
|
|
3414
3519
|
};
|
|
3415
3520
|
}>>[];
|
|
@@ -3437,7 +3542,15 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
3437
3542
|
email: string;
|
|
3438
3543
|
emailVerified: boolean;
|
|
3439
3544
|
name: string;
|
|
3440
|
-
image
|
|
3545
|
+
image?
|
|
3546
|
+
/**
|
|
3547
|
+
* The role of a user created without one. Optional: when unset, and more than one role besides `admin` exists,
|
|
3548
|
+
* creating a user without a role is an error that asks for one. With exactly one other role, that role is the default.
|
|
3549
|
+
*/
|
|
3550
|
+
/**
|
|
3551
|
+
* The role of a user created without one. Optional: when unset, and more than one role besides `admin` exists,
|
|
3552
|
+
* creating a user without a role is an error that asks for one. With exactly one other role, that role is the default.
|
|
3553
|
+
*/ : string | null | undefined;
|
|
3441
3554
|
};
|
|
3442
3555
|
};
|
|
3443
3556
|
}>>[];
|
|
@@ -3466,7 +3579,15 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
3466
3579
|
email: string;
|
|
3467
3580
|
emailVerified: boolean;
|
|
3468
3581
|
name: string;
|
|
3469
|
-
image
|
|
3582
|
+
image?
|
|
3583
|
+
/**
|
|
3584
|
+
* The role of a user created without one. Optional: when unset, and more than one role besides `admin` exists,
|
|
3585
|
+
* creating a user without a role is an error that asks for one. With exactly one other role, that role is the default.
|
|
3586
|
+
*/
|
|
3587
|
+
/**
|
|
3588
|
+
* The role of a user created without one. Optional: when unset, and more than one role besides `admin` exists,
|
|
3589
|
+
* creating a user without a role is an error that asks for one. With exactly one other role, that role is the default.
|
|
3590
|
+
*/ : string | null | undefined;
|
|
3470
3591
|
};
|
|
3471
3592
|
};
|
|
3472
3593
|
}>>[];
|
|
@@ -3490,7 +3611,15 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
3490
3611
|
email: string;
|
|
3491
3612
|
emailVerified: boolean;
|
|
3492
3613
|
name: string;
|
|
3493
|
-
image
|
|
3614
|
+
image?
|
|
3615
|
+
/**
|
|
3616
|
+
* The role of a user created without one. Optional: when unset, and more than one role besides `admin` exists,
|
|
3617
|
+
* creating a user without a role is an error that asks for one. With exactly one other role, that role is the default.
|
|
3618
|
+
*/
|
|
3619
|
+
/**
|
|
3620
|
+
* The role of a user created without one. Optional: when unset, and more than one role besides `admin` exists,
|
|
3621
|
+
* creating a user without a role is an error that asks for one. With exactly one other role, that role is the default.
|
|
3622
|
+
*/ : string | null | undefined;
|
|
3494
3623
|
};
|
|
3495
3624
|
};
|
|
3496
3625
|
}>>[];
|
|
@@ -3513,7 +3642,15 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
3513
3642
|
email: string;
|
|
3514
3643
|
emailVerified: boolean;
|
|
3515
3644
|
name: string;
|
|
3516
|
-
image
|
|
3645
|
+
image?
|
|
3646
|
+
/**
|
|
3647
|
+
* The role of a user created without one. Optional: when unset, and more than one role besides `admin` exists,
|
|
3648
|
+
* creating a user without a role is an error that asks for one. With exactly one other role, that role is the default.
|
|
3649
|
+
*/
|
|
3650
|
+
/**
|
|
3651
|
+
* The role of a user created without one. Optional: when unset, and more than one role besides `admin` exists,
|
|
3652
|
+
* creating a user without a role is an error that asks for one. With exactly one other role, that role is the default.
|
|
3653
|
+
*/ : string | null | undefined;
|
|
3517
3654
|
};
|
|
3518
3655
|
} & {
|
|
3519
3656
|
session: {
|
|
@@ -3610,7 +3747,15 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
3610
3747
|
email: string;
|
|
3611
3748
|
emailVerified: boolean;
|
|
3612
3749
|
name: string;
|
|
3613
|
-
image
|
|
3750
|
+
image?
|
|
3751
|
+
/**
|
|
3752
|
+
* The role of a user created without one. Optional: when unset, and more than one role besides `admin` exists,
|
|
3753
|
+
* creating a user without a role is an error that asks for one. With exactly one other role, that role is the default.
|
|
3754
|
+
*/
|
|
3755
|
+
/**
|
|
3756
|
+
* The role of a user created without one. Optional: when unset, and more than one role besides `admin` exists,
|
|
3757
|
+
* creating a user without a role is an error that asks for one. With exactly one other role, that role is the default.
|
|
3758
|
+
*/ : string | null | undefined;
|
|
3614
3759
|
};
|
|
3615
3760
|
};
|
|
3616
3761
|
}>>[];
|
|
@@ -3642,7 +3787,15 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
3642
3787
|
email: string;
|
|
3643
3788
|
emailVerified: boolean;
|
|
3644
3789
|
name: string;
|
|
3645
|
-
image
|
|
3790
|
+
image?
|
|
3791
|
+
/**
|
|
3792
|
+
* The role of a user created without one. Optional: when unset, and more than one role besides `admin` exists,
|
|
3793
|
+
* creating a user without a role is an error that asks for one. With exactly one other role, that role is the default.
|
|
3794
|
+
*/
|
|
3795
|
+
/**
|
|
3796
|
+
* The role of a user created without one. Optional: when unset, and more than one role besides `admin` exists,
|
|
3797
|
+
* creating a user without a role is an error that asks for one. With exactly one other role, that role is the default.
|
|
3798
|
+
*/ : string | null | undefined;
|
|
3646
3799
|
};
|
|
3647
3800
|
};
|
|
3648
3801
|
}>>[];
|
|
@@ -3970,7 +4123,15 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
3970
4123
|
email: string;
|
|
3971
4124
|
emailVerified: boolean;
|
|
3972
4125
|
name: string;
|
|
3973
|
-
image
|
|
4126
|
+
image?
|
|
4127
|
+
/**
|
|
4128
|
+
* The role of a user created without one. Optional: when unset, and more than one role besides `admin` exists,
|
|
4129
|
+
* creating a user without a role is an error that asks for one. With exactly one other role, that role is the default.
|
|
4130
|
+
*/
|
|
4131
|
+
/**
|
|
4132
|
+
* The role of a user created without one. Optional: when unset, and more than one role besides `admin` exists,
|
|
4133
|
+
* creating a user without a role is an error that asks for one. With exactly one other role, that role is the default.
|
|
4134
|
+
*/ : string | null | undefined;
|
|
3974
4135
|
};
|
|
3975
4136
|
};
|
|
3976
4137
|
}>>[];
|
|
@@ -4009,7 +4170,15 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
4009
4170
|
email: string;
|
|
4010
4171
|
emailVerified: boolean;
|
|
4011
4172
|
name: string;
|
|
4012
|
-
image
|
|
4173
|
+
image?
|
|
4174
|
+
/**
|
|
4175
|
+
* The role of a user created without one. Optional: when unset, and more than one role besides `admin` exists,
|
|
4176
|
+
* creating a user without a role is an error that asks for one. With exactly one other role, that role is the default.
|
|
4177
|
+
*/
|
|
4178
|
+
/**
|
|
4179
|
+
* The role of a user created without one. Optional: when unset, and more than one role besides `admin` exists,
|
|
4180
|
+
* creating a user without a role is an error that asks for one. With exactly one other role, that role is the default.
|
|
4181
|
+
*/ : string | null | undefined;
|
|
4013
4182
|
};
|
|
4014
4183
|
};
|
|
4015
4184
|
}>>[];
|
|
@@ -4044,7 +4213,15 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
4044
4213
|
email: string;
|
|
4045
4214
|
emailVerified: boolean;
|
|
4046
4215
|
name: string;
|
|
4047
|
-
image
|
|
4216
|
+
image?
|
|
4217
|
+
/**
|
|
4218
|
+
* The role of a user created without one. Optional: when unset, and more than one role besides `admin` exists,
|
|
4219
|
+
* creating a user without a role is an error that asks for one. With exactly one other role, that role is the default.
|
|
4220
|
+
*/
|
|
4221
|
+
/**
|
|
4222
|
+
* The role of a user created without one. Optional: when unset, and more than one role besides `admin` exists,
|
|
4223
|
+
* creating a user without a role is an error that asks for one. With exactly one other role, that role is the default.
|
|
4224
|
+
*/ : string | null | undefined;
|
|
4048
4225
|
};
|
|
4049
4226
|
};
|
|
4050
4227
|
}>>[];
|
|
@@ -4084,7 +4261,15 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
4084
4261
|
email: string;
|
|
4085
4262
|
emailVerified: boolean;
|
|
4086
4263
|
name: string;
|
|
4087
|
-
image
|
|
4264
|
+
image?
|
|
4265
|
+
/**
|
|
4266
|
+
* The role of a user created without one. Optional: when unset, and more than one role besides `admin` exists,
|
|
4267
|
+
* creating a user without a role is an error that asks for one. With exactly one other role, that role is the default.
|
|
4268
|
+
*/
|
|
4269
|
+
/**
|
|
4270
|
+
* The role of a user created without one. Optional: when unset, and more than one role besides `admin` exists,
|
|
4271
|
+
* creating a user without a role is an error that asks for one. With exactly one other role, that role is the default.
|
|
4272
|
+
*/ : string | null | undefined;
|
|
4088
4273
|
};
|
|
4089
4274
|
};
|
|
4090
4275
|
}>>[];
|
|
@@ -4141,7 +4326,15 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
4141
4326
|
email: string;
|
|
4142
4327
|
emailVerified: boolean;
|
|
4143
4328
|
name: string;
|
|
4144
|
-
image
|
|
4329
|
+
image?
|
|
4330
|
+
/**
|
|
4331
|
+
* The role of a user created without one. Optional: when unset, and more than one role besides `admin` exists,
|
|
4332
|
+
* creating a user without a role is an error that asks for one. With exactly one other role, that role is the default.
|
|
4333
|
+
*/
|
|
4334
|
+
/**
|
|
4335
|
+
* The role of a user created without one. Optional: when unset, and more than one role besides `admin` exists,
|
|
4336
|
+
* creating a user without a role is an error that asks for one. With exactly one other role, that role is the default.
|
|
4337
|
+
*/ : string | null | undefined;
|
|
4145
4338
|
};
|
|
4146
4339
|
};
|
|
4147
4340
|
}>>[];
|
|
@@ -4233,6 +4426,14 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
4233
4426
|
window: number;
|
|
4234
4427
|
max: number;
|
|
4235
4428
|
customRules: {
|
|
4429
|
+
[signUpPaths.sendCode]: {
|
|
4430
|
+
window: number;
|
|
4431
|
+
max: number;
|
|
4432
|
+
};
|
|
4433
|
+
[signUpPaths.create]: {
|
|
4434
|
+
window: number;
|
|
4435
|
+
max: number;
|
|
4436
|
+
};
|
|
4236
4437
|
'/sign-in/email': {
|
|
4237
4438
|
window: number;
|
|
4238
4439
|
max: number;
|
|
@@ -4293,6 +4494,8 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
4293
4494
|
defaultRole: string | undefined;
|
|
4294
4495
|
mail: boolean;
|
|
4295
4496
|
passwordReset: boolean;
|
|
4497
|
+
/** Open sign-up is on and offered: the app asks for it, and can mail the codes. */
|
|
4498
|
+
signUp: boolean;
|
|
4296
4499
|
socialProviders: string[];
|
|
4297
4500
|
operator: string | undefined;
|
|
4298
4501
|
platformSignIn: {
|
|
@@ -15,6 +15,7 @@ import { rolesPlugin } from "./roles/roles-plugin.js";
|
|
|
15
15
|
import { platformSignInFor, platformSignInProvider, readPlatformSignIn, resolvePlatformSignIn } from "./platform-sign-in.js";
|
|
16
16
|
import { signInPolicyPlugin } from "./policy-plugin.js";
|
|
17
17
|
import { emailCodeOptions, twoFactorCodeOptions, unusedEmailCodePaths } from "./sign-in-mail.js";
|
|
18
|
+
import { signUpPaths, signUpPlugin } from "./sign-up/sign-up-plugin.js";
|
|
18
19
|
import { staffSignInPlugin } from "./staff-plugin.js";
|
|
19
20
|
import { twoFactorAfterPasswordOrCode } from "./two-factor-challenge.js";
|
|
20
21
|
import { APIError } from "better-auth/api";
|
|
@@ -30,9 +31,9 @@ const tokenLifetime = "15m";
|
|
|
30
31
|
const authBasePath = "/api/auth";
|
|
31
32
|
/**
|
|
32
33
|
* Better Auth for a single-tenant admin: sign-in with a password, an emailed code or a passkey, two-factor
|
|
33
|
-
* authentication, and the sign-in policy admins set; no public sign-up
|
|
34
|
-
* `createUser`, or by an admin through the admin plugin), roles from the admin plugin's `role`
|
|
35
|
-
* at `/api/auth/token` with the keys at `/api/auth/jwks`.
|
|
34
|
+
* authentication, and the sign-in policy admins set; no public sign-up unless `signUp` turns it on (every account is
|
|
35
|
+
* created on the host with `createUser`, or by an admin through the admin plugin), roles from the admin plugin's `role`
|
|
36
|
+
* column, 15 minute JWTs at `/api/auth/token` with the keys at `/api/auth/jwks`.
|
|
36
37
|
*/
|
|
37
38
|
/** The configured roles with `admin` first; `admin` is added when missing. */
|
|
38
39
|
const normalizeRoles = (roles) => roleDefinitions(roles).names;
|
|
@@ -43,7 +44,7 @@ const organizationPaths = ["/organization/set-active", "/organization/accept-inv
|
|
|
43
44
|
* The options `createAuth` gives Better Auth, with the roles, default role, mailer, operator provider and platform
|
|
44
45
|
* sign-in provider already resolved.
|
|
45
46
|
*/
|
|
46
|
-
const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, definitions, defaultRole, mailer, operator, platformSignIn, organizations, context = noContext }) => {
|
|
47
|
+
const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, definitions, defaultRole, mailer, signUpRole, operator, platformSignIn, organizations, context = noContext }) => {
|
|
47
48
|
const secure = options.baseURL.startsWith("https://");
|
|
48
49
|
const signIn = platformSignInFor(platformSignIn, Object.keys(options.socialProviders ?? {}));
|
|
49
50
|
const hosts = passkeyHosts([options.baseURL, ...origins]);
|
|
@@ -114,6 +115,11 @@ const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, defini
|
|
|
114
115
|
formerDomain: hosts[0]
|
|
115
116
|
}),
|
|
116
117
|
...mailer ? [emailOTP(emailCodeOptions(mailer))] : [],
|
|
118
|
+
...mailer && signUpRole ? [signUpPlugin({
|
|
119
|
+
role: signUpRole,
|
|
120
|
+
mailer,
|
|
121
|
+
appName: options.appName ?? "Protobase"
|
|
122
|
+
})] : [],
|
|
117
123
|
...signIn ? [genericOAuth({ config: [platformSignInProvider(signIn)] })] : [],
|
|
118
124
|
signInPolicyPlugin({
|
|
119
125
|
mail: Boolean(mailer),
|
|
@@ -175,6 +181,14 @@ const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, defini
|
|
|
175
181
|
"/invitation/sign-up": {
|
|
176
182
|
window: 60,
|
|
177
183
|
max: options.signInPerMinute ?? 5
|
|
184
|
+
},
|
|
185
|
+
[signUpPaths.sendCode]: {
|
|
186
|
+
window: 60,
|
|
187
|
+
max: options.signInPerMinute ?? 5
|
|
188
|
+
},
|
|
189
|
+
[signUpPaths.create]: {
|
|
190
|
+
window: 60,
|
|
191
|
+
max: options.signInPerMinute ?? 5
|
|
178
192
|
}
|
|
179
193
|
}
|
|
180
194
|
},
|
|
@@ -188,10 +202,17 @@ const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, defini
|
|
|
188
202
|
}
|
|
189
203
|
};
|
|
190
204
|
};
|
|
205
|
+
const checkSignUpRole = (role, roles) => {
|
|
206
|
+
if (role === "admin") throw new Error("signUp.role cannot be admin: give the least a stranger may hold");
|
|
207
|
+
if (!roles.includes(role)) throw new Error(`signUp.role "${role}" is not one of the roles: ${roles.join(", ")}`);
|
|
208
|
+
return role;
|
|
209
|
+
};
|
|
191
210
|
const createAuth = (options) => {
|
|
211
|
+
if (options.signUp && options.organizations) throw new Error("signUp is not available with organizations: their people join by invitation");
|
|
192
212
|
const definitions = roleDefinitions(options.roles, { organizations: options.organizations !== void 0 });
|
|
193
213
|
const roles = definitions.names;
|
|
194
214
|
if (options.defaultRole !== void 0 && !roles.includes(options.defaultRole)) throw new Error(`defaultRole "${options.defaultRole}" is not one of the roles: ${roles.join(", ")}`);
|
|
215
|
+
const signUpRole = options.signUp && checkSignUpRole(options.signUp.role, roles);
|
|
195
216
|
const others = roles.filter((role) => role !== "admin");
|
|
196
217
|
const defaultRole = options.defaultRole ?? (options.organizations ? "user" : others.length === 1 ? others[0] : void 0);
|
|
197
218
|
const mailer = options.mailer === void 0 ? platformMailer() : options.mailer || void 0;
|
|
@@ -212,6 +233,7 @@ const createAuth = (options) => {
|
|
|
212
233
|
definitions,
|
|
213
234
|
defaultRole,
|
|
214
235
|
mailer,
|
|
236
|
+
signUpRole,
|
|
215
237
|
operator,
|
|
216
238
|
platformSignIn,
|
|
217
239
|
...organizations && {
|
|
@@ -233,6 +255,8 @@ const createAuth = (options) => {
|
|
|
233
255
|
defaultRole,
|
|
234
256
|
mail: Boolean(mailer),
|
|
235
257
|
passwordReset: Boolean(mailer),
|
|
258
|
+
/** Open sign-up is on and offered: the app asks for it, and can mail the codes. */
|
|
259
|
+
signUp: Boolean(signUpRole && mailer),
|
|
236
260
|
socialProviders,
|
|
237
261
|
operator: operator?.name,
|
|
238
262
|
platformSignIn: platformSignIn && {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { APIError } from "better-auth/api";
|
|
2
|
+
//#region src/better-auth/new-account-password.ts
|
|
3
|
+
const minPasswordLength = 12;
|
|
4
|
+
const maxPasswordLength = 128;
|
|
5
|
+
/**
|
|
6
|
+
* Checks the password someone chose for a new account they make themselves, from an invitation or by signing up,
|
|
7
|
+
* against the effective sign-in policy: none while passwords are forbidden, one unless emailed codes are allowed, and
|
|
8
|
+
* of the length Better Auth's own sign-in takes.
|
|
9
|
+
*/
|
|
10
|
+
const checkNewAccountPassword = (policy, password) => {
|
|
11
|
+
if (password !== void 0 && policy.password === "forbidden") throw new APIError("BAD_REQUEST", {
|
|
12
|
+
code: "SIGN_IN_METHOD_FORBIDDEN",
|
|
13
|
+
message: "This app does not sign in with passwords; leave the password out."
|
|
14
|
+
});
|
|
15
|
+
if (password === void 0 && policy.emailCode !== "allowed") throw new APIError("BAD_REQUEST", {
|
|
16
|
+
code: "PASSWORD_REQUIRED",
|
|
17
|
+
message: "Choose a password to sign in with."
|
|
18
|
+
});
|
|
19
|
+
if (password !== void 0 && (password.length < minPasswordLength || password.length > maxPasswordLength)) throw new APIError("BAD_REQUEST", {
|
|
20
|
+
code: "PASSWORD_LENGTH",
|
|
21
|
+
message: `Choose a password of ${minPasswordLength} to ${maxPasswordLength} characters.`
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
//#endregion
|
|
25
|
+
export { checkNewAccountPassword };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { noGlobalRole } from "./role-definitions.js";
|
|
2
2
|
import { findOrganization } from "./store.js";
|
|
3
3
|
import { invitationLink, invitationToken } from "./invitation-token.js";
|
|
4
|
+
import { checkNewAccountPassword } from "../new-account-password.js";
|
|
4
5
|
import { effectiveSignInPolicy } from "../sign-in-policy.js";
|
|
5
6
|
import { readSignInPolicy } from "../policy-store.js";
|
|
6
7
|
import { acceptInvitation, invitationOfToken } from "./accept-invitation.js";
|
|
@@ -8,8 +9,6 @@ import { APIError, createAuthEndpoint, createAuthMiddleware, sessionMiddleware }
|
|
|
8
9
|
import { setSessionCookie } from "better-auth/cookies";
|
|
9
10
|
import * as z$1 from "zod";
|
|
10
11
|
//#region src/better-auth/organizations/invitation-endpoints.ts
|
|
11
|
-
const minPasswordLength = 12;
|
|
12
|
-
const maxPasswordLength = 128;
|
|
13
12
|
const tokenBody = z$1.object({ token: z$1.string().min(1) });
|
|
14
13
|
const startIn = (context, sessionToken, organizationId) => context.internalAdapter.updateSession(sessionToken, { activeOrganizationId: organizationId });
|
|
15
14
|
/**
|
|
@@ -71,20 +70,8 @@ const invitationEndpoints = ({ resolved, mail }) => ({
|
|
|
71
70
|
code: "ACCOUNT_EXISTS",
|
|
72
71
|
message: "This address has an account already: sign in to accept the invitation."
|
|
73
72
|
});
|
|
74
|
-
const policy = effectiveSignInPolicy((await readSignInPolicy(ctx.context.adapter)).policy, { mail });
|
|
75
73
|
const { password } = ctx.body;
|
|
76
|
-
|
|
77
|
-
code: "SIGN_IN_METHOD_FORBIDDEN",
|
|
78
|
-
message: "This app does not sign in with passwords; leave the password out."
|
|
79
|
-
});
|
|
80
|
-
if (password === void 0 && policy.emailCode !== "allowed") throw new APIError("BAD_REQUEST", {
|
|
81
|
-
code: "PASSWORD_REQUIRED",
|
|
82
|
-
message: "Choose a password to sign in with."
|
|
83
|
-
});
|
|
84
|
-
if (password !== void 0 && (password.length < minPasswordLength || password.length > maxPasswordLength)) throw new APIError("BAD_REQUEST", {
|
|
85
|
-
code: "PASSWORD_LENGTH",
|
|
86
|
-
message: `Choose a password of ${minPasswordLength} to ${maxPasswordLength} characters.`
|
|
87
|
-
});
|
|
74
|
+
checkNewAccountPassword(effectiveSignInPolicy((await readSignInPolicy(ctx.context.adapter)).policy, { mail }), password);
|
|
88
75
|
const user = await internalAdapter.createUser({
|
|
89
76
|
email: invitation.email.toLowerCase(),
|
|
90
77
|
name: ctx.body.name,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { APIError } from "better-auth/api";
|
|
2
|
+
const signUpCodeLifetime = "5 minutes";
|
|
3
|
+
const allowedAttempts = 3;
|
|
4
|
+
const identifierOf = (email) => `sign-up-otp-${email}`;
|
|
5
|
+
const hashOf = async (code) => {
|
|
6
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(code));
|
|
7
|
+
return btoa(String.fromCharCode(...new Uint8Array(digest)));
|
|
8
|
+
};
|
|
9
|
+
const sixDigits = () => {
|
|
10
|
+
const limit = Math.floor(2 ** 32 / 1e6) * 1e6;
|
|
11
|
+
for (;;) {
|
|
12
|
+
const [value] = crypto.getRandomValues(/* @__PURE__ */ new Uint32Array(1));
|
|
13
|
+
if (value < limit) return String(value % 1e6).padStart(6, "0");
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
const invalidCode = () => new APIError("BAD_REQUEST", {
|
|
17
|
+
code: "SIGN_UP_CODE_INVALID",
|
|
18
|
+
message: "The code is not right, or has expired. Ask for a new one."
|
|
19
|
+
});
|
|
20
|
+
/** A new code for `email`, replacing any earlier one; the store keeps only its hash. */
|
|
21
|
+
const issueSignUpCode = async (context, email) => {
|
|
22
|
+
const code = sixDigits();
|
|
23
|
+
const identifier = identifierOf(email);
|
|
24
|
+
await context.internalAdapter.deleteVerificationByIdentifier(identifier);
|
|
25
|
+
await context.internalAdapter.createVerificationValue({
|
|
26
|
+
identifier,
|
|
27
|
+
value: `${await hashOf(code)}:0`,
|
|
28
|
+
expiresAt: new Date(Date.now() + 3e5)
|
|
29
|
+
});
|
|
30
|
+
return code;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Takes the code for `email` when `code` is it, so it works once; a wrong one counts a try. Taking the stored value is
|
|
34
|
+
* atomic, so of two requests with the right code only one gets through. Throws `SIGN_UP_CODE_INVALID` otherwise.
|
|
35
|
+
*/
|
|
36
|
+
const consumeSignUpCode = async (context, email, code) => {
|
|
37
|
+
const identifier = identifierOf(email);
|
|
38
|
+
const consumed = await context.internalAdapter.consumeVerificationValue(identifier);
|
|
39
|
+
if (!consumed || new Date(consumed.expiresAt) < /* @__PURE__ */ new Date()) throw invalidCode();
|
|
40
|
+
const separator = consumed.value.lastIndexOf(":");
|
|
41
|
+
const hash = consumed.value.slice(0, separator);
|
|
42
|
+
const attempts = Number(consumed.value.slice(separator + 1)) + 1;
|
|
43
|
+
if (hash === await hashOf(code)) return;
|
|
44
|
+
if (attempts < allowedAttempts) await context.internalAdapter.createVerificationValue({
|
|
45
|
+
identifier,
|
|
46
|
+
value: `${hash}:${attempts}`,
|
|
47
|
+
expiresAt: new Date(consumed.expiresAt)
|
|
48
|
+
});
|
|
49
|
+
throw invalidCode();
|
|
50
|
+
};
|
|
51
|
+
//#endregion
|
|
52
|
+
export { consumeSignUpCode, issueSignUpCode, signUpCodeLifetime };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { signUpCodeLifetime } from "./sign-up-code.js";
|
|
2
|
+
//#region src/better-auth/sign-up/sign-up-mail.ts
|
|
3
|
+
/** The email with the code that makes an account for the address. */
|
|
4
|
+
const signUpCodeEmail = ({ to, code, appName }) => ({
|
|
5
|
+
to,
|
|
6
|
+
subject: "Your code to create an account",
|
|
7
|
+
text: [
|
|
8
|
+
`Use this code to create your account for ${appName}:`,
|
|
9
|
+
"",
|
|
10
|
+
code,
|
|
11
|
+
"",
|
|
12
|
+
`It works for ${signUpCodeLifetime}, once.`,
|
|
13
|
+
"If you did not try to create an account, ignore this email: nobody can create one for your address without the code.",
|
|
14
|
+
""
|
|
15
|
+
].join("\n")
|
|
16
|
+
});
|
|
17
|
+
/** The email to an address that asked for a sign-up code while it has an account already: no code, a pointer to sign in instead. */
|
|
18
|
+
const accountExistsEmail = ({ to, appName }) => ({
|
|
19
|
+
to,
|
|
20
|
+
subject: "You already have an account",
|
|
21
|
+
text: [
|
|
22
|
+
`Someone asked to create an account for ${appName} with this address, which has one already, so no new account was made.`,
|
|
23
|
+
"",
|
|
24
|
+
"Sign in with it instead. If this was not you, ignore this email.",
|
|
25
|
+
""
|
|
26
|
+
].join("\n")
|
|
27
|
+
});
|
|
28
|
+
//#endregion
|
|
29
|
+
export { accountExistsEmail, signUpCodeEmail };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { checkNewAccountPassword } from "../new-account-password.js";
|
|
2
|
+
import { effectiveSignInPolicy } from "../sign-in-policy.js";
|
|
3
|
+
import { readSignInPolicy } from "../policy-store.js";
|
|
4
|
+
import { consumeSignUpCode, issueSignUpCode } from "./sign-up-code.js";
|
|
5
|
+
import { accountExistsEmail, signUpCodeEmail } from "./sign-up-mail.js";
|
|
6
|
+
import { APIError, createAuthEndpoint } from "better-auth/api";
|
|
7
|
+
import { setSessionCookie } from "better-auth/cookies";
|
|
8
|
+
import * as z$1 from "zod";
|
|
9
|
+
//#region src/better-auth/sign-up/sign-up-plugin.ts
|
|
10
|
+
const address = z$1.string().trim().toLowerCase().pipe(z$1.email());
|
|
11
|
+
/** The paths of the sign-up endpoints, as `ctx.path` and the rate limits name them. */
|
|
12
|
+
const signUpPaths = {
|
|
13
|
+
sendCode: "/sign-up/send-code",
|
|
14
|
+
create: "/sign-up/create"
|
|
15
|
+
};
|
|
16
|
+
const noAdminYet = () => new APIError("FORBIDDEN", {
|
|
17
|
+
code: "NO_ADMIN_YET",
|
|
18
|
+
message: "This app has no admin yet; accounts are created on the host until it has one."
|
|
19
|
+
});
|
|
20
|
+
/**
|
|
21
|
+
* Open sign-up: anyone makes their own account, with `role`. The address comes first, and a code mailed to it, so an
|
|
22
|
+
* account exists only for an address its owner reads, verified from the start, and nobody can claim an address before
|
|
23
|
+
* its owner does. Asking for a code answers the same whether the address has an account or not; one that has gets a
|
|
24
|
+
* mail saying so instead of a code. While the store has no user, nobody signs up: the first admin is made on the host.
|
|
25
|
+
*/
|
|
26
|
+
const signUpPlugin = ({ role, mailer, appName }) => ({
|
|
27
|
+
id: "protobase-sign-up",
|
|
28
|
+
endpoints: {
|
|
29
|
+
/** `POST /sign-up/send-code` with `{ email }`: mails a code for making an account with the address. */
|
|
30
|
+
sendSignUpCode: createAuthEndpoint(signUpPaths.sendCode, {
|
|
31
|
+
method: "POST",
|
|
32
|
+
body: z$1.object({ email: address })
|
|
33
|
+
}, async (ctx) => {
|
|
34
|
+
const { email } = ctx.body;
|
|
35
|
+
const { internalAdapter, adapter } = ctx.context;
|
|
36
|
+
if (await adapter.count({ model: "user" }) === 0) throw noAdminYet();
|
|
37
|
+
const message = await internalAdapter.findUserByEmail(email) ? accountExistsEmail({
|
|
38
|
+
to: email,
|
|
39
|
+
appName
|
|
40
|
+
}) : signUpCodeEmail({
|
|
41
|
+
to: email,
|
|
42
|
+
code: await issueSignUpCode(ctx.context, email),
|
|
43
|
+
appName
|
|
44
|
+
});
|
|
45
|
+
await ctx.context.runInBackgroundOrAwait(mailer.send(message));
|
|
46
|
+
return ctx.json({ sent: true });
|
|
47
|
+
}),
|
|
48
|
+
/**
|
|
49
|
+
* `POST /sign-up/create` with `{ email, code, name, password? }`: makes the account with the mailed code, verified
|
|
50
|
+
* and with the sign-up role, and signs the person in. A password is needed unless the sign-in policy offers emailed
|
|
51
|
+
* codes; the policy's required methods are then set up as after any sign-in.
|
|
52
|
+
*/
|
|
53
|
+
createSignUpAccount: createAuthEndpoint(signUpPaths.create, {
|
|
54
|
+
method: "POST",
|
|
55
|
+
body: z$1.object({
|
|
56
|
+
email: address,
|
|
57
|
+
code: z$1.string().trim().min(1),
|
|
58
|
+
name: z$1.string().trim().min(1).max(200),
|
|
59
|
+
password: z$1.string().optional()
|
|
60
|
+
})
|
|
61
|
+
}, async (ctx) => {
|
|
62
|
+
const { email, code, name, password } = ctx.body;
|
|
63
|
+
const { internalAdapter, adapter } = ctx.context;
|
|
64
|
+
if (await adapter.count({ model: "user" }) === 0) throw noAdminYet();
|
|
65
|
+
checkNewAccountPassword(effectiveSignInPolicy((await readSignInPolicy(adapter)).policy, { mail: true }), password);
|
|
66
|
+
await consumeSignUpCode(ctx.context, email, code);
|
|
67
|
+
if (await internalAdapter.findUserByEmail(email)) throw new APIError("BAD_REQUEST", {
|
|
68
|
+
code: "ACCOUNT_EXISTS",
|
|
69
|
+
message: "This address has an account already: sign in instead."
|
|
70
|
+
});
|
|
71
|
+
const user = await internalAdapter.createUser({
|
|
72
|
+
email,
|
|
73
|
+
name,
|
|
74
|
+
emailVerified: true,
|
|
75
|
+
role
|
|
76
|
+
}, { method: "sign-up" });
|
|
77
|
+
if (password !== void 0) await internalAdapter.linkAccount({
|
|
78
|
+
userId: user.id,
|
|
79
|
+
providerId: "credential",
|
|
80
|
+
accountId: user.id,
|
|
81
|
+
password: await ctx.context.password.hash(password)
|
|
82
|
+
});
|
|
83
|
+
const session = await internalAdapter.createSession(user.id, false);
|
|
84
|
+
await setSessionCookie(ctx, {
|
|
85
|
+
session,
|
|
86
|
+
user
|
|
87
|
+
});
|
|
88
|
+
return ctx.json({ user: {
|
|
89
|
+
id: user.id,
|
|
90
|
+
email: user.email,
|
|
91
|
+
name: user.name,
|
|
92
|
+
role
|
|
93
|
+
} });
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
//#endregion
|
|
98
|
+
export { signUpPaths, signUpPlugin };
|
|
@@ -9,7 +9,7 @@ import { Hono } from "hono";
|
|
|
9
9
|
* `GET {authBase}/status`: `needsAdmin` while the admin store has no user, so a login page can say how to create one;
|
|
10
10
|
* `signInMethods`, the ways to sign in the sign-in policy leaves on (`password`, `emailCode`, `passkey`), so it offers
|
|
11
11
|
* only those; `passwordReset` when reset mail can be sent and passwords are on, so it offers "Forgot password?" only
|
|
12
|
-
* then; the ids of the `socialProviders` people can sign in with, so it offers "Continue with GitHub" when `github` is
|
|
12
|
+
* then; `signUp` when the app has open sign-up, so it offers "Create an account"; the ids of the `socialProviders` people can sign in with, so it offers "Continue with GitHub" when `github` is
|
|
13
13
|
* one; `platformSignIn`, the id and name of the platform's provider when it is one of them; `staffSignIn`, the
|
|
14
14
|
* operator provider's name, when staff can sign in as people; `passkeyDomains`, when people have passkeys for more than
|
|
15
15
|
* one of the app's hosts, the RP IDs a passkey sign-in can ask for, the passkey domain first; with organizations, who may create them and the roles
|
|
@@ -35,6 +35,7 @@ const statusRoute = (auth, { anonymous = false } = {}) => {
|
|
|
35
35
|
needsAdmin: !await hasUsers(auth),
|
|
36
36
|
signInMethods,
|
|
37
37
|
passwordReset: auth.passwordReset && policy.password === "allowed",
|
|
38
|
+
...auth.signUp && { signUp: true },
|
|
38
39
|
socialProviders,
|
|
39
40
|
...platformSignIn && { platformSignIn },
|
|
40
41
|
...staffSignIn && { staffSignIn },
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region src/changes/event-code-error.ts
|
|
2
|
+
/**
|
|
3
|
+
* An error the app's code threw while a change was dispatched: an event's `match` or filter, a subscription's `when`, or
|
|
4
|
+
* an access rule of the roles it delivers as. Dispatch keeps the change as failed and moves on; any other error, such
|
|
5
|
+
* as the database's, fails the whole dispatch, which is retried.
|
|
6
|
+
*/
|
|
7
|
+
var EventCodeError = class extends Error {
|
|
8
|
+
name = "EventCodeError";
|
|
9
|
+
constructor(what, error) {
|
|
10
|
+
super(`${what} threw: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
//#endregion
|
|
14
|
+
export { EventCodeError };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { EventCodeError } from "./event-code-error.js";
|
|
1
2
|
import { evaluateFilter } from "@protobase/schema";
|
|
2
3
|
import { checkEventFilter } from "@protobase/schema/internal";
|
|
3
4
|
//#region src/changes/match-events.ts
|
|
@@ -39,7 +40,13 @@ const eventTest = (model, event, match) => {
|
|
|
39
40
|
if (!match) throw new Error(`Resource "${model.name}": event "${event.name}" has no match function`);
|
|
40
41
|
tests.push((change) => match(contextOf(change)));
|
|
41
42
|
}
|
|
42
|
-
return (change) =>
|
|
43
|
+
return (change) => {
|
|
44
|
+
try {
|
|
45
|
+
return tests.every((test) => test(change));
|
|
46
|
+
} catch (error) {
|
|
47
|
+
throw new EventCodeError(`Event ${event.name} on ${change.resource} ${change.key}`, error);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
43
50
|
};
|
|
44
51
|
/**
|
|
45
52
|
* The names of the resource's events a change raises, in declaration order. The filters are checked once, here; `matches`
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { EventCodeError } from "../changes/event-code-error.js";
|
|
1
2
|
import { eventPatternMatches } from "./subscription.js";
|
|
2
3
|
import { evaluateFilter } from "@protobase/schema";
|
|
3
4
|
import { checkEventFilter } from "@protobase/schema/internal";
|
|
@@ -48,7 +49,12 @@ const subscriptionRouter = (subscriptions, models) => {
|
|
|
48
49
|
if (options.enabled === false) return false;
|
|
49
50
|
if (options.tenant !== void 0 && String(options.tenant) !== String(change.tenant)) return false;
|
|
50
51
|
const filter = filters.get(`${subscription.name}\0${change.resource}`);
|
|
51
|
-
|
|
52
|
+
if (!filter) return true;
|
|
53
|
+
try {
|
|
54
|
+
return evaluateFilter(filter, change.after ?? change.before) === true;
|
|
55
|
+
} catch (error) {
|
|
56
|
+
throw new EventCodeError(`The when of subscription "${subscription.name}" on ${change.resource} ${change.key}`, error);
|
|
57
|
+
}
|
|
52
58
|
};
|
|
53
59
|
return (change, events) => subscriptions.flatMap((subscription) => {
|
|
54
60
|
if (!passes(subscription, change)) return [];
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { requestAccess } from "../request-access.js";
|
|
2
|
+
import { EventCodeError } from "../changes/event-code-error.js";
|
|
2
3
|
import { checkRecord, evaluateFilter } from "@protobase/schema";
|
|
3
4
|
//#region src/webhooks/subscriber-view.ts
|
|
4
5
|
/**
|
|
@@ -64,8 +65,14 @@ const subscriberEvents = (deps) => {
|
|
|
64
65
|
if (!entry) throw new Error(`Not implemented: a change of "${change.resource}", which is no resource of the app`);
|
|
65
66
|
const cacheKey = `${subscription.name}\0${change.resource}`;
|
|
66
67
|
if (!accesses.has(cacheKey)) accesses.set(cacheKey, requestAccess(deps, entry, session));
|
|
67
|
-
const
|
|
68
|
-
|
|
68
|
+
const readable = async () => {
|
|
69
|
+
const access = await accesses.get(cacheKey);
|
|
70
|
+
return await mayRead(access, change.after ?? change.before) ? access : void 0;
|
|
71
|
+
};
|
|
72
|
+
const access = await readable().catch((error) => {
|
|
73
|
+
throw new EventCodeError(`The access check of subscription "${subscription.name}" on ${change.resource} ${change.key}`, error);
|
|
74
|
+
});
|
|
75
|
+
if (!access) return void 0;
|
|
69
76
|
return subscriptionEvent(change, event, {
|
|
70
77
|
fields: Object.keys(access.readable.fields),
|
|
71
78
|
thin
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { dispatchJobName } from "../changes/capture-sql.js";
|
|
2
2
|
import { coalesce } from "../changes/coalesce.js";
|
|
3
|
+
import { EventCodeError } from "../changes/event-code-error.js";
|
|
3
4
|
import { eventMatcher } from "../changes/match-events.js";
|
|
4
5
|
import { deleteOutbox, readOutbox } from "../changes/outbox.js";
|
|
5
6
|
import { resourceChange } from "../changes/resource-change.js";
|
|
@@ -41,6 +42,9 @@ const deliveryGroup = ({ subscription, change }) => `${subscription.name}:${chan
|
|
|
41
42
|
* time: it turns the committed outbox rows into events, makes the first try of every unordered delivery itself,
|
|
42
43
|
* queues the failures and the ordered deliveries, and deletes the rows in that same transaction. A process that dies
|
|
43
44
|
* before that commits leaves the rows, and the job's retry delivers them again, under the same `webhook-id`.
|
|
45
|
+
* - `webhooks.dispatch-change`, one change whose app code threw in dispatch: an event's `match` or filter, a
|
|
46
|
+
* subscription's `when`, or an access rule of its roles. Kept as failed, so dispatch moves on to later changes; a
|
|
47
|
+
* retry after the fix queues the change's deliveries. Any other error fails the whole dispatch, which is retried.
|
|
44
48
|
* - `webhooks.deliver`, one try of an unordered delivery; a failure queues the next try on the Standard Webhooks
|
|
45
49
|
* schedule, or Retry-After, and the last one is kept as failed.
|
|
46
50
|
* - `webhooks.deliver-ordered`, the ordered deliveries of one dispatch for one subscription and record, retried with
|
|
@@ -138,20 +142,54 @@ const createWebhooks = ({ deps, subscriptions, functions, schema, log, table })
|
|
|
138
142
|
}
|
|
139
143
|
}
|
|
140
144
|
});
|
|
141
|
-
const
|
|
145
|
+
const changeDeliveries = async (db) => {
|
|
142
146
|
const route = subscriptionRouter(await current(db), models);
|
|
143
147
|
const eventOf = subscriberEvents(deps);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
148
|
+
return async (change) => {
|
|
149
|
+
const deliveries = [];
|
|
150
|
+
for (const { subscription, event } of route(change, matchers.get(change.resource)(change))) {
|
|
151
|
+
const shaped = await eventOf(subscription, change, event);
|
|
152
|
+
if (shaped) deliveries.push({
|
|
153
|
+
subscription,
|
|
154
|
+
event: shaped,
|
|
155
|
+
change
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
return deliveries;
|
|
159
|
+
};
|
|
160
|
+
};
|
|
161
|
+
const queueOrdered = async (db, deliveries) => {
|
|
162
|
+
const groups = /* @__PURE__ */ new Map();
|
|
163
|
+
for (const delivery of deliveries.filter(({ subscription }) => subscription.options.ordered)) {
|
|
164
|
+
const group = deliveryGroup(delivery);
|
|
165
|
+
const payload = groups.get(group) ?? {
|
|
166
|
+
subscription: delivery.subscription.name,
|
|
167
|
+
events: []
|
|
168
|
+
};
|
|
169
|
+
payload.events.push(delivery.event);
|
|
170
|
+
groups.set(group, payload);
|
|
152
171
|
}
|
|
153
|
-
|
|
172
|
+
for (const [group, payload] of groups) await deliverOrdered.enqueue(db, payload, { group });
|
|
154
173
|
};
|
|
174
|
+
const dispatchChange = defineJob({
|
|
175
|
+
name: "webhooks.dispatch-change",
|
|
176
|
+
maxAttempts: 1,
|
|
177
|
+
timeout: "5 minutes",
|
|
178
|
+
run: async ({ change }, { db }) => {
|
|
179
|
+
const deliveries = await (await changeDeliveries(db))(change);
|
|
180
|
+
await db.transaction().execute(async (trx) => {
|
|
181
|
+
for (const { subscription, event } of deliveries.filter(({ subscription }) => !subscription.options.ordered)) {
|
|
182
|
+
const payload = {
|
|
183
|
+
subscription: subscription.name,
|
|
184
|
+
event,
|
|
185
|
+
attempt: 1
|
|
186
|
+
};
|
|
187
|
+
await deliver.enqueue(trx, payload, { key: deliveryKey(payload) });
|
|
188
|
+
}
|
|
189
|
+
await queueOrdered(trx, deliveries);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
});
|
|
155
193
|
const dispatchBatch = async (db, signal) => {
|
|
156
194
|
const rows = await readOutbox(db, schema, models, batchSize);
|
|
157
195
|
if (rows.length === 0) return 0;
|
|
@@ -159,7 +197,18 @@ const createWebhooks = ({ deps, subscriptions, functions, schema, log, table })
|
|
|
159
197
|
const entry = deps.registry.find(change.resource);
|
|
160
198
|
return entry ? [resourceChange(entry.model, change)] : [];
|
|
161
199
|
});
|
|
162
|
-
const
|
|
200
|
+
const deliveriesOf = await changeDeliveries(db);
|
|
201
|
+
const deliveries = [];
|
|
202
|
+
const failures = [];
|
|
203
|
+
for (const change of changes) try {
|
|
204
|
+
deliveries.push(...await deliveriesOf(change));
|
|
205
|
+
} catch (error) {
|
|
206
|
+
if (!(error instanceof EventCodeError)) throw error;
|
|
207
|
+
failures.push({
|
|
208
|
+
change,
|
|
209
|
+
error
|
|
210
|
+
});
|
|
211
|
+
}
|
|
163
212
|
const now = deliveries.filter(({ subscription }) => !subscription.options.ordered);
|
|
164
213
|
const results = await mapLimit(now, parallelAttempts, ({ subscription, event }) => attempt(subscription, event, db, 1, signal));
|
|
165
214
|
await db.transaction().execute(async (trx) => {
|
|
@@ -179,20 +228,12 @@ const createWebhooks = ({ deps, subscriptions, functions, schema, log, table })
|
|
|
179
228
|
delay
|
|
180
229
|
});
|
|
181
230
|
}
|
|
182
|
-
|
|
183
|
-
for (const
|
|
184
|
-
const group = deliveryGroup(delivery);
|
|
185
|
-
const payload = groups.get(group) ?? {
|
|
186
|
-
subscription: delivery.subscription.name,
|
|
187
|
-
events: []
|
|
188
|
-
};
|
|
189
|
-
payload.events.push(delivery.event);
|
|
190
|
-
groups.set(group, payload);
|
|
191
|
-
}
|
|
192
|
-
for (const [group, payload] of groups) await deliverOrdered.enqueue(trx, payload, { group });
|
|
231
|
+
await queueOrdered(trx, deliveries);
|
|
232
|
+
for (const { change } of failures) await dispatchChange.enqueue(trx, { change });
|
|
193
233
|
await deleteOutbox(trx, schema, rows.map((row) => row.id));
|
|
194
234
|
});
|
|
195
235
|
for (const [index, { subscription, event }] of now.entries()) report(subscription.name, event, 1, results[index]);
|
|
236
|
+
for (const { error } of failures) log(`events kept as failed (webhooks.dispatch-change): ${error.message}`);
|
|
196
237
|
return rows.length;
|
|
197
238
|
};
|
|
198
239
|
const dispatch = defineJob({
|
|
@@ -223,6 +264,7 @@ const createWebhooks = ({ deps, subscriptions, functions, schema, log, table })
|
|
|
223
264
|
});
|
|
224
265
|
return { jobs: [
|
|
225
266
|
dispatch,
|
|
267
|
+
dispatchChange,
|
|
226
268
|
deliver,
|
|
227
269
|
deliverOrdered,
|
|
228
270
|
...table ? [sendTest] : [],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/server",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.7",
|
|
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": {
|
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@better-auth/passkey": "^1.7.7",
|
|
33
|
-
"@protobase/layout": "0.14.
|
|
34
|
-
"@protobase/query": "0.14.
|
|
35
|
-
"@protobase/schema": "0.14.
|
|
33
|
+
"@protobase/layout": "0.14.7",
|
|
34
|
+
"@protobase/query": "0.14.7",
|
|
35
|
+
"@protobase/schema": "0.14.7",
|
|
36
36
|
"@react-email/render": "^2.1.0",
|
|
37
37
|
"@scalar/hono-api-reference": "^0.12.10",
|
|
38
38
|
"@sentry/core": "^11.7.0",
|