@protobase/server 0.29.0 → 0.30.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/auth/jwt.js +16 -8
- package/dist/better-auth/authenticator.d.ts +4 -1
- package/dist/better-auth/authenticator.js +13 -3
- package/dist/better-auth/create-auth.d.ts +4 -0
- package/dist/better-auth/create-auth.js +1 -0
- package/dist/better-auth/token-standing.js +55 -0
- package/dist/forms/rate-limit.js +3 -2
- package/package.json +4 -4
package/dist/auth/jwt.js
CHANGED
|
@@ -32,8 +32,8 @@ const jwtVerifier = (options) => {
|
|
|
32
32
|
})).payload;
|
|
33
33
|
};
|
|
34
34
|
};
|
|
35
|
-
/**
|
|
36
|
-
const
|
|
35
|
+
/** Like `jwtAuthenticator`, and also returns the verified claims, for an authenticator that checks more of them. */
|
|
36
|
+
const jwtSessions = (options) => {
|
|
37
37
|
const { rolesClaim = "roles", tenantClaim = "tenant" } = options;
|
|
38
38
|
const verify = jwtVerifier(options);
|
|
39
39
|
return async (request) => {
|
|
@@ -48,13 +48,21 @@ const jwtAuthenticator = (options) => {
|
|
|
48
48
|
const claimed = typeof tenant === "string" || typeof tenant === "number" ? tenant : void 0;
|
|
49
49
|
const tenantValue = options.tenant === void 0 ? claimed : typeof options.tenant === "function" ? await options.tenant() : options.tenant;
|
|
50
50
|
return {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
payload,
|
|
52
|
+
session: {
|
|
53
|
+
user: {
|
|
54
|
+
id: sub,
|
|
55
|
+
roles: [.../* @__PURE__ */ new Set([...rolesOf(roles), ...extraRoles])]
|
|
56
|
+
},
|
|
57
|
+
...tenantValue !== void 0 && { tenant: tenantValue }
|
|
58
|
+
}
|
|
56
59
|
};
|
|
57
60
|
};
|
|
58
61
|
};
|
|
62
|
+
/** Verifies `Authorization: Bearer <jwt>` against a JWKS; `sub` is the user id. Without the header, `MissingCredentials`. */
|
|
63
|
+
const jwtAuthenticator = (options) => {
|
|
64
|
+
const sessions = jwtSessions(options);
|
|
65
|
+
return async (request) => (await sessions(request)).session;
|
|
66
|
+
};
|
|
59
67
|
//#endregion
|
|
60
|
-
export { jwtAuthenticator, jwtVerifier };
|
|
68
|
+
export { jwtAuthenticator, jwtSessions, jwtVerifier };
|
|
@@ -28,7 +28,10 @@ export declare const betterAuthTokens: (options: Pick<BetterAuthAuthenticatorOpt
|
|
|
28
28
|
* Verifies the bearer JWT that Better Auth's jwt plugin issues. `sub` is the user id, the `role` claim (a comma
|
|
29
29
|
* separated string from the admin plugin) becomes `roles`. With organizations, the `org` claim is the tenant and the
|
|
30
30
|
* organization, and the membership's `app_roles` are added to the global roles; `user`, which stands for no global
|
|
31
|
-
* role, is left out. With `
|
|
31
|
+
* role, is left out. With `auth`, every request also reads the store to check that the token still stands
|
|
32
|
+
* (`checkTokenStanding`), so a ban, a lowered role, a deleted user or a sign-out ends it at once; with `jwksUrl` alone
|
|
33
|
+
* a token works until it expires. With `apps`, the tokens of those apps are admitted too, with the roles each gives
|
|
34
|
+
* its users here.
|
|
32
35
|
*/
|
|
33
36
|
export declare const betterAuthAuthenticator: (options: BetterAuthAuthenticatorOptions) => Authenticator;
|
|
34
37
|
//#endregion
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jwtSessions } from "../auth/jwt.js";
|
|
2
2
|
import { noGlobalRole } from "./organizations/role-definitions.js";
|
|
3
3
|
import { cachedKeys } from "./jwks-cache.js";
|
|
4
4
|
import { withTrustedApps } from "./app-tokens.js";
|
|
5
|
+
import { checkTokenStanding, tokenClaims } from "./token-standing.js";
|
|
5
6
|
//#region src/better-auth/authenticator.ts
|
|
6
7
|
/** How the tokens of this Better Auth instance verify: its key set, and its issuer as both issuer and audience. */
|
|
7
8
|
const betterAuthTokens = (options) => {
|
|
@@ -18,13 +19,16 @@ const betterAuthTokens = (options) => {
|
|
|
18
19
|
* Verifies the bearer JWT that Better Auth's jwt plugin issues. `sub` is the user id, the `role` claim (a comma
|
|
19
20
|
* separated string from the admin plugin) becomes `roles`. With organizations, the `org` claim is the tenant and the
|
|
20
21
|
* organization, and the membership's `app_roles` are added to the global roles; `user`, which stands for no global
|
|
21
|
-
* role, is left out. With `
|
|
22
|
+
* role, is left out. With `auth`, every request also reads the store to check that the token still stands
|
|
23
|
+
* (`checkTokenStanding`), so a ban, a lowered role, a deleted user or a sign-out ends it at once; with `jwksUrl` alone
|
|
24
|
+
* a token works until it expires. With `apps`, the tokens of those apps are admitted too, with the roles each gives
|
|
25
|
+
* its users here.
|
|
22
26
|
*/
|
|
23
27
|
const betterAuthAuthenticator = (options) => {
|
|
24
28
|
const tokens = betterAuthTokens(options);
|
|
25
29
|
const { issuer } = tokens;
|
|
26
30
|
const organizations = options.organizations ?? Boolean(options.auth?.organizations);
|
|
27
|
-
const
|
|
31
|
+
const sessions = jwtSessions({
|
|
28
32
|
...tokens,
|
|
29
33
|
rolesClaim: "role",
|
|
30
34
|
...organizations && {
|
|
@@ -33,6 +37,12 @@ const betterAuthAuthenticator = (options) => {
|
|
|
33
37
|
},
|
|
34
38
|
...options.tenant !== void 0 && { tenant: options.tenant }
|
|
35
39
|
});
|
|
40
|
+
const { auth } = options;
|
|
41
|
+
const verify = async (request) => {
|
|
42
|
+
const { payload, session } = await sessions(request);
|
|
43
|
+
if (auth) await checkTokenStanding(await auth.$context, tokenClaims(payload, session.user.roles, organizations ? session.tenant : void 0), organizations);
|
|
44
|
+
return session;
|
|
45
|
+
};
|
|
36
46
|
return withTrustedApps(!organizations ? verify : async (request) => {
|
|
37
47
|
const session = await verify(request);
|
|
38
48
|
const user = {
|
|
@@ -1165,24 +1165,28 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
1165
1165
|
}) => Promise<{
|
|
1166
1166
|
role: any;
|
|
1167
1167
|
email: string;
|
|
1168
|
+
sid: string;
|
|
1168
1169
|
} | {
|
|
1169
1170
|
org?: undefined;
|
|
1170
1171
|
org_role?: undefined;
|
|
1171
1172
|
app_roles?: undefined;
|
|
1172
1173
|
role: any;
|
|
1173
1174
|
email: string;
|
|
1175
|
+
sid: string;
|
|
1174
1176
|
} | {
|
|
1175
1177
|
org: string;
|
|
1176
1178
|
org_role: OrganizationRole;
|
|
1177
1179
|
app_roles: string[];
|
|
1178
1180
|
role: any;
|
|
1179
1181
|
email: string;
|
|
1182
|
+
sid: string;
|
|
1180
1183
|
} | {
|
|
1181
1184
|
org: string;
|
|
1182
1185
|
org_role?: undefined;
|
|
1183
1186
|
app_roles?: undefined;
|
|
1184
1187
|
role: any;
|
|
1185
1188
|
email: string;
|
|
1189
|
+
sid: string;
|
|
1186
1190
|
}>;
|
|
1187
1191
|
};
|
|
1188
1192
|
disableSettingJwtHeader: true;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { unauthorized } from "../problem.js";
|
|
2
|
+
import { noGlobalRole } from "./organizations/role-definitions.js";
|
|
3
|
+
import { parseRoles } from "./parse-roles.js";
|
|
4
|
+
import { effectiveRoles, globalRoles } from "./organizations/app-roles.js";
|
|
5
|
+
import { findMember, findOrganization } from "./organizations/store.js";
|
|
6
|
+
//#region src/better-auth/token-standing.ts
|
|
7
|
+
const tokenClaims = (payload, roles, tenant) => ({
|
|
8
|
+
userId: payload.sub,
|
|
9
|
+
...typeof payload.sid === "string" && { sessionId: payload.sid },
|
|
10
|
+
roles: roles.filter((role) => role !== noGlobalRole),
|
|
11
|
+
...typeof tenant === "string" && { organizationId: tenant }
|
|
12
|
+
});
|
|
13
|
+
const revoked = () => unauthorized("The bearer token was revoked: get a new one");
|
|
14
|
+
const bannedNow = (user, now) => Boolean(user.banned) && (!user.banExpires || user.banExpires > now);
|
|
15
|
+
const heldWhere = async ({ adapter }, user, organizations, organizationId) => {
|
|
16
|
+
if (!organizations) return parseRoles(user.role);
|
|
17
|
+
const global = globalRoles(user.role);
|
|
18
|
+
if (!organizationId) return global;
|
|
19
|
+
const member = await findMember(adapter, {
|
|
20
|
+
organizationId,
|
|
21
|
+
userId: user.id
|
|
22
|
+
});
|
|
23
|
+
if (member) return effectiveRoles(global, member.appRoles);
|
|
24
|
+
return global.length > 0 && await findOrganization(adapter, organizationId) !== null ? global : void 0;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Whether a token still stands, read from the store on every request: its user exists and is not banned, its session
|
|
28
|
+
* is not signed out or expired, and each role it carries is one the user still holds there. A token that fails is a
|
|
29
|
+
* 401, so the client asks its session for a new one, which carries the roles as they are now or is refused.
|
|
30
|
+
*/
|
|
31
|
+
const checkTokenStanding = async (context, claims, organizations, now = /* @__PURE__ */ new Date()) => {
|
|
32
|
+
const { adapter } = context;
|
|
33
|
+
if (claims.sessionId !== void 0) {
|
|
34
|
+
const session = await adapter.findOne({
|
|
35
|
+
model: "session",
|
|
36
|
+
where: [{
|
|
37
|
+
field: "id",
|
|
38
|
+
value: claims.sessionId
|
|
39
|
+
}]
|
|
40
|
+
});
|
|
41
|
+
if (!session || session.userId !== claims.userId || session.expiresAt <= now) throw revoked();
|
|
42
|
+
}
|
|
43
|
+
const user = await adapter.findOne({
|
|
44
|
+
model: "user",
|
|
45
|
+
where: [{
|
|
46
|
+
field: "id",
|
|
47
|
+
value: claims.userId
|
|
48
|
+
}]
|
|
49
|
+
});
|
|
50
|
+
if (!user || bannedNow(user, now)) throw revoked();
|
|
51
|
+
const held = await heldWhere(context, user, organizations, claims.organizationId);
|
|
52
|
+
if (!held || claims.roles.some((role) => !held.includes(role))) throw revoked();
|
|
53
|
+
};
|
|
54
|
+
//#endregion
|
|
55
|
+
export { checkTokenStanding, tokenClaims };
|
package/dist/forms/rate-limit.js
CHANGED
|
@@ -3,8 +3,9 @@ import { getIP } from "better-auth/api";
|
|
|
3
3
|
const windowMs = 6e4;
|
|
4
4
|
/**
|
|
5
5
|
* The client a request counts against, as Better Auth's sign-in limit reads it: an `X-Forwarded-For` of one valid
|
|
6
|
-
* address (IPv6 by its /64),
|
|
7
|
-
*
|
|
6
|
+
* address (IPv6 by its /64), which `protobase serve` sets from the connection and its trusted proxies, or `unknown`,
|
|
7
|
+
* one bucket for every request without one (several addresses included, since the leftmost is whatever the client
|
|
8
|
+
* sent). In development and tests a request without one is `127.0.0.1`.
|
|
8
9
|
*/
|
|
9
10
|
const clientAddress = (headers) => getIP(headers, {}) ?? "unknown";
|
|
10
11
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.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": {
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@better-auth/passkey": "^1.7.7",
|
|
34
34
|
"@better-auth/sso": "^1.7.7",
|
|
35
|
-
"@protobase/layout": "0.
|
|
36
|
-
"@protobase/query": "0.
|
|
37
|
-
"@protobase/schema": "0.
|
|
35
|
+
"@protobase/layout": "0.30.0",
|
|
36
|
+
"@protobase/query": "0.30.0",
|
|
37
|
+
"@protobase/schema": "0.30.0",
|
|
38
38
|
"@react-email/render": "^2.1.0",
|
|
39
39
|
"@scalar/hono-api-reference": "^0.12.10",
|
|
40
40
|
"@sentry/core": "^11.7.0",
|