@protobase/server 0.25.12 → 0.26.1
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/admin-options.d.ts +7 -0
- package/dist/better-auth/app-tokens.js +2 -2
- package/dist/better-auth/authenticator.js +1 -1
- package/dist/better-auth/create-auth.d.ts +121 -2
- package/dist/better-auth/create-auth.js +70 -51
- package/dist/better-auth/email-confirmed.js +31 -0
- package/dist/better-auth/organizations/accept-invitation.js +1 -1
- package/dist/better-auth/organizations/app-roles.js +1 -1
- package/dist/better-auth/organizations/options.js +1 -1
- package/dist/better-auth/organizations/organization-plugin.js +1 -1
- package/dist/better-auth/organizations/session-organization.js +1 -1
- package/dist/better-auth/passkey-plugin.js +1 -1
- package/dist/better-auth/policy-plugin.js +27 -12
- package/dist/better-auth/roles/active-role.js +1 -1
- package/dist/better-auth/sign-up/sign-up-account.js +26 -0
- package/dist/better-auth/sign-up/sign-up-plugin.js +6 -14
- package/dist/better-auth/staff-plugin.js +1 -1
- package/dist/better-auth/staff-sign-in-endpoints.js +3 -3
- package/dist/better-auth/status-route.js +1 -1
- package/dist/better-auth/user-table.d.ts +1 -0
- package/dist/better-auth/user-table.js +1 -0
- package/dist/better-auth/users/user-endpoints.js +1 -1
- package/dist/create-admin.d.ts +10 -1
- package/dist/create-admin.js +26 -2
- package/dist/gotrue/access-tokens.js +15 -0
- package/dist/gotrue/auth-codes.js +33 -0
- package/dist/gotrue/bearer.js +22 -0
- package/dist/gotrue/deps.js +8 -0
- package/dist/gotrue/email-code-routes.js +121 -0
- package/dist/gotrue/errors.js +52 -0
- package/dist/gotrue/forward.js +45 -0
- package/dist/gotrue/keys.js +14 -0
- package/dist/gotrue/pending-sign-up.js +26 -0
- package/dist/gotrue/provider-routes.js +79 -0
- package/dist/gotrue/request-body.js +22 -0
- package/dist/gotrue/routes.js +32 -0
- package/dist/gotrue/session-json.js +34 -0
- package/dist/gotrue/sessions-plugin.js +83 -0
- package/dist/gotrue/token-route.js +42 -0
- package/dist/gotrue/user-json.js +46 -0
- package/dist/gotrue/user-routes.js +99 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +7 -7
- package/dist/live/notices-sql.js +4 -2
- package/dist/realtime/change-checker.js +102 -0
- package/dist/realtime/change-filter.js +106 -0
- package/dist/realtime/change-hub.js +100 -0
- package/dist/realtime/change-payload.js +31 -0
- package/dist/realtime/phoenix-message.js +63 -0
- package/dist/realtime/postgres-changes.js +62 -0
- package/dist/realtime/realtime-app.js +31 -0
- package/dist/realtime/realtime-routes.js +47 -0
- package/dist/realtime/realtime-socket.js +135 -0
- package/dist/rest/caller-settings.js +27 -0
- package/package.json +10 -5
package/dist/admin-options.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { MapOptions } from "./maps/map-settings.js";
|
|
|
4
4
|
import { PluginOption } from "./plugins/plugin-settings.js";
|
|
5
5
|
import { SourceOption } from "./sources/source-settings.js";
|
|
6
6
|
import { CorsOrigins } from "./api-cors.js";
|
|
7
|
+
import { ApiKeysOptions } from "./api-keys/api-key-settings.js";
|
|
7
8
|
import { AssistantOptions } from "./assistant/assistant-settings.js";
|
|
8
9
|
import { LocaleOption } from "./locale/locale-option.js";
|
|
9
10
|
import { RuntimeOptions } from "./runtime/runtime-settings.js";
|
|
@@ -12,6 +13,12 @@ import { ResolveOptions, RoleLimit } from "@protobase/schema";
|
|
|
12
13
|
export type AdminOptions = {
|
|
13
14
|
/** Default `/api/v1`. */
|
|
14
15
|
basePath?: string;
|
|
16
|
+
/**
|
|
17
|
+
* The app's publishable and secret keys, with which supabase-js calls the Realtime-compatible `/realtime/v1` (see
|
|
18
|
+
* `ApiKeysOptions`); default `PROTOBASE_PUBLISHABLE_KEY` and `PROTOBASE_SECRET_KEY`. Without either, or with
|
|
19
|
+
* `false`, the app serves no `/realtime/v1`.
|
|
20
|
+
*/
|
|
21
|
+
apiKeys?: ApiKeysOptions | false;
|
|
15
22
|
/** Applied to every request's transaction with SET LOCAL. Default 15000. */
|
|
16
23
|
statementTimeoutMs?: number;
|
|
17
24
|
/** Reads that filter, sort or count values by a column no index serves. Default mode `reject`. */
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { checkedUrl } from "../checked-url.js";
|
|
2
2
|
import { forbidden } from "../problem.js";
|
|
3
|
-
import "./auth-base-path.js";
|
|
4
|
-
import { noGlobalRole } from "./organizations/role-definitions.js";
|
|
5
3
|
import { jwtAuthenticator } from "../auth/jwt.js";
|
|
4
|
+
import { noGlobalRole } from "./organizations/role-definitions.js";
|
|
5
|
+
import "./auth-base-path.js";
|
|
6
6
|
import { createRemoteJWKSet, decodeJwt, errors } from "jose";
|
|
7
7
|
//#region src/better-auth/app-tokens.ts
|
|
8
8
|
const claimedIssuer = (request) => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { noGlobalRole } from "./organizations/role-definitions.js";
|
|
2
1
|
import { jwtAuthenticator } from "../auth/jwt.js";
|
|
2
|
+
import { noGlobalRole } from "./organizations/role-definitions.js";
|
|
3
3
|
import { withTrustedApps } from "./app-tokens.js";
|
|
4
4
|
import { cachedKeys } from "./jwks-cache.js";
|
|
5
5
|
//#region src/better-auth/authenticator.ts
|
|
@@ -12,6 +12,7 @@ import { PasswordHash } from "./password-hashes.js";
|
|
|
12
12
|
import { UserTableOptions } from "./user-table.js";
|
|
13
13
|
import "../index.js";
|
|
14
14
|
import { BetterAuthOptions } from "better-auth";
|
|
15
|
+
import { JwtOptions } from "better-auth/plugins";
|
|
15
16
|
//#region src/better-auth/create-auth.d.ts
|
|
16
17
|
export type CreateAuthOptions = {
|
|
17
18
|
/** A `pg` Pool, or `{ dialect, type: 'postgres', schemaName? }` for a Kysely dialect: the admin store, apart from the application's tables (its own schema or database). */
|
|
@@ -160,6 +161,12 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
160
161
|
readonly input: false;
|
|
161
162
|
readonly fieldName: "last_organization_id";
|
|
162
163
|
} | undefined;
|
|
164
|
+
emailConfirmedAt: {
|
|
165
|
+
readonly type: "date";
|
|
166
|
+
readonly required: false;
|
|
167
|
+
readonly input: false;
|
|
168
|
+
readonly fieldName: "email_confirmed_at";
|
|
169
|
+
};
|
|
163
170
|
};
|
|
164
171
|
modelName: string;
|
|
165
172
|
fields: {
|
|
@@ -1136,6 +1143,11 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
1136
1143
|
};
|
|
1137
1144
|
};
|
|
1138
1145
|
};
|
|
1146
|
+
jwks: {
|
|
1147
|
+
keyPairConfigs: {
|
|
1148
|
+
readonly alg: "ES256";
|
|
1149
|
+
}[];
|
|
1150
|
+
};
|
|
1139
1151
|
jwt: {
|
|
1140
1152
|
expirationTime: string;
|
|
1141
1153
|
issuer: string;
|
|
@@ -1318,7 +1330,7 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
1318
1330
|
$Infer: {
|
|
1319
1331
|
body: {
|
|
1320
1332
|
payload: import("jose").JWTPayload;
|
|
1321
|
-
overrideOptions?:
|
|
1333
|
+
overrideOptions?: JwtOptions | undefined;
|
|
1322
1334
|
};
|
|
1323
1335
|
};
|
|
1324
1336
|
};
|
|
@@ -1335,7 +1347,7 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
1335
1347
|
$Infer: {
|
|
1336
1348
|
body: {
|
|
1337
1349
|
token: string;
|
|
1338
|
-
issuer
|
|
1350
|
+
issuer? /** Sign-in attempts allowed per minute and client, default 5; also the sign-up requests allowed. */ : string;
|
|
1339
1351
|
};
|
|
1340
1352
|
response: {
|
|
1341
1353
|
payload: {
|
|
@@ -4411,6 +4423,88 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
4411
4423
|
handler: import("better-auth").Middleware<import("better-auth").MiddlewareOptions, (inputContext: import("better-auth").MiddlewareInputContext<import("better-auth").MiddlewareOptions>) => Promise<void>>;
|
|
4412
4424
|
}[];
|
|
4413
4425
|
};
|
|
4426
|
+
} | {
|
|
4427
|
+
id: "protobase-gotrue-sessions";
|
|
4428
|
+
endpoints: {
|
|
4429
|
+
goTrueSession: import("better-auth").StrictEndpoint<string, {
|
|
4430
|
+
method: "POST";
|
|
4431
|
+
body: import("zod").ZodObject<{
|
|
4432
|
+
sessionToken: import("zod").ZodString;
|
|
4433
|
+
}, import("zod/v4/core").$strip>;
|
|
4434
|
+
}, {
|
|
4435
|
+
access_token: string;
|
|
4436
|
+
token_type: string;
|
|
4437
|
+
expires_in: number;
|
|
4438
|
+
expires_at: number;
|
|
4439
|
+
refresh_token: string;
|
|
4440
|
+
user: {
|
|
4441
|
+
app_metadata: {
|
|
4442
|
+
provider?: undefined;
|
|
4443
|
+
providers?: undefined;
|
|
4444
|
+
} | {
|
|
4445
|
+
provider: string;
|
|
4446
|
+
providers: string[];
|
|
4447
|
+
};
|
|
4448
|
+
user_metadata: Record<string, unknown>;
|
|
4449
|
+
identities: {
|
|
4450
|
+
identity_id: string;
|
|
4451
|
+
id: string;
|
|
4452
|
+
user_id: string;
|
|
4453
|
+
provider: string;
|
|
4454
|
+
created_at: string;
|
|
4455
|
+
updated_at: string;
|
|
4456
|
+
}[];
|
|
4457
|
+
created_at: string;
|
|
4458
|
+
updated_at: string;
|
|
4459
|
+
is_anonymous: boolean;
|
|
4460
|
+
email_confirmed_at?: string | undefined;
|
|
4461
|
+
confirmed_at?: string | undefined;
|
|
4462
|
+
id: string;
|
|
4463
|
+
aud: string;
|
|
4464
|
+
role: string;
|
|
4465
|
+
email: string;
|
|
4466
|
+
};
|
|
4467
|
+
}>;
|
|
4468
|
+
goTrueRefresh: import("better-auth").StrictEndpoint<string, {
|
|
4469
|
+
method: "POST";
|
|
4470
|
+
body: import("zod").ZodObject<{
|
|
4471
|
+
refreshToken: import("zod").ZodString;
|
|
4472
|
+
}, import("zod/v4/core").$strip>;
|
|
4473
|
+
}, {
|
|
4474
|
+
access_token: string;
|
|
4475
|
+
token_type: string;
|
|
4476
|
+
expires_in: number;
|
|
4477
|
+
expires_at: number;
|
|
4478
|
+
refresh_token: string;
|
|
4479
|
+
user: {
|
|
4480
|
+
app_metadata: {
|
|
4481
|
+
provider?: undefined;
|
|
4482
|
+
providers?: undefined;
|
|
4483
|
+
} | {
|
|
4484
|
+
provider: string;
|
|
4485
|
+
providers: string[];
|
|
4486
|
+
};
|
|
4487
|
+
user_metadata: Record<string, unknown>;
|
|
4488
|
+
identities: {
|
|
4489
|
+
identity_id: string;
|
|
4490
|
+
id: string;
|
|
4491
|
+
user_id: string;
|
|
4492
|
+
provider: string;
|
|
4493
|
+
created_at: string;
|
|
4494
|
+
updated_at: string;
|
|
4495
|
+
}[];
|
|
4496
|
+
created_at: string;
|
|
4497
|
+
updated_at: string;
|
|
4498
|
+
is_anonymous: boolean;
|
|
4499
|
+
email_confirmed_at?: string | undefined;
|
|
4500
|
+
confirmed_at?: string | undefined;
|
|
4501
|
+
id: string;
|
|
4502
|
+
aud: string;
|
|
4503
|
+
role: string;
|
|
4504
|
+
email: string;
|
|
4505
|
+
};
|
|
4506
|
+
}>;
|
|
4507
|
+
};
|
|
4414
4508
|
} | {
|
|
4415
4509
|
id: "protobase-staff-sign-in";
|
|
4416
4510
|
schema: {
|
|
@@ -5759,6 +5853,27 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
5759
5853
|
} & Record<string, unknown>;
|
|
5760
5854
|
}>;
|
|
5761
5855
|
};
|
|
5856
|
+
update: {
|
|
5857
|
+
before: (user: Partial<{
|
|
5858
|
+
id: string;
|
|
5859
|
+
createdAt: Date;
|
|
5860
|
+
updatedAt: Date;
|
|
5861
|
+
email: string;
|
|
5862
|
+
emailVerified: boolean;
|
|
5863
|
+
name: string;
|
|
5864
|
+
image?: string | null | undefined;
|
|
5865
|
+
}> & Record<string, unknown>) => Promise<{
|
|
5866
|
+
data: Partial<{
|
|
5867
|
+
id: string;
|
|
5868
|
+
createdAt: Date;
|
|
5869
|
+
updatedAt: Date;
|
|
5870
|
+
email: string;
|
|
5871
|
+
emailVerified: boolean;
|
|
5872
|
+
name: string;
|
|
5873
|
+
image?: string | null | undefined;
|
|
5874
|
+
}> & Record<string, unknown>;
|
|
5875
|
+
}>;
|
|
5876
|
+
};
|
|
5762
5877
|
};
|
|
5763
5878
|
};
|
|
5764
5879
|
rateLimit: {
|
|
@@ -5851,6 +5966,10 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
5851
5966
|
passwordReset: boolean;
|
|
5852
5967
|
/** Open sign-up is on and offered: the app asks for it, and can mail the codes. */
|
|
5853
5968
|
signUp: boolean;
|
|
5969
|
+
/** The role of accounts made by open sign-up, while it is offered. */
|
|
5970
|
+
signUpRole: string | undefined;
|
|
5971
|
+
/** The app keeps each user's metadata, in the column `users.metadata` names. */
|
|
5972
|
+
userMetadata: boolean;
|
|
5854
5973
|
socialProviders: string[];
|
|
5855
5974
|
/** The app's name it signs in and mails as: `appName`, else `PROTOBASE_APP_NAME`, else `Protobase`. */
|
|
5856
5975
|
appName: string;
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { multiHostBaseURL, readOrigins, resolveOrigins } from "./origins.js";
|
|
2
2
|
import { defaultAppName, readAppName } from "../app-name.js";
|
|
3
|
+
import { goTrueIssuer, goTrueKeyPair } from "../gotrue/keys.js";
|
|
4
|
+
import { roleDefinitions } from "./organizations/role-definitions.js";
|
|
5
|
+
import { narrowClaims } from "./roles/active-role.js";
|
|
6
|
+
import { signInPolicyPlugin } from "./policy-plugin.js";
|
|
7
|
+
import { goTrueSessionsPlugin } from "../gotrue/sessions-plugin.js";
|
|
3
8
|
import { authBasePath } from "./auth-base-path.js";
|
|
4
9
|
import { platformMailer } from "../mail/platform-mailer.js";
|
|
5
10
|
import { readOperatorSettings, resolveOperator } from "./operator-provider.js";
|
|
6
11
|
import { defaultInvitationUrl } from "./invitations/invitation-link-hook.js";
|
|
7
12
|
import { lastAdminGuardPlugin } from "./last-admin-guard.js";
|
|
8
|
-
import { roleDefinitions } from "./organizations/role-definitions.js";
|
|
9
13
|
import { lastOrganizationField, organizationClaims, sessionOrganizationHooks } from "./organizations/session-organization.js";
|
|
10
14
|
import { organizationPlugin } from "./organizations/organization-plugin.js";
|
|
11
15
|
import { protobaseOrganizationPlugin } from "./organizations/protobase-plugin.js";
|
|
@@ -14,13 +18,12 @@ import { passkeyHosts, readPasskeyDomain, resolvePasskeyDomain } from "./passkey
|
|
|
14
18
|
import { coreNames, pluginNames } from "./snake-case-names.js";
|
|
15
19
|
import { passkeyPlugin } from "./passkey-plugin.js";
|
|
16
20
|
import { passwordResetOptions } from "./password-reset.js";
|
|
17
|
-
import { narrowClaims } from "./roles/active-role.js";
|
|
18
21
|
import { rolesPlugin } from "./roles/roles-plugin.js";
|
|
19
22
|
import { platformSignInFor, platformSignInProvider, readPlatformSignIn, resolvePlatformSignIn } from "./platform-sign-in.js";
|
|
20
|
-
import { signInPolicyPlugin } from "./policy-plugin.js";
|
|
21
23
|
import { emailInDomains, resolveSamlProviders, samlDisabledPaths, samlGuardPlugin, samlPlugin } from "./saml-sign-in.js";
|
|
22
24
|
import { emailCodeOptions, twoFactorCodeOptions, unusedEmailCodePaths } from "./sign-in-mail.js";
|
|
23
25
|
import { signUpPaths, signUpPlugin } from "./sign-up/sign-up-plugin.js";
|
|
26
|
+
import { confirmedOnCreate, confirmedOnUpdate, emailConfirmedField } from "./email-confirmed.js";
|
|
24
27
|
import { rehashPasswordsPlugin, verifyAcceptedHash } from "./password-hashes.js";
|
|
25
28
|
import { staffSignInPlugin } from "./staff-plugin.js";
|
|
26
29
|
import { twoFactorAfterPasswordOrCode } from "./two-factor-challenge.js";
|
|
@@ -65,7 +68,38 @@ const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, defini
|
|
|
65
68
|
const samlIds = saml.map((provider) => provider.id);
|
|
66
69
|
const users = userTable(options.users);
|
|
67
70
|
const passwordHashes = options.passwordHashes ?? [];
|
|
71
|
+
const policy = {
|
|
72
|
+
mail: Boolean(mailer),
|
|
73
|
+
socialProviders: [...Object.keys(options.socialProviders ?? {}), ...samlIds],
|
|
74
|
+
...operator && { operator: operator.name },
|
|
75
|
+
...signIn && { platformSignIn: {
|
|
76
|
+
provider: signIn.provider,
|
|
77
|
+
name: signIn.name
|
|
78
|
+
} }
|
|
79
|
+
};
|
|
80
|
+
const jwtOptions = {
|
|
81
|
+
schema: pluginNames.jwt,
|
|
82
|
+
jwks: { keyPairConfigs: [goTrueKeyPair] },
|
|
83
|
+
jwt: {
|
|
84
|
+
expirationTime: tokenLifetime,
|
|
85
|
+
issuer: options.baseURL,
|
|
86
|
+
audience: options.baseURL,
|
|
87
|
+
definePayload: async ({ user, session }) => {
|
|
88
|
+
const { activeRole, activeOrganizationId } = session;
|
|
89
|
+
const claims = {
|
|
90
|
+
role: user.role,
|
|
91
|
+
...organizations && await organizationClaims((await context()).adapter, user, activeOrganizationId)
|
|
92
|
+
};
|
|
93
|
+
return {
|
|
94
|
+
email: user.email,
|
|
95
|
+
...narrowClaims(claims, [activeRole, user.lastRole], Boolean(organizations))
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
disableSettingJwtHeader: true
|
|
100
|
+
};
|
|
68
101
|
const userFields = {
|
|
102
|
+
...userFieldsNamed(emailConfirmedField, users),
|
|
69
103
|
...organizations && userFieldsNamed(lastOrganizationField, users),
|
|
70
104
|
...metadataField(users)
|
|
71
105
|
};
|
|
@@ -105,26 +139,7 @@ const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, defini
|
|
|
105
139
|
defaultRole: defaultRole ?? "unassigned",
|
|
106
140
|
roles: Object.fromEntries(roles.map((role) => [role, role === "admin" ? adminAc : userAc]))
|
|
107
141
|
}),
|
|
108
|
-
jwt(
|
|
109
|
-
schema: pluginNames.jwt,
|
|
110
|
-
jwt: {
|
|
111
|
-
expirationTime: tokenLifetime,
|
|
112
|
-
issuer: options.baseURL,
|
|
113
|
-
audience: options.baseURL,
|
|
114
|
-
definePayload: async ({ user, session }) => {
|
|
115
|
-
const { activeRole, activeOrganizationId } = session;
|
|
116
|
-
const claims = {
|
|
117
|
-
role: user.role,
|
|
118
|
-
...organizations && await organizationClaims((await context()).adapter, user, activeOrganizationId)
|
|
119
|
-
};
|
|
120
|
-
return {
|
|
121
|
-
email: user.email,
|
|
122
|
-
...narrowClaims(claims, [activeRole, user.lastRole], Boolean(organizations))
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
},
|
|
126
|
-
disableSettingJwtHeader: true
|
|
127
|
-
}),
|
|
142
|
+
jwt(jwtOptions),
|
|
128
143
|
twoFactorAfterPasswordOrCode({
|
|
129
144
|
schema: pluginNames.twoFactor,
|
|
130
145
|
allowPasswordless: true,
|
|
@@ -142,14 +157,11 @@ const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, defini
|
|
|
142
157
|
})] : [],
|
|
143
158
|
...signIn ? [genericOAuth({ config: [platformSignInProvider(signIn)] })] : [],
|
|
144
159
|
...saml.length > 0 ? [samlPlugin(saml, options.baseURL), samlGuardPlugin(samlIds)] : [],
|
|
145
|
-
signInPolicyPlugin(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
provider: signIn.provider,
|
|
151
|
-
name: signIn.name
|
|
152
|
-
} }
|
|
160
|
+
signInPolicyPlugin(policy),
|
|
161
|
+
goTrueSessionsPlugin({
|
|
162
|
+
issuer: goTrueIssuer(options.baseURL),
|
|
163
|
+
jwt: jwtOptions,
|
|
164
|
+
policy
|
|
153
165
|
}),
|
|
154
166
|
staffSignInPlugin({
|
|
155
167
|
...operator && { operator },
|
|
@@ -182,27 +194,30 @@ const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, defini
|
|
|
182
194
|
...saml.length > 0 ? samlDisabledPaths : []
|
|
183
195
|
],
|
|
184
196
|
databaseHooks: {
|
|
185
|
-
user: {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
197
|
+
user: {
|
|
198
|
+
create: {
|
|
199
|
+
before: async (user, context) => {
|
|
200
|
+
const given = String(user.role ?? "").split(",").map((name) => name.trim()).filter(Boolean);
|
|
201
|
+
const unknown = given.find((name) => !roles.includes(name));
|
|
202
|
+
if (given.length === 0 || unknown !== void 0) throw new APIError("BAD_REQUEST", { message: `Choose a role for the new user: ${roles.join(", ")}` });
|
|
203
|
+
const samlProvider = context?.path === "/sso/saml2/sp/acs/:providerId" ? saml.find((provider) => provider.id === context.params?.providerId) : void 0;
|
|
204
|
+
if (samlProvider && !emailInDomains(user.email, samlProvider.domains)) throw new APIError("FORBIDDEN", {
|
|
205
|
+
code: "EMAIL_DOMAIN_NOT_ALLOWED",
|
|
206
|
+
message: "This address is not one the identity provider may sign up."
|
|
207
|
+
});
|
|
208
|
+
const withId = options.generateUserId ? {
|
|
209
|
+
...user,
|
|
210
|
+
id: await newUserId(options.generateUserId)
|
|
211
|
+
} : user;
|
|
212
|
+
return { data: confirmedOnCreate(context?.path === "/admin/create-user" || samlProvider ? {
|
|
213
|
+
...withId,
|
|
214
|
+
emailVerified: true
|
|
215
|
+
} : withId, /* @__PURE__ */ new Date()) };
|
|
216
|
+
},
|
|
217
|
+
...options.onUserCreated && { after: options.onUserCreated }
|
|
203
218
|
},
|
|
204
|
-
|
|
205
|
-
}
|
|
219
|
+
update: { before: async (user) => ({ data: confirmedOnUpdate(user, /* @__PURE__ */ new Date()) }) }
|
|
220
|
+
},
|
|
206
221
|
...organizations && { session: sessionOrganizationHooks(context) }
|
|
207
222
|
},
|
|
208
223
|
rateLimit: {
|
|
@@ -308,6 +323,10 @@ const createAuth = (options) => {
|
|
|
308
323
|
passwordReset: Boolean(mailer),
|
|
309
324
|
/** Open sign-up is on and offered: the app asks for it, and can mail the codes. */
|
|
310
325
|
signUp: Boolean(signUpRole && mailer),
|
|
326
|
+
/** The role of accounts made by open sign-up, while it is offered. */
|
|
327
|
+
signUpRole: mailer ? signUpRole : void 0,
|
|
328
|
+
/** The app keeps each user's metadata, in the column `users.metadata` names. */
|
|
329
|
+
userMetadata: Boolean(options.users?.metadata),
|
|
311
330
|
socialProviders,
|
|
312
331
|
/** The app's name it signs in and mails as: `appName`, else `PROTOBASE_APP_NAME`, else `Protobase`. */
|
|
313
332
|
appName: appName ?? "Protobase",
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
//#region src/better-auth/email-confirmed.ts
|
|
2
|
+
/** The user field that holds the time, in the `email_confirmed_at` column unless the app names it otherwise. */
|
|
3
|
+
const emailConfirmedField = { emailConfirmedAt: {
|
|
4
|
+
type: "date",
|
|
5
|
+
required: false,
|
|
6
|
+
input: false,
|
|
7
|
+
fieldName: "email_confirmed_at"
|
|
8
|
+
} };
|
|
9
|
+
/** A new user, confirmed now when created verified. A time the data gives itself, such as an import's, is kept. */
|
|
10
|
+
const confirmedOnCreate = (user, now) => user.emailConfirmedAt === void 0 && user.emailVerified === true ? {
|
|
11
|
+
...user,
|
|
12
|
+
emailConfirmedAt: now
|
|
13
|
+
} : user;
|
|
14
|
+
/**
|
|
15
|
+
* An update, with the confirmation time it implies: now, when it verifies the address, which Better Auth does only for
|
|
16
|
+
* one not verified yet; empty, when it changes the address without verifying it. A time the update gives is kept.
|
|
17
|
+
*/
|
|
18
|
+
const confirmedOnUpdate = (data, now) => {
|
|
19
|
+
if (data.emailConfirmedAt !== void 0) return data;
|
|
20
|
+
if (data.emailVerified === true) return {
|
|
21
|
+
...data,
|
|
22
|
+
emailConfirmedAt: now
|
|
23
|
+
};
|
|
24
|
+
if (data.email !== void 0) return {
|
|
25
|
+
...data,
|
|
26
|
+
emailConfirmedAt: null
|
|
27
|
+
};
|
|
28
|
+
return data;
|
|
29
|
+
};
|
|
30
|
+
//#endregion
|
|
31
|
+
export { confirmedOnCreate, confirmedOnUpdate, emailConfirmedField };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { invalidInvitation, invitationIdOf, verifyInvitationToken } from "../invitations/invitation-token.js";
|
|
2
1
|
import { parseAppRoles } from "./app-roles.js";
|
|
3
2
|
import { findMember, membersOf } from "./store.js";
|
|
3
|
+
import { invalidInvitation, invitationIdOf, verifyInvitationToken } from "../invitations/invitation-token.js";
|
|
4
4
|
import { APIError } from "better-auth/api";
|
|
5
5
|
//#region src/better-auth/organizations/accept-invitation.ts
|
|
6
6
|
/** The pending invitation an emailed link's token names, or a 400 `INVITATION_INVALID`. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { parseRoles } from "../parse-roles.js";
|
|
2
1
|
import { membershipRoles, noGlobalRole } from "./role-definitions.js";
|
|
2
|
+
import { parseRoles } from "../parse-roles.js";
|
|
3
3
|
//#region src/better-auth/organizations/app-roles.ts
|
|
4
4
|
/** A user's global roles, from the admin plugin's `role` column; the placeholder `user` is none. */
|
|
5
5
|
const globalRoles = (role) => parseRoles(role).filter((name) => name !== noGlobalRole);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { defaultInvitationUrl } from "../invitations/invitation-link-hook.js";
|
|
2
1
|
import { membershipRoles } from "./role-definitions.js";
|
|
2
|
+
import { defaultInvitationUrl } from "../invitations/invitation-link-hook.js";
|
|
3
3
|
//#region src/better-auth/organizations/options.ts
|
|
4
4
|
const resolveOrganizations = (options, definitions, baseURL) => {
|
|
5
5
|
const holdable = membershipRoles(definitions);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { invitationLink, invitationToken } from "../invitations/invitation-token.js";
|
|
2
1
|
import "./role-definitions.js";
|
|
3
2
|
import { checkAppRoleChange, effectiveRoles, globalRoles, parseAppRoles } from "./app-roles.js";
|
|
4
3
|
import { findMember } from "./store.js";
|
|
4
|
+
import { invitationLink, invitationToken } from "../invitations/invitation-token.js";
|
|
5
5
|
import { sendAuthEmail } from "../auth-mail/send-auth-email.js";
|
|
6
6
|
import { APIError } from "better-auth/api";
|
|
7
7
|
import { organization } from "better-auth/plugins/organization";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { globalRoles } from "./app-roles.js";
|
|
2
|
-
import { initialOrganization } from "./active-organization.js";
|
|
3
2
|
import { findMember, findOrganization, membershipsOf, organizationRoleOf, summaries } from "./store.js";
|
|
3
|
+
import { initialOrganization } from "./active-organization.js";
|
|
4
4
|
//#region src/better-auth/organizations/session-organization.ts
|
|
5
5
|
/** The user field that keeps the organization someone last worked in, for their next sign-in. */
|
|
6
6
|
const lastOrganizationField = { lastOrganizationId: {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { pluginNames } from "./snake-case-names.js";
|
|
2
2
|
import * as z$1 from "zod";
|
|
3
3
|
import { APIError, createAuthEndpoint, getSessionFromCtx } from "better-auth/api";
|
|
4
|
+
import { generateRandomString } from "better-auth/crypto";
|
|
4
5
|
import { PASSKEY_ERROR_CODES, passkey } from "@better-auth/passkey";
|
|
5
6
|
import { generateAuthenticationOptions, verifyAuthenticationResponse } from "@simplewebauthn/server";
|
|
6
7
|
import { setSessionCookie } from "better-auth/cookies";
|
|
7
|
-
import { generateRandomString } from "better-auth/crypto";
|
|
8
8
|
//#region src/better-auth/passkey-plugin.ts
|
|
9
9
|
const challengeCookie = "better-auth-passkey";
|
|
10
10
|
const challengeSeconds = 300;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { isStaffSession } from "./staff-checks.js";
|
|
2
1
|
import { effectiveSignInPolicy, missingRequiredMethods, parseSignInPolicy, signInPolicyProblem } from "./sign-in-policy.js";
|
|
3
2
|
import { readSignInPolicy, saveSignInPolicy, signInPolicySchema } from "./policy-store.js";
|
|
4
3
|
import { isPoliced, lastPasskeyRequired, policyRefusal, setupRequired } from "./sign-in-enforcement.js";
|
|
5
4
|
import { adminOnly } from "./admin-only.js";
|
|
5
|
+
import { isStaffSession } from "./staff-checks.js";
|
|
6
6
|
import * as z$1 from "zod";
|
|
7
7
|
import { APIError, createAuthEndpoint, createAuthMiddleware, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
|
|
8
8
|
//#region src/better-auth/policy-plugin.ts
|
|
@@ -31,14 +31,25 @@ const accountMethods = async (context, user, socialProviders) => {
|
|
|
31
31
|
};
|
|
32
32
|
};
|
|
33
33
|
const refuse = (status, refusal) => new APIError(status, refusal);
|
|
34
|
+
const currentPolicy = async (context, mail) => effectiveSignInPolicy((await readSignInPolicy(context.adapter)).policy, { mail });
|
|
35
|
+
const providersUnder = (policy, { socialProviders, platformSignIn }) => [...socialProviders, ...platformSignIn && policy.platformSignIn === "allowed" ? [platformSignIn.provider] : []];
|
|
36
|
+
/**
|
|
37
|
+
* Why `user` gets no API token yet, or `undefined` when they may have one: the policy requires a method, such as
|
|
38
|
+
* two-factor authentication, that their account has not set up. `read` is the effective policy, when read already.
|
|
39
|
+
*/
|
|
40
|
+
const tokenRefusal = async (context, user, options, read) => {
|
|
41
|
+
const policy = read ?? await currentPolicy(context, options.mail);
|
|
42
|
+
if (policy.twoFactor !== "required" && policy.passkey !== "required") return void 0;
|
|
43
|
+
const account = await accountMethods(context, user, providersUnder(policy, options));
|
|
44
|
+
return missingRequiredMethods(policy, account).length > 0 ? setupRequired : void 0;
|
|
45
|
+
};
|
|
34
46
|
/**
|
|
35
47
|
* Protobase's sign-in policy as a Better Auth plugin: its table, the endpoints that read and change it, and a hook that
|
|
36
48
|
* applies it to Better Auth's own endpoints. A method the policy turns off answers `403 SIGN_IN_METHOD_FORBIDDEN`;
|
|
37
49
|
* someone without what it requires gets a session, so they can set it up, but no API token (`403 SIGN_IN_SETUP_REQUIRED`).
|
|
38
50
|
*/
|
|
39
|
-
const signInPolicyPlugin = (
|
|
40
|
-
const
|
|
41
|
-
const providersUnder = (policy) => [...socialProviders, ...platformSignIn && policy.platformSignIn === "allowed" ? [platformSignIn.provider] : []];
|
|
51
|
+
const signInPolicyPlugin = (options) => {
|
|
52
|
+
const { mail, operator, platformSignIn } = options;
|
|
42
53
|
const policyAdmin = adminOnly("Only an admin can change how people sign in.");
|
|
43
54
|
const policyAnswer = async (context) => {
|
|
44
55
|
const stored = await readSignInPolicy(context.adapter);
|
|
@@ -74,7 +85,7 @@ const signInPolicyPlugin = ({ mail, socialProviders, operator, platformSignIn })
|
|
|
74
85
|
message: "Choose one of the offered rules for every sign-in method."
|
|
75
86
|
});
|
|
76
87
|
const user = ctx.context.session.user;
|
|
77
|
-
const admin = await accountMethods(ctx.context, user, providersUnder(effectiveSignInPolicy(policy, { mail })));
|
|
88
|
+
const admin = await accountMethods(ctx.context, user, providersUnder(effectiveSignInPolicy(policy, { mail }), options));
|
|
78
89
|
const problem = signInPolicyProblem(policy, {
|
|
79
90
|
mail,
|
|
80
91
|
admin
|
|
@@ -94,8 +105,8 @@ const signInPolicyPlugin = ({ mail, socialProviders, operator, platformSignIn })
|
|
|
94
105
|
method: "GET",
|
|
95
106
|
use: [sessionMiddleware]
|
|
96
107
|
}, async (ctx) => {
|
|
97
|
-
const policy = await currentPolicy(ctx.context);
|
|
98
|
-
const { socialSignIn: _, ...account } = await accountMethods(ctx.context, ctx.context.session.user, providersUnder(policy));
|
|
108
|
+
const policy = await currentPolicy(ctx.context, mail);
|
|
109
|
+
const { socialSignIn: _, ...account } = await accountMethods(ctx.context, ctx.context.session.user, providersUnder(policy, options));
|
|
99
110
|
return ctx.json({
|
|
100
111
|
policy,
|
|
101
112
|
mail,
|
|
@@ -112,7 +123,7 @@ const signInPolicyPlugin = ({ mail, socialProviders, operator, platformSignIn })
|
|
|
112
123
|
code: "SIGN_IN_CODES_ONLY",
|
|
113
124
|
message: "Only sign-in codes can be emailed."
|
|
114
125
|
});
|
|
115
|
-
const policy = await currentPolicy(ctx.context);
|
|
126
|
+
const policy = await currentPolicy(ctx.context, mail);
|
|
116
127
|
const refusal = policyRefusal(policy, {
|
|
117
128
|
path,
|
|
118
129
|
body: ctx.body,
|
|
@@ -124,9 +135,13 @@ const signInPolicyPlugin = ({ mail, socialProviders, operator, platformSignIn })
|
|
|
124
135
|
if (!(path === "/token" && (policy.twoFactor === "required" || policy.passkey === "required") || path === "/passkey/delete-passkey" && policy.passkey === "required")) return;
|
|
125
136
|
const session = await getSessionFromCtx(ctx);
|
|
126
137
|
if (!session || isStaffSession(session.session)) return;
|
|
127
|
-
const
|
|
128
|
-
if (path === "/token"
|
|
129
|
-
|
|
138
|
+
const user = session.user;
|
|
139
|
+
if (path === "/token") {
|
|
140
|
+
const refusal = await tokenRefusal(ctx.context, user, options, policy);
|
|
141
|
+
if (refusal) throw refuse("FORBIDDEN", refusal);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if ((await accountMethods(ctx.context, user, providersUnder(policy, options))).passkeys <= 1) throw refuse("FORBIDDEN", lastPasskeyRequired);
|
|
130
145
|
})
|
|
131
146
|
}] }
|
|
132
147
|
};
|
|
@@ -144,4 +159,4 @@ const vouchForCreatedAccount = async (context, email) => {
|
|
|
144
159
|
await context.internalAdapter.updateUser(found.user.id, { emailVerified: true });
|
|
145
160
|
};
|
|
146
161
|
//#endregion
|
|
147
|
-
export { signInPolicyPlugin };
|
|
162
|
+
export { signInPolicyPlugin, tokenRefusal };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { parseRoles } from "../parse-roles.js";
|
|
2
1
|
import { noGlobalRole } from "../organizations/role-definitions.js";
|
|
2
|
+
import { parseRoles } from "../parse-roles.js";
|
|
3
3
|
import { globalRoles } from "../organizations/app-roles.js";
|
|
4
4
|
//#region src/better-auth/roles/active-role.ts
|
|
5
5
|
/**
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { APIError } from "better-auth/api";
|
|
2
|
+
//#region src/better-auth/sign-up/sign-up-account.ts
|
|
3
|
+
/** Makes the account of someone who proved their address with a sign-up code: verified, with the sign-up role. */
|
|
4
|
+
const createSignedUpAccount = async (context, { email, name, role, passwordHash, metadata }) => {
|
|
5
|
+
const { internalAdapter } = context;
|
|
6
|
+
if (await internalAdapter.findUserByEmail(email)) throw new APIError("BAD_REQUEST", {
|
|
7
|
+
code: "ACCOUNT_EXISTS",
|
|
8
|
+
message: "This address has an account already: sign in instead."
|
|
9
|
+
});
|
|
10
|
+
const user = await internalAdapter.createUser({
|
|
11
|
+
email,
|
|
12
|
+
name,
|
|
13
|
+
emailVerified: true,
|
|
14
|
+
role,
|
|
15
|
+
...metadata && { metadata }
|
|
16
|
+
}, { method: "sign-up" });
|
|
17
|
+
if (passwordHash !== void 0) await internalAdapter.linkAccount({
|
|
18
|
+
userId: user.id,
|
|
19
|
+
providerId: "credential",
|
|
20
|
+
accountId: user.id,
|
|
21
|
+
password: passwordHash
|
|
22
|
+
});
|
|
23
|
+
return user;
|
|
24
|
+
};
|
|
25
|
+
//#endregion
|
|
26
|
+
export { createSignedUpAccount };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { sendAuthEmail } from "../auth-mail/send-auth-email.js";
|
|
2
1
|
import { effectiveSignInPolicy } from "../sign-in-policy.js";
|
|
3
2
|
import { readSignInPolicy } from "../policy-store.js";
|
|
3
|
+
import { sendAuthEmail } from "../auth-mail/send-auth-email.js";
|
|
4
4
|
import { checkNewAccountPassword } from "../new-account-password.js";
|
|
5
|
+
import { createSignedUpAccount } from "./sign-up-account.js";
|
|
5
6
|
import { consumeSignUpCode, issueSignUpCode, signUpCodeLifetime } from "./sign-up-code.js";
|
|
6
7
|
import { consumeSignUpTicket, issueSignUpTicket } from "./sign-up-ticket.js";
|
|
7
8
|
import * as z$1 from "zod";
|
|
@@ -79,21 +80,12 @@ const signUpPlugin = ({ role, mail }) => ({
|
|
|
79
80
|
if (await adapter.count({ model: "user" }) === 0) throw noAdminYet();
|
|
80
81
|
checkNewAccountPassword(effectiveSignInPolicy((await readSignInPolicy(adapter)).policy, { mail: true }), password);
|
|
81
82
|
const email = await consumeSignUpTicket(ctx.context, ticket);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
message: "This address has an account already: sign in instead."
|
|
85
|
-
});
|
|
86
|
-
const user = await internalAdapter.createUser({
|
|
83
|
+
const passwordHash = password === void 0 ? void 0 : await ctx.context.password.hash(password);
|
|
84
|
+
const user = await createSignedUpAccount(ctx.context, {
|
|
87
85
|
email,
|
|
88
86
|
name,
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}, { method: "sign-up" });
|
|
92
|
-
if (password !== void 0) await internalAdapter.linkAccount({
|
|
93
|
-
userId: user.id,
|
|
94
|
-
providerId: "credential",
|
|
95
|
-
accountId: user.id,
|
|
96
|
-
password: await ctx.context.password.hash(password)
|
|
87
|
+
role,
|
|
88
|
+
...passwordHash !== void 0 && { passwordHash }
|
|
97
89
|
});
|
|
98
90
|
const session = await internalAdapter.createSession(user.id, false);
|
|
99
91
|
await setSessionCookie(ctx, {
|