@agent-id/nextjs 0.1.1 → 0.1.2
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 +2 -2
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @agent-id/nextjs
|
|
2
2
|
|
|
3
|
-
Next.js proxy that automatically detects AI-agent traffic and requires a valid [AgentID](https://
|
|
3
|
+
Next.js proxy that automatically detects AI-agent traffic and requires a valid [AgentID](https://agentidapp.vercel.app) JWT. Human browser traffic always passes through untouched.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -66,7 +66,7 @@ Agents add one header to every request:
|
|
|
66
66
|
Authorization: Bearer <agentid-jwt>
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
The JWT is obtained by completing a BankID flow at [
|
|
69
|
+
The JWT is obtained by completing a BankID flow at [agentidapp.vercel.app](https://agentidapp.vercel.app). Tokens are valid for 1 hour.
|
|
70
70
|
|
|
71
71
|
## Options
|
|
72
72
|
|
package/dist/index.d.mts
CHANGED
|
@@ -37,7 +37,7 @@ interface VerifierOptions {
|
|
|
37
37
|
* URL of the AgentID JWKS endpoint.
|
|
38
38
|
* Must use HTTPS (except `localhost` / `127.0.0.1` in development).
|
|
39
39
|
*
|
|
40
|
-
* @default 'https://
|
|
40
|
+
* @default 'https://agentidapp.vercel.app/api/jwks'
|
|
41
41
|
*/
|
|
42
42
|
jwksUrl?: string;
|
|
43
43
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -37,7 +37,7 @@ interface VerifierOptions {
|
|
|
37
37
|
* URL of the AgentID JWKS endpoint.
|
|
38
38
|
* Must use HTTPS (except `localhost` / `127.0.0.1` in development).
|
|
39
39
|
*
|
|
40
|
-
* @default 'https://
|
|
40
|
+
* @default 'https://agentidapp.vercel.app/api/jwks'
|
|
41
41
|
*/
|
|
42
42
|
jwksUrl?: string;
|
|
43
43
|
/**
|
package/dist/index.js
CHANGED
|
@@ -100,7 +100,7 @@ function extractToken(headers) {
|
|
|
100
100
|
|
|
101
101
|
// src/verify.ts
|
|
102
102
|
var import_jose = require("jose");
|
|
103
|
-
var DEFAULT_JWKS_URL = "https://
|
|
103
|
+
var DEFAULT_JWKS_URL = "https://agentidapp.vercel.app/api/jwks";
|
|
104
104
|
var jwksSets = /* @__PURE__ */ new Map();
|
|
105
105
|
function getJwks(rawUrl) {
|
|
106
106
|
if (!jwksSets.has(rawUrl)) {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/middleware.ts","../src/detect.ts","../src/verify.ts"],"sourcesContent":["export { createAgentIDMiddleware } from \"./middleware.js\";\nexport type { AgentIDMiddlewareOptions } from \"./middleware.js\";\n\nexport { verifyAgentIDToken } from \"./verify.js\";\nexport type { VerifyTokenOptions } from \"./verify.js\";\n\nexport { isAgentRequest, extractToken } from \"./detect.js\";\n\nexport type {\n AgentIDClaims,\n AgentIDResult,\n AgentIDVerified,\n AgentIDUnverified,\n VerifierOptions,\n} from \"./types.js\";\n\n// ── App Router helper ────────────────────────────────────────────────────────\n\nimport type { NextRequest } from \"next/server\";\nimport type { AgentIDResult, AgentIDClaims } from \"./types.js\";\n\n/**\n * Extract the verified AgentID identity from a Next.js App Router request.\n *\n * This function reads the headers set by `createAgentIDMiddleware`. It must\n * only be called from route handlers that sit behind the proxy.\n *\n * @example\n * ```ts\n * // app/api/data/route.ts\n * import { getAgentIDResult } from '@agent-id/nextjs';\n *\n * export async function GET(request: NextRequest) {\n * const agent = getAgentIDResult(request);\n * if (!agent.verified) {\n * return Response.json({ error: 'Unauthorized' }, { status: 403 });\n * }\n * return Response.json({ sub: agent.claims.sub });\n * }\n * ```\n */\nexport function getAgentIDResult(request: NextRequest): AgentIDResult {\n const verified = request.headers.get(\"x-agentid-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-agentid-claims\");\n if (!claimsJson) {\n return { verified: false, reason: \"invalid_token\" };\n }\n\n try {\n const claims = JSON.parse(claimsJson) as AgentIDClaims;\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 { verifyAgentIDToken } from \"./verify.js\";\nimport type { VerifierOptions, AgentIDClaims } from \"./types.js\";\n\nexport type AgentIDMiddlewareOptions = 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 proxy into downstream route handlers.\n * They are stripped from the *incoming* request first to prevent spoofing.\n */\nconst MANAGED_HEADERS = [\n \"x-agentid-verified\",\n \"x-agentid-sub\",\n \"x-agentid-claims\",\n] as const;\n\n/**\n * Factory that returns a Next.js proxy function which detects AI-agent\n * traffic and enforces AgentID JWT authentication.\n *\n * @example\n * ```ts\n * // proxy.ts (project root)\n * import { createAgentIDMiddleware } from '@agent-id/nextjs';\n *\n * const agentID = createAgentIDMiddleware({\n * blockUnauthorizedAgents: true,\n * });\n *\n * export function proxy(request: NextRequest) {\n * return agentID(request);\n * }\n *\n * export const config = { matcher: '/api/:path*' };\n * ```\n */\nexport function createAgentIDMiddleware(\n options: AgentIDMiddlewareOptions = {}\n) {\n const {\n jwksUrl,\n blockUnauthorizedAgents = true,\n clockTolerance = 30,\n onUnauthorizedAgent,\n } = options;\n\n return async function agentIDMiddleware(\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 AgentID headers ──────────────────\n // Without this, a malicious agent could send x-agentid-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 AgentID token. Provide \"Authorization: Bearer <token>\".',\n blockUnauthorizedAgents,\n onUnauthorizedAgent\n );\n }\n\n // ── Verify the JWT ─────────────────────────────────────────────────────\n let claims: AgentIDClaims;\n try {\n claims = await verifyAgentIDToken(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 AgentID token.\",\n blockUnauthorizedAgents,\n onUnauthorizedAgent\n );\n }\n\n // ── Verified — inject identity into request headers ────────────────────\n // Route handlers read these via getAgentIDResult(request).\n requestHeaders.set(\"x-agentid-verified\", \"true\");\n requestHeaders.set(\"x-agentid-sub\", claims.sub);\n // Full claims encoded as JSON for type-safe extraction by helpers.\n requestHeaders.set(\"x-agentid-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: AgentIDMiddlewareOptions[\"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-agentid-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 /Claude-User/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 / AgentID clients\n /mcp-client/i,\n /agentid-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 AgentID JWT from request headers.\n *\n * Checks in order:\n * 1. `Authorization: Bearer <token>` (preferred)\n * 2. `X-AgentID-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-agentid-token\")?.trim();\n if (custom) return custom;\n\n return null;\n}\n","import { createRemoteJWKSet, jwtVerify } from \"jose\";\nimport type { AgentIDClaims } 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 AgentID 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 \"agentid\".\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 verifyAgentIDToken(\n token: string,\n options: VerifyTokenOptions = {}\n): Promise<AgentIDClaims> {\n const jwksUrl = options.jwksUrl ?? DEFAULT_JWKS_URL;\n const JWKS = getJwks(jwksUrl);\n\n const { payload } = await jwtVerify(token, JWKS, {\n issuer: \"agentid\",\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 AgentID-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 AgentIDClaims;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/middleware.ts","../src/detect.ts","../src/verify.ts"],"sourcesContent":["export { createAgentIDMiddleware } from \"./middleware.js\";\nexport type { AgentIDMiddlewareOptions } from \"./middleware.js\";\n\nexport { verifyAgentIDToken } from \"./verify.js\";\nexport type { VerifyTokenOptions } from \"./verify.js\";\n\nexport { isAgentRequest, extractToken } from \"./detect.js\";\n\nexport type {\n AgentIDClaims,\n AgentIDResult,\n AgentIDVerified,\n AgentIDUnverified,\n VerifierOptions,\n} from \"./types.js\";\n\n// ── App Router helper ────────────────────────────────────────────────────────\n\nimport type { NextRequest } from \"next/server\";\nimport type { AgentIDResult, AgentIDClaims } from \"./types.js\";\n\n/**\n * Extract the verified AgentID identity from a Next.js App Router request.\n *\n * This function reads the headers set by `createAgentIDMiddleware`. It must\n * only be called from route handlers that sit behind the proxy.\n *\n * @example\n * ```ts\n * // app/api/data/route.ts\n * import { getAgentIDResult } from '@agent-id/nextjs';\n *\n * export async function GET(request: NextRequest) {\n * const agent = getAgentIDResult(request);\n * if (!agent.verified) {\n * return Response.json({ error: 'Unauthorized' }, { status: 403 });\n * }\n * return Response.json({ sub: agent.claims.sub });\n * }\n * ```\n */\nexport function getAgentIDResult(request: NextRequest): AgentIDResult {\n const verified = request.headers.get(\"x-agentid-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-agentid-claims\");\n if (!claimsJson) {\n return { verified: false, reason: \"invalid_token\" };\n }\n\n try {\n const claims = JSON.parse(claimsJson) as AgentIDClaims;\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 { verifyAgentIDToken } from \"./verify.js\";\nimport type { VerifierOptions, AgentIDClaims } from \"./types.js\";\n\nexport type AgentIDMiddlewareOptions = 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 proxy into downstream route handlers.\n * They are stripped from the *incoming* request first to prevent spoofing.\n */\nconst MANAGED_HEADERS = [\n \"x-agentid-verified\",\n \"x-agentid-sub\",\n \"x-agentid-claims\",\n] as const;\n\n/**\n * Factory that returns a Next.js proxy function which detects AI-agent\n * traffic and enforces AgentID JWT authentication.\n *\n * @example\n * ```ts\n * // proxy.ts (project root)\n * import { createAgentIDMiddleware } from '@agent-id/nextjs';\n *\n * const agentID = createAgentIDMiddleware({\n * blockUnauthorizedAgents: true,\n * });\n *\n * export function proxy(request: NextRequest) {\n * return agentID(request);\n * }\n *\n * export const config = { matcher: '/api/:path*' };\n * ```\n */\nexport function createAgentIDMiddleware(\n options: AgentIDMiddlewareOptions = {}\n) {\n const {\n jwksUrl,\n blockUnauthorizedAgents = true,\n clockTolerance = 30,\n onUnauthorizedAgent,\n } = options;\n\n return async function agentIDMiddleware(\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 AgentID headers ──────────────────\n // Without this, a malicious agent could send x-agentid-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 AgentID token. Provide \"Authorization: Bearer <token>\".',\n blockUnauthorizedAgents,\n onUnauthorizedAgent\n );\n }\n\n // ── Verify the JWT ─────────────────────────────────────────────────────\n let claims: AgentIDClaims;\n try {\n claims = await verifyAgentIDToken(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 AgentID token.\",\n blockUnauthorizedAgents,\n onUnauthorizedAgent\n );\n }\n\n // ── Verified — inject identity into request headers ────────────────────\n // Route handlers read these via getAgentIDResult(request).\n requestHeaders.set(\"x-agentid-verified\", \"true\");\n requestHeaders.set(\"x-agentid-sub\", claims.sub);\n // Full claims encoded as JSON for type-safe extraction by helpers.\n requestHeaders.set(\"x-agentid-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: AgentIDMiddlewareOptions[\"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-agentid-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 /Claude-User/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 / AgentID clients\n /mcp-client/i,\n /agentid-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 AgentID JWT from request headers.\n *\n * Checks in order:\n * 1. `Authorization: Bearer <token>` (preferred)\n * 2. `X-AgentID-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-agentid-token\")?.trim();\n if (custom) return custom;\n\n return null;\n}\n","import { createRemoteJWKSet, jwtVerify } from \"jose\";\nimport type { AgentIDClaims } from \"./types.js\";\n\nconst DEFAULT_JWKS_URL = \"https://agentidapp.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 AgentID 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 \"agentid\".\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 verifyAgentIDToken(\n token: string,\n options: VerifyTokenOptions = {}\n): Promise<AgentIDClaims> {\n const jwksUrl = options.jwksUrl ?? DEFAULT_JWKS_URL;\n const JWKS = getJwks(jwksUrl);\n\n const { payload } = await jwtVerify(token, JWKS, {\n issuer: \"agentid\",\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 AgentID-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 AgentIDClaims;\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.mjs
CHANGED
|
@@ -70,7 +70,7 @@ function extractToken(headers) {
|
|
|
70
70
|
|
|
71
71
|
// src/verify.ts
|
|
72
72
|
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
73
|
-
var DEFAULT_JWKS_URL = "https://
|
|
73
|
+
var DEFAULT_JWKS_URL = "https://agentidapp.vercel.app/api/jwks";
|
|
74
74
|
var jwksSets = /* @__PURE__ */ new Map();
|
|
75
75
|
function getJwks(rawUrl) {
|
|
76
76
|
if (!jwksSets.has(rawUrl)) {
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +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 { verifyAgentIDToken } from \"./verify.js\";\nimport type { VerifierOptions, AgentIDClaims } from \"./types.js\";\n\nexport type AgentIDMiddlewareOptions = 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 proxy into downstream route handlers.\n * They are stripped from the *incoming* request first to prevent spoofing.\n */\nconst MANAGED_HEADERS = [\n \"x-agentid-verified\",\n \"x-agentid-sub\",\n \"x-agentid-claims\",\n] as const;\n\n/**\n * Factory that returns a Next.js proxy function which detects AI-agent\n * traffic and enforces AgentID JWT authentication.\n *\n * @example\n * ```ts\n * // proxy.ts (project root)\n * import { createAgentIDMiddleware } from '@agent-id/nextjs';\n *\n * const agentID = createAgentIDMiddleware({\n * blockUnauthorizedAgents: true,\n * });\n *\n * export function proxy(request: NextRequest) {\n * return agentID(request);\n * }\n *\n * export const config = { matcher: '/api/:path*' };\n * ```\n */\nexport function createAgentIDMiddleware(\n options: AgentIDMiddlewareOptions = {}\n) {\n const {\n jwksUrl,\n blockUnauthorizedAgents = true,\n clockTolerance = 30,\n onUnauthorizedAgent,\n } = options;\n\n return async function agentIDMiddleware(\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 AgentID headers ──────────────────\n // Without this, a malicious agent could send x-agentid-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 AgentID token. Provide \"Authorization: Bearer <token>\".',\n blockUnauthorizedAgents,\n onUnauthorizedAgent\n );\n }\n\n // ── Verify the JWT ─────────────────────────────────────────────────────\n let claims: AgentIDClaims;\n try {\n claims = await verifyAgentIDToken(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 AgentID token.\",\n blockUnauthorizedAgents,\n onUnauthorizedAgent\n );\n }\n\n // ── Verified — inject identity into request headers ────────────────────\n // Route handlers read these via getAgentIDResult(request).\n requestHeaders.set(\"x-agentid-verified\", \"true\");\n requestHeaders.set(\"x-agentid-sub\", claims.sub);\n // Full claims encoded as JSON for type-safe extraction by helpers.\n requestHeaders.set(\"x-agentid-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: AgentIDMiddlewareOptions[\"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-agentid-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 /Claude-User/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 / AgentID clients\n /mcp-client/i,\n /agentid-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 AgentID JWT from request headers.\n *\n * Checks in order:\n * 1. `Authorization: Bearer <token>` (preferred)\n * 2. `X-AgentID-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-agentid-token\")?.trim();\n if (custom) return custom;\n\n return null;\n}\n","import { createRemoteJWKSet, jwtVerify } from \"jose\";\nimport type { AgentIDClaims } 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 AgentID 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 \"agentid\".\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 verifyAgentIDToken(\n token: string,\n options: VerifyTokenOptions = {}\n): Promise<AgentIDClaims> {\n const jwksUrl = options.jwksUrl ?? DEFAULT_JWKS_URL;\n const JWKS = getJwks(jwksUrl);\n\n const { payload } = await jwtVerify(token, JWKS, {\n issuer: \"agentid\",\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 AgentID-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 AgentIDClaims;\n}\n","export { createAgentIDMiddleware } from \"./middleware.js\";\nexport type { AgentIDMiddlewareOptions } from \"./middleware.js\";\n\nexport { verifyAgentIDToken } from \"./verify.js\";\nexport type { VerifyTokenOptions } from \"./verify.js\";\n\nexport { isAgentRequest, extractToken } from \"./detect.js\";\n\nexport type {\n AgentIDClaims,\n AgentIDResult,\n AgentIDVerified,\n AgentIDUnverified,\n VerifierOptions,\n} from \"./types.js\";\n\n// ── App Router helper ────────────────────────────────────────────────────────\n\nimport type { NextRequest } from \"next/server\";\nimport type { AgentIDResult, AgentIDClaims } from \"./types.js\";\n\n/**\n * Extract the verified AgentID identity from a Next.js App Router request.\n *\n * This function reads the headers set by `createAgentIDMiddleware`. It must\n * only be called from route handlers that sit behind the proxy.\n *\n * @example\n * ```ts\n * // app/api/data/route.ts\n * import { getAgentIDResult } from '@agent-id/nextjs';\n *\n * export async function GET(request: NextRequest) {\n * const agent = getAgentIDResult(request);\n * if (!agent.verified) {\n * return Response.json({ error: 'Unauthorized' }, { status: 403 });\n * }\n * return Response.json({ sub: agent.claims.sub });\n * }\n * ```\n */\nexport function getAgentIDResult(request: NextRequest): AgentIDResult {\n const verified = request.headers.get(\"x-agentid-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-agentid-claims\");\n if (!claimsJson) {\n return { verified: false, reason: \"invalid_token\" };\n }\n\n try {\n const claims = JSON.parse(claimsJson) as AgentIDClaims;\n return { verified: true, claims };\n } catch {\n return { verified: false, reason: \"invalid_token\" };\n }\n}\n"],"mappings":"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","names":[]}
|
|
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 { verifyAgentIDToken } from \"./verify.js\";\nimport type { VerifierOptions, AgentIDClaims } from \"./types.js\";\n\nexport type AgentIDMiddlewareOptions = 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 proxy into downstream route handlers.\n * They are stripped from the *incoming* request first to prevent spoofing.\n */\nconst MANAGED_HEADERS = [\n \"x-agentid-verified\",\n \"x-agentid-sub\",\n \"x-agentid-claims\",\n] as const;\n\n/**\n * Factory that returns a Next.js proxy function which detects AI-agent\n * traffic and enforces AgentID JWT authentication.\n *\n * @example\n * ```ts\n * // proxy.ts (project root)\n * import { createAgentIDMiddleware } from '@agent-id/nextjs';\n *\n * const agentID = createAgentIDMiddleware({\n * blockUnauthorizedAgents: true,\n * });\n *\n * export function proxy(request: NextRequest) {\n * return agentID(request);\n * }\n *\n * export const config = { matcher: '/api/:path*' };\n * ```\n */\nexport function createAgentIDMiddleware(\n options: AgentIDMiddlewareOptions = {}\n) {\n const {\n jwksUrl,\n blockUnauthorizedAgents = true,\n clockTolerance = 30,\n onUnauthorizedAgent,\n } = options;\n\n return async function agentIDMiddleware(\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 AgentID headers ──────────────────\n // Without this, a malicious agent could send x-agentid-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 AgentID token. Provide \"Authorization: Bearer <token>\".',\n blockUnauthorizedAgents,\n onUnauthorizedAgent\n );\n }\n\n // ── Verify the JWT ─────────────────────────────────────────────────────\n let claims: AgentIDClaims;\n try {\n claims = await verifyAgentIDToken(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 AgentID token.\",\n blockUnauthorizedAgents,\n onUnauthorizedAgent\n );\n }\n\n // ── Verified — inject identity into request headers ────────────────────\n // Route handlers read these via getAgentIDResult(request).\n requestHeaders.set(\"x-agentid-verified\", \"true\");\n requestHeaders.set(\"x-agentid-sub\", claims.sub);\n // Full claims encoded as JSON for type-safe extraction by helpers.\n requestHeaders.set(\"x-agentid-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: AgentIDMiddlewareOptions[\"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-agentid-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 /Claude-User/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 / AgentID clients\n /mcp-client/i,\n /agentid-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 AgentID JWT from request headers.\n *\n * Checks in order:\n * 1. `Authorization: Bearer <token>` (preferred)\n * 2. `X-AgentID-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-agentid-token\")?.trim();\n if (custom) return custom;\n\n return null;\n}\n","import { createRemoteJWKSet, jwtVerify } from \"jose\";\nimport type { AgentIDClaims } from \"./types.js\";\n\nconst DEFAULT_JWKS_URL = \"https://agentidapp.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 AgentID 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 \"agentid\".\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 verifyAgentIDToken(\n token: string,\n options: VerifyTokenOptions = {}\n): Promise<AgentIDClaims> {\n const jwksUrl = options.jwksUrl ?? DEFAULT_JWKS_URL;\n const JWKS = getJwks(jwksUrl);\n\n const { payload } = await jwtVerify(token, JWKS, {\n issuer: \"agentid\",\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 AgentID-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 AgentIDClaims;\n}\n","export { createAgentIDMiddleware } from \"./middleware.js\";\nexport type { AgentIDMiddlewareOptions } from \"./middleware.js\";\n\nexport { verifyAgentIDToken } from \"./verify.js\";\nexport type { VerifyTokenOptions } from \"./verify.js\";\n\nexport { isAgentRequest, extractToken } from \"./detect.js\";\n\nexport type {\n AgentIDClaims,\n AgentIDResult,\n AgentIDVerified,\n AgentIDUnverified,\n VerifierOptions,\n} from \"./types.js\";\n\n// ── App Router helper ────────────────────────────────────────────────────────\n\nimport type { NextRequest } from \"next/server\";\nimport type { AgentIDResult, AgentIDClaims } from \"./types.js\";\n\n/**\n * Extract the verified AgentID identity from a Next.js App Router request.\n *\n * This function reads the headers set by `createAgentIDMiddleware`. It must\n * only be called from route handlers that sit behind the proxy.\n *\n * @example\n * ```ts\n * // app/api/data/route.ts\n * import { getAgentIDResult } from '@agent-id/nextjs';\n *\n * export async function GET(request: NextRequest) {\n * const agent = getAgentIDResult(request);\n * if (!agent.verified) {\n * return Response.json({ error: 'Unauthorized' }, { status: 403 });\n * }\n * return Response.json({ sub: agent.claims.sub });\n * }\n * ```\n */\nexport function getAgentIDResult(request: NextRequest): AgentIDResult {\n const verified = request.headers.get(\"x-agentid-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-agentid-claims\");\n if (!claimsJson) {\n return { verified: false, reason: \"invalid_token\" };\n }\n\n try {\n const claims = JSON.parse(claimsJson) as AgentIDClaims;\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
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-id/nextjs",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Agent-ID verifier middleware for Next.js — blocks unauthorized AI agents from your API routes",
|
|
5
|
-
"keywords": ["agent-id", "
|
|
5
|
+
"keywords": ["agent-id", "bankid", "jwt", "nextjs", "middleware", "ai-agent", "mcp", "bot-detection"],
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"main": "./dist/index.cjs",
|
|
8
8
|
"module": "./dist/index.js",
|