@protobase/server 0.25.9 → 0.25.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,6 +8,8 @@ import { OrganizationsOptions, ResolvedOrganizations } from "./organizations/opt
8
8
  import { PlatformSignIn } from "./platform-sign-in.js";
9
9
  import { SsoOptions } from "./saml-sign-in.js";
10
10
  import { signUpPaths } from "./sign-up/sign-up-plugin.js";
11
+ import { PasswordHash } from "./password-hashes.js";
12
+ import { UserTableOptions } from "./user-table.js";
11
13
  import "../index.js";
12
14
  import { BetterAuthOptions } from "better-auth";
13
15
  //#region src/better-auth/create-auth.d.ts
@@ -80,9 +82,22 @@ export type CreateAuthOptions = {
80
82
  onUserCreated?: UserCreatedHook;
81
83
  /**
82
84
  * The id of a new user, by any route. Default Better Auth's random 32-character text. `.owner()` fields hold it: a
83
- * `text` column for the default, or an integer column when this takes the next value of the app's own users sequence.
85
+ * `text` column for the default, an integer column when this takes the next value of the app's own users sequence,
86
+ * or a `uuid` column with `'uuid'`, which makes a random UUID in code and makes every id column of the store a `uuid`.
84
87
  */
85
- generateUserId?: () => string | Promise<string>;
88
+ generateUserId?: (() => string | Promise<string>) | 'uuid';
89
+ /**
90
+ * The user table, when it is one another system made, such as Supabase's `auth.users`: its name, the columns named
91
+ * otherwise than Protobase names them, and a JSON column for each user's metadata, for example `{ table: 'users',
92
+ * columns: { role: 'app_role' }, metadata: 'raw_user_meta_data' }`. The other tables keep their names. Default the
93
+ * table `protobase auth migration` makes.
94
+ */
95
+ users?: UserTableOptions;
96
+ /**
97
+ * Password hashes another system wrote that sign-in accepts besides Better Auth's own, such as `['bcrypt']` for
98
+ * Supabase Auth's. The first sign-in with one stores the password again as Better Auth hashes it. Default none.
99
+ */
100
+ passwordHashes?: PasswordHash[];
86
101
  /**
87
102
  * The identity provider the operator's staff sign in with to sign in as people of this app, for support. Defaults
88
103
  * to the one the platform passes in `PROTOBASE_OPERATOR_ISSUER`, `PROTOBASE_OPERATOR_CLIENT_ID` and
@@ -123,22 +138,35 @@ export type UserCreatedHook = NonNullable<UserCreateHooks['after']>;
123
138
  export declare const normalizeRoles: (roles?: RolesInput) => string[];
124
139
  export declare const createAuth: (options: CreateAuthOptions) => import("better-auth").Auth<{
125
140
  user: {
141
+ modelName: string;
126
142
  fields: {
143
+ name: string;
144
+ email: string;
127
145
  emailVerified: string;
146
+ image: string;
128
147
  createdAt: string;
129
148
  updatedAt: string;
130
149
  };
131
150
  } | {
132
151
  additionalFields: {
133
- readonly lastOrganizationId: {
152
+ metadata?: {
153
+ readonly type: "json";
154
+ readonly required: false;
155
+ readonly fieldName: string;
156
+ } | undefined;
157
+ lastOrganizationId?: {
134
158
  readonly type: "string";
135
159
  readonly required: false;
136
160
  readonly input: false;
137
161
  readonly fieldName: "last_organization_id";
138
- };
162
+ } | undefined;
139
163
  };
164
+ modelName: string;
140
165
  fields: {
166
+ name: string;
167
+ email: string;
141
168
  emailVerified: string;
169
+ image: string;
142
170
  createdAt: string;
143
171
  updatedAt: string;
144
172
  };
@@ -178,7 +206,7 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
178
206
  updatedAt: string;
179
207
  };
180
208
  };
181
- plugins: [{
209
+ plugins: ({
182
210
  id: "admin";
183
211
  version: string;
184
212
  init(): {
@@ -1094,7 +1122,7 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
1094
1122
  }>;
1095
1123
  };
1096
1124
  }>;
1097
- }, {
1125
+ } | {
1098
1126
  id: "jwt";
1099
1127
  version: string;
1100
1128
  options: NoInfer<{
@@ -1362,7 +1390,7 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
1362
1390
  };
1363
1391
  };
1364
1392
  };
1365
- }, {
1393
+ } | {
1366
1394
  hooks: {
1367
1395
  after: {
1368
1396
  matcher: (context: Parameters<(context: import("better-auth").HookEndpointContext) => boolean>[0]) => boolean;
@@ -2030,7 +2058,7 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
2030
2058
  ACCOUNT_TEMPORARILY_LOCKED: import("better-auth").RawError<"ACCOUNT_TEMPORARILY_LOCKED">;
2031
2059
  INVALID_TWO_FACTOR_COOKIE: import("better-auth").RawError<"INVALID_TWO_FACTOR_COOKIE">;
2032
2060
  };
2033
- }, {
2061
+ } | {
2034
2062
  endpoints: {
2035
2063
  generatePasskeyAuthenticationOptions: import("better-auth").StrictEndpoint<"/passkey/generate-authenticate-options", {
2036
2064
  method: "GET";
@@ -2332,7 +2360,7 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
2332
2360
  expiresAt: Date;
2333
2361
  token: string;
2334
2362
  ipAddress?: string | null | undefined;
2335
- userAgent? /** Every origin the app is served on, `baseURL`'s first. */ : string | null | undefined;
2363
+ userAgent?: string | null | undefined;
2336
2364
  };
2337
2365
  user: Record<string, any> & {
2338
2366
  id: string;
@@ -2511,7 +2539,7 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
2511
2539
  RESOLVED_USER_INVALID: import("better-auth").RawError<"RESOLVED_USER_INVALID">;
2512
2540
  };
2513
2541
  options: import("@better-auth/passkey").PasskeyOptions | undefined;
2514
- }, ...({
2542
+ } | {
2515
2543
  id: "email-otp";
2516
2544
  version: string;
2517
2545
  init(ctx: import("better-auth").AuthContext): {
@@ -5198,8 +5226,8 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
5198
5226
  appRoles: string[];
5199
5227
  id: string;
5200
5228
  createdAt: Date;
5201
- userId: string;
5202
5229
  role: string;
5230
+ userId: string;
5203
5231
  organizationId: string;
5204
5232
  };
5205
5233
  }>;
@@ -5212,7 +5240,7 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
5212
5240
  handler: import("better-auth").Middleware<import("better-auth").MiddlewareOptions, (inputContext: import("better-auth").MiddlewareInputContext<import("better-auth").MiddlewareOptions>) => Promise<void>>;
5213
5241
  }[];
5214
5242
  };
5215
- })[], {
5243
+ } | {
5216
5244
  id: "protobase-users";
5217
5245
  endpoints: {
5218
5246
  getInvitation: import("better-auth").StrictEndpoint<"/invitation", {
@@ -5262,8 +5290,8 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
5262
5290
  appRoles: string[];
5263
5291
  id: string;
5264
5292
  createdAt: Date;
5265
- userId: string;
5266
5293
  role: string;
5294
+ userId: string;
5267
5295
  organizationId: string;
5268
5296
  };
5269
5297
  }>;
@@ -5678,7 +5706,15 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
5678
5706
  handler: import("better-auth").Middleware<import("better-auth").MiddlewareOptions, (inputContext: import("better-auth").MiddlewareInputContext<import("better-auth").MiddlewareOptions>) => Promise<void>>;
5679
5707
  }[];
5680
5708
  };
5681
- }];
5709
+ } | {
5710
+ id: "protobase-rehash-passwords";
5711
+ hooks: {
5712
+ after: {
5713
+ matcher: (ctx: import("better-auth").HookEndpointContext) => boolean;
5714
+ handler: import("better-auth").Middleware<import("better-auth").MiddlewareOptions, (inputContext: import("better-auth").MiddlewareInputContext<import("better-auth").MiddlewareOptions>) => Promise<void>>;
5715
+ }[];
5716
+ };
5717
+ })[];
5682
5718
  disabledPaths: string[];
5683
5719
  databaseHooks: {
5684
5720
  session?: {
@@ -5757,6 +5793,9 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
5757
5793
  };
5758
5794
  };
5759
5795
  advanced: {
5796
+ database?: {
5797
+ generateId: "uuid";
5798
+ } | undefined;
5760
5799
  useSecureCookies: boolean;
5761
5800
  defaultCookieAttributes: {
5762
5801
  sameSite: "lax";
@@ -5785,6 +5824,12 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
5785
5824
  url: string;
5786
5825
  token: string;
5787
5826
  }) => Promise<void>) | undefined;
5827
+ password?: {
5828
+ verify: ({ hash, password }: {
5829
+ hash: string;
5830
+ password: string;
5831
+ }) => Promise<boolean>;
5832
+ } | undefined;
5788
5833
  enabled: true;
5789
5834
  disableSignUp: true;
5790
5835
  minPasswordLength: number;
@@ -21,8 +21,10 @@ import { signInPolicyPlugin } from "./policy-plugin.js";
21
21
  import { emailInDomains, resolveSamlProviders, samlDisabledPaths, samlGuardPlugin, samlPlugin } from "./saml-sign-in.js";
22
22
  import { emailCodeOptions, twoFactorCodeOptions, unusedEmailCodePaths } from "./sign-in-mail.js";
23
23
  import { signUpPaths, signUpPlugin } from "./sign-up/sign-up-plugin.js";
24
+ import { rehashPasswordsPlugin, verifyAcceptedHash } from "./password-hashes.js";
24
25
  import { staffSignInPlugin } from "./staff-plugin.js";
25
26
  import { twoFactorAfterPasswordOrCode } from "./two-factor-challenge.js";
27
+ import { metadataField, userFieldsNamed, userModel, userTable, withUserColumns } from "./user-table.js";
26
28
  import { usersPlugin } from "./users/users-plugin.js";
27
29
  import { APIError } from "better-auth/api";
28
30
  import { betterAuth } from "better-auth";
@@ -44,6 +46,7 @@ const tokenLifetime = "15m";
44
46
  /** The configured roles with `admin` first; `admin` is added when missing. */
45
47
  const normalizeRoles = (roles) => roleDefinitions(roles).names;
46
48
  const adminImpersonationPaths = ["/admin/impersonate-user", "/admin/stop-impersonating"];
49
+ const newUserId = (generate) => generate === "uuid" ? crypto.randomUUID() : generate();
47
50
  const noContext = () => Promise.reject(/* @__PURE__ */ new Error("The auth context is read only once createAuth has made the instance"));
48
51
  const organizationPaths = ["/organization/set-active", "/organization/accept-invitation"];
49
52
  /**
@@ -60,6 +63,12 @@ const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, defini
60
63
  baseURL: options.baseURL
61
64
  };
62
65
  const samlIds = saml.map((provider) => provider.id);
66
+ const users = userTable(options.users);
67
+ const passwordHashes = options.passwordHashes ?? [];
68
+ const userFields = {
69
+ ...organizations && userFieldsNamed(lastOrganizationField, users),
70
+ ...metadataField(users)
71
+ };
63
72
  return {
64
73
  appName,
65
74
  database: options.database,
@@ -75,13 +84,14 @@ const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, defini
75
84
  enabled: true,
76
85
  disableSignUp: true,
77
86
  minPasswordLength: 12,
87
+ ...passwordHashes.length > 0 && { password: { verify: verifyAcceptedHash(passwordHashes) } },
78
88
  ...mail && passwordResetOptions(mail)
79
89
  },
80
90
  ...options.socialProviders && { socialProviders: options.socialProviders },
81
- user: organizations ? {
82
- ...coreNames.user,
83
- additionalFields: lastOrganizationField
84
- } : coreNames.user,
91
+ user: Object.keys(userFields).length > 0 ? {
92
+ ...userModel(users),
93
+ additionalFields: userFields
94
+ } : userModel(users),
85
95
  session: coreNames.session,
86
96
  verification: coreNames.verification,
87
97
  account: {
@@ -162,8 +172,9 @@ const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, defini
162
172
  invitationUrl: organizations?.invitationUrl ?? defaultInvitationUrl(options.baseURL),
163
173
  invitationSeconds: organizations?.invitationExpiresIn ?? 604800,
164
174
  ...organizations && { organizations }
165
- })
166
- ],
175
+ }),
176
+ ...passwordHashes.length > 0 ? [rehashPasswordsPlugin(passwordHashes)] : []
177
+ ].map((plugin) => withUserColumns(plugin, users)),
167
178
  disabledPaths: [
168
179
  ...unusedEmailCodePaths,
169
180
  ...adminImpersonationPaths,
@@ -183,7 +194,7 @@ const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, defini
183
194
  });
184
195
  const withId = options.generateUserId ? {
185
196
  ...user,
186
- id: await options.generateUserId()
197
+ id: await newUserId(options.generateUserId)
187
198
  } : user;
188
199
  return { data: context?.path === "/admin/create-user" || samlProvider ? {
189
200
  ...withId,
@@ -231,7 +242,8 @@ const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, defini
231
242
  sameSite: "lax",
232
243
  secure
233
244
  },
234
- backgroundTasks: { handler: () => {} }
245
+ backgroundTasks: { handler: () => {} },
246
+ ...options.generateUserId === "uuid" && { database: { generateId: "uuid" } }
235
247
  }
236
248
  };
237
249
  };
@@ -0,0 +1,4 @@
1
+ //#region src/better-auth/password-hashes.d.ts
2
+ /** The hash formats sign-in accepts besides Better Auth's own. */
3
+ export type PasswordHash = 'bcrypt';
4
+ //#endregion
@@ -0,0 +1,31 @@
1
+ import { APIError, createAuthMiddleware } from "better-auth/api";
2
+ import { verifyPassword } from "better-auth/crypto";
3
+ import bcrypt from "bcryptjs";
4
+ //#region src/better-auth/password-hashes.ts
5
+ const formats = { bcrypt: {
6
+ matches: (hash) => /^\$2[aby]\$/.test(hash),
7
+ verify: (password, hash) => bcrypt.compare(password, hash)
8
+ } };
9
+ const formatOf = (accepted, hash) => accepted.map((name) => formats[name]).find((format) => format.matches(hash));
10
+ /** Better Auth's `password.verify`: the accepted formats first, then its own scrypt. */
11
+ const verifyAcceptedHash = (accepted) => ({ hash, password }) => formatOf(accepted, hash)?.verify(password, hash) ?? verifyPassword({
12
+ hash,
13
+ password
14
+ });
15
+ /** After a sign-in with a password in one of the accepted formats, stores it again as Better Auth hashes it. */
16
+ const rehashPasswordsPlugin = (accepted) => ({
17
+ id: "protobase-rehash-passwords",
18
+ hooks: { after: [{
19
+ matcher: (ctx) => ctx.path === "/sign-in/email",
20
+ handler: createAuthMiddleware(async (ctx) => {
21
+ if (ctx.context.returned instanceof APIError) return;
22
+ const { email, password } = ctx.body;
23
+ const found = await ctx.context.internalAdapter.findUserByEmail(email.toLowerCase(), { includeAccounts: true });
24
+ const account = found?.accounts.find((candidate) => candidate.providerId === "credential" && candidate.accountId === found.user.id);
25
+ if (!found || !account?.password || !formatOf(accepted, account.password)) return;
26
+ await ctx.context.internalAdapter.updatePassword(found.user.id, await ctx.context.password.hash(password));
27
+ })
28
+ }] }
29
+ });
30
+ //#endregion
31
+ export { rehashPasswordsPlugin, verifyAcceptedHash };
@@ -1,11 +1,6 @@
1
1
  //#region src/better-auth/snake-case-names.ts
2
- /** Better Auth's own tables, as its `user`, `session`, `account` and `verification` options name them. */
2
+ /** Better Auth's own tables, as its `session`, `account` and `verification` options name them; the user table's are in `user-table.ts`. */
3
3
  const coreNames = {
4
- user: { fields: {
5
- emailVerified: "email_verified",
6
- createdAt: "created_at",
7
- updatedAt: "updated_at"
8
- } },
9
4
  session: { fields: {
10
5
  expiresAt: "expires_at",
11
6
  createdAt: "created_at",
@@ -0,0 +1,31 @@
1
+ import "better-auth";
2
+ //#region src/better-auth/user-table.d.ts
3
+ /** Each column of the user table, by the name Protobase gives it, with Better Auth's field it holds. */
4
+ declare const userFields: {
5
+ readonly name: "name";
6
+ readonly email: "email";
7
+ readonly email_verified: "emailVerified";
8
+ readonly image: "image";
9
+ readonly created_at: "createdAt";
10
+ readonly updated_at: "updatedAt";
11
+ readonly role: "role";
12
+ readonly banned: "banned";
13
+ readonly ban_reason: "banReason";
14
+ readonly ban_expires: "banExpires";
15
+ readonly two_factor_enabled: "twoFactorEnabled";
16
+ readonly last_role: "lastRole";
17
+ readonly last_organization_id: "lastOrganizationId";
18
+ };
19
+ export type UserColumn = keyof typeof userFields;
20
+ export type UserTableOptions = {
21
+ /** The table's name, default `user`, for example `users` for Supabase's `auth.users`. */
22
+ table?: string;
23
+ /**
24
+ * The columns named otherwise than Protobase names them: `{ role: 'app_role' }`, where `role` holds something else
25
+ * already, such as the Postgres role in Supabase's `auth.users`.
26
+ */
27
+ columns?: Partial<Record<UserColumn, string>>;
28
+ /** A JSON column that holds each user's metadata, such as `raw_user_meta_data`; none by default. */
29
+ metadata?: string;
30
+ };
31
+ //#endregion
@@ -0,0 +1,69 @@
1
+ //#region src/better-auth/user-table.ts
2
+ /** Each column of the user table, by the name Protobase gives it, with Better Auth's field it holds. */
3
+ const userFields = {
4
+ name: "name",
5
+ email: "email",
6
+ email_verified: "emailVerified",
7
+ image: "image",
8
+ created_at: "createdAt",
9
+ updated_at: "updatedAt",
10
+ role: "role",
11
+ banned: "banned",
12
+ ban_reason: "banReason",
13
+ ban_expires: "banExpires",
14
+ two_factor_enabled: "twoFactorEnabled",
15
+ last_role: "lastRole",
16
+ last_organization_id: "lastOrganizationId"
17
+ };
18
+ /** The user table's names, from `createAuth`'s `users` option; unknown columns are an error. */
19
+ const userTable = ({ table = "user", columns = {}, metadata } = {}) => {
20
+ const unknown = Object.keys(columns).find((name) => !(name in userFields));
21
+ if (unknown !== void 0) throw new Error(`users.columns names "${unknown}", which is not a column of the user table: ${Object.keys(userFields).join(", ")}`);
22
+ const column = (name) => columns[name] ?? name;
23
+ return {
24
+ modelName: table,
25
+ column,
26
+ fieldNames: Object.fromEntries(Object.entries(userFields).map(([name, field]) => [field, column(name)])),
27
+ metadata
28
+ };
29
+ };
30
+ /** Better Auth's `user` option: the table and its core columns. */
31
+ const userModel = ({ modelName, column }) => ({
32
+ modelName,
33
+ fields: {
34
+ name: column("name"),
35
+ email: column("email"),
36
+ emailVerified: column("email_verified"),
37
+ image: column("image"),
38
+ createdAt: column("created_at"),
39
+ updatedAt: column("updated_at")
40
+ }
41
+ });
42
+ /** Each user field the user metadata column adds; none without one. */
43
+ const metadataField = ({ metadata }) => metadata ? { metadata: {
44
+ type: "json",
45
+ required: false,
46
+ fieldName: metadata
47
+ } } : void 0;
48
+ /** The fields, each with its column named as the app names it. */
49
+ const userFieldsNamed = (fields, { fieldNames }) => Object.fromEntries(Object.entries(fields).map(([field, attribute]) => [field, {
50
+ ...attribute,
51
+ fieldName: fieldNames[field] ?? attribute.fieldName
52
+ }]));
53
+ /** The plugin, with the columns it adds to the user table named as the app names them. */
54
+ const withUserColumns = (plugin, table) => {
55
+ const user = plugin.schema?.user;
56
+ if (!user) return plugin;
57
+ return {
58
+ ...plugin,
59
+ schema: {
60
+ ...plugin.schema,
61
+ user: {
62
+ ...user,
63
+ fields: userFieldsNamed(user.fields, table)
64
+ }
65
+ }
66
+ };
67
+ };
68
+ //#endregion
69
+ export { metadataField, userFieldsNamed, userModel, userTable, withUserColumns };
@@ -8,12 +8,14 @@ export type LinkedAccount = {
8
8
  providerId: string;
9
9
  accountId: string;
10
10
  };
11
+ /** `metadata` lands in the user metadata column, which `createAuth`'s `users.metadata` names; it needs one. */
11
12
  export type NewUser = {
12
13
  email: string;
13
14
  password: string;
14
15
  name?: string;
15
16
  role?: string;
16
17
  accounts?: LinkedAccount[];
18
+ metadata?: Record<string, unknown>;
17
19
  };
18
20
  export type StoredUser = {
19
21
  id: string;
@@ -37,7 +37,8 @@ const createUser = async (auth, input) => {
37
37
  email: input.email,
38
38
  password: input.password,
39
39
  name: input.name ?? input.email.split("@")[0],
40
- role
40
+ role,
41
+ ...input.metadata && { data: { metadata: input.metadata } }
41
42
  } });
42
43
  const { internalAdapter } = await auth.$context;
43
44
  for (const account of accounts) await internalAdapter.linkAccount({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/server",
3
- "version": "0.25.9",
3
+ "version": "0.25.10",
4
4
  "description": "The Protobase REST API as a Hono app, with Better Auth sign-in.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -32,14 +32,15 @@
32
32
  "dependencies": {
33
33
  "@better-auth/passkey": "^1.7.7",
34
34
  "@better-auth/sso": "^1.7.7",
35
- "@protobase/layout": "0.25.9",
36
- "@protobase/query": "0.25.9",
37
- "@protobase/schema": "0.25.9",
35
+ "@protobase/layout": "0.25.10",
36
+ "@protobase/query": "0.25.10",
37
+ "@protobase/schema": "0.25.10",
38
38
  "@react-email/render": "^2.1.0",
39
39
  "@scalar/hono-api-reference": "^0.12.10",
40
40
  "@sentry/core": "^11.7.0",
41
41
  "@simplewebauthn/server": "^13.3.1",
42
42
  "altcha-lib": "^2.6.0",
43
+ "bcryptjs": "^3.0.3",
43
44
  "better-auth": "^1.7.7",
44
45
  "cron-parser": "^5.10.1",
45
46
  "file-type": "^22.1.1",