@protobase/server 0.53.6 → 0.53.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -160,6 +160,13 @@ export declare const buildAppAuth: (settings: AuthSettings, input: AppAuthInput)
|
|
|
160
160
|
handler: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<void>>;
|
|
161
161
|
}[];
|
|
162
162
|
};
|
|
163
|
+
} | {
|
|
164
|
+
id: "protobase-locked-accounts";
|
|
165
|
+
init: (ctx: import("better-auth").AuthContext) => {
|
|
166
|
+
context: {
|
|
167
|
+
adapter: import("better-auth").DBAdapter;
|
|
168
|
+
};
|
|
169
|
+
};
|
|
163
170
|
} | {
|
|
164
171
|
id: "admin";
|
|
165
172
|
version: string;
|
|
@@ -970,8 +977,8 @@ export declare const buildAppAuth: (settings: AuthSettings, input: AppAuthInput)
|
|
|
970
977
|
$Infer: {
|
|
971
978
|
body: {
|
|
972
979
|
permissions: {
|
|
973
|
-
readonly user?: ("create" | "list" | "set-role" | "ban" | "impersonate" | "
|
|
974
|
-
readonly session?: ("
|
|
980
|
+
readonly user?: ("create" | "update" | "delete" | "list" | "set-role" | "ban" | "impersonate" | "set-password" | "set-email" | "get" | "impersonate-admins")[] | undefined;
|
|
981
|
+
readonly session?: ("delete" | "list" | "revoke")[] | undefined;
|
|
975
982
|
};
|
|
976
983
|
} & {
|
|
977
984
|
userId?: string | undefined;
|
|
@@ -279,6 +279,13 @@ export declare const createAuth: (options: CreateAuthOptions, plugins?: AuthPlug
|
|
|
279
279
|
handler: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<void>>;
|
|
280
280
|
}[];
|
|
281
281
|
};
|
|
282
|
+
} | {
|
|
283
|
+
id: "protobase-locked-accounts";
|
|
284
|
+
init: (ctx: import("better-auth").AuthContext) => {
|
|
285
|
+
context: {
|
|
286
|
+
adapter: import("better-auth").DBAdapter;
|
|
287
|
+
};
|
|
288
|
+
};
|
|
282
289
|
} | {
|
|
283
290
|
id: "admin";
|
|
284
291
|
version: string;
|
|
@@ -1089,8 +1096,8 @@ export declare const createAuth: (options: CreateAuthOptions, plugins?: AuthPlug
|
|
|
1089
1096
|
$Infer: {
|
|
1090
1097
|
body: {
|
|
1091
1098
|
permissions: {
|
|
1092
|
-
readonly user?: ("create" | "list" | "set-role" | "ban" | "impersonate" | "
|
|
1093
|
-
readonly session?: ("
|
|
1099
|
+
readonly user?: ("create" | "update" | "delete" | "list" | "set-role" | "ban" | "impersonate" | "set-password" | "set-email" | "get" | "impersonate-admins")[] | undefined;
|
|
1100
|
+
readonly session?: ("delete" | "list" | "revoke")[] | undefined;
|
|
1094
1101
|
};
|
|
1095
1102
|
} & {
|
|
1096
1103
|
userId?: string | undefined;
|
|
@@ -1357,7 +1364,7 @@ export declare const createAuth: (options: CreateAuthOptions, plugins?: AuthPlug
|
|
|
1357
1364
|
expiresAt: Date;
|
|
1358
1365
|
token: string;
|
|
1359
1366
|
ipAddress?: string | null | undefined;
|
|
1360
|
-
userAgent
|
|
1367
|
+
userAgent? /** Public origin of the API, for example `https://admin.example.com`. Issuer and audience of the tokens; new passkeys are made for its host unless `passkeyDomain` names another. */ : string | null | undefined;
|
|
1361
1368
|
};
|
|
1362
1369
|
user: Record<string, any> & {
|
|
1363
1370
|
id: string;
|
|
@@ -1945,7 +1952,9 @@ export declare const createAuth: (options: CreateAuthOptions, plugins?: AuthPlug
|
|
|
1945
1952
|
email: string;
|
|
1946
1953
|
emailVerified: boolean;
|
|
1947
1954
|
name: string;
|
|
1948
|
-
image
|
|
1955
|
+
image?
|
|
1956
|
+
/** The app's name, from `PROTOBASE_APP_NAME` or `appName`; default `Protobase`. */
|
|
1957
|
+
/** The app's name, from `PROTOBASE_APP_NAME` or `appName`; default `Protobase`. */ : string | null | undefined;
|
|
1949
1958
|
};
|
|
1950
1959
|
}>;
|
|
1951
1960
|
generateTOTP: import("better-call").StrictEndpoint<string, {
|
|
@@ -5014,7 +5023,7 @@ export declare const createAuth: (options: CreateAuthOptions, plugins?: AuthPlug
|
|
|
5014
5023
|
organization: {
|
|
5015
5024
|
name?: string;
|
|
5016
5025
|
slug?: string;
|
|
5017
|
-
logo
|
|
5026
|
+
logo? /** Encrypt the OAuth access, refresh and ID tokens Better Auth stores for social sign-ins, with `secret`. Default off. */ : string | null;
|
|
5018
5027
|
metadata?: Record<string, any>;
|
|
5019
5028
|
[key: string]: any;
|
|
5020
5029
|
};
|
|
@@ -18,6 +18,7 @@ import { lastAdminGuardPlugin } from "./last-admin-guard.js";
|
|
|
18
18
|
import { setPasswordPath, setPasswordPlugin } from "./set-password.js";
|
|
19
19
|
import { signUpPaths, signUpPlugin } from "./sign-up/sign-up-plugin.js";
|
|
20
20
|
import { leakedPasswordsPlugin } from "./leaked-passwords.js";
|
|
21
|
+
import { lockedAccountsPlugin, readLockedAccounts } from "./locked-accounts.js";
|
|
21
22
|
import { userDeletion } from "./user-deletion.js";
|
|
22
23
|
import { revokeMcpGrants } from "./mcp/revoke-grants.js";
|
|
23
24
|
import { mcpUnusedPaths, resolveMcp } from "./mcp/mcp-plugins.js";
|
|
@@ -62,6 +63,7 @@ const platformOperator = () => readOperatorSettings(globalThis.process?.env ?? {
|
|
|
62
63
|
const platformSignInSettings = () => readPlatformSignIn(globalThis.process?.env ?? {});
|
|
63
64
|
const platformOrigins = () => readOrigins(globalThis.process?.env ?? {});
|
|
64
65
|
const platformPasskeyDomain = () => readPasskeyDomain(globalThis.process?.env ?? {});
|
|
66
|
+
const platformLockedAccounts = () => readLockedAccounts(globalThis.process?.env ?? {});
|
|
65
67
|
const tokenLifetime = "15m";
|
|
66
68
|
/**
|
|
67
69
|
* Better Auth for a single-tenant admin: sign-in with a password, an emailed code or a passkey, two-factor
|
|
@@ -88,7 +90,7 @@ const organizationPaths = ["/organization/set-active", "/organization/accept-inv
|
|
|
88
90
|
* The options `createAuth` gives Better Auth, with the roles, default role, mailer, operator provider and platform
|
|
89
91
|
* sign-in provider already resolved.
|
|
90
92
|
*/
|
|
91
|
-
const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, definitions, defaultRole, appName = defaultAppName, mailer, signUpRole, operator, platformSignIn, saml = [], organizations, mcp, context = noContext, signInPolicy = defaultSignInPolicy, mobile, deletion, ...plugins }) => {
|
|
93
|
+
const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, definitions, defaultRole, appName = defaultAppName, mailer, signUpRole, operator, platformSignIn, saml = [], organizations, mcp, context = noContext, signInPolicy = defaultSignInPolicy, mobile, deletion, lockedAccounts = [], ...plugins }) => {
|
|
92
94
|
const secure = options.baseURL.startsWith("https://");
|
|
93
95
|
const signIn = platformSignInFor(platformSignIn, Object.keys(options.socialProviders ?? {}));
|
|
94
96
|
const hosts = passkeyHosts([options.baseURL, ...origins]);
|
|
@@ -177,6 +179,7 @@ const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, defini
|
|
|
177
179
|
...options.encryptOAuthTokens && { encryptOAuthTokens: true }
|
|
178
180
|
},
|
|
179
181
|
plugins: [
|
|
182
|
+
...lockedAccounts.length > 0 ? [lockedAccountsPlugin(lockedAccounts)] : [],
|
|
180
183
|
admin({
|
|
181
184
|
schema: pluginNames.admin,
|
|
182
185
|
defaultRole: defaultRole ?? "unassigned",
|
|
@@ -362,10 +365,12 @@ const createAuth = (options, plugins = {}) => {
|
|
|
362
365
|
const context = () => instance?.$context ?? Promise.reject(/* @__PURE__ */ new Error("The auth instance is not made yet"));
|
|
363
366
|
const mobile = mobileRequests();
|
|
364
367
|
const deletion = userDeletion();
|
|
368
|
+
const lockedAccounts = platformLockedAccounts();
|
|
365
369
|
const authOptions = betterAuthOptions(options, {
|
|
366
370
|
...plugins,
|
|
367
371
|
mobile,
|
|
368
372
|
deletion,
|
|
373
|
+
lockedAccounts,
|
|
369
374
|
origins,
|
|
370
375
|
passkeyDomain,
|
|
371
376
|
roles,
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { APIError } from "better-auth/api";
|
|
2
|
+
//#region src/better-auth/locked-accounts.ts
|
|
3
|
+
const lockedAccountsVariable = "PROTOBASE_LOCKED_ACCOUNTS";
|
|
4
|
+
/** The addresses the platform passes in `PROTOBASE_LOCKED_ACCOUNTS`, comma separated, in lower case; none when it is unset. */
|
|
5
|
+
const readLockedAccounts = (env) => [...new Set((env["PROTOBASE_LOCKED_ACCOUNTS"] ?? "").split(",").map((email) => email.trim().toLowerCase()).filter(Boolean))];
|
|
6
|
+
const lockedFields = {
|
|
7
|
+
user: (field) => [
|
|
8
|
+
"email",
|
|
9
|
+
"emailVerified",
|
|
10
|
+
"twoFactorEnabled"
|
|
11
|
+
].includes(field),
|
|
12
|
+
account: (field) => [
|
|
13
|
+
"userId",
|
|
14
|
+
"providerId",
|
|
15
|
+
"accountId",
|
|
16
|
+
"password"
|
|
17
|
+
].includes(field),
|
|
18
|
+
passkey: (field) => !["counter", "updatedAt"].includes(field),
|
|
19
|
+
twoFactor: (field) => ![
|
|
20
|
+
"failedVerificationCount",
|
|
21
|
+
"lockedUntil",
|
|
22
|
+
"updatedAt"
|
|
23
|
+
].includes(field)
|
|
24
|
+
};
|
|
25
|
+
const refuse = () => {
|
|
26
|
+
throw new APIError("FORBIDDEN", {
|
|
27
|
+
code: "ACCOUNT_LOCKED",
|
|
28
|
+
message: "This account is locked: its address, password, passkeys, two-factor authentication and linked accounts stay as they are, and it cannot be deleted."
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* The adapter with the accounts of `emails` locked: their user's address, password, passkeys, two-factor
|
|
33
|
+
* authentication and linked accounts stay as they are, and the user is not deleted, whatever endpoint or host call
|
|
34
|
+
* writes. A locked account's first account row, made with its user, is the one write of those it allows.
|
|
35
|
+
*/
|
|
36
|
+
const lockAccounts = (adapter, emails) => {
|
|
37
|
+
const lockedIds = async () => new Set((await adapter.findMany({
|
|
38
|
+
model: "user",
|
|
39
|
+
where: [{
|
|
40
|
+
field: "email",
|
|
41
|
+
operator: "in",
|
|
42
|
+
value: emails
|
|
43
|
+
}],
|
|
44
|
+
select: ["id"]
|
|
45
|
+
})).map((user) => user.id));
|
|
46
|
+
const matchesLocked = async (model, where) => {
|
|
47
|
+
const ids = await lockedIds();
|
|
48
|
+
if (ids.size === 0) return false;
|
|
49
|
+
const owner = model === "user" ? "id" : "userId";
|
|
50
|
+
return (await adapter.findMany({
|
|
51
|
+
model,
|
|
52
|
+
where,
|
|
53
|
+
select: [owner]
|
|
54
|
+
})).some((row) => ids.has(row[owner]));
|
|
55
|
+
};
|
|
56
|
+
const checkUpdate = async (model, where, fields) => {
|
|
57
|
+
const locked = lockedFields[model];
|
|
58
|
+
if (locked && fields.some(locked) && await matchesLocked(model, where)) refuse();
|
|
59
|
+
};
|
|
60
|
+
const checkDelete = async (model, where) => {
|
|
61
|
+
if (lockedFields[model] && await matchesLocked(model, where)) refuse();
|
|
62
|
+
};
|
|
63
|
+
const checkCreate = async (model, data) => {
|
|
64
|
+
if (model === "user" || !lockedFields[model]) return;
|
|
65
|
+
if (!(await lockedIds()).has(data.userId)) return;
|
|
66
|
+
if (model === "account" && await adapter.count({
|
|
67
|
+
model: "account",
|
|
68
|
+
where: [{
|
|
69
|
+
field: "userId",
|
|
70
|
+
value: data.userId
|
|
71
|
+
}]
|
|
72
|
+
}) === 0) return;
|
|
73
|
+
refuse();
|
|
74
|
+
};
|
|
75
|
+
return {
|
|
76
|
+
...adapter,
|
|
77
|
+
create: async (data) => {
|
|
78
|
+
await checkCreate(data.model, data.data);
|
|
79
|
+
return adapter.create(data);
|
|
80
|
+
},
|
|
81
|
+
update: async (data) => {
|
|
82
|
+
await checkUpdate(data.model, data.where, Object.keys(data.update));
|
|
83
|
+
return adapter.update(data);
|
|
84
|
+
},
|
|
85
|
+
updateMany: async (data) => {
|
|
86
|
+
await checkUpdate(data.model, data.where, Object.keys(data.update));
|
|
87
|
+
return adapter.updateMany(data);
|
|
88
|
+
},
|
|
89
|
+
incrementOne: async (data) => {
|
|
90
|
+
await checkUpdate(data.model, data.where, [...Object.keys(data.increment), ...Object.keys(data.set ?? {})]);
|
|
91
|
+
return adapter.incrementOne(data);
|
|
92
|
+
},
|
|
93
|
+
delete: async (data) => {
|
|
94
|
+
await checkDelete(data.model, data.where);
|
|
95
|
+
return adapter.delete(data);
|
|
96
|
+
},
|
|
97
|
+
deleteMany: async (data) => {
|
|
98
|
+
await checkDelete(data.model, data.where);
|
|
99
|
+
return adapter.deleteMany(data);
|
|
100
|
+
},
|
|
101
|
+
consumeOne: async (data) => {
|
|
102
|
+
await checkDelete(data.model, data.where);
|
|
103
|
+
return adapter.consumeOne(data);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* Locks the accounts of `emails` (see `lockAccounts`) by guarding the auth store's adapter, which Better Auth's own
|
|
109
|
+
* writes, its plugins' (passkeys and two-factor authentication write the store directly, past the database hooks)
|
|
110
|
+
* and the host calls all go through, also in a transaction.
|
|
111
|
+
*/
|
|
112
|
+
const lockedAccountsPlugin = (emails) => ({
|
|
113
|
+
id: "protobase-locked-accounts",
|
|
114
|
+
init: (ctx) => {
|
|
115
|
+
const adapter = ctx.adapter;
|
|
116
|
+
return { context: { adapter: {
|
|
117
|
+
...lockAccounts(adapter, emails),
|
|
118
|
+
transaction: (callback) => adapter.transaction((trx) => callback(lockAccounts(trx, emails)))
|
|
119
|
+
} } };
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
//#endregion
|
|
123
|
+
export { lockAccounts, lockedAccountsPlugin, lockedAccountsVariable, readLockedAccounts };
|
package/dist/package.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/server",
|
|
3
|
-
"version": "0.53.
|
|
3
|
+
"version": "0.53.7",
|
|
4
4
|
"description": "The Protobase REST API as a Hono app, with Better Auth sign-in.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -40,9 +40,9 @@
|
|
|
40
40
|
"@better-auth/passkey": "^1.7.12",
|
|
41
41
|
"@better-auth/sso": "^1.7.12",
|
|
42
42
|
"@modelcontextprotocol/server": "^2.3.1",
|
|
43
|
-
"@protobase/layout": "0.53.
|
|
44
|
-
"@protobase/query": "0.53.
|
|
45
|
-
"@protobase/schema": "0.53.
|
|
43
|
+
"@protobase/layout": "0.53.7",
|
|
44
|
+
"@protobase/query": "0.53.7",
|
|
45
|
+
"@protobase/schema": "0.53.7",
|
|
46
46
|
"@react-email/render": "^2.1.0",
|
|
47
47
|
"@scalar/hono-api-reference": "^0.12.10",
|
|
48
48
|
"@sentry/core": "^11.7.0",
|