@protobase/server 0.29.0 → 0.29.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/auth/jwt.js CHANGED
@@ -32,8 +32,8 @@ const jwtVerifier = (options) => {
32
32
  })).payload;
33
33
  };
34
34
  };
35
- /** Verifies `Authorization: Bearer <jwt>` against a JWKS; `sub` is the user id. Without the header, `MissingCredentials`. */
36
- const jwtAuthenticator = (options) => {
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
- user: {
52
- id: sub,
53
- roles: [.../* @__PURE__ */ new Set([...rolesOf(roles), ...extraRoles])]
54
- },
55
- ...tenantValue !== void 0 && { tenant: tenantValue }
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 `apps`, the tokens of those apps are admitted too, with the roles each gives its users here.
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 { jwtAuthenticator } from "../auth/jwt.js";
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 `apps`, the tokens of those apps are admitted too, with the roles each gives its users here.
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 verify = jwtAuthenticator({
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;
@@ -94,6 +94,7 @@ const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, defini
94
94
  };
95
95
  return {
96
96
  email: user.email,
97
+ sid: session.id,
97
98
  ...narrowClaims(claims, [activeRole, user.lastRole], Boolean(organizations))
98
99
  };
99
100
  }
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/server",
3
- "version": "0.29.0",
3
+ "version": "0.29.1",
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.29.0",
36
- "@protobase/query": "0.29.0",
37
- "@protobase/schema": "0.29.0",
35
+ "@protobase/layout": "0.29.1",
36
+ "@protobase/query": "0.29.1",
37
+ "@protobase/schema": "0.29.1",
38
38
  "@react-email/render": "^2.1.0",
39
39
  "@scalar/hono-api-reference": "^0.12.10",
40
40
  "@sentry/core": "^11.7.0",