@agent-id/nextjs 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 agent-id
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,113 @@
1
+ # @agent-id/nextjs
2
+
3
+ Next.js middleware that automatically detects AI-agent traffic and requires a valid [AgentPass](https://agentpass.vercel.app) JWT. Human browser traffic always passes through untouched.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @agent-id/nextjs
9
+ ```
10
+
11
+ Requires `next >= 14`.
12
+
13
+ ## Setup — 2 steps
14
+
15
+ ### 1. Add the middleware
16
+
17
+ Create (or update) `middleware.ts` in your project root:
18
+
19
+ ```ts
20
+ import { createAgentPassMiddleware } from '@agent-id/nextjs';
21
+ import type { NextRequest } from 'next/server';
22
+
23
+ const agentPass = createAgentPassMiddleware();
24
+
25
+ export function middleware(request: NextRequest) {
26
+ return agentPass(request);
27
+ }
28
+
29
+ // Protect your API routes
30
+ export const config = {
31
+ matcher: '/api/:path*',
32
+ };
33
+ ```
34
+
35
+ That's it. The middleware now:
36
+ - Lets all human browser traffic through unchanged
37
+ - Requires a valid AgentPass JWT from any AI agent / bot
38
+ - Returns `403 AGENT_UNAUTHORIZED` when the JWT is missing or invalid
39
+
40
+ ### 2. Read the verified identity in your route handlers (optional)
41
+
42
+ ```ts
43
+ // app/api/anything/route.ts
44
+ import { getAgentPassResult } from '@agent-id/nextjs';
45
+ import type { NextRequest } from 'next/server';
46
+
47
+ export async function GET(request: NextRequest) {
48
+ const agent = getAgentPassResult(request);
49
+
50
+ if (agent.verified) {
51
+ // Verified AI agent — claims are fully typed
52
+ console.log(agent.claims.sub); // pseudonymous stable user ID
53
+ console.log(agent.claims.auth_method); // "bankid"
54
+ }
55
+
56
+ // Human traffic: agent.verified === false, agent.reason === 'not_agent'
57
+ return Response.json({ ok: true });
58
+ }
59
+ ```
60
+
61
+ ## How agents authenticate
62
+
63
+ Agents add one header to every request:
64
+
65
+ ```
66
+ Authorization: Bearer <agentpass-jwt>
67
+ ```
68
+
69
+ The JWT is obtained by completing a BankID flow at [agentpass.vercel.app](https://agentpass.vercel.app). Tokens are valid for 1 hour.
70
+
71
+ ## Options
72
+
73
+ All options are optional — `createAgentPassMiddleware()` with no arguments works out of the box.
74
+
75
+ ```ts
76
+ createAgentPassMiddleware({
77
+ // Return 403 when an agent has no valid token (default: true).
78
+ // Set to false to let unverified agents through (useful for logging / gradual rollout).
79
+ blockUnauthorizedAgents: true,
80
+
81
+ // Override the JWKS endpoint — only needed if you self-host AgentPass.
82
+ jwksUrl: 'https://your-agentpass.example.com/api/jwks',
83
+
84
+ // Clock skew tolerance in seconds (default: 30).
85
+ clockTolerance: 30,
86
+
87
+ // Fully custom response when an agent is rejected.
88
+ onUnauthorizedAgent: (request, reason) =>
89
+ NextResponse.json({ error: 'No AgentPass token', reason }, { status: 403 }),
90
+ })
91
+ ```
92
+
93
+ ## What gets verified
94
+
95
+ Verification is **fully offline** after the first request. The public key is fetched once from the AgentPass JWKS endpoint and cached for 1 hour — no per-request network call.
96
+
97
+ | Check | Requirement |
98
+ |---|---|
99
+ | Signature | RS256 — `alg:none` and HS256 are explicitly rejected |
100
+ | Issuer (`iss`) | Must equal `"agentpass"` |
101
+ | Expiry (`exp`) | Must be in the future |
102
+ | `auth_method` | Must equal `"bankid"` |
103
+
104
+ ## JWT claims
105
+
106
+ | Field | Description |
107
+ |---|---|
108
+ | `sub` | Pseudonymous stable user ID (HMAC-SHA256 of BankID personal number — non-reversible, same person always gets the same ID) |
109
+ | `auth_method` | Always `"bankid"` |
110
+ | `iss` | `"agentpass"` |
111
+ | `exp` | Unix timestamp — 1 hour from issue |
112
+ | `iat` | Unix timestamp — when issued |
113
+ | `jti` | Unique token ID |
@@ -0,0 +1,163 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+
3
+ /**
4
+ * JWT claims present in every AgentPass token.
5
+ *
6
+ * The `sub` field is a pseudonymous identifier derived via HMAC-SHA256 of the
7
+ * user's BankID personal number. It is stable per person but non-reversible —
8
+ * organisations cannot extract the personal number from it.
9
+ */
10
+ interface AgentPassClaims {
11
+ /** Pseudonymous, stable per-user identifier (HMAC-SHA256 of personal number). */
12
+ sub: string;
13
+ /** Issuer — always "agentpass". */
14
+ iss: string;
15
+ /** Issued-at Unix timestamp. */
16
+ iat: number;
17
+ /** Expiry Unix timestamp (1 hour after iat). */
18
+ exp: number;
19
+ /** Unique JWT ID — enables token logging / replay detection on your side. */
20
+ jti: string;
21
+ /** Authentication method — always "bankid" in this version. */
22
+ auth_method: "bankid";
23
+ }
24
+ interface AgentPassVerified {
25
+ verified: true;
26
+ claims: AgentPassClaims;
27
+ }
28
+ interface AgentPassUnverified {
29
+ verified: false;
30
+ /** Why verification was skipped or failed. */
31
+ reason: "not_agent" | "no_token" | "invalid_token";
32
+ }
33
+ /** Result attached to every request processed by the AgentPass middleware. */
34
+ type AgentPassResult = AgentPassVerified | AgentPassUnverified;
35
+ interface VerifierOptions {
36
+ /**
37
+ * URL of the AgentPass JWKS endpoint.
38
+ * Must use HTTPS (except `localhost` / `127.0.0.1` in development).
39
+ *
40
+ * @default 'https://agentpass.vercel.app/api/jwks'
41
+ */
42
+ jwksUrl?: string;
43
+ /**
44
+ * Block agent requests that carry no valid AgentPass token.
45
+ * When `false` the middleware still runs but sets an unverified result
46
+ * on the request instead of returning 403.
47
+ *
48
+ * @default true
49
+ */
50
+ blockUnauthorizedAgents?: boolean;
51
+ /**
52
+ * Allowed clock skew in seconds when verifying JWT expiry.
53
+ * Protects against minor clock drift between issuer and verifier.
54
+ *
55
+ * @default 30
56
+ */
57
+ clockTolerance?: number;
58
+ }
59
+
60
+ type AgentPassMiddlewareOptions = VerifierOptions & {
61
+ /**
62
+ * Optional callback invoked when an agent is detected but carries no
63
+ * valid token. Return a `NextResponse` to override the default 403.
64
+ */
65
+ onUnauthorizedAgent?: (request: NextRequest, reason: string) => NextResponse | void;
66
+ };
67
+ /**
68
+ * Factory that returns a Next.js middleware function which detects AI-agent
69
+ * traffic and enforces AgentPass JWT authentication.
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * // middleware.ts (project root)
74
+ * import { createAgentPassMiddleware } from '@agent-id/nextjs';
75
+ *
76
+ * const agentPass = createAgentPassMiddleware({
77
+ * blockUnauthorizedAgents: true,
78
+ * });
79
+ *
80
+ * export function middleware(request: NextRequest) {
81
+ * return agentPass(request);
82
+ * }
83
+ *
84
+ * export const config = { matcher: '/api/:path*' };
85
+ * ```
86
+ */
87
+ declare function createAgentPassMiddleware(options?: AgentPassMiddlewareOptions): (request: NextRequest) => Promise<NextResponse>;
88
+
89
+ interface VerifyTokenOptions {
90
+ jwksUrl?: string;
91
+ clockTolerance?: number;
92
+ }
93
+ /**
94
+ * Verify an AgentPass JWT and return its decoded claims.
95
+ *
96
+ * Security guarantees
97
+ * ───────────────────
98
+ * • Signature — RS256, verified against the live JWKS public key.
99
+ * • Algorithm — strict allowlist `['RS256']`; alg:none and HS256 are
100
+ * rejected before signature verification even begins.
101
+ * • Issuer — must be exactly "agentpass".
102
+ * • Expiry — enforced; configurable clock tolerance (default 30 s).
103
+ * • kid — jose matches the JWT `kid` header to the JWKS automatically.
104
+ * • auth_method — validated at runtime; must equal "bankid".
105
+ *
106
+ * @throws if the token is invalid, expired, or fails any check.
107
+ */
108
+ declare function verifyAgentPassToken(token: string, options?: VerifyTokenOptions): Promise<AgentPassClaims>;
109
+
110
+ /**
111
+ * Bot / AI-agent detection for Next.js (Edge Runtime compatible).
112
+ *
113
+ * Uses layered heuristics:
114
+ * 1. Explicit bot / AI-agent User-Agent strings
115
+ * 2. Cloudflare Bot Management score (if header is present)
116
+ * 3. Absence of browser signals (no Accept-Language + non-browser UA)
117
+ *
118
+ * The detector is intentionally conservative — when in doubt it lets the
119
+ * request through (fail open). A false negative (undetected bot) means the
120
+ * request passes without a JWT check. A false positive (human flagged as bot)
121
+ * would block legitimate traffic, which is much worse.
122
+ */
123
+ /**
124
+ * Returns `true` if the request appears to originate from an automated
125
+ * agent or bot rather than a human browser.
126
+ *
127
+ * @param headers - The `Headers` object from a Next.js `NextRequest`
128
+ */
129
+ declare function isAgentRequest(headers: Headers): boolean;
130
+ /**
131
+ * Extract the AgentPass JWT from request headers.
132
+ *
133
+ * Checks in order:
134
+ * 1. `Authorization: Bearer <token>` (preferred)
135
+ * 2. `X-AgentPass-Token` (fallback when Authorization is stripped by proxies)
136
+ *
137
+ * @returns The raw JWT string, or `null` if absent.
138
+ */
139
+ declare function extractToken(headers: Headers): string | null;
140
+
141
+ /**
142
+ * Extract the verified AgentPass identity from a Next.js App Router request.
143
+ *
144
+ * This function reads the headers set by `createAgentPassMiddleware`. It must
145
+ * only be called from route handlers that sit behind the middleware.
146
+ *
147
+ * @example
148
+ * ```ts
149
+ * // app/api/data/route.ts
150
+ * import { getAgentPassResult } from '@agent-id/nextjs';
151
+ *
152
+ * export async function GET(request: NextRequest) {
153
+ * const agentPass = getAgentPassResult(request);
154
+ * if (!agentPass.verified) {
155
+ * return Response.json({ error: 'Unauthorized' }, { status: 403 });
156
+ * }
157
+ * return Response.json({ sub: agentPass.claims.sub });
158
+ * }
159
+ * ```
160
+ */
161
+ declare function getAgentPassResult(request: NextRequest): AgentPassResult;
162
+
163
+ export { type AgentPassClaims, type AgentPassMiddlewareOptions, type AgentPassResult, type AgentPassUnverified, type AgentPassVerified, type VerifierOptions, type VerifyTokenOptions, createAgentPassMiddleware, extractToken, getAgentPassResult, isAgentRequest, verifyAgentPassToken };
@@ -0,0 +1,163 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+
3
+ /**
4
+ * JWT claims present in every AgentPass token.
5
+ *
6
+ * The `sub` field is a pseudonymous identifier derived via HMAC-SHA256 of the
7
+ * user's BankID personal number. It is stable per person but non-reversible —
8
+ * organisations cannot extract the personal number from it.
9
+ */
10
+ interface AgentPassClaims {
11
+ /** Pseudonymous, stable per-user identifier (HMAC-SHA256 of personal number). */
12
+ sub: string;
13
+ /** Issuer — always "agentpass". */
14
+ iss: string;
15
+ /** Issued-at Unix timestamp. */
16
+ iat: number;
17
+ /** Expiry Unix timestamp (1 hour after iat). */
18
+ exp: number;
19
+ /** Unique JWT ID — enables token logging / replay detection on your side. */
20
+ jti: string;
21
+ /** Authentication method — always "bankid" in this version. */
22
+ auth_method: "bankid";
23
+ }
24
+ interface AgentPassVerified {
25
+ verified: true;
26
+ claims: AgentPassClaims;
27
+ }
28
+ interface AgentPassUnverified {
29
+ verified: false;
30
+ /** Why verification was skipped or failed. */
31
+ reason: "not_agent" | "no_token" | "invalid_token";
32
+ }
33
+ /** Result attached to every request processed by the AgentPass middleware. */
34
+ type AgentPassResult = AgentPassVerified | AgentPassUnverified;
35
+ interface VerifierOptions {
36
+ /**
37
+ * URL of the AgentPass JWKS endpoint.
38
+ * Must use HTTPS (except `localhost` / `127.0.0.1` in development).
39
+ *
40
+ * @default 'https://agentpass.vercel.app/api/jwks'
41
+ */
42
+ jwksUrl?: string;
43
+ /**
44
+ * Block agent requests that carry no valid AgentPass token.
45
+ * When `false` the middleware still runs but sets an unverified result
46
+ * on the request instead of returning 403.
47
+ *
48
+ * @default true
49
+ */
50
+ blockUnauthorizedAgents?: boolean;
51
+ /**
52
+ * Allowed clock skew in seconds when verifying JWT expiry.
53
+ * Protects against minor clock drift between issuer and verifier.
54
+ *
55
+ * @default 30
56
+ */
57
+ clockTolerance?: number;
58
+ }
59
+
60
+ type AgentPassMiddlewareOptions = VerifierOptions & {
61
+ /**
62
+ * Optional callback invoked when an agent is detected but carries no
63
+ * valid token. Return a `NextResponse` to override the default 403.
64
+ */
65
+ onUnauthorizedAgent?: (request: NextRequest, reason: string) => NextResponse | void;
66
+ };
67
+ /**
68
+ * Factory that returns a Next.js middleware function which detects AI-agent
69
+ * traffic and enforces AgentPass JWT authentication.
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * // middleware.ts (project root)
74
+ * import { createAgentPassMiddleware } from '@agent-id/nextjs';
75
+ *
76
+ * const agentPass = createAgentPassMiddleware({
77
+ * blockUnauthorizedAgents: true,
78
+ * });
79
+ *
80
+ * export function middleware(request: NextRequest) {
81
+ * return agentPass(request);
82
+ * }
83
+ *
84
+ * export const config = { matcher: '/api/:path*' };
85
+ * ```
86
+ */
87
+ declare function createAgentPassMiddleware(options?: AgentPassMiddlewareOptions): (request: NextRequest) => Promise<NextResponse>;
88
+
89
+ interface VerifyTokenOptions {
90
+ jwksUrl?: string;
91
+ clockTolerance?: number;
92
+ }
93
+ /**
94
+ * Verify an AgentPass JWT and return its decoded claims.
95
+ *
96
+ * Security guarantees
97
+ * ───────────────────
98
+ * • Signature — RS256, verified against the live JWKS public key.
99
+ * • Algorithm — strict allowlist `['RS256']`; alg:none and HS256 are
100
+ * rejected before signature verification even begins.
101
+ * • Issuer — must be exactly "agentpass".
102
+ * • Expiry — enforced; configurable clock tolerance (default 30 s).
103
+ * • kid — jose matches the JWT `kid` header to the JWKS automatically.
104
+ * • auth_method — validated at runtime; must equal "bankid".
105
+ *
106
+ * @throws if the token is invalid, expired, or fails any check.
107
+ */
108
+ declare function verifyAgentPassToken(token: string, options?: VerifyTokenOptions): Promise<AgentPassClaims>;
109
+
110
+ /**
111
+ * Bot / AI-agent detection for Next.js (Edge Runtime compatible).
112
+ *
113
+ * Uses layered heuristics:
114
+ * 1. Explicit bot / AI-agent User-Agent strings
115
+ * 2. Cloudflare Bot Management score (if header is present)
116
+ * 3. Absence of browser signals (no Accept-Language + non-browser UA)
117
+ *
118
+ * The detector is intentionally conservative — when in doubt it lets the
119
+ * request through (fail open). A false negative (undetected bot) means the
120
+ * request passes without a JWT check. A false positive (human flagged as bot)
121
+ * would block legitimate traffic, which is much worse.
122
+ */
123
+ /**
124
+ * Returns `true` if the request appears to originate from an automated
125
+ * agent or bot rather than a human browser.
126
+ *
127
+ * @param headers - The `Headers` object from a Next.js `NextRequest`
128
+ */
129
+ declare function isAgentRequest(headers: Headers): boolean;
130
+ /**
131
+ * Extract the AgentPass JWT from request headers.
132
+ *
133
+ * Checks in order:
134
+ * 1. `Authorization: Bearer <token>` (preferred)
135
+ * 2. `X-AgentPass-Token` (fallback when Authorization is stripped by proxies)
136
+ *
137
+ * @returns The raw JWT string, or `null` if absent.
138
+ */
139
+ declare function extractToken(headers: Headers): string | null;
140
+
141
+ /**
142
+ * Extract the verified AgentPass identity from a Next.js App Router request.
143
+ *
144
+ * This function reads the headers set by `createAgentPassMiddleware`. It must
145
+ * only be called from route handlers that sit behind the middleware.
146
+ *
147
+ * @example
148
+ * ```ts
149
+ * // app/api/data/route.ts
150
+ * import { getAgentPassResult } from '@agent-id/nextjs';
151
+ *
152
+ * export async function GET(request: NextRequest) {
153
+ * const agentPass = getAgentPassResult(request);
154
+ * if (!agentPass.verified) {
155
+ * return Response.json({ error: 'Unauthorized' }, { status: 403 });
156
+ * }
157
+ * return Response.json({ sub: agentPass.claims.sub });
158
+ * }
159
+ * ```
160
+ */
161
+ declare function getAgentPassResult(request: NextRequest): AgentPassResult;
162
+
163
+ export { type AgentPassClaims, type AgentPassMiddlewareOptions, type AgentPassResult, type AgentPassUnverified, type AgentPassVerified, type VerifierOptions, type VerifyTokenOptions, createAgentPassMiddleware, extractToken, getAgentPassResult, isAgentRequest, verifyAgentPassToken };
package/dist/index.js ADDED
@@ -0,0 +1,244 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ createAgentPassMiddleware: () => createAgentPassMiddleware,
24
+ extractToken: () => extractToken,
25
+ getAgentPassResult: () => getAgentPassResult,
26
+ isAgentRequest: () => isAgentRequest,
27
+ verifyAgentPassToken: () => verifyAgentPassToken
28
+ });
29
+ module.exports = __toCommonJS(index_exports);
30
+
31
+ // src/middleware.ts
32
+ var import_server = require("next/server");
33
+
34
+ // src/detect.ts
35
+ var BOT_UA_PATTERNS = [
36
+ // OpenAI
37
+ /GPTBot/i,
38
+ /ChatGPT-User/i,
39
+ /OAI-SearchBot/i,
40
+ // Anthropic / Claude
41
+ /ClaudeBot/i,
42
+ /Claude-Web/i,
43
+ /anthropic-ai/i,
44
+ // Google
45
+ /Googlebot/i,
46
+ /Google-Extended/i,
47
+ /AdsBot-Google/i,
48
+ // Microsoft / Bing
49
+ /bingbot/i,
50
+ /msnbot/i,
51
+ // AI search engines
52
+ /PerplexityBot/i,
53
+ /YouBot/i,
54
+ // Common HTTP automation libraries
55
+ /python-requests/i,
56
+ /node-fetch/i,
57
+ /\baxios\b/i,
58
+ /\bgot\b\//i,
59
+ /\bundici\b/i,
60
+ /\bcurl\b/i,
61
+ /\bwget\b/i,
62
+ /\bhttpie\b/i,
63
+ // Generic crawler signals (word-boundary matched to reduce false positives)
64
+ /\bbot\b/i,
65
+ /\bcrawler\b/i,
66
+ /\bspider\b/i,
67
+ /\bscraper\b/i,
68
+ /\bfetcher\b/i,
69
+ // MCP / AgentPass clients
70
+ /mcp-client/i,
71
+ /agentpass-client/i
72
+ ];
73
+ var BROWSER_UA_RE = /Mozilla\/5\.0/i;
74
+ function isAgentRequest(headers) {
75
+ const ua = headers.get("user-agent") ?? "";
76
+ if (!ua) return true;
77
+ if (BOT_UA_PATTERNS.some((p) => p.test(ua))) return true;
78
+ const cfRaw = headers.get("cf-bot-management");
79
+ if (cfRaw) {
80
+ try {
81
+ const parsed = JSON.parse(cfRaw);
82
+ if (typeof parsed.score === "number" && parsed.score < 30) return true;
83
+ } catch {
84
+ }
85
+ }
86
+ if (!BROWSER_UA_RE.test(ua) && !headers.get("accept-language")) return true;
87
+ return false;
88
+ }
89
+ function extractToken(headers) {
90
+ const auth = headers.get("authorization") ?? "";
91
+ if (auth.startsWith("Bearer ")) {
92
+ const token = auth.slice(7).trim();
93
+ if (token) return token;
94
+ }
95
+ const custom = headers.get("x-agentpass-token")?.trim();
96
+ if (custom) return custom;
97
+ return null;
98
+ }
99
+
100
+ // src/verify.ts
101
+ var import_jose = require("jose");
102
+ var DEFAULT_JWKS_URL = "https://agentpass.vercel.app/api/jwks";
103
+ var jwksSets = /* @__PURE__ */ new Map();
104
+ function getJwks(rawUrl) {
105
+ if (!jwksSets.has(rawUrl)) {
106
+ const url = new URL(rawUrl);
107
+ const isLocal = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1";
108
+ if (url.protocol !== "https:" && !isLocal) {
109
+ throw new Error(
110
+ `[agent-id] jwksUrl must use HTTPS, received: ${rawUrl}`
111
+ );
112
+ }
113
+ jwksSets.set(
114
+ rawUrl,
115
+ (0, import_jose.createRemoteJWKSet)(url, {
116
+ cacheMaxAge: 60 * 60 * 1e3
117
+ // 1 hour in ms
118
+ })
119
+ );
120
+ }
121
+ return jwksSets.get(rawUrl);
122
+ }
123
+ async function verifyAgentPassToken(token, options = {}) {
124
+ const jwksUrl = options.jwksUrl ?? DEFAULT_JWKS_URL;
125
+ const JWKS = getJwks(jwksUrl);
126
+ const { payload } = await (0, import_jose.jwtVerify)(token, JWKS, {
127
+ issuer: "agentpass",
128
+ // ↓ Critical — explicit allowlist prevents algorithm-confusion attacks.
129
+ // Any token claiming alg:"none", alg:"HS256", or anything else is
130
+ // rejected before signature verification.
131
+ algorithms: ["RS256"],
132
+ clockTolerance: options.clockTolerance ?? 30
133
+ });
134
+ if (payload.auth_method !== "bankid") {
135
+ throw new Error(
136
+ `[agent-id] JWT has invalid auth_method: expected "bankid", got "${payload.auth_method ?? "undefined"}"`
137
+ );
138
+ }
139
+ if (typeof payload.sub !== "string" || payload.sub.length === 0) {
140
+ throw new Error("[agent-id] JWT is missing the sub claim");
141
+ }
142
+ return payload;
143
+ }
144
+
145
+ // src/middleware.ts
146
+ var MANAGED_HEADERS = [
147
+ "x-agentpass-verified",
148
+ "x-agentpass-sub",
149
+ "x-agentpass-claims"
150
+ ];
151
+ function createAgentPassMiddleware(options = {}) {
152
+ const {
153
+ jwksUrl,
154
+ blockUnauthorizedAgents = true,
155
+ clockTolerance = 30,
156
+ onUnauthorizedAgent
157
+ } = options;
158
+ return async function agentPassMiddleware(request) {
159
+ const requestHeaders = new Headers(request.headers);
160
+ for (const name of MANAGED_HEADERS) {
161
+ requestHeaders.delete(name);
162
+ }
163
+ if (!isAgentRequest(requestHeaders)) {
164
+ return import_server.NextResponse.next({ request: { headers: requestHeaders } });
165
+ }
166
+ const token = extractToken(requestHeaders);
167
+ if (!token) {
168
+ return unauthorized(
169
+ request,
170
+ requestHeaders,
171
+ 'Agent request missing AgentPass token. Provide "Authorization: Bearer <token>".',
172
+ blockUnauthorizedAgents,
173
+ onUnauthorizedAgent
174
+ );
175
+ }
176
+ let claims;
177
+ try {
178
+ claims = await verifyAgentPassToken(token, {
179
+ ...jwksUrl !== void 0 && { jwksUrl },
180
+ clockTolerance
181
+ });
182
+ } catch (err) {
183
+ console.warn(
184
+ "[agent-id] Token verification failed:",
185
+ err instanceof Error ? err.message : String(err)
186
+ );
187
+ return unauthorized(
188
+ request,
189
+ requestHeaders,
190
+ "Invalid or expired AgentPass token.",
191
+ blockUnauthorizedAgents,
192
+ onUnauthorizedAgent
193
+ );
194
+ }
195
+ requestHeaders.set("x-agentpass-verified", "true");
196
+ requestHeaders.set("x-agentpass-sub", claims.sub);
197
+ requestHeaders.set("x-agentpass-claims", JSON.stringify(claims));
198
+ return import_server.NextResponse.next({ request: { headers: requestHeaders } });
199
+ };
200
+ }
201
+ function unauthorized(request, requestHeaders, message, block, onUnauthorized) {
202
+ if (onUnauthorized) {
203
+ const custom = onUnauthorized(request, message);
204
+ if (custom) return custom;
205
+ }
206
+ if (block) {
207
+ return import_server.NextResponse.json(
208
+ { error: "AGENT_UNAUTHORIZED", message },
209
+ { status: 403 }
210
+ );
211
+ }
212
+ requestHeaders.set("x-agentpass-verified", "false");
213
+ return import_server.NextResponse.next({ request: { headers: requestHeaders } });
214
+ }
215
+
216
+ // src/index.ts
217
+ function getAgentPassResult(request) {
218
+ const verified = request.headers.get("x-agentpass-verified");
219
+ if (verified !== "true") {
220
+ return {
221
+ verified: false,
222
+ reason: verified === "false" ? "no_token" : "not_agent"
223
+ };
224
+ }
225
+ const claimsJson = request.headers.get("x-agentpass-claims");
226
+ if (!claimsJson) {
227
+ return { verified: false, reason: "invalid_token" };
228
+ }
229
+ try {
230
+ const claims = JSON.parse(claimsJson);
231
+ return { verified: true, claims };
232
+ } catch {
233
+ return { verified: false, reason: "invalid_token" };
234
+ }
235
+ }
236
+ // Annotate the CommonJS export names for ESM import in node:
237
+ 0 && (module.exports = {
238
+ createAgentPassMiddleware,
239
+ extractToken,
240
+ getAgentPassResult,
241
+ isAgentRequest,
242
+ verifyAgentPassToken
243
+ });
244
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/middleware.ts","../src/detect.ts","../src/verify.ts"],"sourcesContent":["export { createAgentPassMiddleware } from \"./middleware.js\";\nexport type { AgentPassMiddlewareOptions } from \"./middleware.js\";\n\nexport { verifyAgentPassToken } from \"./verify.js\";\nexport type { VerifyTokenOptions } from \"./verify.js\";\n\nexport { isAgentRequest, extractToken } from \"./detect.js\";\n\nexport type {\n AgentPassClaims,\n AgentPassResult,\n AgentPassVerified,\n AgentPassUnverified,\n VerifierOptions,\n} from \"./types.js\";\n\n// ── App Router helper ────────────────────────────────────────────────────────\n\nimport type { NextRequest } from \"next/server\";\nimport type { AgentPassResult, AgentPassClaims } from \"./types.js\";\n\n/**\n * Extract the verified AgentPass identity from a Next.js App Router request.\n *\n * This function reads the headers set by `createAgentPassMiddleware`. It must\n * only be called from route handlers that sit behind the middleware.\n *\n * @example\n * ```ts\n * // app/api/data/route.ts\n * import { getAgentPassResult } from '@agent-id/nextjs';\n *\n * export async function GET(request: NextRequest) {\n * const agentPass = getAgentPassResult(request);\n * if (!agentPass.verified) {\n * return Response.json({ error: 'Unauthorized' }, { status: 403 });\n * }\n * return Response.json({ sub: agentPass.claims.sub });\n * }\n * ```\n */\nexport function getAgentPassResult(request: NextRequest): AgentPassResult {\n const verified = request.headers.get(\"x-agentpass-verified\");\n\n if (verified !== \"true\") {\n return {\n verified: false,\n reason: verified === \"false\" ? \"no_token\" : \"not_agent\",\n };\n }\n\n const claimsJson = request.headers.get(\"x-agentpass-claims\");\n if (!claimsJson) {\n return { verified: false, reason: \"invalid_token\" };\n }\n\n try {\n const claims = JSON.parse(claimsJson) as AgentPassClaims;\n return { verified: true, claims };\n } catch {\n return { verified: false, reason: \"invalid_token\" };\n }\n}\n","import { NextRequest, NextResponse } from \"next/server\";\nimport { isAgentRequest, extractToken } from \"./detect.js\";\nimport { verifyAgentPassToken } from \"./verify.js\";\nimport type { VerifierOptions, AgentPassClaims } from \"./types.js\";\n\nexport type AgentPassMiddlewareOptions = VerifierOptions & {\n /**\n * Optional callback invoked when an agent is detected but carries no\n * valid token. Return a `NextResponse` to override the default 403.\n */\n onUnauthorizedAgent?: (\n request: NextRequest,\n reason: string\n ) => NextResponse | void;\n};\n\n/**\n * Headers injected by this middleware into downstream route handlers.\n * They are stripped from the *incoming* request first to prevent spoofing.\n */\nconst MANAGED_HEADERS = [\n \"x-agentpass-verified\",\n \"x-agentpass-sub\",\n \"x-agentpass-claims\",\n] as const;\n\n/**\n * Factory that returns a Next.js middleware function which detects AI-agent\n * traffic and enforces AgentPass JWT authentication.\n *\n * @example\n * ```ts\n * // middleware.ts (project root)\n * import { createAgentPassMiddleware } from '@agent-id/nextjs';\n *\n * const agentPass = createAgentPassMiddleware({\n * blockUnauthorizedAgents: true,\n * });\n *\n * export function middleware(request: NextRequest) {\n * return agentPass(request);\n * }\n *\n * export const config = { matcher: '/api/:path*' };\n * ```\n */\nexport function createAgentPassMiddleware(\n options: AgentPassMiddlewareOptions = {}\n) {\n const {\n jwksUrl,\n blockUnauthorizedAgents = true,\n clockTolerance = 30,\n onUnauthorizedAgent,\n } = options;\n\n return async function agentPassMiddleware(\n request: NextRequest\n ): Promise<NextResponse> {\n // Clone incoming headers so we can safely mutate them.\n const requestHeaders = new Headers(request.headers);\n\n // ── Security: strip client-supplied AgentPass headers ──────────────────\n // Without this, a malicious agent could send x-agentpass-verified: true\n // and bypass the check in route handlers.\n for (const name of MANAGED_HEADERS) {\n requestHeaders.delete(name);\n }\n\n // ── Non-agent traffic ──────────────────────────────────────────────────\n if (!isAgentRequest(requestHeaders)) {\n return NextResponse.next({ request: { headers: requestHeaders } });\n }\n\n // ── Agent detected — require a valid JWT ───────────────────────────────\n const token = extractToken(requestHeaders);\n\n if (!token) {\n return unauthorized(\n request,\n requestHeaders,\n 'Agent request missing AgentPass token. Provide \"Authorization: Bearer <token>\".',\n blockUnauthorizedAgents,\n onUnauthorizedAgent\n );\n }\n\n // ── Verify the JWT ─────────────────────────────────────────────────────\n let claims: AgentPassClaims;\n try {\n claims = await verifyAgentPassToken(token, {\n ...(jwksUrl !== undefined && { jwksUrl }),\n clockTolerance,\n });\n } catch (err) {\n // Never surface the raw error to the caller — it might leak internals.\n console.warn(\n \"[agent-id] Token verification failed:\",\n err instanceof Error ? err.message : String(err)\n );\n return unauthorized(\n request,\n requestHeaders,\n \"Invalid or expired AgentPass token.\",\n blockUnauthorizedAgents,\n onUnauthorizedAgent\n );\n }\n\n // ── Verified — inject identity into request headers ────────────────────\n // Route handlers read these via getAgentPassResult(request).\n requestHeaders.set(\"x-agentpass-verified\", \"true\");\n requestHeaders.set(\"x-agentpass-sub\", claims.sub);\n // Full claims encoded as JSON for type-safe extraction by helpers.\n requestHeaders.set(\"x-agentpass-claims\", JSON.stringify(claims));\n\n return NextResponse.next({ request: { headers: requestHeaders } });\n };\n}\n\n// ── Internal helper ──────────────────────────────────────────────────────────\n\nfunction unauthorized(\n request: NextRequest,\n requestHeaders: Headers,\n message: string,\n block: boolean,\n onUnauthorized: AgentPassMiddlewareOptions[\"onUnauthorizedAgent\"]\n): NextResponse {\n if (onUnauthorized) {\n const custom = onUnauthorized(request, message);\n if (custom) return custom;\n }\n\n if (block) {\n return NextResponse.json(\n { error: \"AGENT_UNAUTHORIZED\", message },\n { status: 403 }\n );\n }\n\n // Pass through with a \"not verified\" marker so route handlers can still\n // distinguish agent traffic from human traffic.\n requestHeaders.set(\"x-agentpass-verified\", \"false\");\n return NextResponse.next({ request: { headers: requestHeaders } });\n}\n","/**\n * Bot / AI-agent detection for Next.js (Edge Runtime compatible).\n *\n * Uses layered heuristics:\n * 1. Explicit bot / AI-agent User-Agent strings\n * 2. Cloudflare Bot Management score (if header is present)\n * 3. Absence of browser signals (no Accept-Language + non-browser UA)\n *\n * The detector is intentionally conservative — when in doubt it lets the\n * request through (fail open). A false negative (undetected bot) means the\n * request passes without a JWT check. A false positive (human flagged as bot)\n * would block legitimate traffic, which is much worse.\n */\n\nconst BOT_UA_PATTERNS: RegExp[] = [\n // OpenAI\n /GPTBot/i,\n /ChatGPT-User/i,\n /OAI-SearchBot/i,\n // Anthropic / Claude\n /ClaudeBot/i,\n /Claude-Web/i,\n /anthropic-ai/i,\n // Google\n /Googlebot/i,\n /Google-Extended/i,\n /AdsBot-Google/i,\n // Microsoft / Bing\n /bingbot/i,\n /msnbot/i,\n // AI search engines\n /PerplexityBot/i,\n /YouBot/i,\n // Common HTTP automation libraries\n /python-requests/i,\n /node-fetch/i,\n /\\baxios\\b/i,\n /\\bgot\\b\\//i,\n /\\bundici\\b/i,\n /\\bcurl\\b/i,\n /\\bwget\\b/i,\n /\\bhttpie\\b/i,\n // Generic crawler signals (word-boundary matched to reduce false positives)\n /\\bbot\\b/i,\n /\\bcrawler\\b/i,\n /\\bspider\\b/i,\n /\\bscraper\\b/i,\n /\\bfetcher\\b/i,\n // MCP / AgentPass clients\n /mcp-client/i,\n /agentpass-client/i,\n];\n\n// Every major browser includes \"Mozilla/5.0\" — its absence is a strong signal\nconst BROWSER_UA_RE = /Mozilla\\/5\\.0/i;\n\n/**\n * Returns `true` if the request appears to originate from an automated\n * agent or bot rather than a human browser.\n *\n * @param headers - The `Headers` object from a Next.js `NextRequest`\n */\nexport function isAgentRequest(headers: Headers): boolean {\n const ua = headers.get(\"user-agent\") ?? \"\";\n\n // No User-Agent → definitely automated\n if (!ua) return true;\n\n // Explicit bot / agent UA match\n if (BOT_UA_PATTERNS.some((p) => p.test(ua))) return true;\n\n // Cloudflare Bot Management: the `cf-bot-management` header contains a\n // JSON blob with a `score` field (0–100). Score < 30 → highly likely bot.\n const cfRaw = headers.get(\"cf-bot-management\");\n if (cfRaw) {\n try {\n const parsed = JSON.parse(cfRaw) as { score?: unknown };\n if (typeof parsed.score === \"number\" && parsed.score < 30) return true;\n } catch {\n // Malformed header — fail open (pass through)\n }\n }\n\n // Heuristic: non-browser UA + no Accept-Language → likely automated\n if (!BROWSER_UA_RE.test(ua) && !headers.get(\"accept-language\")) return true;\n\n return false;\n}\n\n/**\n * Extract the AgentPass JWT from request headers.\n *\n * Checks in order:\n * 1. `Authorization: Bearer <token>` (preferred)\n * 2. `X-AgentPass-Token` (fallback when Authorization is stripped by proxies)\n *\n * @returns The raw JWT string, or `null` if absent.\n */\nexport function extractToken(headers: Headers): string | null {\n // Primary: standard Authorization header\n const auth = headers.get(\"authorization\") ?? \"\";\n if (auth.startsWith(\"Bearer \")) {\n const token = auth.slice(7).trim();\n if (token) return token;\n }\n\n // Fallback: custom header\n const custom = headers.get(\"x-agentpass-token\")?.trim();\n if (custom) return custom;\n\n return null;\n}\n","import { createRemoteJWKSet, jwtVerify } from \"jose\";\nimport type { AgentPassClaims } from \"./types.js\";\n\nconst DEFAULT_JWKS_URL = \"https://agentpass.vercel.app/api/jwks\";\n\n/**\n * Module-level JWKS cache — one RemoteJWKSet instance per unique URL.\n * jose caches the fetched key material internally; re-fetches when the\n * cache TTL (1 h) expires or a new `kid` is seen.\n */\nconst jwksSets = new Map<string, ReturnType<typeof createRemoteJWKSet>>();\n\nfunction getJwks(rawUrl: string): ReturnType<typeof createRemoteJWKSet> {\n if (!jwksSets.has(rawUrl)) {\n const url = new URL(rawUrl); // throws on malformed URL\n\n // Security: HTTPS is mandatory to prevent MITM on the public-key fetch.\n // Localhost is whitelisted for local development / CI.\n const isLocal =\n url.hostname === \"localhost\" ||\n url.hostname === \"127.0.0.1\" ||\n url.hostname === \"::1\";\n\n if (url.protocol !== \"https:\" && !isLocal) {\n throw new Error(\n `[agent-id] jwksUrl must use HTTPS, received: ${rawUrl}`\n );\n }\n\n jwksSets.set(\n rawUrl,\n createRemoteJWKSet(url, {\n cacheMaxAge: 60 * 60 * 1_000, // 1 hour in ms\n })\n );\n }\n\n return jwksSets.get(rawUrl)!;\n}\n\nexport interface VerifyTokenOptions {\n jwksUrl?: string;\n clockTolerance?: number;\n}\n\n/**\n * Verify an AgentPass JWT and return its decoded claims.\n *\n * Security guarantees\n * ───────────────────\n * • Signature — RS256, verified against the live JWKS public key.\n * • Algorithm — strict allowlist `['RS256']`; alg:none and HS256 are\n * rejected before signature verification even begins.\n * • Issuer — must be exactly \"agentpass\".\n * • Expiry — enforced; configurable clock tolerance (default 30 s).\n * • kid — jose matches the JWT `kid` header to the JWKS automatically.\n * • auth_method — validated at runtime; must equal \"bankid\".\n *\n * @throws if the token is invalid, expired, or fails any check.\n */\nexport async function verifyAgentPassToken(\n token: string,\n options: VerifyTokenOptions = {}\n): Promise<AgentPassClaims> {\n const jwksUrl = options.jwksUrl ?? DEFAULT_JWKS_URL;\n const JWKS = getJwks(jwksUrl);\n\n const { payload } = await jwtVerify(token, JWKS, {\n issuer: \"agentpass\",\n // ↓ Critical — explicit allowlist prevents algorithm-confusion attacks.\n // Any token claiming alg:\"none\", alg:\"HS256\", or anything else is\n // rejected before signature verification.\n algorithms: [\"RS256\"],\n clockTolerance: options.clockTolerance ?? 30,\n });\n\n // Runtime validation of AgentPass-specific claims.\n if (payload.auth_method !== \"bankid\") {\n throw new Error(\n `[agent-id] JWT has invalid auth_method: expected \"bankid\", got \"${\n payload.auth_method ?? \"undefined\"\n }\"`\n );\n }\n if (typeof payload.sub !== \"string\" || payload.sub.length === 0) {\n throw new Error(\"[agent-id] JWT is missing the sub claim\");\n }\n\n return payload as unknown as AgentPassClaims;\n}\n"],"mappings":"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","names":[]}
package/dist/index.mjs ADDED
@@ -0,0 +1,213 @@
1
+ // src/middleware.ts
2
+ import { NextResponse } from "next/server";
3
+
4
+ // src/detect.ts
5
+ var BOT_UA_PATTERNS = [
6
+ // OpenAI
7
+ /GPTBot/i,
8
+ /ChatGPT-User/i,
9
+ /OAI-SearchBot/i,
10
+ // Anthropic / Claude
11
+ /ClaudeBot/i,
12
+ /Claude-Web/i,
13
+ /anthropic-ai/i,
14
+ // Google
15
+ /Googlebot/i,
16
+ /Google-Extended/i,
17
+ /AdsBot-Google/i,
18
+ // Microsoft / Bing
19
+ /bingbot/i,
20
+ /msnbot/i,
21
+ // AI search engines
22
+ /PerplexityBot/i,
23
+ /YouBot/i,
24
+ // Common HTTP automation libraries
25
+ /python-requests/i,
26
+ /node-fetch/i,
27
+ /\baxios\b/i,
28
+ /\bgot\b\//i,
29
+ /\bundici\b/i,
30
+ /\bcurl\b/i,
31
+ /\bwget\b/i,
32
+ /\bhttpie\b/i,
33
+ // Generic crawler signals (word-boundary matched to reduce false positives)
34
+ /\bbot\b/i,
35
+ /\bcrawler\b/i,
36
+ /\bspider\b/i,
37
+ /\bscraper\b/i,
38
+ /\bfetcher\b/i,
39
+ // MCP / AgentPass clients
40
+ /mcp-client/i,
41
+ /agentpass-client/i
42
+ ];
43
+ var BROWSER_UA_RE = /Mozilla\/5\.0/i;
44
+ function isAgentRequest(headers) {
45
+ const ua = headers.get("user-agent") ?? "";
46
+ if (!ua) return true;
47
+ if (BOT_UA_PATTERNS.some((p) => p.test(ua))) return true;
48
+ const cfRaw = headers.get("cf-bot-management");
49
+ if (cfRaw) {
50
+ try {
51
+ const parsed = JSON.parse(cfRaw);
52
+ if (typeof parsed.score === "number" && parsed.score < 30) return true;
53
+ } catch {
54
+ }
55
+ }
56
+ if (!BROWSER_UA_RE.test(ua) && !headers.get("accept-language")) return true;
57
+ return false;
58
+ }
59
+ function extractToken(headers) {
60
+ const auth = headers.get("authorization") ?? "";
61
+ if (auth.startsWith("Bearer ")) {
62
+ const token = auth.slice(7).trim();
63
+ if (token) return token;
64
+ }
65
+ const custom = headers.get("x-agentpass-token")?.trim();
66
+ if (custom) return custom;
67
+ return null;
68
+ }
69
+
70
+ // src/verify.ts
71
+ import { createRemoteJWKSet, jwtVerify } from "jose";
72
+ var DEFAULT_JWKS_URL = "https://agentpass.vercel.app/api/jwks";
73
+ var jwksSets = /* @__PURE__ */ new Map();
74
+ function getJwks(rawUrl) {
75
+ if (!jwksSets.has(rawUrl)) {
76
+ const url = new URL(rawUrl);
77
+ const isLocal = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1";
78
+ if (url.protocol !== "https:" && !isLocal) {
79
+ throw new Error(
80
+ `[agent-id] jwksUrl must use HTTPS, received: ${rawUrl}`
81
+ );
82
+ }
83
+ jwksSets.set(
84
+ rawUrl,
85
+ createRemoteJWKSet(url, {
86
+ cacheMaxAge: 60 * 60 * 1e3
87
+ // 1 hour in ms
88
+ })
89
+ );
90
+ }
91
+ return jwksSets.get(rawUrl);
92
+ }
93
+ async function verifyAgentPassToken(token, options = {}) {
94
+ const jwksUrl = options.jwksUrl ?? DEFAULT_JWKS_URL;
95
+ const JWKS = getJwks(jwksUrl);
96
+ const { payload } = await jwtVerify(token, JWKS, {
97
+ issuer: "agentpass",
98
+ // ↓ Critical — explicit allowlist prevents algorithm-confusion attacks.
99
+ // Any token claiming alg:"none", alg:"HS256", or anything else is
100
+ // rejected before signature verification.
101
+ algorithms: ["RS256"],
102
+ clockTolerance: options.clockTolerance ?? 30
103
+ });
104
+ if (payload.auth_method !== "bankid") {
105
+ throw new Error(
106
+ `[agent-id] JWT has invalid auth_method: expected "bankid", got "${payload.auth_method ?? "undefined"}"`
107
+ );
108
+ }
109
+ if (typeof payload.sub !== "string" || payload.sub.length === 0) {
110
+ throw new Error("[agent-id] JWT is missing the sub claim");
111
+ }
112
+ return payload;
113
+ }
114
+
115
+ // src/middleware.ts
116
+ var MANAGED_HEADERS = [
117
+ "x-agentpass-verified",
118
+ "x-agentpass-sub",
119
+ "x-agentpass-claims"
120
+ ];
121
+ function createAgentPassMiddleware(options = {}) {
122
+ const {
123
+ jwksUrl,
124
+ blockUnauthorizedAgents = true,
125
+ clockTolerance = 30,
126
+ onUnauthorizedAgent
127
+ } = options;
128
+ return async function agentPassMiddleware(request) {
129
+ const requestHeaders = new Headers(request.headers);
130
+ for (const name of MANAGED_HEADERS) {
131
+ requestHeaders.delete(name);
132
+ }
133
+ if (!isAgentRequest(requestHeaders)) {
134
+ return NextResponse.next({ request: { headers: requestHeaders } });
135
+ }
136
+ const token = extractToken(requestHeaders);
137
+ if (!token) {
138
+ return unauthorized(
139
+ request,
140
+ requestHeaders,
141
+ 'Agent request missing AgentPass token. Provide "Authorization: Bearer <token>".',
142
+ blockUnauthorizedAgents,
143
+ onUnauthorizedAgent
144
+ );
145
+ }
146
+ let claims;
147
+ try {
148
+ claims = await verifyAgentPassToken(token, {
149
+ ...jwksUrl !== void 0 && { jwksUrl },
150
+ clockTolerance
151
+ });
152
+ } catch (err) {
153
+ console.warn(
154
+ "[agent-id] Token verification failed:",
155
+ err instanceof Error ? err.message : String(err)
156
+ );
157
+ return unauthorized(
158
+ request,
159
+ requestHeaders,
160
+ "Invalid or expired AgentPass token.",
161
+ blockUnauthorizedAgents,
162
+ onUnauthorizedAgent
163
+ );
164
+ }
165
+ requestHeaders.set("x-agentpass-verified", "true");
166
+ requestHeaders.set("x-agentpass-sub", claims.sub);
167
+ requestHeaders.set("x-agentpass-claims", JSON.stringify(claims));
168
+ return NextResponse.next({ request: { headers: requestHeaders } });
169
+ };
170
+ }
171
+ function unauthorized(request, requestHeaders, message, block, onUnauthorized) {
172
+ if (onUnauthorized) {
173
+ const custom = onUnauthorized(request, message);
174
+ if (custom) return custom;
175
+ }
176
+ if (block) {
177
+ return NextResponse.json(
178
+ { error: "AGENT_UNAUTHORIZED", message },
179
+ { status: 403 }
180
+ );
181
+ }
182
+ requestHeaders.set("x-agentpass-verified", "false");
183
+ return NextResponse.next({ request: { headers: requestHeaders } });
184
+ }
185
+
186
+ // src/index.ts
187
+ function getAgentPassResult(request) {
188
+ const verified = request.headers.get("x-agentpass-verified");
189
+ if (verified !== "true") {
190
+ return {
191
+ verified: false,
192
+ reason: verified === "false" ? "no_token" : "not_agent"
193
+ };
194
+ }
195
+ const claimsJson = request.headers.get("x-agentpass-claims");
196
+ if (!claimsJson) {
197
+ return { verified: false, reason: "invalid_token" };
198
+ }
199
+ try {
200
+ const claims = JSON.parse(claimsJson);
201
+ return { verified: true, claims };
202
+ } catch {
203
+ return { verified: false, reason: "invalid_token" };
204
+ }
205
+ }
206
+ export {
207
+ createAgentPassMiddleware,
208
+ extractToken,
209
+ getAgentPassResult,
210
+ isAgentRequest,
211
+ verifyAgentPassToken
212
+ };
213
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/middleware.ts","../src/detect.ts","../src/verify.ts","../src/index.ts"],"sourcesContent":["import { NextRequest, NextResponse } from \"next/server\";\nimport { isAgentRequest, extractToken } from \"./detect.js\";\nimport { verifyAgentPassToken } from \"./verify.js\";\nimport type { VerifierOptions, AgentPassClaims } from \"./types.js\";\n\nexport type AgentPassMiddlewareOptions = VerifierOptions & {\n /**\n * Optional callback invoked when an agent is detected but carries no\n * valid token. Return a `NextResponse` to override the default 403.\n */\n onUnauthorizedAgent?: (\n request: NextRequest,\n reason: string\n ) => NextResponse | void;\n};\n\n/**\n * Headers injected by this middleware into downstream route handlers.\n * They are stripped from the *incoming* request first to prevent spoofing.\n */\nconst MANAGED_HEADERS = [\n \"x-agentpass-verified\",\n \"x-agentpass-sub\",\n \"x-agentpass-claims\",\n] as const;\n\n/**\n * Factory that returns a Next.js middleware function which detects AI-agent\n * traffic and enforces AgentPass JWT authentication.\n *\n * @example\n * ```ts\n * // middleware.ts (project root)\n * import { createAgentPassMiddleware } from '@agent-id/nextjs';\n *\n * const agentPass = createAgentPassMiddleware({\n * blockUnauthorizedAgents: true,\n * });\n *\n * export function middleware(request: NextRequest) {\n * return agentPass(request);\n * }\n *\n * export const config = { matcher: '/api/:path*' };\n * ```\n */\nexport function createAgentPassMiddleware(\n options: AgentPassMiddlewareOptions = {}\n) {\n const {\n jwksUrl,\n blockUnauthorizedAgents = true,\n clockTolerance = 30,\n onUnauthorizedAgent,\n } = options;\n\n return async function agentPassMiddleware(\n request: NextRequest\n ): Promise<NextResponse> {\n // Clone incoming headers so we can safely mutate them.\n const requestHeaders = new Headers(request.headers);\n\n // ── Security: strip client-supplied AgentPass headers ──────────────────\n // Without this, a malicious agent could send x-agentpass-verified: true\n // and bypass the check in route handlers.\n for (const name of MANAGED_HEADERS) {\n requestHeaders.delete(name);\n }\n\n // ── Non-agent traffic ──────────────────────────────────────────────────\n if (!isAgentRequest(requestHeaders)) {\n return NextResponse.next({ request: { headers: requestHeaders } });\n }\n\n // ── Agent detected — require a valid JWT ───────────────────────────────\n const token = extractToken(requestHeaders);\n\n if (!token) {\n return unauthorized(\n request,\n requestHeaders,\n 'Agent request missing AgentPass token. Provide \"Authorization: Bearer <token>\".',\n blockUnauthorizedAgents,\n onUnauthorizedAgent\n );\n }\n\n // ── Verify the JWT ─────────────────────────────────────────────────────\n let claims: AgentPassClaims;\n try {\n claims = await verifyAgentPassToken(token, {\n ...(jwksUrl !== undefined && { jwksUrl }),\n clockTolerance,\n });\n } catch (err) {\n // Never surface the raw error to the caller — it might leak internals.\n console.warn(\n \"[agent-id] Token verification failed:\",\n err instanceof Error ? err.message : String(err)\n );\n return unauthorized(\n request,\n requestHeaders,\n \"Invalid or expired AgentPass token.\",\n blockUnauthorizedAgents,\n onUnauthorizedAgent\n );\n }\n\n // ── Verified — inject identity into request headers ────────────────────\n // Route handlers read these via getAgentPassResult(request).\n requestHeaders.set(\"x-agentpass-verified\", \"true\");\n requestHeaders.set(\"x-agentpass-sub\", claims.sub);\n // Full claims encoded as JSON for type-safe extraction by helpers.\n requestHeaders.set(\"x-agentpass-claims\", JSON.stringify(claims));\n\n return NextResponse.next({ request: { headers: requestHeaders } });\n };\n}\n\n// ── Internal helper ──────────────────────────────────────────────────────────\n\nfunction unauthorized(\n request: NextRequest,\n requestHeaders: Headers,\n message: string,\n block: boolean,\n onUnauthorized: AgentPassMiddlewareOptions[\"onUnauthorizedAgent\"]\n): NextResponse {\n if (onUnauthorized) {\n const custom = onUnauthorized(request, message);\n if (custom) return custom;\n }\n\n if (block) {\n return NextResponse.json(\n { error: \"AGENT_UNAUTHORIZED\", message },\n { status: 403 }\n );\n }\n\n // Pass through with a \"not verified\" marker so route handlers can still\n // distinguish agent traffic from human traffic.\n requestHeaders.set(\"x-agentpass-verified\", \"false\");\n return NextResponse.next({ request: { headers: requestHeaders } });\n}\n","/**\n * Bot / AI-agent detection for Next.js (Edge Runtime compatible).\n *\n * Uses layered heuristics:\n * 1. Explicit bot / AI-agent User-Agent strings\n * 2. Cloudflare Bot Management score (if header is present)\n * 3. Absence of browser signals (no Accept-Language + non-browser UA)\n *\n * The detector is intentionally conservative — when in doubt it lets the\n * request through (fail open). A false negative (undetected bot) means the\n * request passes without a JWT check. A false positive (human flagged as bot)\n * would block legitimate traffic, which is much worse.\n */\n\nconst BOT_UA_PATTERNS: RegExp[] = [\n // OpenAI\n /GPTBot/i,\n /ChatGPT-User/i,\n /OAI-SearchBot/i,\n // Anthropic / Claude\n /ClaudeBot/i,\n /Claude-Web/i,\n /anthropic-ai/i,\n // Google\n /Googlebot/i,\n /Google-Extended/i,\n /AdsBot-Google/i,\n // Microsoft / Bing\n /bingbot/i,\n /msnbot/i,\n // AI search engines\n /PerplexityBot/i,\n /YouBot/i,\n // Common HTTP automation libraries\n /python-requests/i,\n /node-fetch/i,\n /\\baxios\\b/i,\n /\\bgot\\b\\//i,\n /\\bundici\\b/i,\n /\\bcurl\\b/i,\n /\\bwget\\b/i,\n /\\bhttpie\\b/i,\n // Generic crawler signals (word-boundary matched to reduce false positives)\n /\\bbot\\b/i,\n /\\bcrawler\\b/i,\n /\\bspider\\b/i,\n /\\bscraper\\b/i,\n /\\bfetcher\\b/i,\n // MCP / AgentPass clients\n /mcp-client/i,\n /agentpass-client/i,\n];\n\n// Every major browser includes \"Mozilla/5.0\" — its absence is a strong signal\nconst BROWSER_UA_RE = /Mozilla\\/5\\.0/i;\n\n/**\n * Returns `true` if the request appears to originate from an automated\n * agent or bot rather than a human browser.\n *\n * @param headers - The `Headers` object from a Next.js `NextRequest`\n */\nexport function isAgentRequest(headers: Headers): boolean {\n const ua = headers.get(\"user-agent\") ?? \"\";\n\n // No User-Agent → definitely automated\n if (!ua) return true;\n\n // Explicit bot / agent UA match\n if (BOT_UA_PATTERNS.some((p) => p.test(ua))) return true;\n\n // Cloudflare Bot Management: the `cf-bot-management` header contains a\n // JSON blob with a `score` field (0–100). Score < 30 → highly likely bot.\n const cfRaw = headers.get(\"cf-bot-management\");\n if (cfRaw) {\n try {\n const parsed = JSON.parse(cfRaw) as { score?: unknown };\n if (typeof parsed.score === \"number\" && parsed.score < 30) return true;\n } catch {\n // Malformed header — fail open (pass through)\n }\n }\n\n // Heuristic: non-browser UA + no Accept-Language → likely automated\n if (!BROWSER_UA_RE.test(ua) && !headers.get(\"accept-language\")) return true;\n\n return false;\n}\n\n/**\n * Extract the AgentPass JWT from request headers.\n *\n * Checks in order:\n * 1. `Authorization: Bearer <token>` (preferred)\n * 2. `X-AgentPass-Token` (fallback when Authorization is stripped by proxies)\n *\n * @returns The raw JWT string, or `null` if absent.\n */\nexport function extractToken(headers: Headers): string | null {\n // Primary: standard Authorization header\n const auth = headers.get(\"authorization\") ?? \"\";\n if (auth.startsWith(\"Bearer \")) {\n const token = auth.slice(7).trim();\n if (token) return token;\n }\n\n // Fallback: custom header\n const custom = headers.get(\"x-agentpass-token\")?.trim();\n if (custom) return custom;\n\n return null;\n}\n","import { createRemoteJWKSet, jwtVerify } from \"jose\";\nimport type { AgentPassClaims } from \"./types.js\";\n\nconst DEFAULT_JWKS_URL = \"https://agentpass.vercel.app/api/jwks\";\n\n/**\n * Module-level JWKS cache — one RemoteJWKSet instance per unique URL.\n * jose caches the fetched key material internally; re-fetches when the\n * cache TTL (1 h) expires or a new `kid` is seen.\n */\nconst jwksSets = new Map<string, ReturnType<typeof createRemoteJWKSet>>();\n\nfunction getJwks(rawUrl: string): ReturnType<typeof createRemoteJWKSet> {\n if (!jwksSets.has(rawUrl)) {\n const url = new URL(rawUrl); // throws on malformed URL\n\n // Security: HTTPS is mandatory to prevent MITM on the public-key fetch.\n // Localhost is whitelisted for local development / CI.\n const isLocal =\n url.hostname === \"localhost\" ||\n url.hostname === \"127.0.0.1\" ||\n url.hostname === \"::1\";\n\n if (url.protocol !== \"https:\" && !isLocal) {\n throw new Error(\n `[agent-id] jwksUrl must use HTTPS, received: ${rawUrl}`\n );\n }\n\n jwksSets.set(\n rawUrl,\n createRemoteJWKSet(url, {\n cacheMaxAge: 60 * 60 * 1_000, // 1 hour in ms\n })\n );\n }\n\n return jwksSets.get(rawUrl)!;\n}\n\nexport interface VerifyTokenOptions {\n jwksUrl?: string;\n clockTolerance?: number;\n}\n\n/**\n * Verify an AgentPass JWT and return its decoded claims.\n *\n * Security guarantees\n * ───────────────────\n * • Signature — RS256, verified against the live JWKS public key.\n * • Algorithm — strict allowlist `['RS256']`; alg:none and HS256 are\n * rejected before signature verification even begins.\n * • Issuer — must be exactly \"agentpass\".\n * • Expiry — enforced; configurable clock tolerance (default 30 s).\n * • kid — jose matches the JWT `kid` header to the JWKS automatically.\n * • auth_method — validated at runtime; must equal \"bankid\".\n *\n * @throws if the token is invalid, expired, or fails any check.\n */\nexport async function verifyAgentPassToken(\n token: string,\n options: VerifyTokenOptions = {}\n): Promise<AgentPassClaims> {\n const jwksUrl = options.jwksUrl ?? DEFAULT_JWKS_URL;\n const JWKS = getJwks(jwksUrl);\n\n const { payload } = await jwtVerify(token, JWKS, {\n issuer: \"agentpass\",\n // ↓ Critical — explicit allowlist prevents algorithm-confusion attacks.\n // Any token claiming alg:\"none\", alg:\"HS256\", or anything else is\n // rejected before signature verification.\n algorithms: [\"RS256\"],\n clockTolerance: options.clockTolerance ?? 30,\n });\n\n // Runtime validation of AgentPass-specific claims.\n if (payload.auth_method !== \"bankid\") {\n throw new Error(\n `[agent-id] JWT has invalid auth_method: expected \"bankid\", got \"${\n payload.auth_method ?? \"undefined\"\n }\"`\n );\n }\n if (typeof payload.sub !== \"string\" || payload.sub.length === 0) {\n throw new Error(\"[agent-id] JWT is missing the sub claim\");\n }\n\n return payload as unknown as AgentPassClaims;\n}\n","export { createAgentPassMiddleware } from \"./middleware.js\";\nexport type { AgentPassMiddlewareOptions } from \"./middleware.js\";\n\nexport { verifyAgentPassToken } from \"./verify.js\";\nexport type { VerifyTokenOptions } from \"./verify.js\";\n\nexport { isAgentRequest, extractToken } from \"./detect.js\";\n\nexport type {\n AgentPassClaims,\n AgentPassResult,\n AgentPassVerified,\n AgentPassUnverified,\n VerifierOptions,\n} from \"./types.js\";\n\n// ── App Router helper ────────────────────────────────────────────────────────\n\nimport type { NextRequest } from \"next/server\";\nimport type { AgentPassResult, AgentPassClaims } from \"./types.js\";\n\n/**\n * Extract the verified AgentPass identity from a Next.js App Router request.\n *\n * This function reads the headers set by `createAgentPassMiddleware`. It must\n * only be called from route handlers that sit behind the middleware.\n *\n * @example\n * ```ts\n * // app/api/data/route.ts\n * import { getAgentPassResult } from '@agent-id/nextjs';\n *\n * export async function GET(request: NextRequest) {\n * const agentPass = getAgentPassResult(request);\n * if (!agentPass.verified) {\n * return Response.json({ error: 'Unauthorized' }, { status: 403 });\n * }\n * return Response.json({ sub: agentPass.claims.sub });\n * }\n * ```\n */\nexport function getAgentPassResult(request: NextRequest): AgentPassResult {\n const verified = request.headers.get(\"x-agentpass-verified\");\n\n if (verified !== \"true\") {\n return {\n verified: false,\n reason: verified === \"false\" ? \"no_token\" : \"not_agent\",\n };\n }\n\n const claimsJson = request.headers.get(\"x-agentpass-claims\");\n if (!claimsJson) {\n return { verified: false, reason: \"invalid_token\" };\n }\n\n try {\n const claims = JSON.parse(claimsJson) as AgentPassClaims;\n return { verified: true, claims };\n } catch {\n return { verified: false, reason: \"invalid_token\" };\n }\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@agent-id/nextjs",
3
+ "version": "0.1.0",
4
+ "description": "Agent-ID verifier middleware for Next.js — blocks unauthorized AI agents from your API routes",
5
+ "keywords": ["agent-id", "agentpass", "bankid", "jwt", "nextjs", "middleware", "ai-agent", "mcp", "bot-detection"],
6
+ "license": "MIT",
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "import": {
13
+ "types": "./dist/index.d.ts",
14
+ "default": "./dist/index.js"
15
+ },
16
+ "require": {
17
+ "types": "./dist/index.d.cts",
18
+ "default": "./dist/index.cjs"
19
+ }
20
+ }
21
+ },
22
+ "files": ["dist", "README.md", "LICENSE"],
23
+ "scripts": {
24
+ "build": "tsup",
25
+ "dev": "tsup --watch",
26
+ "test": "vitest run",
27
+ "test:watch": "vitest",
28
+ "typecheck": "tsc --noEmit",
29
+ "prepublishOnly": "npm run typecheck && npm run build"
30
+ },
31
+ "peerDependencies": {
32
+ "next": ">=14.0.0"
33
+ },
34
+ "dependencies": {
35
+ "jose": "^6.0.0"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^22.0.0",
39
+ "next": "^16.0.0",
40
+ "tsup": "^8.0.0",
41
+ "typescript": "^5.6.0",
42
+ "vitest": "^3.0.0"
43
+ },
44
+ "engines": {
45
+ "node": ">=18.0.0"
46
+ },
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "https://github.com/your-org/agentpass"
50
+ }
51
+ }