@protobase/server 0.14.6 → 0.15.0

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.
@@ -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
- /** Sign-in attempts allowed per minute and client, default 5. */
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 (every account is created on the host with
91
- * `createUser`, or by an admin through the admin plugin), roles from the admin plugin's `role` column, 15 minute JWTs
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?: string | null | undefined;
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? /** The RP ID of new passkeys. */ : string | null | undefined;
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?: string | null | undefined;
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?: string | null | undefined;
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?: string | null | undefined;
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?: string | null | undefined;
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?: string | null | undefined;
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?: string | null | undefined;
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?: string | null | undefined;
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?: string | null | undefined;
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?: string | null | undefined;
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?: string | null | undefined;
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?: string | null | undefined;
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?: string | null | undefined;
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?: string | null | undefined;
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?: string | null | undefined;
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?: string | null | undefined;
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?: string | null | undefined;
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?: string | null | undefined;
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?: string | null | undefined;
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?: string | null | undefined;
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?: string | null | undefined;
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 (every account is created on the host with
34
- * `createUser`, or by an admin through the admin plugin), roles from the admin plugin's `role` column, 15 minute JWTs
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
- if (password !== void 0 && policy.password === "forbidden") throw new APIError("BAD_REQUEST", {
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,8 @@
1
+ import "zod";
2
+ //#region src/better-auth/sign-up/sign-up-plugin.d.ts
3
+ /** The paths of the sign-up endpoints, as `ctx.path` and the rate limits name them. */
4
+ export declare const signUpPaths: {
5
+ sendCode: string;
6
+ create: string;
7
+ };
8
+ //#endregion
@@ -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 },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/server",
3
- "version": "0.14.6",
3
+ "version": "0.15.0",
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.6",
34
- "@protobase/query": "0.14.6",
35
- "@protobase/schema": "0.14.6",
33
+ "@protobase/layout": "0.15.0",
34
+ "@protobase/query": "0.15.0",
35
+ "@protobase/schema": "0.15.0",
36
36
  "@react-email/render": "^2.1.0",
37
37
  "@scalar/hono-api-reference": "^0.12.10",
38
38
  "@sentry/core": "^11.7.0",