@protobase/server 0.5.0 → 0.6.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.
- package/dist/better-auth/auth-schema.d.ts +14 -0
- package/dist/better-auth/auth-schema.js +35 -0
- package/dist/better-auth/create-auth.d.ts +41 -6
- package/dist/better-auth/create-auth.js +29 -8
- package/dist/better-auth/platform-sign-in.d.ts +30 -0
- package/dist/better-auth/platform-sign-in.js +88 -0
- package/dist/better-auth/policy-plugin.js +10 -5
- package/dist/better-auth/policy-store.js +5 -0
- package/dist/better-auth/sign-in-enforcement.js +11 -3
- package/dist/better-auth/sign-in-policy.d.ts +8 -1
- package/dist/better-auth/sign-in-policy.js +21 -7
- package/dist/better-auth/staff-plugin.js +1 -1
- package/dist/better-auth/status-route.js +10 -4
- package/dist/better-auth/users.d.ts +8 -1
- package/dist/better-auth/users.js +16 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -1
- package/package.json +4 -4
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { AdminAuth } from "./create-auth.js";
|
|
2
|
+
//#region src/better-auth/auth-schema.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* The SQL that brings the admin store up to what this version of Protobase and the project's Better Auth plugins need:
|
|
5
|
+
* new tables, columns and indexes, never a drop, each skipped where it exists. `undefined` when the store is current. A
|
|
6
|
+
* change Better Auth cannot make safely, such as a required column without a default on a table with rows, is an error.
|
|
7
|
+
*/
|
|
8
|
+
export declare const authSchemaMigration: (auth: AdminAuth) => Promise<string | undefined>;
|
|
9
|
+
/**
|
|
10
|
+
* Refuses to go on while the admin store lacks a table or column Better Auth needs, since every sign-in would fail.
|
|
11
|
+
* The server never changes the schema itself: the project's migrations do, before the new version serves.
|
|
12
|
+
*/
|
|
13
|
+
export declare const checkAuthSchema: (auth: AdminAuth) => Promise<void>;
|
|
14
|
+
//#endregion
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { getMigrations } from "better-auth/db/migration";
|
|
2
|
+
//#region src/better-auth/auth-schema.ts
|
|
3
|
+
/** The command that writes the pending changes as a migration file of the project. */
|
|
4
|
+
const authMigrationCommand = "protobase auth migration";
|
|
5
|
+
/**
|
|
6
|
+
* Better Auth's statements, each made to skip what exists already, so a migration written from an empty database also
|
|
7
|
+
* applies to one an earlier Protobase set up in place.
|
|
8
|
+
*/
|
|
9
|
+
const skipExisting = (sql) => sql.split(";\n\n").map((statement) => statement.replace(/^create table (?!if not exists )/, "create table if not exists ").replace(/^create (unique )?index (?!if not exists )/, (_, unique) => `create ${unique ?? ""}index if not exists `).replace(/^(alter table .+? )add column (?!if not exists )/, "$1add column if not exists ")).join(";\n\n");
|
|
10
|
+
/**
|
|
11
|
+
* The SQL that brings the admin store up to what this version of Protobase and the project's Better Auth plugins need:
|
|
12
|
+
* new tables, columns and indexes, never a drop, each skipped where it exists. `undefined` when the store is current. A
|
|
13
|
+
* change Better Auth cannot make safely, such as a required column without a default on a table with rows, is an error.
|
|
14
|
+
*/
|
|
15
|
+
const authSchemaMigration = async (auth) => {
|
|
16
|
+
const { toBeCreated, toBeAdded, toBeAddedIndexes, compileMigrations } = await getMigrations(auth.options);
|
|
17
|
+
if (toBeCreated.length === 0 && toBeAdded.length === 0 && toBeAddedIndexes.length === 0) return void 0;
|
|
18
|
+
return skipExisting(await compileMigrations());
|
|
19
|
+
};
|
|
20
|
+
/** The tables and `table.column`s the admin store lacks; an index missing is not a reason to refuse requests. */
|
|
21
|
+
const missingAuthSchema = async (auth) => {
|
|
22
|
+
const { toBeCreated, toBeAdded } = await getMigrations(auth.options, { throwOnUnsafe: false });
|
|
23
|
+
return [...toBeCreated.map(({ table }) => table), ...toBeAdded.flatMap(({ table, fields }) => Object.keys(fields).map((field) => `${table}.${field}`))];
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Refuses to go on while the admin store lacks a table or column Better Auth needs, since every sign-in would fail.
|
|
27
|
+
* The server never changes the schema itself: the project's migrations do, before the new version serves.
|
|
28
|
+
*/
|
|
29
|
+
const checkAuthSchema = async (auth) => {
|
|
30
|
+
const missing = await missingAuthSchema(auth);
|
|
31
|
+
if (missing.length === 0) return;
|
|
32
|
+
throw new Error(`The auth schema is behind this version of Protobase (missing ${missing.join(", ")}). Write the change as a migration with \`${authMigrationCommand}\`, apply it with the project's migrations, then start again.`);
|
|
33
|
+
};
|
|
34
|
+
//#endregion
|
|
35
|
+
export { authMigrationCommand, authSchemaMigration, checkAuthSchema, missingAuthSchema, skipExisting };
|
|
@@ -2,6 +2,7 @@ import { SignInPolicy } from "./sign-in-policy.js";
|
|
|
2
2
|
import { StaffSignIn } from "./staff-log.js";
|
|
3
3
|
import { Mailer } from "../mail/smtp-mailer.js";
|
|
4
4
|
import { OperatorProvider } from "./operator-provider.js";
|
|
5
|
+
import { PlatformSignIn } from "./platform-sign-in.js";
|
|
5
6
|
import { BetterAuthOptions } from "better-auth";
|
|
6
7
|
//#region src/better-auth/create-auth.d.ts
|
|
7
8
|
export type CreateAuthOptions = {
|
|
@@ -33,7 +34,7 @@ export type CreateAuthOptions = {
|
|
|
33
34
|
/**
|
|
34
35
|
* Sign-in providers for Better Auth, for example `{ github: { clientId, clientSecret } }`; the callback is
|
|
35
36
|
* `{baseURL}/api/auth/callback/<provider>`. A provider also signs up people without an account, with the default role.
|
|
36
|
-
* Default none.
|
|
37
|
+
* One with the id of `signInProvider` takes its place. Default none.
|
|
37
38
|
*/
|
|
38
39
|
socialProviders?: BetterAuthOptions['socialProviders'];
|
|
39
40
|
/** Encrypt the OAuth access, refresh and ID tokens Better Auth stores for social sign-ins, with `secret`. Default off. */
|
|
@@ -47,6 +48,13 @@ export type CreateAuthOptions = {
|
|
|
47
48
|
* whatever the environment says.
|
|
48
49
|
*/
|
|
49
50
|
operator?: OperatorProvider | false;
|
|
51
|
+
/**
|
|
52
|
+
* A sign-in provider the platform adds for people who already have an account: an OpenID Connect provider that never
|
|
53
|
+
* signs anyone up. Defaults to the one the platform passes in `PROTOBASE_SIGN_IN_ISSUER`, `PROTOBASE_SIGN_IN_CLIENT_ID`
|
|
54
|
+
* and `PROTOBASE_SIGN_IN_CLIENT_SECRET` (see `readPlatformSignIn`), when set; `false` turns it off whatever the
|
|
55
|
+
* environment says. Left out when `socialProviders` has a provider with the same id.
|
|
56
|
+
*/
|
|
57
|
+
signInProvider?: PlatformSignIn | false;
|
|
50
58
|
};
|
|
51
59
|
type UserCreateHooks = NonNullable<NonNullable<NonNullable<BetterAuthOptions['databaseHooks']>['user']>['create']>;
|
|
52
60
|
/** Better Auth's `databaseHooks.user.create.after`: the created user (with its `role`) and the request context, `null` outside a request. */
|
|
@@ -54,6 +62,12 @@ export type UserCreatedHook = NonNullable<UserCreateHooks['after']>;
|
|
|
54
62
|
/** The configured roles with `admin` first; `admin` is added when missing. */
|
|
55
63
|
export declare const normalizeRoles: (roles?: string[]) => string[];
|
|
56
64
|
export declare const createAuth: (options: CreateAuthOptions) => import("better-auth").Auth<{
|
|
65
|
+
account: {
|
|
66
|
+
encryptOAuthTokens?: true | undefined;
|
|
67
|
+
accountLinking: {
|
|
68
|
+
allowDifferentEmails: true;
|
|
69
|
+
};
|
|
70
|
+
};
|
|
57
71
|
plugins: [{
|
|
58
72
|
id: "admin";
|
|
59
73
|
version: string;
|
|
@@ -2458,7 +2472,7 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
2458
2472
|
RESOLVED_USER_INVALID: import("better-auth").RawError<"RESOLVED_USER_INVALID">;
|
|
2459
2473
|
};
|
|
2460
2474
|
options: import("@better-auth/passkey").PasskeyOptions | undefined;
|
|
2461
|
-
}, ...{
|
|
2475
|
+
}, ...({
|
|
2462
2476
|
id: "email-otp";
|
|
2463
2477
|
version: string;
|
|
2464
2478
|
init(ctx: import("better-auth").AuthContext): {
|
|
@@ -2990,7 +3004,20 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
2990
3004
|
INVALID_OTP: import("better-auth").RawError<"INVALID_OTP">;
|
|
2991
3005
|
TOO_MANY_ATTEMPTS: import("better-auth").RawError<"TOO_MANY_ATTEMPTS">;
|
|
2992
3006
|
};
|
|
2993
|
-
}
|
|
3007
|
+
} | {
|
|
3008
|
+
id: "generic-oauth";
|
|
3009
|
+
version: string;
|
|
3010
|
+
init: (ctx: import("better-auth").AuthContext) => Promise<{
|
|
3011
|
+
context: {
|
|
3012
|
+
socialProviders: import("better-auth").OAuthProvider<object, Partial<import("better-auth").ProviderOptions<object>>>[];
|
|
3013
|
+
};
|
|
3014
|
+
}>;
|
|
3015
|
+
options: import("better-auth/plugins").GenericOAuthOptions<string>;
|
|
3016
|
+
$ERROR_CODES: {
|
|
3017
|
+
INVALID_OAUTH_CONFIGURATION: import("better-auth").RawError<"INVALID_OAUTH_CONFIGURATION">;
|
|
3018
|
+
TOKEN_URL_NOT_FOUND: import("better-auth").RawError<"TOKEN_URL_NOT_FOUND">;
|
|
3019
|
+
};
|
|
3020
|
+
})[], {
|
|
2994
3021
|
id: "protobase-sign-in-policy";
|
|
2995
3022
|
schema: {
|
|
2996
3023
|
readonly signInPolicy: {
|
|
@@ -3016,6 +3043,11 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
3016
3043
|
readonly required: true;
|
|
3017
3044
|
readonly defaultValue: "allowed";
|
|
3018
3045
|
};
|
|
3046
|
+
readonly platformSignIn: {
|
|
3047
|
+
readonly type: "string";
|
|
3048
|
+
readonly required: true;
|
|
3049
|
+
readonly defaultValue: "allowed";
|
|
3050
|
+
};
|
|
3019
3051
|
readonly createdAt: {
|
|
3020
3052
|
readonly type: "date";
|
|
3021
3053
|
readonly required: true;
|
|
@@ -3124,6 +3156,7 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
3124
3156
|
}, {
|
|
3125
3157
|
savedBy?: string | undefined;
|
|
3126
3158
|
savedAt?: string | undefined;
|
|
3159
|
+
platformSignIn?: string | undefined;
|
|
3127
3160
|
operator?: string | undefined;
|
|
3128
3161
|
policy: SignInPolicy;
|
|
3129
3162
|
effective: SignInPolicy;
|
|
@@ -3226,6 +3259,7 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
3226
3259
|
}, {
|
|
3227
3260
|
savedBy?: string | undefined;
|
|
3228
3261
|
savedAt?: string | undefined;
|
|
3262
|
+
platformSignIn?: string | undefined;
|
|
3229
3263
|
operator?: string | undefined;
|
|
3230
3264
|
policy: SignInPolicy;
|
|
3231
3265
|
effective: SignInPolicy;
|
|
@@ -3556,9 +3590,6 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
3556
3590
|
handler: () => void;
|
|
3557
3591
|
};
|
|
3558
3592
|
};
|
|
3559
|
-
account?: {
|
|
3560
|
-
encryptOAuthTokens: true;
|
|
3561
|
-
} | undefined;
|
|
3562
3593
|
socialProviders?: import("better-auth").SocialProviders | undefined;
|
|
3563
3594
|
appName: string;
|
|
3564
3595
|
database: any;
|
|
@@ -3585,6 +3616,10 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
3585
3616
|
passwordReset: boolean;
|
|
3586
3617
|
socialProviders: string[];
|
|
3587
3618
|
operator: string | undefined;
|
|
3619
|
+
platformSignIn: {
|
|
3620
|
+
provider: string;
|
|
3621
|
+
name: string;
|
|
3622
|
+
} | undefined;
|
|
3588
3623
|
};
|
|
3589
3624
|
export type AdminAuth = ReturnType<typeof createAuth>;
|
|
3590
3625
|
//#endregion
|
|
@@ -2,6 +2,7 @@ import { readMailSettings } from "../mail/mail-settings.js";
|
|
|
2
2
|
import { smtpMailer } from "../mail/smtp-mailer.js";
|
|
3
3
|
import { readOperatorSettings, resolveOperator } from "./operator-provider.js";
|
|
4
4
|
import { passwordResetOptions } from "./password-reset.js";
|
|
5
|
+
import { platformSignInFor, platformSignInProvider, readPlatformSignIn, resolvePlatformSignIn } from "./platform-sign-in.js";
|
|
5
6
|
import { signInPolicyPlugin } from "./policy-plugin.js";
|
|
6
7
|
import { emailCodeOptions, twoFactorCodeOptions, unusedEmailCodePaths } from "./sign-in-mail.js";
|
|
7
8
|
import { staffSignInPlugin } from "./staff-plugin.js";
|
|
@@ -9,10 +10,11 @@ import { twoFactorAfterPasswordOrCode } from "./two-factor-challenge.js";
|
|
|
9
10
|
import { passkey } from "@better-auth/passkey";
|
|
10
11
|
import { APIError } from "better-auth/api";
|
|
11
12
|
import { betterAuth } from "better-auth";
|
|
12
|
-
import { admin, emailOTP, jwt } from "better-auth/plugins";
|
|
13
|
+
import { admin, emailOTP, genericOAuth, jwt } from "better-auth/plugins";
|
|
13
14
|
import { adminAc, userAc } from "better-auth/plugins/admin/access";
|
|
14
15
|
//#region src/better-auth/create-auth.ts
|
|
15
16
|
const platformOperator = () => readOperatorSettings(globalThis.process?.env ?? {});
|
|
17
|
+
const platformSignInSettings = () => readPlatformSignIn(globalThis.process?.env ?? {});
|
|
16
18
|
const platformMailer = () => {
|
|
17
19
|
const settings = readMailSettings(globalThis.process?.env ?? {});
|
|
18
20
|
return settings && smtpMailer(settings);
|
|
@@ -33,9 +35,13 @@ const normalizeRoles = (roles = ["admin", "user"]) => {
|
|
|
33
35
|
return ["admin", ...new Set(roles.filter((role) => role !== "admin"))];
|
|
34
36
|
};
|
|
35
37
|
const adminImpersonationPaths = ["/admin/impersonate-user", "/admin/stop-impersonating"];
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
+
/**
|
|
39
|
+
* The options `createAuth` gives Better Auth, with the roles, default role, mailer, operator provider and platform
|
|
40
|
+
* sign-in provider already resolved.
|
|
41
|
+
*/
|
|
42
|
+
const betterAuthOptions = (options, { roles, defaultRole, mailer, operator, platformSignIn }) => {
|
|
38
43
|
const secure = options.baseURL.startsWith("https://");
|
|
44
|
+
const signIn = platformSignInFor(platformSignIn, Object.keys(options.socialProviders ?? {}));
|
|
39
45
|
return {
|
|
40
46
|
appName: options.appName ?? "Protobase",
|
|
41
47
|
database: options.database,
|
|
@@ -54,7 +60,10 @@ const betterAuthOptions = (options, { roles, defaultRole, mailer, operator }) =>
|
|
|
54
60
|
...mailer && passwordResetOptions(mailer)
|
|
55
61
|
},
|
|
56
62
|
...options.socialProviders && { socialProviders: options.socialProviders },
|
|
57
|
-
|
|
63
|
+
account: {
|
|
64
|
+
accountLinking: { allowDifferentEmails: true },
|
|
65
|
+
...options.encryptOAuthTokens && { encryptOAuthTokens: true }
|
|
66
|
+
},
|
|
58
67
|
plugins: [
|
|
59
68
|
admin({
|
|
60
69
|
defaultRole: defaultRole ?? "unassigned",
|
|
@@ -78,10 +87,15 @@ const betterAuthOptions = (options, { roles, defaultRole, mailer, operator }) =>
|
|
|
78
87
|
}),
|
|
79
88
|
passkey(),
|
|
80
89
|
...mailer ? [emailOTP(emailCodeOptions(mailer))] : [],
|
|
90
|
+
...signIn ? [genericOAuth({ config: [platformSignInProvider(signIn)] })] : [],
|
|
81
91
|
signInPolicyPlugin({
|
|
82
92
|
mail: Boolean(mailer),
|
|
83
93
|
socialProviders: Object.keys(options.socialProviders ?? {}),
|
|
84
|
-
...operator && { operator: operator.name }
|
|
94
|
+
...operator && { operator: operator.name },
|
|
95
|
+
...signIn && { platformSignIn: {
|
|
96
|
+
provider: signIn.provider,
|
|
97
|
+
name: signIn.name
|
|
98
|
+
} }
|
|
85
99
|
}),
|
|
86
100
|
staffSignInPlugin({
|
|
87
101
|
...operator && { operator },
|
|
@@ -134,20 +148,27 @@ const createAuth = (options) => {
|
|
|
134
148
|
const mailer = options.mailer === void 0 ? platformMailer() : options.mailer || void 0;
|
|
135
149
|
const provider = options.operator === void 0 ? platformOperator() : options.operator || void 0;
|
|
136
150
|
const operator = provider && resolveOperator(provider);
|
|
151
|
+
const signInSettings = options.signInProvider === void 0 ? platformSignInSettings() : options.signInProvider || void 0;
|
|
152
|
+
const socialProviders = Object.keys(options.socialProviders ?? {});
|
|
153
|
+
const platformSignIn = platformSignInFor(signInSettings && resolvePlatformSignIn(signInSettings), socialProviders);
|
|
137
154
|
const auth = betterAuth(betterAuthOptions(options, {
|
|
138
155
|
roles,
|
|
139
156
|
defaultRole,
|
|
140
157
|
mailer,
|
|
141
|
-
operator
|
|
158
|
+
operator,
|
|
159
|
+
platformSignIn
|
|
142
160
|
}));
|
|
143
|
-
const socialProviders = Object.keys(options.socialProviders ?? {});
|
|
144
161
|
return Object.assign(auth, {
|
|
145
162
|
roles,
|
|
146
163
|
defaultRole,
|
|
147
164
|
mail: Boolean(mailer),
|
|
148
165
|
passwordReset: Boolean(mailer),
|
|
149
166
|
socialProviders,
|
|
150
|
-
operator: operator?.name
|
|
167
|
+
operator: operator?.name,
|
|
168
|
+
platformSignIn: platformSignIn && {
|
|
169
|
+
provider: platformSignIn.provider,
|
|
170
|
+
name: platformSignIn.name
|
|
171
|
+
}
|
|
151
172
|
});
|
|
152
173
|
};
|
|
153
174
|
//#endregion
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import "better-auth/plugins";
|
|
2
|
+
//#region src/better-auth/platform-sign-in.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* A sign-in provider the platform adds to the app, for people who already have an account: any OpenID Connect
|
|
5
|
+
* provider, with this app registered as a confidential client whose redirect URI is
|
|
6
|
+
* `{baseURL}/api/auth/callback/{provider}`. It never creates accounts.
|
|
7
|
+
*/
|
|
8
|
+
export type PlatformSignIn = {
|
|
9
|
+
/** The provider's issuer URL; its settings are read from `{issuer}/.well-known/openid-configuration`. */
|
|
10
|
+
issuer: string;
|
|
11
|
+
clientId: string;
|
|
12
|
+
clientSecret: string;
|
|
13
|
+
/**
|
|
14
|
+
* The id of the provider: the button, the callback path, and the namespace of the linked accounts. Default `oidc`.
|
|
15
|
+
* Use `github` only when the provider's `sub` is the GitHub user id, so its accounts are those of Better Auth's own
|
|
16
|
+
* GitHub provider.
|
|
17
|
+
*/
|
|
18
|
+
provider?: string;
|
|
19
|
+
/** Shown on the sign-in page as "Continue with {name}". Default `GitHub` for the `github` provider, `SSO` otherwise. */
|
|
20
|
+
name?: string;
|
|
21
|
+
/** Scopes asked for besides `openid email profile`. */
|
|
22
|
+
scopes?: string[];
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* The provider the platform passes in `PROTOBASE_SIGN_IN_ISSUER`, `PROTOBASE_SIGN_IN_CLIENT_ID` and
|
|
26
|
+
* `PROTOBASE_SIGN_IN_CLIENT_SECRET` (with `PROTOBASE_SIGN_IN_PROVIDER` and `PROTOBASE_SIGN_IN_NAME` optional);
|
|
27
|
+
* `undefined` when none of the three is set. Some but not all of them stops the server at startup.
|
|
28
|
+
*/
|
|
29
|
+
export declare const readPlatformSignIn: (env: Record<string, string | undefined>) => PlatformSignIn | undefined;
|
|
30
|
+
//#endregion
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { decodeJwt } from "jose";
|
|
2
|
+
//#region src/better-auth/platform-sign-in.ts
|
|
3
|
+
const signInIssuerVariable = "PROTOBASE_SIGN_IN_ISSUER";
|
|
4
|
+
const signInClientIdVariable = "PROTOBASE_SIGN_IN_CLIENT_ID";
|
|
5
|
+
const signInClientSecretVariable = "PROTOBASE_SIGN_IN_CLIENT_SECRET";
|
|
6
|
+
const signInProviderVariable = "PROTOBASE_SIGN_IN_PROVIDER";
|
|
7
|
+
const signInNameVariable = "PROTOBASE_SIGN_IN_NAME";
|
|
8
|
+
/**
|
|
9
|
+
* The provider the platform passes in `PROTOBASE_SIGN_IN_ISSUER`, `PROTOBASE_SIGN_IN_CLIENT_ID` and
|
|
10
|
+
* `PROTOBASE_SIGN_IN_CLIENT_SECRET` (with `PROTOBASE_SIGN_IN_PROVIDER` and `PROTOBASE_SIGN_IN_NAME` optional);
|
|
11
|
+
* `undefined` when none of the three is set. Some but not all of them stops the server at startup.
|
|
12
|
+
*/
|
|
13
|
+
const readPlatformSignIn = (env) => {
|
|
14
|
+
const required = [
|
|
15
|
+
signInIssuerVariable,
|
|
16
|
+
signInClientIdVariable,
|
|
17
|
+
signInClientSecretVariable
|
|
18
|
+
];
|
|
19
|
+
const [issuer, clientId, clientSecret] = required.map((name) => env[name]?.trim() || void 0);
|
|
20
|
+
if (!issuer && !clientId && !clientSecret) return void 0;
|
|
21
|
+
if (!issuer || !clientId || !clientSecret) throw new Error(`${required.join(", ")} go together: set all three for the platform sign-in provider, or none`);
|
|
22
|
+
const provider = env[signInProviderVariable]?.trim();
|
|
23
|
+
const name = env[signInNameVariable]?.trim();
|
|
24
|
+
return {
|
|
25
|
+
issuer,
|
|
26
|
+
clientId,
|
|
27
|
+
clientSecret,
|
|
28
|
+
...provider && { provider },
|
|
29
|
+
...name && { name }
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
const providerId = /^[a-z][a-z0-9-]*$/;
|
|
33
|
+
/** The provider with its defaults filled in; an issuer that is not a URL or a malformed id is an error. */
|
|
34
|
+
const resolvePlatformSignIn = (signIn) => {
|
|
35
|
+
if (!URL.canParse(signIn.issuer)) throw new Error(`The platform sign-in provider's issuer must be a URL, not "${signIn.issuer}"`);
|
|
36
|
+
const provider = signIn.provider ?? "oidc";
|
|
37
|
+
if (!providerId.test(provider)) throw new Error(`Invalid sign-in provider id "${provider}": use lowercase letters, digits or "-"`);
|
|
38
|
+
return {
|
|
39
|
+
...signIn,
|
|
40
|
+
provider,
|
|
41
|
+
name: signIn.name ?? (provider === "github" ? "GitHub" : "SSO"),
|
|
42
|
+
scopes: signIn.scopes ?? []
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
/** The platform provider, unless the app configures a provider of its own with the same id, which then wins. */
|
|
46
|
+
const platformSignInFor = (signIn, socialProviders) => signIn && !socialProviders.includes(signIn.provider) ? signIn : void 0;
|
|
47
|
+
/**
|
|
48
|
+
* The person as an ID token Better Auth already verified (the provider's keys, this client and the nonce) says, once
|
|
49
|
+
* its issuer is the configured one; `null` without an ID token, so nothing comes from an unverified source.
|
|
50
|
+
*/
|
|
51
|
+
const idTokenUser = (idToken, issuer) => {
|
|
52
|
+
if (!idToken) return null;
|
|
53
|
+
const claims = decodeJwt(idToken);
|
|
54
|
+
if (claims.iss !== issuer || typeof claims.sub !== "string" || !claims.sub || typeof claims.email !== "string") return null;
|
|
55
|
+
return {
|
|
56
|
+
id: claims.sub,
|
|
57
|
+
sub: claims.sub,
|
|
58
|
+
email: claims.email,
|
|
59
|
+
emailVerified: claims.email_verified === true,
|
|
60
|
+
name: typeof claims.name === "string" && claims.name ? claims.name : claims.email.split("@")[0],
|
|
61
|
+
...typeof claims.picture === "string" && { image: claims.picture }
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* The provider for Better Auth's generic OAuth plugin: discovery, PKCE, the client secret in a Basic header, an ID
|
|
66
|
+
* token that must verify against the provider's keys, and no sign-up. A provider whose settings cannot be read at
|
|
67
|
+
* startup is left out until the next start.
|
|
68
|
+
*/
|
|
69
|
+
const platformSignInProvider = (signIn) => ({
|
|
70
|
+
providerId: signIn.provider,
|
|
71
|
+
name: signIn.name,
|
|
72
|
+
clientId: signIn.clientId,
|
|
73
|
+
clientSecret: signIn.clientSecret,
|
|
74
|
+
discoveryUrl: `${signIn.issuer.replace(/\/$/, "")}/.well-known/openid-configuration`,
|
|
75
|
+
scopes: [
|
|
76
|
+
"openid",
|
|
77
|
+
"email",
|
|
78
|
+
"profile",
|
|
79
|
+
...signIn.scopes
|
|
80
|
+
],
|
|
81
|
+
pkce: true,
|
|
82
|
+
requireIdTokenVerification: true,
|
|
83
|
+
authentication: "basic",
|
|
84
|
+
disableSignUp: true,
|
|
85
|
+
getUserInfo: async (tokens) => idTokenUser(tokens.idToken, signIn.issuer)
|
|
86
|
+
});
|
|
87
|
+
//#endregion
|
|
88
|
+
export { idTokenUser, platformSignInFor, platformSignInProvider, readPlatformSignIn, resolvePlatformSignIn, signInClientIdVariable, signInClientSecretVariable, signInIssuerVariable, signInNameVariable, signInProviderVariable };
|
|
@@ -36,8 +36,9 @@ const refuse = (status, refusal) => new APIError(status, refusal);
|
|
|
36
36
|
* applies it to Better Auth's own endpoints. A method the policy turns off answers `403 SIGN_IN_METHOD_FORBIDDEN`;
|
|
37
37
|
* someone without what it requires gets a session, so they can set it up, but no API token (`403 SIGN_IN_SETUP_REQUIRED`).
|
|
38
38
|
*/
|
|
39
|
-
const signInPolicyPlugin = ({ mail, socialProviders, operator }) => {
|
|
39
|
+
const signInPolicyPlugin = ({ mail, socialProviders, operator, platformSignIn }) => {
|
|
40
40
|
const currentPolicy = async (context) => effectiveSignInPolicy((await readSignInPolicy(context.adapter)).policy, { mail });
|
|
41
|
+
const providersUnder = (policy) => [...socialProviders, ...platformSignIn && policy.platformSignIn === "allowed" ? [platformSignIn.provider] : []];
|
|
41
42
|
const policyAdmin = adminOnly("Only an admin can change how people sign in.");
|
|
42
43
|
const policyAnswer = async (context) => {
|
|
43
44
|
const stored = await readSignInPolicy(context.adapter);
|
|
@@ -47,6 +48,7 @@ const signInPolicyPlugin = ({ mail, socialProviders, operator }) => {
|
|
|
47
48
|
effective: effectiveSignInPolicy(stored.policy, { mail }),
|
|
48
49
|
mail,
|
|
49
50
|
...operator && { operator },
|
|
51
|
+
...platformSignIn && { platformSignIn: platformSignIn.name },
|
|
50
52
|
...stored.savedAt && { savedAt: stored.savedAt },
|
|
51
53
|
...savedBy && { savedBy }
|
|
52
54
|
};
|
|
@@ -72,9 +74,10 @@ const signInPolicyPlugin = ({ mail, socialProviders, operator }) => {
|
|
|
72
74
|
message: "Choose one of the offered rules for every sign-in method."
|
|
73
75
|
});
|
|
74
76
|
const user = ctx.context.session.user;
|
|
77
|
+
const admin = await accountMethods(ctx.context, user, providersUnder(effectiveSignInPolicy(policy, { mail })));
|
|
75
78
|
const problem = signInPolicyProblem(policy, {
|
|
76
79
|
mail,
|
|
77
|
-
admin
|
|
80
|
+
admin
|
|
78
81
|
});
|
|
79
82
|
if (problem) throw refuse("BAD_REQUEST", {
|
|
80
83
|
code: "SIGN_IN_POLICY_REFUSED",
|
|
@@ -92,7 +95,7 @@ const signInPolicyPlugin = ({ mail, socialProviders, operator }) => {
|
|
|
92
95
|
use: [sessionMiddleware]
|
|
93
96
|
}, async (ctx) => {
|
|
94
97
|
const policy = await currentPolicy(ctx.context);
|
|
95
|
-
const { socialSignIn: _, ...account } = await accountMethods(ctx.context, ctx.context.session.user,
|
|
98
|
+
const { socialSignIn: _, ...account } = await accountMethods(ctx.context, ctx.context.session.user, providersUnder(policy));
|
|
96
99
|
return ctx.json({
|
|
97
100
|
policy,
|
|
98
101
|
mail,
|
|
@@ -112,14 +115,16 @@ const signInPolicyPlugin = ({ mail, socialProviders, operator }) => {
|
|
|
112
115
|
const policy = await currentPolicy(ctx.context);
|
|
113
116
|
const refusal = policyRefusal(policy, {
|
|
114
117
|
path,
|
|
115
|
-
body: ctx.body
|
|
118
|
+
body: ctx.body,
|
|
119
|
+
params: ctx.params,
|
|
120
|
+
...platformSignIn && { platformProvider: platformSignIn.provider }
|
|
116
121
|
});
|
|
117
122
|
if (refusal) throw refuse("FORBIDDEN", refusal);
|
|
118
123
|
if (path === "/sign-in/email-otp") return vouchForCreatedAccount(ctx.context, ctx.body.email);
|
|
119
124
|
if (!(path === "/token" && (policy.twoFactor === "required" || policy.passkey === "required") || path === "/passkey/delete-passkey" && policy.passkey === "required")) return;
|
|
120
125
|
const session = await getSessionFromCtx(ctx);
|
|
121
126
|
if (!session || isStaffSession(session.session)) return;
|
|
122
|
-
const account = await accountMethods(ctx.context, session.user,
|
|
127
|
+
const account = await accountMethods(ctx.context, session.user, providersUnder(policy));
|
|
123
128
|
if (path === "/token" && missingRequiredMethods(policy, account).length > 0) throw refuse("FORBIDDEN", setupRequired);
|
|
124
129
|
if (path === "/passkey/delete-passkey" && account.passkeys <= 1) throw refuse("FORBIDDEN", lastPasskeyRequired);
|
|
125
130
|
})
|
|
@@ -24,6 +24,11 @@ const signInPolicySchema = { [signInPolicyModel]: { fields: {
|
|
|
24
24
|
required: true,
|
|
25
25
|
defaultValue: "allowed"
|
|
26
26
|
},
|
|
27
|
+
platformSignIn: {
|
|
28
|
+
type: "string",
|
|
29
|
+
required: true,
|
|
30
|
+
defaultValue: "allowed"
|
|
31
|
+
},
|
|
27
32
|
createdAt: {
|
|
28
33
|
type: "date",
|
|
29
34
|
required: true
|
|
@@ -13,6 +13,8 @@ const passkeyPaths = [
|
|
|
13
13
|
"/passkey/verify-registration"
|
|
14
14
|
];
|
|
15
15
|
const staffSignInPath = "/staff/sign-in";
|
|
16
|
+
const providerPaths = ["/sign-in/social", "/link-social"];
|
|
17
|
+
const callbackPath = "/callback/:id";
|
|
16
18
|
const accountPaths = [
|
|
17
19
|
"/email-otp/send-verification-otp",
|
|
18
20
|
"/passkey/delete-passkey",
|
|
@@ -26,23 +28,29 @@ const isPoliced = (path) => path !== void 0 && ([
|
|
|
26
28
|
...emailCodePaths,
|
|
27
29
|
...passkeyPaths,
|
|
28
30
|
...accountPaths,
|
|
31
|
+
...providerPaths,
|
|
32
|
+
callbackPath,
|
|
29
33
|
"/staff/sign-in"
|
|
30
34
|
].includes(path) || path.startsWith("/reset-password/"));
|
|
31
|
-
const
|
|
35
|
+
const providerOf = ({ path, body, params }) => providerPaths.includes(path) ? body?.provider : path === callbackPath ? params?.id : void 0;
|
|
36
|
+
const methodOf = (request) => {
|
|
37
|
+
const { path, body } = request;
|
|
32
38
|
if (passwordPaths.includes(path) || path.startsWith("/reset-password/")) return "password";
|
|
33
39
|
if (emailCodePaths.includes(path)) return "emailCode";
|
|
34
40
|
if (path === "/email-otp/send-verification-otp" && body?.type === "sign-in") return "emailCode";
|
|
35
41
|
if (passkeyPaths.includes(path)) return "passkey";
|
|
36
42
|
if (path === "/two-factor/enable") return "twoFactor";
|
|
37
43
|
if (path === "/staff/sign-in") return "staffAccess";
|
|
44
|
+
if (request.platformProvider !== void 0 && providerOf(request) === request.platformProvider) return "platformSignIn";
|
|
38
45
|
};
|
|
39
46
|
/**
|
|
40
47
|
* Why the policy refuses a request to Better Auth at `path`, or `undefined` when it lets it through: a method the
|
|
41
48
|
* policy turns off, or turning off two-factor authentication while the policy requires it. Removing the last passkey
|
|
42
49
|
* while passkeys are required depends on the account, so the plugin checks that itself.
|
|
43
50
|
*/
|
|
44
|
-
const policyRefusal = (policy,
|
|
45
|
-
const
|
|
51
|
+
const policyRefusal = (policy, request) => {
|
|
52
|
+
const { path } = request;
|
|
53
|
+
const method = methodOf(request);
|
|
46
54
|
if (method && policy[method] === "forbidden") return {
|
|
47
55
|
code: "SIGN_IN_METHOD_FORBIDDEN",
|
|
48
56
|
message: signInPolicyMethods[method].forbidden
|
|
@@ -7,7 +7,8 @@ export type StaffAccessRule = 'allowed' | 'notify' | 'forbidden';
|
|
|
7
7
|
* The app's sign-in policy. `password`, `emailCode` and `passkey` are ways to sign in; `twoFactor` is the second step
|
|
8
8
|
* after a password or an emailed code. Only a passkey and two-factor authentication can be required: they are set up
|
|
9
9
|
* once per person, so "required" sends everyone without one to set it up. `staffAccess` is whether the operator's
|
|
10
|
-
* staff may sign in as people, for support.
|
|
10
|
+
* staff may sign in as people, for support. `platformSignIn` is whether people may sign in through the provider the
|
|
11
|
+
* platform adds (see `PlatformSignIn`), which the app's developer did not choose.
|
|
11
12
|
*/
|
|
12
13
|
export type SignInPolicy = {
|
|
13
14
|
password: 'allowed' | 'forbidden';
|
|
@@ -15,6 +16,7 @@ export type SignInPolicy = {
|
|
|
15
16
|
passkey: SignInRule;
|
|
16
17
|
twoFactor: SignInRule;
|
|
17
18
|
staffAccess: StaffAccessRule;
|
|
19
|
+
platformSignIn: 'allowed' | 'forbidden';
|
|
18
20
|
};
|
|
19
21
|
/** Each method with the rules it takes, its name, and what the server answers when the policy turns it off. */
|
|
20
22
|
export declare const signInPolicyMethods: {
|
|
@@ -43,6 +45,11 @@ export declare const signInPolicyMethods: {
|
|
|
43
45
|
readonly rules: readonly ["allowed", "notify", "forbidden"];
|
|
44
46
|
readonly forbidden: "Staff sign-in is turned off for this app.";
|
|
45
47
|
};
|
|
48
|
+
readonly platformSignIn: {
|
|
49
|
+
readonly label: "Sign-in through the platform";
|
|
50
|
+
readonly rules: readonly ["allowed", "forbidden"];
|
|
51
|
+
readonly forbidden: "Signing in through the platform is turned off for this app.";
|
|
52
|
+
};
|
|
46
53
|
};
|
|
47
54
|
/** Everything allowed and nothing required: the policy of an app whose admins never saved one. */
|
|
48
55
|
export declare const defaultSignInPolicy: SignInPolicy;
|
|
@@ -37,6 +37,11 @@ const signInPolicyMethods = {
|
|
|
37
37
|
"forbidden"
|
|
38
38
|
],
|
|
39
39
|
forbidden: "Staff sign-in is turned off for this app."
|
|
40
|
+
},
|
|
41
|
+
platformSignIn: {
|
|
42
|
+
label: "Sign-in through the platform",
|
|
43
|
+
rules: ["allowed", "forbidden"],
|
|
44
|
+
forbidden: "Signing in through the platform is turned off for this app."
|
|
40
45
|
}
|
|
41
46
|
};
|
|
42
47
|
/** Everything allowed and nothing required: the policy of an app whose admins never saved one. */
|
|
@@ -45,7 +50,8 @@ const defaultSignInPolicy = {
|
|
|
45
50
|
emailCode: "allowed",
|
|
46
51
|
passkey: "allowed",
|
|
47
52
|
twoFactor: "allowed",
|
|
48
|
-
staffAccess: "allowed"
|
|
53
|
+
staffAccess: "allowed",
|
|
54
|
+
platformSignIn: "allowed"
|
|
49
55
|
};
|
|
50
56
|
const methodNames = Object.keys(signInPolicyMethods);
|
|
51
57
|
/** A policy from a request body or a stored row; `undefined` when a method is missing or has a rule it does not take. */
|
|
@@ -57,13 +63,21 @@ const parseSignInPolicy = (input) => {
|
|
|
57
63
|
/**
|
|
58
64
|
* The policy as it applies. Emailed codes need mail; without mail, password sign-in is on whatever the policy says, so
|
|
59
65
|
* an app whose mail settings went missing does not lock out everyone who had only emailed codes left. Staff sign-in
|
|
60
|
-
* that emails the person is off without mail, since nobody would be told.
|
|
66
|
+
* that emails the person is off without mail, since nobody would be told. Sign-in through the platform is off while
|
|
67
|
+
* passkeys or two-factor authentication are required: it skips the app's second step, and the provider's own cannot
|
|
68
|
+
* be checked.
|
|
61
69
|
*/
|
|
62
|
-
const effectiveSignInPolicy = (policy, { mail }) =>
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
70
|
+
const effectiveSignInPolicy = (policy, { mail }) => {
|
|
71
|
+
const strict = policy.passkey === "required" || policy.twoFactor === "required";
|
|
72
|
+
return {
|
|
73
|
+
...policy,
|
|
74
|
+
...!mail && {
|
|
75
|
+
password: "allowed",
|
|
76
|
+
emailCode: "forbidden"
|
|
77
|
+
},
|
|
78
|
+
...!mail && policy.staffAccess === "notify" && { staffAccess: "forbidden" },
|
|
79
|
+
...strict && { platformSignIn: "forbidden" }
|
|
80
|
+
};
|
|
67
81
|
};
|
|
68
82
|
/** The required methods `account` has not set up yet, in the order a setup page asks for them. */
|
|
69
83
|
const missingRequiredMethods = (policy, account) => [...policy.twoFactor === "required" && !account.twoFactor ? ["twoFactor"] : [], ...policy.passkey === "required" && account.passkeys === 0 ? ["passkey"] : []];
|
|
@@ -9,7 +9,7 @@ import { deleteSessionCookie } from "better-auth/cookies";
|
|
|
9
9
|
* Staff of the operator sign in as a person of this app, for support (see `staffSignInEndpoints`). A staff session
|
|
10
10
|
* shows who started it and why, can be stopped, and cannot change the person's sign-in or the sign-in policy. Every
|
|
11
11
|
* staff sign-in is logged for the app's admins. The plugin is there without an operator provider too, so its table
|
|
12
|
-
* exists from the first
|
|
12
|
+
* exists from the first auth migration.
|
|
13
13
|
*/
|
|
14
14
|
const staffSignInPlugin = ({ operator, mailer }) => ({
|
|
15
15
|
id: "protobase-staff-sign-in",
|
|
@@ -7,24 +7,30 @@ import { Hono } from "hono";
|
|
|
7
7
|
* `GET {authBase}/status`: `needsAdmin` while the admin store has no user, so a login page can say how to create one;
|
|
8
8
|
* `signInMethods`, the ways to sign in the sign-in policy leaves on (`password`, `emailCode`, `passkey`), so it offers
|
|
9
9
|
* only those; `passwordReset` when reset mail can be sent and passwords are on, so it offers "Forgot password?" only
|
|
10
|
-
* then; the ids of the
|
|
11
|
-
* `
|
|
10
|
+
* then; the ids of the `socialProviders` people can sign in with, so it offers "Continue with GitHub" when `github` is
|
|
11
|
+
* one; `platformSignIn`, the id and name of the platform's provider when it is one of them; and `staffSignIn`, the
|
|
12
|
+
* operator provider's name, when staff can sign in as people.
|
|
12
13
|
*/
|
|
13
14
|
const statusRoute = (auth) => {
|
|
14
15
|
const app = new Hono();
|
|
15
16
|
app.get("/status", async (c) => {
|
|
16
|
-
const
|
|
17
|
+
const context = await auth.$context;
|
|
18
|
+
const policy = effectiveSignInPolicy((await readSignInPolicy(context.adapter)).policy, { mail: auth.mail });
|
|
17
19
|
const signInMethods = [
|
|
18
20
|
...policy.password === "allowed" ? ["password"] : [],
|
|
19
21
|
...policy.emailCode === "allowed" ? ["emailCode"] : [],
|
|
20
22
|
...policy.passkey !== "forbidden" ? ["passkey"] : []
|
|
21
23
|
];
|
|
22
24
|
const staffSignIn = auth.operator && policy.staffAccess !== "forbidden" ? auth.operator : void 0;
|
|
25
|
+
const platform = auth.platformSignIn;
|
|
26
|
+
const platformSignIn = platform && policy.platformSignIn === "allowed" && context.socialProviders.some((provider) => provider.id === platform.provider) ? platform : void 0;
|
|
27
|
+
const socialProviders = [...auth.socialProviders, ...platformSignIn ? [platformSignIn.provider] : []];
|
|
23
28
|
return c.json({
|
|
24
29
|
needsAdmin: !await hasUsers(auth),
|
|
25
30
|
signInMethods,
|
|
26
31
|
passwordReset: auth.passwordReset && policy.password === "allowed",
|
|
27
|
-
socialProviders
|
|
32
|
+
socialProviders,
|
|
33
|
+
...platformSignIn && { platformSignIn },
|
|
28
34
|
...staffSignIn && { staffSignIn }
|
|
29
35
|
});
|
|
30
36
|
});
|
|
@@ -3,11 +3,17 @@ import { parseRoles } from "./parse-roles.js";
|
|
|
3
3
|
//#region src/better-auth/users.d.ts
|
|
4
4
|
/** The roles this auth instance allows, `admin` first. */
|
|
5
5
|
export declare const roleChoices: (auth: AdminAuth) => string[];
|
|
6
|
+
/** An account at a sign-in provider, as Better Auth keys it: `{ providerId: 'github', accountId: '<GitHub user id>' }`. */
|
|
7
|
+
export type LinkedAccount = {
|
|
8
|
+
providerId: string;
|
|
9
|
+
accountId: string;
|
|
10
|
+
};
|
|
6
11
|
export type NewUser = {
|
|
7
12
|
email: string;
|
|
8
13
|
password: string;
|
|
9
14
|
name?: string;
|
|
10
15
|
role?: string;
|
|
16
|
+
accounts?: LinkedAccount[];
|
|
11
17
|
};
|
|
12
18
|
export type StoredUser = {
|
|
13
19
|
id: string;
|
|
@@ -21,7 +27,8 @@ export declare const hasUsers: (auth: AdminAuth) => Promise<boolean>;
|
|
|
21
27
|
/**
|
|
22
28
|
* Creates a user directly in the admin store. Host side only (no HTTP route creates accounts while the store is empty):
|
|
23
29
|
* the first user is always an `admin`, later ones get `role` (default `user`). The address counts as verified, so the
|
|
24
|
-
* user can also sign in with an emailed code.
|
|
30
|
+
* user can also sign in with an emailed code. `accounts` link the user to sign-in providers up front, so a provider
|
|
31
|
+
* signs them in by its own id whatever address it reports; one that signs in another user already is refused.
|
|
25
32
|
*/
|
|
26
33
|
export declare const createUser: (auth: AdminAuth, input: NewUser) => Promise<{
|
|
27
34
|
id: string;
|
|
@@ -10,22 +10,37 @@ const checkRoles = (auth, role) => {
|
|
|
10
10
|
};
|
|
11
11
|
/** Whether the admin store has any user yet. */
|
|
12
12
|
const hasUsers = async (auth) => (await auth.$context).adapter.count({ model: "user" }).then((count) => count > 0);
|
|
13
|
+
const checkAccounts = async (auth, accounts) => {
|
|
14
|
+
const { internalAdapter } = await auth.$context;
|
|
15
|
+
for (const account of accounts) {
|
|
16
|
+
if (!account.providerId || !account.accountId || account.providerId === "credential") throw new Error(`Cannot link an account "${account.providerId}:${account.accountId}"`);
|
|
17
|
+
if (await internalAdapter.findAccountByKey(account)) throw new Error(`The ${account.providerId} account ${account.accountId} signs in another user already`);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
13
20
|
/**
|
|
14
21
|
* Creates a user directly in the admin store. Host side only (no HTTP route creates accounts while the store is empty):
|
|
15
22
|
* the first user is always an `admin`, later ones get `role` (default `user`). The address counts as verified, so the
|
|
16
|
-
* user can also sign in with an emailed code.
|
|
23
|
+
* user can also sign in with an emailed code. `accounts` link the user to sign-in providers up front, so a provider
|
|
24
|
+
* signs them in by its own id whatever address it reports; one that signs in another user already is refused.
|
|
17
25
|
*/
|
|
18
26
|
const createUser = async (auth, input) => {
|
|
19
27
|
const first = !await hasUsers(auth);
|
|
20
28
|
const requested = input.role ?? auth.defaultRole;
|
|
21
29
|
if (!first && requested === void 0) throw new Error(`A role is required: choose one of ${auth.roles.join(", ")}`);
|
|
22
30
|
const role = first ? "admin" : checkRoles(auth, requested).join(",");
|
|
31
|
+
const accounts = input.accounts ?? [];
|
|
32
|
+
await checkAccounts(auth, accounts);
|
|
23
33
|
const { user } = await auth.api.createUser({ body: {
|
|
24
34
|
email: input.email,
|
|
25
35
|
password: input.password,
|
|
26
36
|
name: input.name ?? input.email.split("@")[0],
|
|
27
37
|
role
|
|
28
38
|
} });
|
|
39
|
+
const { internalAdapter } = await auth.$context;
|
|
40
|
+
for (const account of accounts) await internalAdapter.linkAccount({
|
|
41
|
+
userId: user.id,
|
|
42
|
+
...account
|
|
43
|
+
});
|
|
29
44
|
return {
|
|
30
45
|
id: user.id,
|
|
31
46
|
email: user.email,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { SignInPolicy, SignInRule, StaffAccessRule, defaultSignInPolicy, signInPolicyMethods } from "./better-auth/sign-in-policy.js";
|
|
2
2
|
import { StaffSignIn } from "./better-auth/staff-log.js";
|
|
3
3
|
import { OperatorProvider, readOperatorSettings } from "./better-auth/operator-provider.js";
|
|
4
|
+
import { PlatformSignIn, readPlatformSignIn } from "./better-auth/platform-sign-in.js";
|
|
4
5
|
import { AdminAuth, CreateAuthOptions, UserCreatedHook, createAuth, normalizeRoles } from "./better-auth/create-auth.js";
|
|
5
6
|
import { ResourceSource, configExports } from "./resource-source.js";
|
|
6
7
|
import { ChatMessage, ChatTextPart, Completion, CompletionOptions, ModelError, ModelSettings, ReasoningDetail, ReasoningEffort, ToolCall, ToolSpec, streamChatCompletion } from "./assistant/chat-completions.js";
|
|
@@ -20,8 +21,9 @@ import { JwtOptions, jwtAuthenticator } from "./auth/jwt.js";
|
|
|
20
21
|
import { HttpProblem, badRequest, forbidden, notFound, unauthorized } from "./problem.js";
|
|
21
22
|
import { BetterAuthAuthenticatorOptions, betterAuthAuthenticator } from "./better-auth/authenticator.js";
|
|
22
23
|
import { parseRoles } from "./better-auth/parse-roles.js";
|
|
23
|
-
import { NewUser, StoredUser, createUser, deleteUser, hasUsers, listUsers, roleChoices, setUserBanned, setUserRole } from "./better-auth/users.js";
|
|
24
|
+
import { LinkedAccount, NewUser, StoredUser, createUser, deleteUser, hasUsers, listUsers, roleChoices, setUserBanned, setUserRole } from "./better-auth/users.js";
|
|
24
25
|
import { IssueTokenOptions, issueToken, maxTokenTtlSeconds } from "./better-auth/tokens.js";
|
|
26
|
+
import { authSchemaMigration, checkAuthSchema } from "./better-auth/auth-schema.js";
|
|
25
27
|
import { AssistantBackend, assistantProtocolRoutes } from "./assistant/protocol-routes.js";
|
|
26
28
|
import { fileConversationStore } from "./assistant/file-conversation-store.js";
|
|
27
29
|
import { withCacheBreakpoints } from "./assistant/cache-breakpoints.js";
|
|
@@ -29,4 +31,4 @@ import { TurnOptions, runTurn } from "./assistant/turn.js";
|
|
|
29
31
|
import { WidgetOptions, maxWidgetPropsBytes, widget } from "./assistant/widget.js";
|
|
30
32
|
import { QueryOptions, QueryResult, runReadOnlyQuery, runReadWriteQuery } from "./assistant/run-query.js";
|
|
31
33
|
import { readOnlyQueryTool, readWriteQueryTool } from "./assistant/query-tools.js";
|
|
32
|
-
export { type Admin, type AdminAuth, type AdminOptions, type ApprovalAnswer, type ApprovalRequest, type AssistantBackend, type AssistantOptions, type AssistantSettings, type AssistantTool, type AuditEvent, type AuditQueue, type Authenticator, type BetterAuthAuthenticatorOptions, type ChatMessage, type ChatTextPart, type Completion, type CompletionOptions, type Conversation, type ConversationRecord, type ConversationStore, type ConversationsOptions, type CreateAdminInput, type CreateAuthOptions, HttpProblem, type IssueTokenOptions, type JwtOptions, ModelError, type ModelSettings, type NewUser, type OperatorProvider, type PipelineHook, type ProjectConfig, type QueryOptions, type QueryResult, type ReasoningDetail, type ReasoningEffort, type RecordKeyInput, type ResourceSource, type RuntimeOptions, type ScanGuardMode, type Session, type SignInPolicy, type SignInRule, type StaffAccessRule, type StaffSignIn, type StoredUser, type TenantValue, type ToolCall, type ToolContext, type ToolRecord, type ToolRecords, type ToolSpec, type TurnOptions, type User, type UserCreatedHook, type ViewSource, type WidgetOptions, type WriteEvent, type WriteOperation, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, badRequest, betterAuthAuthenticator, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createUser, defaultSignInPolicy, defineConfig, defineTool, deleteUser, fileConversationStore, forbidden, hasUsers, issueToken, jwtAuthenticator, listUsers, maxTokenTtlSeconds, maxWidgetPropsBytes, mergeConfig, normalizeRoles, notFound, parseRoles, readOnlyQueryTool, readOperatorSettings, readWriteQueryTool, requestApproval, roleChoices, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, seesAssistant, seesRuntime, setUserBanned, setUserRole, signInPolicyMethods, streamChatCompletion, unauthorized, widget, withCacheBreakpoints };
|
|
34
|
+
export { type Admin, type AdminAuth, type AdminOptions, type ApprovalAnswer, type ApprovalRequest, type AssistantBackend, type AssistantOptions, type AssistantSettings, type AssistantTool, type AuditEvent, type AuditQueue, type Authenticator, type BetterAuthAuthenticatorOptions, type ChatMessage, type ChatTextPart, type Completion, type CompletionOptions, type Conversation, type ConversationRecord, type ConversationStore, type ConversationsOptions, type CreateAdminInput, type CreateAuthOptions, HttpProblem, type IssueTokenOptions, type JwtOptions, type LinkedAccount, ModelError, type ModelSettings, type NewUser, type OperatorProvider, type PipelineHook, type PlatformSignIn, type ProjectConfig, type QueryOptions, type QueryResult, type ReasoningDetail, type ReasoningEffort, type RecordKeyInput, type ResourceSource, type RuntimeOptions, type ScanGuardMode, type Session, type SignInPolicy, type SignInRule, type StaffAccessRule, type StaffSignIn, type StoredUser, type TenantValue, type ToolCall, type ToolContext, type ToolRecord, type ToolRecords, type ToolSpec, type TurnOptions, type User, type UserCreatedHook, type ViewSource, type WidgetOptions, type WriteEvent, type WriteOperation, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaMigration, badRequest, betterAuthAuthenticator, checkAuthSchema, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createUser, defaultSignInPolicy, defineConfig, defineTool, deleteUser, fileConversationStore, forbidden, hasUsers, issueToken, jwtAuthenticator, listUsers, maxTokenTtlSeconds, maxWidgetPropsBytes, mergeConfig, normalizeRoles, notFound, parseRoles, readOnlyQueryTool, readOperatorSettings, readPlatformSignIn, readWriteQueryTool, requestApproval, roleChoices, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, seesAssistant, seesRuntime, setUserBanned, setUserRole, signInPolicyMethods, streamChatCompletion, unauthorized, widget, withCacheBreakpoints };
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import { ModelError, streamChatCompletion } from "./assistant/chat-completions.j
|
|
|
12
12
|
import { runTurn } from "./assistant/turn.js";
|
|
13
13
|
import { consoleAuditQueue } from "./audit/console-queue.js";
|
|
14
14
|
import { readOperatorSettings } from "./better-auth/operator-provider.js";
|
|
15
|
+
import { readPlatformSignIn } from "./better-auth/platform-sign-in.js";
|
|
15
16
|
import { defaultSignInPolicy, signInPolicyMethods } from "./better-auth/sign-in-policy.js";
|
|
16
17
|
import { parseRoles } from "./better-auth/parse-roles.js";
|
|
17
18
|
import { createAuth, normalizeRoles } from "./better-auth/create-auth.js";
|
|
@@ -23,5 +24,6 @@ import { defineConfig, mergeConfig } from "./project-config.js";
|
|
|
23
24
|
import { jwtAuthenticator } from "./auth/jwt.js";
|
|
24
25
|
import { betterAuthAuthenticator } from "./better-auth/authenticator.js";
|
|
25
26
|
import { issueToken, maxTokenTtlSeconds } from "./better-auth/tokens.js";
|
|
27
|
+
import { authSchemaMigration, checkAuthSchema } from "./better-auth/auth-schema.js";
|
|
26
28
|
import { maxWidgetPropsBytes, widget } from "./assistant/widget.js";
|
|
27
|
-
export { HttpProblem, ModelError, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, badRequest, betterAuthAuthenticator, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createUser, defaultSignInPolicy, defineConfig, defineTool, deleteUser, fileConversationStore, forbidden, hasUsers, issueToken, jwtAuthenticator, listUsers, maxTokenTtlSeconds, maxWidgetPropsBytes, mergeConfig, normalizeRoles, notFound, parseRoles, readOnlyQueryTool, readOperatorSettings, readWriteQueryTool, requestApproval, roleChoices, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, seesAssistant, seesRuntime, setUserBanned, setUserRole, signInPolicyMethods, streamChatCompletion, unauthorized, widget, withCacheBreakpoints };
|
|
29
|
+
export { HttpProblem, ModelError, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaMigration, badRequest, betterAuthAuthenticator, checkAuthSchema, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createUser, defaultSignInPolicy, defineConfig, defineTool, deleteUser, fileConversationStore, forbidden, hasUsers, issueToken, jwtAuthenticator, listUsers, maxTokenTtlSeconds, maxWidgetPropsBytes, mergeConfig, normalizeRoles, notFound, parseRoles, readOnlyQueryTool, readOperatorSettings, readPlatformSignIn, readWriteQueryTool, requestApproval, roleChoices, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, seesAssistant, seesRuntime, setUserBanned, setUserRole, signInPolicyMethods, streamChatCompletion, unauthorized, widget, withCacheBreakpoints };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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.
|
|
34
|
-
"@protobase/query": "0.
|
|
35
|
-
"@protobase/schema": "0.
|
|
33
|
+
"@protobase/layout": "0.6.0",
|
|
34
|
+
"@protobase/query": "0.6.0",
|
|
35
|
+
"@protobase/schema": "0.6.0",
|
|
36
36
|
"@scalar/hono-api-reference": "^0.12.10",
|
|
37
37
|
"@simplewebauthn/server": "^13.3.1",
|
|
38
38
|
"better-auth": "^1.7.7",
|