@shivaedev/mcp 0.1.11 → 0.2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.1 - 2026-10-31
4
+
5
+ ### Changed
6
+
7
+ - Update `@shivaedev/access` from 0.3.3 to 0.3.4.
8
+ - Update `@shivaedev/errors` from 0.3.0 to 0.4.0.
9
+ - Update `@shivaedev/identity` from 0.12.0 to 0.13.0.
10
+
11
+ ## 0.2.0 - 2026-10-31
12
+
13
+ ### Added
14
+
15
+ - Return `revokeClient(userId, clientId)` and `revokeAllClients(userId)` Effects from `betterAuthMcp` to delete consent and stored access and refresh tokens.
16
+ - Allow provider `scopes`, including `offline_access`, and an in-process `jwks` source for token verification.
17
+
18
+ ### Fixed
19
+
20
+ - Refuse banned users and access tokens whose client consent no longer exists with the MCP unauthorized resource challenge.
21
+
22
+ ### Breaking
23
+
24
+ - Custom Better Auth wrappers passed to `betterAuthMcp` must expose the native context's adapter as well as its user lookup. Pass the full Better Auth instance. Custom signed tokens must identify their OAuth client and have stored consent for that user and client.
25
+
3
26
  ## 0.1.11 - 2026-10-31
4
27
 
5
28
  ### Changed
package/README.md CHANGED
@@ -67,7 +67,7 @@ Application roles and grants come from `@shivaedev/access`; identity resolves th
67
67
 
68
68
  ### Connect OAuth to identity and the host
69
69
 
70
- The Better Auth adapter is an optional edge module. Add its provider to the auth instance used by identity, alongside Better Auth's JWT plugin. `mcpProvider` configures the `mcp` scope and the endpoint's OAuth resource. The application's login and consent pages implement the provider's ordinary UI flow; the provider owns authorization codes, PKCE and tokens.
70
+ The Better Auth adapter is an optional edge module. Add its provider to the auth instance used by identity, alongside Better Auth's JWT plugin. `mcpProvider` defaults to the `mcp` scope and configures the endpoint's OAuth resource. Pass `scopes: ["mcp", "offline_access"]` to allow refresh tokens; the client must also register the `refresh_token` grant and request `offline_access` during authorization. The application's login and consent pages implement the provider's ordinary UI flow; the provider owns authorization codes, PKCE and tokens.
71
71
 
72
72
  ```ts
73
73
  import { admin, jwt } from "better-auth/plugins";
@@ -121,7 +121,36 @@ const application = Effect.gen(function* () {
121
121
 
122
122
  `authPool`, `Api` and `Handlers` are the application's existing auth database, RPC group and handlers. Run the Better Auth migrations with the OAuth and JWT plugins included. For existing role management, keep the application's existing role plugin and `RoleSet` in this configuration.
123
123
 
124
- The edge adapter verifies access-token signatures, issuer, endpoint audience and `mcp` scope, then loads the current user through Better Auth. Identity resolves the stored role on each request. An invalid token receives `401` with a `WWW-Authenticate` protected-resource metadata pointer. The metadata names the authorization server and its discovery endpoint, so a client can start OAuth. MCP bearer credentials are separate from browser session cookies.
124
+ The edge adapter verifies access-token signatures, issuer, endpoint audience and `mcp` scope, then loads the current user and the token client's consent through Better Auth. Banned users and clients without consent receive the same unauthorized response as invalid tokens. Identity resolves the stored role on each request. An invalid token receives `401` with a `WWW-Authenticate` protected-resource metadata pointer. The metadata names the authorization server and its discovery endpoint, so a client can start OAuth. MCP bearer credentials are separate from browser session cookies.
125
+
126
+ ### Revoke client grants
127
+
128
+ The adapter returns `revokeClient(userId, clientId)` and `revokeAllClients(userId)` as Effects that fail with `AuthUnavailable` when storage fails. The application authorizes these operations. Each deletes matching consent, stored access tokens and refresh tokens through Better Auth's adapter. Use the first when disconnecting a client and the second after a password reset. JWT access tokens are refused because each request requires the client's consent.
129
+
130
+ ```ts
131
+ yield* oauth.revokeClient(userId, clientId);
132
+ yield* oauth.revokeAllClients(userId);
133
+ ```
134
+
135
+ Deleting consent through Better Auth also refuses MCP access immediately. Use these revocation methods to delete refresh tokens as well.
136
+
137
+ ### Dynamic origins and local keys
138
+
139
+ When Better Auth uses a dynamic `baseURL`, give it a `fallback`, for example `{ allowedHosts: ["app.example.com"], fallback: "https://app.example.com" }`. The adapter loads OAuth metadata outside a request, so Better Auth needs that origin to determine the issuer.
140
+
141
+ By default verification fetches the authorization server's JWKS URL. Pass `jwks` to use an in-process public key source instead, including on story hosts whose public origin has no HTTP route:
142
+
143
+ ```ts
144
+ const oauth = yield* betterAuthMcp({
145
+ auth: mounted.auth,
146
+ resource,
147
+ origin,
148
+ roles: Roles,
149
+ jwks: () => mounted.auth.api.getJwks(),
150
+ });
151
+ ```
152
+
153
+ The callback returns the public JWK set from the JWT plugin. Signature, issuer, audience and `mcp` scope checks still apply. Signing keys stay with the auth instance.
125
154
 
126
155
  ### Install and limits
127
156
 
@@ -0,0 +1,6 @@
1
+ import { verifyJwsAccessToken } from "better-auth/oauth2";
2
+ import { Effect, Option } from "effect";
3
+ import { AuthUnavailable } from "@shivaedev/errors/taxonomy.ts";
4
+ export type JwksSource = Parameters<typeof verifyJwsAccessToken>[1]["jwksFetch"];
5
+ export declare function bearer(jwks: JwksSource, audience: string, issuer: string): (token: string | undefined) => Effect.Effect<Option.Option<never> | Option.Option<import("better-auth").JWTPayload>, AuthUnavailable, never>;
6
+ //# sourceMappingURL=bearer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bearer.d.ts","sourceRoot":"","sources":["../../src/adapters/bearer.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAExC,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAEhE,MAAM,MAAM,UAAU,GAAG,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC;AAyBjF,wBAAgB,MAAM,CAAC,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,WACzD,MAAM,GAAG,SAAS,mHAejC"}
@@ -0,0 +1,37 @@
1
+ import { APIError, isAPIError } from "better-auth/api";
2
+ import { verifyJwsAccessToken } from "better-auth/oauth2";
3
+ import { Effect, Option } from "effect";
4
+ import { redact } from "@shivaedev/errors/redact.ts";
5
+ import { AuthUnavailable } from "@shivaedev/errors/taxonomy.ts";
6
+ const INVALID_TOKEN_ERRORS = new Set([
7
+ "ERR_JWT_EXPIRED",
8
+ "ERR_JWT_CLAIM_VALIDATION_FAILED",
9
+ "ERR_JWT_INVALID",
10
+ "ERR_JWS_INVALID",
11
+ "ERR_JWS_SIGNATURE_VERIFICATION_FAILED",
12
+ "ERR_JWKS_NO_MATCHING_KEY",
13
+ "ERR_JOSE_ALG_NOT_ALLOWED",
14
+ "ERR_JOSE_NOT_SUPPORTED",
15
+ ]);
16
+ function bearerFailure(cause) {
17
+ if ((isAPIError(cause) && (cause.status === "UNAUTHORIZED" || cause.status === "FORBIDDEN"))
18
+ || (cause instanceof Error && INVALID_TOKEN_ERRORS.has(String(Reflect.get(cause, "code"))))) {
19
+ return Effect.succeed(Option.none());
20
+ }
21
+ return Effect.logError("MCP authentication verification failed", redact(cause)).pipe(Effect.andThen(Effect.fail(new AuthUnavailable({ message: "Authentication is temporarily unavailable" }))));
22
+ }
23
+ export function bearer(jwks, audience, issuer) {
24
+ return (token) => {
25
+ if (!token) {
26
+ return Effect.succeed(Option.none());
27
+ }
28
+ return Effect.tryPromise(async () => {
29
+ const claims = await verifyJwsAccessToken(token, { jwksFetch: jwks, verifyOptions: { audience, issuer } });
30
+ if (typeof claims.scope !== "string" || !claims.scope.split(" ").includes("mcp") || claims.cnf !== undefined) {
31
+ throw new APIError("UNAUTHORIZED");
32
+ }
33
+ return claims;
34
+ }).pipe(Effect.map(Option.some), Effect.catch(({ cause }) => bearerFailure(cause)));
35
+ };
36
+ }
37
+ //# sourceMappingURL=bearer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bearer.js","sourceRoot":"","sources":["../../src/adapters/bearer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAE,MAAM,EAAE,MAAM,6BAA6B,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAIhE,MAAM,oBAAoB,GAAG,IAAI,GAAG,CAAC;IACpC,iBAAiB;IACjB,iCAAiC;IACjC,iBAAiB;IACjB,iBAAiB;IACjB,uCAAuC;IACvC,0BAA0B;IAC1B,0BAA0B;IAC1B,wBAAwB;CACxB,CAAC,CAAC;AAEH,SAAS,aAAa,CAAC,KAAc;IACpC,IACC,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,KAAK,cAAc,IAAI,KAAK,CAAC,MAAM,KAAK,WAAW,CAAC,CAAC;WACrF,CAAC,KAAK,YAAY,KAAK,IAAI,oBAAoB,CAAC,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,EAC1F,CAAC;QACF,OAAO,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;IACtC,CAAC;IACD,OAAO,MAAM,CAAC,QAAQ,CAAC,wCAAwC,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CACnF,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,eAAe,CAAC,EAAE,OAAO,EAAE,2CAA2C,EAAE,CAAC,CAAC,CAAC,CAC1G,CAAC;AACH,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,IAAgB,EAAE,QAAgB,EAAE,MAAc;IACxE,OAAO,CAAC,KAAyB,EAAE,EAAE;QACpC,IAAI,CAAC,KAAK,EAAE,CAAC;YACZ,OAAO,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;QACtC,CAAC;QACD,OAAO,MAAM,CAAC,UAAU,CAAC,KAAK,IAAI,EAAE;YACnC,MAAM,MAAM,GAAG,MAAM,oBAAoB,CAAC,KAAK,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,aAAa,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC;YAC3G,IAAI,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,GAAG,KAAK,SAAS,EAAE,CAAC;gBAC9G,MAAM,IAAI,QAAQ,CAAC,cAAc,CAAC,CAAC;YACpC,CAAC;YACD,OAAO,MAAM,CAAC;QACf,CAAC,CAAC,CAAC,IAAI,CACN,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,EACvB,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CACjD,CAAC;IACH,CAAC,CAAC;AACH,CAAC"}
@@ -1,35 +1,26 @@
1
1
  import { type OAuthOptions, oauthProvider, type Scope } from "@better-auth/oauth-provider";
2
- import type { AuthContext, BetterAuthOptions } from "better-auth";
3
2
  import { Effect } from "effect";
4
3
  import { HttpRouter } from "effect/http";
5
4
  import type { RoleSet } from "@shivaedev/access/roleSet.ts";
6
5
  import { AuthUnavailable } from "@shivaedev/errors/taxonomy.ts";
7
6
  import type { OriginPolicy } from "@shivaedev/identity/server/origin.ts";
8
7
  import type { SessionPolicy, UserSessionShape } from "@shivaedev/identity/server/session.ts";
9
- export declare function mcpProvider({ resource, ...options }: Omit<OAuthOptions<Scope[]>, "resources" | "scopes" | "clientRegistrationDefaultResources"> & {
8
+ import { type JwksSource } from "./bearer.ts";
9
+ import { type OauthAuth } from "./oauthGrants.ts";
10
+ export declare function mcpProvider({ resource, scopes, ...options }: Omit<OAuthOptions<Scope[]>, "resources" | "clientRegistrationDefaultResources"> & {
10
11
  readonly resource: string;
11
12
  }): ReturnType<typeof oauthProvider>;
12
- interface OauthAuth {
13
- readonly $context: Promise<{
14
- readonly internalAdapter: Pick<AuthContext["internalAdapter"], "findUserById">;
15
- }>;
16
- readonly api: {
17
- readonly "getOAuthServerConfig": () => Promise<{
18
- readonly issuer: string;
19
- readonly "jwks_uri"?: string;
20
- }>;
21
- };
22
- readonly options: BetterAuthOptions;
23
- }
24
13
  export declare const betterAuthMcp: (options: {
25
14
  readonly auth: OauthAuth;
26
15
  readonly resource: string;
16
+ readonly jwks?: JwksSource;
27
17
  readonly origin: OriginPolicy;
28
18
  readonly roles?: RoleSet;
29
19
  }) => Effect.Effect<{
20
+ revokeAllClients: (userId: string) => Effect.Effect<void, AuthUnavailable, never>;
21
+ revokeClient: (userId: string, clientId: string) => Effect.Effect<void, AuthUnavailable, never>;
30
22
  resourceMetadataUrl: string;
31
23
  routes: import("effect/Layer").Layer<never, never, HttpRouter.HttpRouter | HttpRouter.Request<"Error", import("effect/http/HttpServerError").InternalError> | HttpRouter.Request<"Error", import("effect/http/HttpServerError").RequestParseError> | HttpRouter.Request<"Error", import("effect/http/HttpServerError").RouteNotFound>>;
32
24
  sessions: SessionPolicy<UserSessionShape>;
33
25
  }, AuthUnavailable, never>;
34
- export {};
35
26
  //# sourceMappingURL=betterAuth.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"betterAuth.d.ts","sourceRoot":"","sources":["../../src/adapters/betterAuth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,YAAY,EAAE,aAAa,EAAmC,KAAK,KAAK,EAAE,MAAM,6BAA6B,CAAC;AAE5H,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAElE,OAAO,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAE,UAAU,EAAyC,MAAM,aAAa,CAAC;AAChF,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,8BAA8B,CAAC;AAE5D,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAChE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sCAAsC,CAAC;AACzE,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAE7F,wBAAgB,WAAW,CAAC,EAC3B,QAAQ,EACR,GAAG,OAAO,EACV,EAAE,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,CAAC,EAAE,WAAW,GAAG,QAAQ,GAAG,oCAAoC,CAAC,GAAG;IAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,UAAU,CACzI,OAAO,aAAa,CACpB,CAEA;AAED,UAAU,SAAS;IAClB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;QAAE,QAAQ,CAAC,eAAe,EAAE,IAAI,CAAC,WAAW,CAAC,iBAAiB,CAAC,EAAE,cAAc,CAAC,CAAA;KAAE,CAAC,CAAC;IAC/G,QAAQ,CAAC,GAAG,EAAE;QAAE,QAAQ,CAAC,sBAAsB,EAAE,MAAM,OAAO,CAAC;YAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;KAAE,CAAC;IAC5H,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAC;CACpC;AAWD,eAAO,MAAM,aAAa;mBACV,SAAS;uBACL,MAAM;qBACR,YAAY;qBACZ,OAAO;;;;;0BA6DvB,CAAC"}
1
+ {"version":3,"file":"betterAuth.d.ts","sourceRoot":"","sources":["../../src/adapters/betterAuth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,YAAY,EAAE,aAAa,EAAmC,KAAK,KAAK,EAAE,MAAM,6BAA6B,CAAC;AAC5H,OAAO,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAE,UAAU,EAAyC,MAAM,aAAa,CAAC;AAChF,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,8BAA8B,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAChE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sCAAsC,CAAC;AACzE,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAC7F,OAAO,EAAU,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAe,KAAK,SAAS,EAAe,MAAM,kBAAkB,CAAC;AAE5E,wBAAgB,WAAW,CAAC,EAC3B,QAAQ,EACR,MAAgB,EAChB,GAAG,OAAO,EACV,EAAE,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,CAAC,EAAE,WAAW,GAAG,oCAAoC,CAAC,GAAG;IAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,UAAU,CAC9H,OAAO,aAAa,CACpB,CAEA;AAED,eAAO,MAAM,aAAa;mBACV,SAAS;uBACL,MAAM;oBACT,UAAU;qBACT,YAAY;qBACZ,OAAO;;;;;;;0BAgDvB,CAAC"}
@@ -1,18 +1,11 @@
1
1
  import { oauthProvider, oauthProviderAuthServerMetadata } from "@better-auth/oauth-provider";
2
- import { oauthProviderResourceClient } from "@better-auth/oauth-provider/resource-client";
3
- import { isAPIError } from "better-auth/api";
4
2
  import { Effect, Option } from "effect";
5
3
  import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http";
6
- import { redact } from "@shivaedev/errors/redact.ts";
7
4
  import { AuthUnavailable } from "@shivaedev/errors/taxonomy.ts";
8
- export function mcpProvider({ resource, ...options }) {
9
- return oauthProvider({ ...options, clientRegistrationDefaultResources: [resource], resources: [resource], scopes: ["mcp"] });
10
- }
11
- function bearerFailure(cause) {
12
- if (isAPIError(cause) && (cause.status === "UNAUTHORIZED" || cause.status === "FORBIDDEN")) {
13
- return Effect.succeed(Option.none());
14
- }
15
- return Effect.logError("MCP authentication verification failed", redact(cause)).pipe(Effect.andThen(Effect.fail(new AuthUnavailable({ message: "Authentication is temporarily unavailable" }))));
5
+ import { bearer } from "./bearer.js";
6
+ import { grantedUser, oauthGrants } from "./oauthGrants.js";
7
+ export function mcpProvider({ resource, scopes = ["mcp"], ...options }) {
8
+ return oauthProvider({ ...options, clientRegistrationDefaultResources: [resource], resources: [resource], scopes });
16
9
  }
17
10
  export const betterAuthMcp = Effect.fn("Mcp.betterAuthMcp")(function* (options) {
18
11
  const config = yield* Effect.tryPromise({
@@ -23,7 +16,7 @@ export const betterAuthMcp = Effect.fn("Mcp.betterAuthMcp")(function* (options)
23
16
  const url = new URL(resource);
24
17
  const metadataPath = `/.well-known/oauth-protected-resource${url.pathname}`;
25
18
  const resourceMetadataUrl = new URL(metadataPath, url).href;
26
- const client = oauthProviderResourceClient(auth).getActions();
19
+ const verify = bearer(options.jwks ?? config.jwks_uri ?? new URL("/api/auth/jwks", url).href, resource, config.issuer);
27
20
  const sessions = {
28
21
  origin: options.origin,
29
22
  ...(options.roles === undefined ? {} : { roles: options.roles }),
@@ -31,19 +24,11 @@ export const betterAuthMcp = Effect.fn("Mcp.betterAuthMcp")(function* (options)
31
24
  get: (headers) => Effect.gen(function* () {
32
25
  const authorization = headers.authorization;
33
26
  const token = authorization?.startsWith("Bearer ") ? authorization.slice(7) : undefined;
34
- const claims = yield* Effect.tryPromise(() => client.verifyBearerToken(token, {
35
- jwksUrl: config.jwks_uri ?? new URL("/api/auth/jwks", url).href,
36
- requiredScopes: ["mcp"],
37
- verifyOptions: { audience: resource, issuer: config.issuer },
38
- })).pipe(Effect.map(Option.some), Effect.catch(({ cause }) => bearerFailure(cause)));
39
- if (Option.isNone(claims) || typeof claims.value.sub !== "string") {
27
+ const claims = yield* verify(token);
28
+ if (Option.isNone(claims) || typeof claims.value.sub !== "string" || typeof claims.value.client_id !== "string") {
40
29
  return Option.none();
41
30
  }
42
- const id = claims.value.sub;
43
- const user = yield* Effect.tryPromise({
44
- catch: () => new AuthUnavailable({ message: "OAuth user lookup failed" }),
45
- try: async () => (await auth.$context).internalAdapter.findUserById(id),
46
- });
31
+ const user = yield* grantedUser(auth, claims.value.sub, claims.value.client_id);
47
32
  if (user === null) {
48
33
  return Option.none();
49
34
  }
@@ -61,6 +46,6 @@ export const betterAuthMcp = Effect.fn("Mcp.betterAuthMcp")(function* (options)
61
46
  yield* router.add("GET", metadataPath, HttpServerResponse.jsonUnsafe({ "authorization_servers": [config.issuer], resource, "scopes_supported": ["mcp"] }));
62
47
  yield* router.add("GET", "/.well-known/oauth-authorization-server/api/auth", authorizationMetadata);
63
48
  }));
64
- return { resourceMetadataUrl, routes, sessions };
49
+ return { ...oauthGrants(auth), resourceMetadataUrl, routes, sessions };
65
50
  });
66
51
  //# sourceMappingURL=betterAuth.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"betterAuth.js","sourceRoot":"","sources":["../../src/adapters/betterAuth.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"betterAuth.js","sourceRoot":"","sources":["../../src/adapters/betterAuth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ import type { AuthContext, BetterAuthOptions } from "better-auth";
2
+ import { Effect } from "effect";
3
+ import { AuthUnavailable } from "@shivaedev/errors/taxonomy.ts";
4
+ export interface OauthAuth {
5
+ readonly $context: Promise<{
6
+ readonly adapter: Pick<AuthContext["adapter"], "findOne"> & {
7
+ readonly transaction: <A>(run: (adapter: Pick<AuthContext["adapter"], "deleteMany">) => Promise<A>) => Promise<A>;
8
+ };
9
+ readonly internalAdapter: Pick<AuthContext["internalAdapter"], "findUserById">;
10
+ }>;
11
+ readonly api: {
12
+ readonly "getOAuthServerConfig": () => Promise<{
13
+ readonly issuer: string;
14
+ readonly "jwks_uri"?: string;
15
+ }>;
16
+ };
17
+ readonly options: BetterAuthOptions;
18
+ }
19
+ export declare function oauthGrants(auth: OauthAuth): {
20
+ revokeAllClients: (userId: string) => Effect.Effect<void, AuthUnavailable, never>;
21
+ revokeClient: (userId: string, clientId: string) => Effect.Effect<void, AuthUnavailable, never>;
22
+ };
23
+ export declare const grantedUser: (auth: OauthAuth, userId: string, clientId: string) => Effect.Effect<{
24
+ id: string;
25
+ createdAt: Date;
26
+ updatedAt: Date;
27
+ email: string;
28
+ emailVerified: boolean;
29
+ name: string;
30
+ image?: string | null | undefined;
31
+ } | null, AuthUnavailable, never>;
32
+ //# sourceMappingURL=oauthGrants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauthGrants.d.ts","sourceRoot":"","sources":["../../src/adapters/oauthGrants.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAEhE,MAAM,WAAW,SAAS;IACzB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;QAC1B,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC,GAAG;YAC3D,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,EAAE,YAAY,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;SAClH,CAAC;QACF,QAAQ,CAAC,eAAe,EAAE,IAAI,CAAC,WAAW,CAAC,iBAAiB,CAAC,EAAE,cAAc,CAAC,CAAC;KAC/E,CAAC,CAAC;IACH,QAAQ,CAAC,GAAG,EAAE;QAAE,QAAQ,CAAC,sBAAsB,EAAE,MAAM,OAAO,CAAC;YAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;KAAE,CAAC;IAC5H,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAC;CACpC;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS;IAgBzC,gBAAgB,WAAW,MAAM;IACjC,YAAY,WAAW,MAAM,YAAY,MAAM;EAEhD;AAED,eAAO,MAAM,WAAW;;;;;;;;iCAmBtB,CAAC"}
@@ -0,0 +1,43 @@
1
+ import { Effect } from "effect";
2
+ import { AuthUnavailable } from "@shivaedev/errors/taxonomy.ts";
3
+ export function oauthGrants(auth) {
4
+ const revoke = Effect.fn("Mcp.revoke")(function* (userId, clientId) {
5
+ yield* Effect.tryPromise({
6
+ catch: () => new AuthUnavailable({ message: "OAuth grant revocation failed" }),
7
+ try: async () => {
8
+ const { adapter } = await auth.$context;
9
+ const where = [{ field: "userId", value: userId }, ...(clientId === undefined ? [] : [{ field: "clientId", value: clientId }])];
10
+ await adapter.transaction(async (transaction) => {
11
+ for (const model of ["oauthConsent", "oauthAccessToken", "oauthRefreshToken"]) {
12
+ await transaction.deleteMany({ model, where });
13
+ }
14
+ });
15
+ },
16
+ });
17
+ });
18
+ return {
19
+ revokeAllClients: (userId) => revoke(userId),
20
+ revokeClient: (userId, clientId) => revoke(userId, clientId),
21
+ };
22
+ }
23
+ export const grantedUser = Effect.fn("Mcp.grantedUser")(function* (auth, userId, clientId) {
24
+ return yield* Effect.tryPromise({
25
+ catch: () => new AuthUnavailable({ message: "OAuth grant lookup failed" }),
26
+ try: async () => {
27
+ const { adapter, internalAdapter } = await auth.$context;
28
+ const user = await internalAdapter.findUserById(userId);
29
+ if (user === null || Reflect.get(user, "banned") === true) {
30
+ return null;
31
+ }
32
+ const consent = await adapter.findOne({
33
+ model: "oauthConsent",
34
+ where: [
35
+ { field: "userId", value: userId },
36
+ { field: "clientId", value: clientId },
37
+ ],
38
+ });
39
+ return consent === null ? null : user;
40
+ },
41
+ });
42
+ });
43
+ //# sourceMappingURL=oauthGrants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauthGrants.js","sourceRoot":"","sources":["../../src/adapters/oauthGrants.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAahE,MAAM,UAAU,WAAW,CAAC,IAAe;IAC1C,MAAM,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC,YAAY,CAAC,CAAC,QAAQ,CAAC,EAAE,MAAc,EAAE,QAAiB;QAClF,KAAK,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC;YACxB,KAAK,EAAE,GAAG,EAAE,CAAC,IAAI,eAAe,CAAC,EAAE,OAAO,EAAE,+BAA+B,EAAE,CAAC;YAC9E,GAAG,EAAE,KAAK,IAAI,EAAE;gBACf,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC;gBACxC,MAAM,KAAK,GAAG,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,GAAG,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAC;gBAChI,MAAM,OAAO,CAAC,WAAW,CAAC,KAAK,EAAE,WAAW,EAAE,EAAE;oBAC/C,KAAK,MAAM,KAAK,IAAI,CAAC,cAAc,EAAE,kBAAkB,EAAE,mBAAmB,CAAC,EAAE,CAAC;wBAC/E,MAAM,WAAW,CAAC,UAAU,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;oBAChD,CAAC;gBACF,CAAC,CAAC,CAAC;YACJ,CAAC;SACD,CAAC,CAAC;IACJ,CAAC,CAAC,CAAC;IACH,OAAO;QACN,gBAAgB,EAAE,CAAC,MAAc,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC;QACpD,YAAY,EAAE,CAAC,MAAc,EAAE,QAAgB,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC;KAC5E,CAAC;AACH,CAAC;AAED,MAAM,CAAC,MAAM,WAAW,GAAG,MAAM,CAAC,EAAE,CAAC,iBAAiB,CAAC,CAAC,QAAQ,CAAC,EAAE,IAAe,EAAE,MAAc,EAAE,QAAgB;IACnH,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC;QAC/B,KAAK,EAAE,GAAG,EAAE,CAAC,IAAI,eAAe,CAAC,EAAE,OAAO,EAAE,2BAA2B,EAAE,CAAC;QAC1E,GAAG,EAAE,KAAK,IAAI,EAAE;YACf,MAAM,EAAE,OAAO,EAAE,eAAe,EAAE,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC;YACzD,MAAM,IAAI,GAAG,MAAM,eAAe,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;YACxD,IAAI,IAAI,KAAK,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,QAAQ,CAAC,KAAK,IAAI,EAAE,CAAC;gBAC3D,OAAO,IAAI,CAAC;YACb,CAAC;YACD,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC;gBACrC,KAAK,EAAE,cAAc;gBACrB,KAAK,EAAE;oBACN,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE;oBAClC,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE;iBACtC;aACD,CAAC,CAAC;YACH,OAAO,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;QACvC,CAAC;KACD,CAAC,CAAC;AACJ,CAAC,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shivaedev/mcp",
3
- "version": "0.1.11",
3
+ "version": "0.2.1",
4
4
  "description": "Identity-aware Effect MCP tools mounted on the application host",
5
5
  "keywords": [
6
6
  "effect"
@@ -43,18 +43,18 @@
43
43
  "README.md"
44
44
  ],
45
45
  "dependencies": {
46
- "@shivaedev/access": "0.3.3",
47
- "@shivaedev/errors": "0.3.0",
48
- "@shivaedev/identity": "0.11.1"
46
+ "@shivaedev/access": "0.3.4",
47
+ "@shivaedev/errors": "0.4.0",
48
+ "@shivaedev/identity": "0.13.0"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@better-auth/oauth-provider": "1.7.12",
52
52
  "@effect/platform-node": "4.0.6",
53
53
  "@effect/sql-pg": "4.0.6",
54
- "@shivaedev/quality": "0.50.0",
55
- "@shivaedev/server": "0.12.0",
56
- "@shivaedev/test-sql": "0.15.0",
57
- "@shivaedev/test-story": "0.30.0",
54
+ "@shivaedev/quality": "0.52.0",
55
+ "@shivaedev/server": "0.14.0",
56
+ "@shivaedev/test-sql": "0.15.1",
57
+ "@shivaedev/test-story": "0.31.1",
58
58
  "@types/node": "24.19.2",
59
59
  "better-auth": "1.7.12",
60
60
  "effect": "4.0.6",
@@ -0,0 +1,48 @@
1
+ import { APIError, isAPIError } from "better-auth/api";
2
+ import { verifyJwsAccessToken } from "better-auth/oauth2";
3
+ import { Effect, Option } from "effect";
4
+ import { redact } from "@shivaedev/errors/redact.ts";
5
+ import { AuthUnavailable } from "@shivaedev/errors/taxonomy.ts";
6
+
7
+ export type JwksSource = Parameters<typeof verifyJwsAccessToken>[1]["jwksFetch"];
8
+
9
+ const INVALID_TOKEN_ERRORS = new Set([
10
+ "ERR_JWT_EXPIRED",
11
+ "ERR_JWT_CLAIM_VALIDATION_FAILED",
12
+ "ERR_JWT_INVALID",
13
+ "ERR_JWS_INVALID",
14
+ "ERR_JWS_SIGNATURE_VERIFICATION_FAILED",
15
+ "ERR_JWKS_NO_MATCHING_KEY",
16
+ "ERR_JOSE_ALG_NOT_ALLOWED",
17
+ "ERR_JOSE_NOT_SUPPORTED",
18
+ ]);
19
+
20
+ function bearerFailure(cause: unknown) {
21
+ if (
22
+ (isAPIError(cause) && (cause.status === "UNAUTHORIZED" || cause.status === "FORBIDDEN"))
23
+ || (cause instanceof Error && INVALID_TOKEN_ERRORS.has(String(Reflect.get(cause, "code"))))
24
+ ) {
25
+ return Effect.succeed(Option.none());
26
+ }
27
+ return Effect.logError("MCP authentication verification failed", redact(cause)).pipe(
28
+ Effect.andThen(Effect.fail(new AuthUnavailable({ message: "Authentication is temporarily unavailable" }))),
29
+ );
30
+ }
31
+
32
+ export function bearer(jwks: JwksSource, audience: string, issuer: string) {
33
+ return (token: string | undefined) => {
34
+ if (!token) {
35
+ return Effect.succeed(Option.none());
36
+ }
37
+ return Effect.tryPromise(async () => {
38
+ const claims = await verifyJwsAccessToken(token, { jwksFetch: jwks, verifyOptions: { audience, issuer } });
39
+ if (typeof claims.scope !== "string" || !claims.scope.split(" ").includes("mcp") || claims.cnf !== undefined) {
40
+ throw new APIError("UNAUTHORIZED");
41
+ }
42
+ return claims;
43
+ }).pipe(
44
+ Effect.map(Option.some),
45
+ Effect.catch(({ cause }) => bearerFailure(cause)),
46
+ );
47
+ };
48
+ }
@@ -1,42 +1,27 @@
1
1
  import { type OAuthOptions, oauthProvider, oauthProviderAuthServerMetadata, type Scope } from "@better-auth/oauth-provider";
2
- import { oauthProviderResourceClient } from "@better-auth/oauth-provider/resource-client";
3
- import type { AuthContext, BetterAuthOptions } from "better-auth";
4
- import { isAPIError } from "better-auth/api";
5
2
  import { Effect, Option } from "effect";
6
3
  import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http";
7
4
  import type { RoleSet } from "@shivaedev/access/roleSet.ts";
8
- import { redact } from "@shivaedev/errors/redact.ts";
9
5
  import { AuthUnavailable } from "@shivaedev/errors/taxonomy.ts";
10
6
  import type { OriginPolicy } from "@shivaedev/identity/server/origin.ts";
11
7
  import type { SessionPolicy, UserSessionShape } from "@shivaedev/identity/server/session.ts";
8
+ import { bearer, type JwksSource } from "./bearer.ts";
9
+ import { grantedUser, type OauthAuth, oauthGrants } from "./oauthGrants.ts";
12
10
 
13
11
  export function mcpProvider({
14
12
  resource,
13
+ scopes = ["mcp"],
15
14
  ...options
16
- }: Omit<OAuthOptions<Scope[]>, "resources" | "scopes" | "clientRegistrationDefaultResources"> & { readonly resource: string }): ReturnType<
15
+ }: Omit<OAuthOptions<Scope[]>, "resources" | "clientRegistrationDefaultResources"> & { readonly resource: string }): ReturnType<
17
16
  typeof oauthProvider
18
17
  > {
19
- return oauthProvider({ ...options, clientRegistrationDefaultResources: [resource], resources: [resource], scopes: ["mcp"] });
20
- }
21
-
22
- interface OauthAuth {
23
- readonly $context: Promise<{ readonly internalAdapter: Pick<AuthContext["internalAdapter"], "findUserById"> }>;
24
- readonly api: { readonly "getOAuthServerConfig": () => Promise<{ readonly issuer: string; readonly "jwks_uri"?: string }> };
25
- readonly options: BetterAuthOptions;
26
- }
27
-
28
- function bearerFailure(cause: unknown) {
29
- if (isAPIError(cause) && (cause.status === "UNAUTHORIZED" || cause.status === "FORBIDDEN")) {
30
- return Effect.succeed(Option.none());
31
- }
32
- return Effect.logError("MCP authentication verification failed", redact(cause)).pipe(
33
- Effect.andThen(Effect.fail(new AuthUnavailable({ message: "Authentication is temporarily unavailable" }))),
34
- );
18
+ return oauthProvider({ ...options, clientRegistrationDefaultResources: [resource], resources: [resource], scopes });
35
19
  }
36
20
 
37
21
  export const betterAuthMcp = Effect.fn("Mcp.betterAuthMcp")(function* (options: {
38
22
  readonly auth: OauthAuth;
39
23
  readonly resource: string;
24
+ readonly jwks?: JwksSource;
40
25
  readonly origin: OriginPolicy;
41
26
  readonly roles?: RoleSet;
42
27
  }) {
@@ -48,7 +33,7 @@ export const betterAuthMcp = Effect.fn("Mcp.betterAuthMcp")(function* (options:
48
33
  const url = new URL(resource);
49
34
  const metadataPath = `/.well-known/oauth-protected-resource${url.pathname}` as const;
50
35
  const resourceMetadataUrl = new URL(metadataPath, url).href;
51
- const client = oauthProviderResourceClient(auth).getActions();
36
+ const verify = bearer(options.jwks ?? config.jwks_uri ?? new URL("/api/auth/jwks", url).href, resource, config.issuer);
52
37
  const sessions: SessionPolicy<UserSessionShape> = {
53
38
  origin: options.origin,
54
39
  ...(options.roles === undefined ? {} : { roles: options.roles }),
@@ -57,24 +42,11 @@ export const betterAuthMcp = Effect.fn("Mcp.betterAuthMcp")(function* (options:
57
42
  Effect.gen(function* () {
58
43
  const authorization = headers.authorization;
59
44
  const token = authorization?.startsWith("Bearer ") ? authorization.slice(7) : undefined;
60
- const claims = yield* Effect.tryPromise(() =>
61
- client.verifyBearerToken(token, {
62
- jwksUrl: config.jwks_uri ?? new URL("/api/auth/jwks", url).href,
63
- requiredScopes: ["mcp"],
64
- verifyOptions: { audience: resource, issuer: config.issuer },
65
- }),
66
- ).pipe(
67
- Effect.map(Option.some),
68
- Effect.catch(({ cause }) => bearerFailure(cause)),
69
- );
70
- if (Option.isNone(claims) || typeof claims.value.sub !== "string") {
45
+ const claims = yield* verify(token);
46
+ if (Option.isNone(claims) || typeof claims.value.sub !== "string" || typeof claims.value.client_id !== "string") {
71
47
  return Option.none();
72
48
  }
73
- const id = claims.value.sub;
74
- const user = yield* Effect.tryPromise({
75
- catch: () => new AuthUnavailable({ message: "OAuth user lookup failed" }),
76
- try: async () => (await auth.$context).internalAdapter.findUserById(id),
77
- });
49
+ const user = yield* grantedUser(auth, claims.value.sub, claims.value.client_id);
78
50
  if (user === null) {
79
51
  return Option.none();
80
52
  }
@@ -98,5 +70,5 @@ export const betterAuthMcp = Effect.fn("Mcp.betterAuthMcp")(function* (options:
98
70
  yield* router.add("GET", "/.well-known/oauth-authorization-server/api/auth", authorizationMetadata);
99
71
  }),
100
72
  );
101
- return { resourceMetadataUrl, routes, sessions };
73
+ return { ...oauthGrants(auth), resourceMetadataUrl, routes, sessions };
102
74
  });
@@ -0,0 +1,56 @@
1
+ import type { AuthContext, BetterAuthOptions } from "better-auth";
2
+ import { Effect } from "effect";
3
+ import { AuthUnavailable } from "@shivaedev/errors/taxonomy.ts";
4
+
5
+ export interface OauthAuth {
6
+ readonly $context: Promise<{
7
+ readonly adapter: Pick<AuthContext["adapter"], "findOne"> & {
8
+ readonly transaction: <A>(run: (adapter: Pick<AuthContext["adapter"], "deleteMany">) => Promise<A>) => Promise<A>;
9
+ };
10
+ readonly internalAdapter: Pick<AuthContext["internalAdapter"], "findUserById">;
11
+ }>;
12
+ readonly api: { readonly "getOAuthServerConfig": () => Promise<{ readonly issuer: string; readonly "jwks_uri"?: string }> };
13
+ readonly options: BetterAuthOptions;
14
+ }
15
+
16
+ export function oauthGrants(auth: OauthAuth) {
17
+ const revoke = Effect.fn("Mcp.revoke")(function* (userId: string, clientId?: string) {
18
+ yield* Effect.tryPromise({
19
+ catch: () => new AuthUnavailable({ message: "OAuth grant revocation failed" }),
20
+ try: async () => {
21
+ const { adapter } = await auth.$context;
22
+ const where = [{ field: "userId", value: userId }, ...(clientId === undefined ? [] : [{ field: "clientId", value: clientId }])];
23
+ await adapter.transaction(async (transaction) => {
24
+ for (const model of ["oauthConsent", "oauthAccessToken", "oauthRefreshToken"]) {
25
+ await transaction.deleteMany({ model, where });
26
+ }
27
+ });
28
+ },
29
+ });
30
+ });
31
+ return {
32
+ revokeAllClients: (userId: string) => revoke(userId),
33
+ revokeClient: (userId: string, clientId: string) => revoke(userId, clientId),
34
+ };
35
+ }
36
+
37
+ export const grantedUser = Effect.fn("Mcp.grantedUser")(function* (auth: OauthAuth, userId: string, clientId: string) {
38
+ return yield* Effect.tryPromise({
39
+ catch: () => new AuthUnavailable({ message: "OAuth grant lookup failed" }),
40
+ try: async () => {
41
+ const { adapter, internalAdapter } = await auth.$context;
42
+ const user = await internalAdapter.findUserById(userId);
43
+ if (user === null || Reflect.get(user, "banned") === true) {
44
+ return null;
45
+ }
46
+ const consent = await adapter.findOne({
47
+ model: "oauthConsent",
48
+ where: [
49
+ { field: "userId", value: userId },
50
+ { field: "clientId", value: clientId },
51
+ ],
52
+ });
53
+ return consent === null ? null : user;
54
+ },
55
+ });
56
+ });