@nestjs/authentication 0.1.2 → 0.1.4

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/README.md CHANGED
@@ -18,7 +18,7 @@
18
18
 
19
19
  ## Description
20
20
 
21
- Authentication module for [Nest](https://github.com/nestjs/nest): a global guard with `@Public()` and `@Authenticate()`, credential providers as ordinary injectable classes (bearer JWT, cookie sessions, your own), server-side sessions with rotation, refresh tokens, TOTP second factor, magic links, OIDC sign-in, email verification and password reset, across HTTP, GraphQL, WebSockets and microservices, with no third-party dependencies.
21
+ Authentication module for [Nest](https://github.com/nestjs/nest): a global guard with `@Public()` and `@Authenticate()`, credential providers as ordinary injectable classes (bearer JWT, cookie sessions, your own), server-side sessions with rotation, refresh tokens, TOTP second factor, magic links, OIDC sign-in, email verification and password reset, across HTTP, GraphQL, WebSockets, microservices and MCP tool calls (`@nestjs/mcp`), with no third-party dependencies.
22
22
 
23
23
  ## Installation
24
24
 
@@ -32,8 +32,13 @@ export declare class AuthenticationScope {
32
32
  get current(): Scope | undefined;
33
33
  run<R>(scope: Scope, fn: () => R): R;
34
34
  /**
35
- * The HTTP request and response of the current call: an HTTP handler, or
36
- * a GraphQL resolver whose context carries `req` (and `res`).
35
+ * The HTTP request and response of the current call: an HTTP handler, a
36
+ * GraphQL resolver whose context carries `req` (and `res`), or an
37
+ * `@nestjs/mcp` call that arrived over HTTP (its context, the second of
38
+ * its arguments, carries `request`). An MCP call has no response of its own
39
+ * to set cookies on: the MCP SDK writes the HTTP response. An `@nestjs/ai`
40
+ * agent function called in an HTTP request carries it the same way, and has
41
+ * no response either: the route that runs the agent owns it.
37
42
  */
38
43
  exchange(): HttpExchange | undefined;
39
44
  }
@@ -39,8 +39,13 @@ let AuthenticationScope = class AuthenticationScope {
39
39
  return this.storage.run(scope, fn);
40
40
  }
41
41
  /**
42
- * The HTTP request and response of the current call: an HTTP handler, or
43
- * a GraphQL resolver whose context carries `req` (and `res`).
42
+ * The HTTP request and response of the current call: an HTTP handler, a
43
+ * GraphQL resolver whose context carries `req` (and `res`), or an
44
+ * `@nestjs/mcp` call that arrived over HTTP (its context, the second of
45
+ * its arguments, carries `request`). An MCP call has no response of its own
46
+ * to set cookies on: the MCP SDK writes the HTTP response. An `@nestjs/ai`
47
+ * agent function called in an HTTP request carries it the same way, and has
48
+ * no response either: the route that runs the agent owns it.
44
49
  */
45
50
  exchange() {
46
51
  const context = this.current?.context;
@@ -53,6 +58,11 @@ let AuthenticationScope = class AuthenticationScope {
53
58
  const gql = context.getArgByIndex(2);
54
59
  return gql?.req ? { request: gql.req, response: gql.res } : undefined;
55
60
  }
61
+ case 'mcp':
62
+ case 'ai': {
63
+ const request = context.getArgByIndex(1)?.request;
64
+ return request?.headers ? { request } : undefined;
65
+ }
56
66
  default:
57
67
  return undefined;
58
68
  }
@@ -113,16 +113,15 @@ export function authenticationStoreContract(create, options = {}) {
113
113
  // Two instances: one clock a little behind the other.
114
114
  const behind = new SessionService(storage, { session: { ...options, now: () => clock - 2_000 } });
115
115
  const ahead = new SessionService(storage, { session: { ...options, now: () => clock } });
116
- for (let round = 0; round < 3; round++) {
117
- const { token } = await ahead.create(uid());
118
- clock += 61_000; // idle for 61 s on `ahead`, 59 s on `behind`
119
- const [seenIdle, touched] = await Promise.all([ahead.validate(token), behind.validate(token)]);
120
- same(seenIdle, null, 'the request that saw the session idle is refused');
121
- assert.ok(touched, 'the other instance, within the idle timeout, touches the session');
122
- clock += 1_000;
123
- assert.ok(await ahead.validate(token), 'the touched session is still there');
124
- }
125
- }, concurrent);
116
+ const { token } = await ahead.create(uid());
117
+ clock += 61_000; // idle for 61 s on `ahead`, 59 s on `behind`
118
+ // One after the other: run together, `behind` may read and touch first, and then `ahead` rightly
119
+ // finds the session alive. In this order, a `validate()` that deleted what it saw idle fails below.
120
+ same(await ahead.validate(token), null, 'the request that saw the session idle is refused');
121
+ assert.ok(await behind.validate(token), 'the other instance, within the idle timeout, touches the session');
122
+ clock += 1_000;
123
+ assert.ok(await ahead.validate(token), 'the touched session is still there');
124
+ });
126
125
  add('sessions', 'a request racing a sign-out or a rotation never brings the session back (SessionService)', async (sources) => {
127
126
  let clock = T0;
128
127
  const sessions = new SessionService(storageOf(sources, 'sessions'), {
@@ -38,13 +38,16 @@ export declare function getRawResult(context: ExecutionContext): State | undefin
38
38
  * (`WsAuthenticator.authenticateConnection()` stores the upgrade request
39
39
  * there for the `ws` library)
40
40
  * - `graphql`: `context.req.headers`
41
+ * - `mcp`: the headers of the HTTP request the call arrived in; `undefined`
42
+ * over stdio, where the MCP spec takes credentials from the environment
41
43
  * - `rpc`: `undefined`; metadata is transport specific
42
44
  */
43
45
  export declare function getRequestHeaders(context: ExecutionContext): Headers | undefined;
44
46
  /**
45
47
  * The request behind a call, as far as authentication needs it: headers,
46
- * and the method for HTTP and GraphQL (a WebSocket handshake has none, so
47
- * it counts as a safe request). `undefined` for RPC.
48
+ * and the method for HTTP, GraphQL, MCP and agent functions (a WebSocket
49
+ * handshake has none, so it counts as a safe request). `undefined` for RPC,
50
+ * for MCP over stdio, and for an agent function called in a job.
48
51
  */
49
52
  export declare function requestOf(context: ExecutionContext): {
50
53
  headers: Headers;
@@ -28,12 +28,30 @@ function callKeys(context) {
28
28
  }
29
29
  return keys;
30
30
  }
31
+ /**
32
+ * The context of an `@nestjs/mcp` call (a tool, resource or prompt) or of an
33
+ * `@nestjs/ai` agent function call, read from its arguments, `[input,
34
+ * context]`, so this package needs neither dependency. Its `request` is the
35
+ * HTTP platform's request: the one an MCP call arrived in (a plain
36
+ * `{ method, url, headers }` object for `@nestjs/mcp/testing`'s in-memory
37
+ * client), or the one the agent runs in. Absent over stdio, and in a job.
38
+ */
39
+ function callContextOf(context) {
40
+ const call = context.getArgByIndex(1);
41
+ return typeof call === 'object' && call !== null ? call : undefined;
42
+ }
31
43
  /**
32
44
  * The object that carries the result for one call, mirrored as `user` /
33
45
  * `session` where Passport-era code and `@nestjs/authorization` look:
34
46
  * the request (http), the client socket (ws), the transport context (rpc),
35
47
  * `context.req` (graphql; over graphql-ws, the socket's upgrade request, which
36
- * every operation of the socket shares).
48
+ * every operation of the socket shares), and for an MCP call (`mcp`) the HTTP
49
+ * request it arrived in, else (over stdio) its own context object. Each MCP
50
+ * request travels in an HTTP request of its own, so the request is per call,
51
+ * and it is the one the endpoint's route was authenticated on: a guard there
52
+ * and on the call authenticate once. An agent function call (`ai`) is carried
53
+ * the same way: the HTTP request the agent runs in, which the route that runs
54
+ * it authenticated, else (in a job) its own context object.
37
55
  */
38
56
  function carrierOf(context) {
39
57
  switch (context.getType()) {
@@ -47,6 +65,11 @@ function carrierOf(context) {
47
65
  }
48
66
  case 'graphql':
49
67
  return context.getArgByIndex(2)?.req;
68
+ case 'mcp':
69
+ case 'ai': {
70
+ const call = callContextOf(context);
71
+ return call?.request ?? call;
72
+ }
50
73
  default:
51
74
  return undefined;
52
75
  }
@@ -178,6 +201,8 @@ function stateCarrierOf(context) {
178
201
  * (`WsAuthenticator.authenticateConnection()` stores the upgrade request
179
202
  * there for the `ws` library)
180
203
  * - `graphql`: `context.req.headers`
204
+ * - `mcp`: the headers of the HTTP request the call arrived in; `undefined`
205
+ * over stdio, where the MCP spec takes credentials from the environment
181
206
  * - `rpc`: `undefined`; metadata is transport specific
182
207
  */
183
208
  export function getRequestHeaders(context) {
@@ -185,8 +210,9 @@ export function getRequestHeaders(context) {
185
210
  }
186
211
  /**
187
212
  * The request behind a call, as far as authentication needs it: headers,
188
- * and the method for HTTP and GraphQL (a WebSocket handshake has none, so
189
- * it counts as a safe request). `undefined` for RPC.
213
+ * and the method for HTTP, GraphQL, MCP and agent functions (a WebSocket
214
+ * handshake has none, so it counts as a safe request). `undefined` for RPC,
215
+ * for MCP over stdio, and for an agent function called in a job.
190
216
  */
191
217
  export function requestOf(context) {
192
218
  switch (context.getType()) {
@@ -199,6 +225,11 @@ export function requestOf(context) {
199
225
  }
200
226
  case 'graphql':
201
227
  return context.getArgByIndex(2)?.req;
228
+ case 'mcp':
229
+ case 'ai': {
230
+ const request = callContextOf(context)?.request;
231
+ return request?.headers ? request : undefined;
232
+ }
202
233
  default:
203
234
  return undefined;
204
235
  }
@@ -23,13 +23,15 @@ export declare function refusalOf(error: AuthenticationError): Refusal;
23
23
  * for `AuthenticationError`s thrown by handlers: the error each transport's
24
24
  * exception filter understands, for the refusal's status.
25
25
  *
26
- * - HTTP and GraphQL: Nest's own `UnauthorizedException` (or
26
+ * - HTTP, GraphQL and MCP: Nest's own `UnauthorizedException` (or
27
27
  * `ForbiddenException` for a 403, `ConflictException` for a 409), with
28
28
  * Nest's own body: `{"message":"Unauthorized","statusCode":401}`, or with a
29
29
  * message, `{"message":"Refresh token reused","error":"Unauthorized","statusCode":401}`.
30
30
  * A `code` is Nest's `errorCode` (on the exception, and in the body), and
31
31
  * the description in `error` when there is a message; `details` follow.
32
32
  * Over HTTP, `challenge` becomes the `WWW-Authenticate` header of a 401.
33
+ * `@nestjs/mcp` answers a tool call's exception with a tool error carrying
34
+ * its message, and a resource's or prompt's with a JSON-RPC error.
33
35
  * - ws and rpc: a `WsException` / `RpcException` carrying
34
36
  * `{ statusCode, message }` (plus `status: 'error'` for ws, and `error`,
35
37
  * `errorCode` and `details` as above). Those filters report
@@ -21,13 +21,15 @@ export function refusalOf(error) {
21
21
  * for `AuthenticationError`s thrown by handlers: the error each transport's
22
22
  * exception filter understands, for the refusal's status.
23
23
  *
24
- * - HTTP and GraphQL: Nest's own `UnauthorizedException` (or
24
+ * - HTTP, GraphQL and MCP: Nest's own `UnauthorizedException` (or
25
25
  * `ForbiddenException` for a 403, `ConflictException` for a 409), with
26
26
  * Nest's own body: `{"message":"Unauthorized","statusCode":401}`, or with a
27
27
  * message, `{"message":"Refresh token reused","error":"Unauthorized","statusCode":401}`.
28
28
  * A `code` is Nest's `errorCode` (on the exception, and in the body), and
29
29
  * the description in `error` when there is a message; `details` follow.
30
30
  * Over HTTP, `challenge` becomes the `WWW-Authenticate` header of a 401.
31
+ * `@nestjs/mcp` answers a tool call's exception with a tool error carrying
32
+ * its message, and a resource's or prompt's with a JSON-RPC error.
31
33
  * - ws and rpc: a `WsException` / `RpcException` carrying
32
34
  * `{ statusCode, message }` (plus `status: 'error'` for ws, and `error`,
33
35
  * `errorCode` and `details` as above). Those filters report
@@ -66,6 +68,16 @@ export async function refusal(context, { status = 401, message, code, details, c
66
68
  adapterHost?.httpAdapter?.setHeader(context.switchToHttp().getResponse(), 'WWW-Authenticate', challenge);
67
69
  }
68
70
  break;
71
+ case 'mcp':
72
+ // Nest's own exception, as for HTTP, which `@nestjs/mcp` answers with: a tool result with `isError: true` and
73
+ // the message, for a tool; a JSON-RPC error with the message and the status, for a resource or a prompt. The
74
+ // MCP SDK writes the HTTP response, so no challenge goes with it: an MCP client learns where to sign in from
75
+ // the endpoint's own 401 (`@nestjs/mcp`'s `auth` option).
76
+ break;
77
+ case 'ai':
78
+ // Nest's own exception too, which `@nestjs/ai` gives the model as the function's error. The route that runs
79
+ // the agent owns the HTTP response, so no challenge goes with it.
80
+ break;
69
81
  }
70
82
  const Exception = status === 409 ? ConflictException : status === 403 ? ForbiddenException : UnauthorizedException;
71
83
  // Nest builds the body: `error` is the code (the description) or the status phrase, then `errorCode`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nestjs/authentication",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Nest - modern, fast, powerful node.js web framework (@authentication)",
5
5
  "author": "Kamil Mysliwiec",
6
6
  "license": "MIT",
@@ -46,21 +46,21 @@
46
46
  "@electric-sql/pglite": "0.5.8",
47
47
  "@electric-sql/pglite-socket": "0.2.11",
48
48
  "@nestjs/apollo": "14.0.1",
49
- "@nestjs/authorization": "0.1.0",
50
- "@nestjs/common": "12.2.0",
51
- "@nestjs/core": "12.2.0",
49
+ "@nestjs/authorization": "0.1.1",
50
+ "@nestjs/common": "12.3.3",
51
+ "@nestjs/core": "12.3.3",
52
52
  "@nestjs/drizzle": "0.0.2",
53
53
  "@nestjs/graphql": "14.0.1",
54
- "@nestjs/idempotency": "0.0.2",
54
+ "@nestjs/idempotency": "0.0.3",
55
55
  "@nestjs/mail": "0.0.1",
56
- "@nestjs/microservices": "12.2.0",
57
- "@nestjs/platform-express": "12.2.0",
58
- "@nestjs/platform-fastify": "12.2.0",
59
- "@nestjs/platform-ws": "12.2.0",
60
- "@nestjs/resilience": "0.0.2",
61
- "@nestjs/testing": "12.2.0",
56
+ "@nestjs/microservices": "12.3.3",
57
+ "@nestjs/platform-express": "12.3.3",
58
+ "@nestjs/platform-fastify": "12.3.3",
59
+ "@nestjs/platform-ws": "12.3.3",
60
+ "@nestjs/resilience": "0.0.3",
61
+ "@nestjs/testing": "12.3.3",
62
62
  "@nestjs/throttler": "6.7.1",
63
- "@nestjs/websockets": "12.2.0",
63
+ "@nestjs/websockets": "12.3.3",
64
64
  "@types/node": "24.19.2",
65
65
  "@types/pg": "8.23.1",
66
66
  "@types/supertest": "7.2.1",
@@ -69,18 +69,18 @@
69
69
  "graphql": "16.14.2",
70
70
  "husky": "9.1.7",
71
71
  "lint-staged": "17.6.0",
72
- "oxlint": "1.89.0",
72
+ "oxlint": "1.91.0",
73
73
  "pg": "8.23.1",
74
74
  "prettier": "3.9.10",
75
75
  "reflect-metadata": "0.2.2",
76
- "release-it": "21.1.1",
76
+ "release-it": "21.1.2",
77
77
  "rxjs": "7.8.2",
78
78
  "supertest": "7.3.1",
79
79
  "typeorm": "1.1.2",
80
80
  "typescript": "7.0.2",
81
81
  "vite": "8.3.5",
82
- "vitest": "5.0.5",
83
- "ws": "8.22.0"
82
+ "vitest": "5.1.0",
83
+ "ws": "8.22.1"
84
84
  },
85
85
  "peerDependencies": {
86
86
  "@nestjs/common": "^12.0.0",