@agent-id/express 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 +21 -0
- package/README.md +106 -0
- package/dist/index.d.mts +152 -0
- package/dist/index.d.ts +152 -0
- package/dist/index.js +222 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +192 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +57 -0
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,106 @@
|
|
|
1
|
+
# @agent-id/express
|
|
2
|
+
|
|
3
|
+
Express.js middleware that automatically detects AI-agent traffic and requires a valid [AgentID](https://agentpass.vercel.app) JWT. Human browser traffic always passes through untouched.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @agent-id/express
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires `express >= 4` and `node >= 18`.
|
|
12
|
+
|
|
13
|
+
## Setup — 2 steps
|
|
14
|
+
|
|
15
|
+
### 1. Add the middleware
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import express from 'express';
|
|
19
|
+
import { agentID } from '@agent-id/express';
|
|
20
|
+
|
|
21
|
+
const app = express();
|
|
22
|
+
|
|
23
|
+
app.use(agentID());
|
|
24
|
+
|
|
25
|
+
app.listen(3000);
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
That's it. The middleware now:
|
|
29
|
+
- Lets all human browser traffic through unchanged
|
|
30
|
+
- Requires a valid AgentID JWT from any AI agent / bot
|
|
31
|
+
- Returns `403 AGENT_UNAUTHORIZED` when the JWT is missing or invalid
|
|
32
|
+
|
|
33
|
+
### 2. Read the verified identity in your route handlers (optional)
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
app.get('/api/data', (req, res) => {
|
|
37
|
+
const agent = req.agentId;
|
|
38
|
+
|
|
39
|
+
if (agent?.verified) {
|
|
40
|
+
// Verified AI agent — claims are fully typed
|
|
41
|
+
console.log(agent.claims.sub); // pseudonymous stable user ID
|
|
42
|
+
console.log(agent.claims.auth_method); // "bankid"
|
|
43
|
+
return res.json({ ok: true, sub: agent.claims.sub });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Human traffic: agent.reason === 'not_agent'
|
|
47
|
+
return res.json({ ok: true });
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`req.agentId` is typed automatically — no extra TypeScript configuration needed.
|
|
52
|
+
|
|
53
|
+
## How agents authenticate
|
|
54
|
+
|
|
55
|
+
Agents add one header to every request:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
Authorization: Bearer <agentid-jwt>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The JWT is obtained by completing a BankID flow at [agentpass.vercel.app](https://agentpass.vercel.app). Tokens are valid for 1 hour.
|
|
62
|
+
|
|
63
|
+
## Options
|
|
64
|
+
|
|
65
|
+
All options are optional — `agentID()` with no arguments works out of the box.
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
app.use(agentID({
|
|
69
|
+
// Return 403 when an agent has no valid token (default: true).
|
|
70
|
+
// Set to false to let unverified agents through (useful for logging / gradual rollout).
|
|
71
|
+
blockUnauthorizedAgents: true,
|
|
72
|
+
|
|
73
|
+
// Override the JWKS endpoint — only needed if you self-host AgentID.
|
|
74
|
+
jwksUrl: 'https://your-agentid.example.com/api/jwks',
|
|
75
|
+
|
|
76
|
+
// Clock skew tolerance in seconds (default: 30).
|
|
77
|
+
clockTolerance: 30,
|
|
78
|
+
|
|
79
|
+
// Fully custom response when an agent is rejected.
|
|
80
|
+
onUnauthorizedAgent: (req, res, next, reason) => {
|
|
81
|
+
res.status(403).json({ error: 'No AgentID token', reason });
|
|
82
|
+
},
|
|
83
|
+
}));
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## What gets verified
|
|
87
|
+
|
|
88
|
+
Verification is **fully offline** after the first request. The public key is fetched once from the AgentID JWKS endpoint and cached for 1 hour — no per-request network call.
|
|
89
|
+
|
|
90
|
+
| Check | Requirement |
|
|
91
|
+
|---|---|
|
|
92
|
+
| Signature | RS256 — `alg:none` and HS256 are explicitly rejected |
|
|
93
|
+
| Issuer (`iss`) | Must equal `"agentid"` |
|
|
94
|
+
| Expiry (`exp`) | Must be in the future |
|
|
95
|
+
| `auth_method` | Must equal `"bankid"` |
|
|
96
|
+
|
|
97
|
+
## JWT claims
|
|
98
|
+
|
|
99
|
+
| Field | Description |
|
|
100
|
+
|---|---|
|
|
101
|
+
| `sub` | Pseudonymous stable user ID (HMAC-SHA256 of BankID personal number — non-reversible, same person always gets the same ID) |
|
|
102
|
+
| `auth_method` | Always `"bankid"` |
|
|
103
|
+
| `iss` | `"agentid"` |
|
|
104
|
+
| `exp` | Unix timestamp — 1 hour from issue |
|
|
105
|
+
| `iat` | Unix timestamp — when issued |
|
|
106
|
+
| `jti` | Unique token ID |
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import { IncomingHttpHeaders } from 'node:http';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* JWT claims present in every AgentID token.
|
|
6
|
+
*
|
|
7
|
+
* The `sub` field is a pseudonymous identifier derived via HMAC-SHA256 of the
|
|
8
|
+
* user's BankID personal number. It is stable per person but non-reversible —
|
|
9
|
+
* organisations cannot extract the personal number from it.
|
|
10
|
+
*/
|
|
11
|
+
interface AgentIDClaims {
|
|
12
|
+
/** Pseudonymous, stable per-user identifier (HMAC-SHA256 of personal number). */
|
|
13
|
+
sub: string;
|
|
14
|
+
/** Issuer — always "agentid". */
|
|
15
|
+
iss: string;
|
|
16
|
+
/** Issued-at Unix timestamp. */
|
|
17
|
+
iat: number;
|
|
18
|
+
/** Expiry Unix timestamp (1 hour after iat). */
|
|
19
|
+
exp: number;
|
|
20
|
+
/** Unique JWT ID — enables token logging / replay detection on your side. */
|
|
21
|
+
jti: string;
|
|
22
|
+
/** Authentication method — always "bankid" in this version. */
|
|
23
|
+
auth_method: "bankid";
|
|
24
|
+
}
|
|
25
|
+
interface AgentIDVerified {
|
|
26
|
+
verified: true;
|
|
27
|
+
claims: AgentIDClaims;
|
|
28
|
+
}
|
|
29
|
+
interface AgentIDUnverified {
|
|
30
|
+
verified: false;
|
|
31
|
+
/** Why verification was skipped or failed. */
|
|
32
|
+
reason: "not_agent" | "no_token" | "invalid_token";
|
|
33
|
+
}
|
|
34
|
+
/** Result attached to every request processed by the AgentID middleware. */
|
|
35
|
+
type AgentIDResult = AgentIDVerified | AgentIDUnverified;
|
|
36
|
+
interface VerifierOptions {
|
|
37
|
+
/**
|
|
38
|
+
* URL of the AgentID JWKS endpoint.
|
|
39
|
+
* Must use HTTPS (except `localhost` / `127.0.0.1` in development).
|
|
40
|
+
*
|
|
41
|
+
* @default 'https://agentpass.vercel.app/api/jwks'
|
|
42
|
+
*/
|
|
43
|
+
jwksUrl?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Block agent requests that carry no valid AgentID token.
|
|
46
|
+
* When `false` the middleware still runs but sets an unverified result
|
|
47
|
+
* on the request instead of returning 403.
|
|
48
|
+
*
|
|
49
|
+
* @default true
|
|
50
|
+
*/
|
|
51
|
+
blockUnauthorizedAgents?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Allowed clock skew in seconds when verifying JWT expiry.
|
|
54
|
+
* Protects against minor clock drift between issuer and verifier.
|
|
55
|
+
*
|
|
56
|
+
* @default 30
|
|
57
|
+
*/
|
|
58
|
+
clockTolerance?: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
declare global {
|
|
62
|
+
namespace Express {
|
|
63
|
+
interface Request {
|
|
64
|
+
/** AgentID verification result set by the agentID() middleware. */
|
|
65
|
+
agentId?: AgentIDResult;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
type AgentIDMiddlewareOptions = VerifierOptions & {
|
|
70
|
+
/**
|
|
71
|
+
* Custom handler invoked when an agent is detected but has no valid token.
|
|
72
|
+
* Call `next()` to pass through, or `res.status(403).json(...)` to block.
|
|
73
|
+
* If omitted, the default behaviour is controlled by `blockUnauthorizedAgents`.
|
|
74
|
+
*/
|
|
75
|
+
onUnauthorizedAgent?: (req: Request, res: Response, next: NextFunction, reason: string) => void;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Express middleware that detects AI-agent traffic and enforces AgentID
|
|
79
|
+
* JWT authentication.
|
|
80
|
+
*
|
|
81
|
+
* Sets `req.agentId` on every request:
|
|
82
|
+
* - `{ verified: false, reason: 'not_agent' }` — human traffic (always passes through)
|
|
83
|
+
* - `{ verified: true, claims }` — agent with valid JWT
|
|
84
|
+
* - `{ verified: false, reason: 'no_token' | 'invalid_token' }` — blocked agent
|
|
85
|
+
* (returns 403 when `blockUnauthorizedAgents: true`)
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```ts
|
|
89
|
+
* import express from 'express';
|
|
90
|
+
* import { agentID } from '@agent-id/express';
|
|
91
|
+
*
|
|
92
|
+
* const app = express();
|
|
93
|
+
* app.use(agentID({ blockUnauthorizedAgents: true }));
|
|
94
|
+
*
|
|
95
|
+
* app.get('/api/data', (req, res) => {
|
|
96
|
+
* if (req.agentId?.verified) {
|
|
97
|
+
* res.json({ sub: req.agentId.claims.sub });
|
|
98
|
+
* } else {
|
|
99
|
+
* res.status(403).json({ error: 'Unauthorized' });
|
|
100
|
+
* }
|
|
101
|
+
* });
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
declare function agentID(options?: AgentIDMiddlewareOptions): (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
105
|
+
|
|
106
|
+
interface VerifyTokenOptions {
|
|
107
|
+
jwksUrl?: string;
|
|
108
|
+
clockTolerance?: number;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Verify an AgentID JWT and return its decoded claims.
|
|
112
|
+
*
|
|
113
|
+
* Security guarantees
|
|
114
|
+
* ───────────────────
|
|
115
|
+
* • Signature — RS256, verified against the live JWKS public key.
|
|
116
|
+
* • Algorithm — strict allowlist `['RS256']`; alg:none and HS256 are
|
|
117
|
+
* rejected before signature verification even begins.
|
|
118
|
+
* • Issuer — must be exactly "agentid".
|
|
119
|
+
* • Expiry — enforced; configurable clock tolerance (default 30 s).
|
|
120
|
+
* • kid — jose matches the JWT `kid` header to the JWKS automatically.
|
|
121
|
+
* • auth_method — validated at runtime; must equal "bankid".
|
|
122
|
+
*
|
|
123
|
+
* @throws if the token is invalid, expired, or fails any check.
|
|
124
|
+
*/
|
|
125
|
+
declare function verifyAgentIDToken(token: string, options?: VerifyTokenOptions): Promise<AgentIDClaims>;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Bot / AI-agent detection for Express.js (Node.js).
|
|
129
|
+
*
|
|
130
|
+
* Mirrors the logic in @agent-id/nextjs but uses `IncomingHttpHeaders`
|
|
131
|
+
* from Node's `http` module instead of the Web `Headers` API.
|
|
132
|
+
*/
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Returns `true` if the request appears to originate from an automated
|
|
136
|
+
* agent or bot rather than a human browser.
|
|
137
|
+
*
|
|
138
|
+
* @param headers - `req.headers` from an Express `Request`
|
|
139
|
+
*/
|
|
140
|
+
declare function isAgentRequest(headers: IncomingHttpHeaders): boolean;
|
|
141
|
+
/**
|
|
142
|
+
* Extract the AgentID JWT from request headers.
|
|
143
|
+
*
|
|
144
|
+
* Checks:
|
|
145
|
+
* 1. `Authorization: Bearer <token>` (preferred)
|
|
146
|
+
* 2. `X-AgentID-Token` (fallback)
|
|
147
|
+
*
|
|
148
|
+
* @returns Raw JWT string or `null`.
|
|
149
|
+
*/
|
|
150
|
+
declare function extractToken(headers: IncomingHttpHeaders): string | null;
|
|
151
|
+
|
|
152
|
+
export { type AgentIDClaims, type AgentIDMiddlewareOptions, type AgentIDResult, type AgentIDUnverified, type AgentIDVerified, type VerifierOptions, type VerifyTokenOptions, agentID, extractToken, isAgentRequest, verifyAgentIDToken };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import { IncomingHttpHeaders } from 'node:http';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* JWT claims present in every AgentID token.
|
|
6
|
+
*
|
|
7
|
+
* The `sub` field is a pseudonymous identifier derived via HMAC-SHA256 of the
|
|
8
|
+
* user's BankID personal number. It is stable per person but non-reversible —
|
|
9
|
+
* organisations cannot extract the personal number from it.
|
|
10
|
+
*/
|
|
11
|
+
interface AgentIDClaims {
|
|
12
|
+
/** Pseudonymous, stable per-user identifier (HMAC-SHA256 of personal number). */
|
|
13
|
+
sub: string;
|
|
14
|
+
/** Issuer — always "agentid". */
|
|
15
|
+
iss: string;
|
|
16
|
+
/** Issued-at Unix timestamp. */
|
|
17
|
+
iat: number;
|
|
18
|
+
/** Expiry Unix timestamp (1 hour after iat). */
|
|
19
|
+
exp: number;
|
|
20
|
+
/** Unique JWT ID — enables token logging / replay detection on your side. */
|
|
21
|
+
jti: string;
|
|
22
|
+
/** Authentication method — always "bankid" in this version. */
|
|
23
|
+
auth_method: "bankid";
|
|
24
|
+
}
|
|
25
|
+
interface AgentIDVerified {
|
|
26
|
+
verified: true;
|
|
27
|
+
claims: AgentIDClaims;
|
|
28
|
+
}
|
|
29
|
+
interface AgentIDUnverified {
|
|
30
|
+
verified: false;
|
|
31
|
+
/** Why verification was skipped or failed. */
|
|
32
|
+
reason: "not_agent" | "no_token" | "invalid_token";
|
|
33
|
+
}
|
|
34
|
+
/** Result attached to every request processed by the AgentID middleware. */
|
|
35
|
+
type AgentIDResult = AgentIDVerified | AgentIDUnverified;
|
|
36
|
+
interface VerifierOptions {
|
|
37
|
+
/**
|
|
38
|
+
* URL of the AgentID JWKS endpoint.
|
|
39
|
+
* Must use HTTPS (except `localhost` / `127.0.0.1` in development).
|
|
40
|
+
*
|
|
41
|
+
* @default 'https://agentpass.vercel.app/api/jwks'
|
|
42
|
+
*/
|
|
43
|
+
jwksUrl?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Block agent requests that carry no valid AgentID token.
|
|
46
|
+
* When `false` the middleware still runs but sets an unverified result
|
|
47
|
+
* on the request instead of returning 403.
|
|
48
|
+
*
|
|
49
|
+
* @default true
|
|
50
|
+
*/
|
|
51
|
+
blockUnauthorizedAgents?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Allowed clock skew in seconds when verifying JWT expiry.
|
|
54
|
+
* Protects against minor clock drift between issuer and verifier.
|
|
55
|
+
*
|
|
56
|
+
* @default 30
|
|
57
|
+
*/
|
|
58
|
+
clockTolerance?: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
declare global {
|
|
62
|
+
namespace Express {
|
|
63
|
+
interface Request {
|
|
64
|
+
/** AgentID verification result set by the agentID() middleware. */
|
|
65
|
+
agentId?: AgentIDResult;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
type AgentIDMiddlewareOptions = VerifierOptions & {
|
|
70
|
+
/**
|
|
71
|
+
* Custom handler invoked when an agent is detected but has no valid token.
|
|
72
|
+
* Call `next()` to pass through, or `res.status(403).json(...)` to block.
|
|
73
|
+
* If omitted, the default behaviour is controlled by `blockUnauthorizedAgents`.
|
|
74
|
+
*/
|
|
75
|
+
onUnauthorizedAgent?: (req: Request, res: Response, next: NextFunction, reason: string) => void;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Express middleware that detects AI-agent traffic and enforces AgentID
|
|
79
|
+
* JWT authentication.
|
|
80
|
+
*
|
|
81
|
+
* Sets `req.agentId` on every request:
|
|
82
|
+
* - `{ verified: false, reason: 'not_agent' }` — human traffic (always passes through)
|
|
83
|
+
* - `{ verified: true, claims }` — agent with valid JWT
|
|
84
|
+
* - `{ verified: false, reason: 'no_token' | 'invalid_token' }` — blocked agent
|
|
85
|
+
* (returns 403 when `blockUnauthorizedAgents: true`)
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```ts
|
|
89
|
+
* import express from 'express';
|
|
90
|
+
* import { agentID } from '@agent-id/express';
|
|
91
|
+
*
|
|
92
|
+
* const app = express();
|
|
93
|
+
* app.use(agentID({ blockUnauthorizedAgents: true }));
|
|
94
|
+
*
|
|
95
|
+
* app.get('/api/data', (req, res) => {
|
|
96
|
+
* if (req.agentId?.verified) {
|
|
97
|
+
* res.json({ sub: req.agentId.claims.sub });
|
|
98
|
+
* } else {
|
|
99
|
+
* res.status(403).json({ error: 'Unauthorized' });
|
|
100
|
+
* }
|
|
101
|
+
* });
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
declare function agentID(options?: AgentIDMiddlewareOptions): (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
105
|
+
|
|
106
|
+
interface VerifyTokenOptions {
|
|
107
|
+
jwksUrl?: string;
|
|
108
|
+
clockTolerance?: number;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Verify an AgentID JWT and return its decoded claims.
|
|
112
|
+
*
|
|
113
|
+
* Security guarantees
|
|
114
|
+
* ───────────────────
|
|
115
|
+
* • Signature — RS256, verified against the live JWKS public key.
|
|
116
|
+
* • Algorithm — strict allowlist `['RS256']`; alg:none and HS256 are
|
|
117
|
+
* rejected before signature verification even begins.
|
|
118
|
+
* • Issuer — must be exactly "agentid".
|
|
119
|
+
* • Expiry — enforced; configurable clock tolerance (default 30 s).
|
|
120
|
+
* • kid — jose matches the JWT `kid` header to the JWKS automatically.
|
|
121
|
+
* • auth_method — validated at runtime; must equal "bankid".
|
|
122
|
+
*
|
|
123
|
+
* @throws if the token is invalid, expired, or fails any check.
|
|
124
|
+
*/
|
|
125
|
+
declare function verifyAgentIDToken(token: string, options?: VerifyTokenOptions): Promise<AgentIDClaims>;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Bot / AI-agent detection for Express.js (Node.js).
|
|
129
|
+
*
|
|
130
|
+
* Mirrors the logic in @agent-id/nextjs but uses `IncomingHttpHeaders`
|
|
131
|
+
* from Node's `http` module instead of the Web `Headers` API.
|
|
132
|
+
*/
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Returns `true` if the request appears to originate from an automated
|
|
136
|
+
* agent or bot rather than a human browser.
|
|
137
|
+
*
|
|
138
|
+
* @param headers - `req.headers` from an Express `Request`
|
|
139
|
+
*/
|
|
140
|
+
declare function isAgentRequest(headers: IncomingHttpHeaders): boolean;
|
|
141
|
+
/**
|
|
142
|
+
* Extract the AgentID JWT from request headers.
|
|
143
|
+
*
|
|
144
|
+
* Checks:
|
|
145
|
+
* 1. `Authorization: Bearer <token>` (preferred)
|
|
146
|
+
* 2. `X-AgentID-Token` (fallback)
|
|
147
|
+
*
|
|
148
|
+
* @returns Raw JWT string or `null`.
|
|
149
|
+
*/
|
|
150
|
+
declare function extractToken(headers: IncomingHttpHeaders): string | null;
|
|
151
|
+
|
|
152
|
+
export { type AgentIDClaims, type AgentIDMiddlewareOptions, type AgentIDResult, type AgentIDUnverified, type AgentIDVerified, type VerifierOptions, type VerifyTokenOptions, agentID, extractToken, isAgentRequest, verifyAgentIDToken };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
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
|
+
agentID: () => agentID,
|
|
24
|
+
extractToken: () => extractToken,
|
|
25
|
+
isAgentRequest: () => isAgentRequest,
|
|
26
|
+
verifyAgentIDToken: () => verifyAgentIDToken
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(index_exports);
|
|
29
|
+
|
|
30
|
+
// src/detect.ts
|
|
31
|
+
var BOT_UA_PATTERNS = [
|
|
32
|
+
// OpenAI
|
|
33
|
+
/GPTBot/i,
|
|
34
|
+
/ChatGPT-User/i,
|
|
35
|
+
/OAI-SearchBot/i,
|
|
36
|
+
// Anthropic / Claude
|
|
37
|
+
/ClaudeBot/i,
|
|
38
|
+
/Claude-Web/i,
|
|
39
|
+
/anthropic-ai/i,
|
|
40
|
+
/Claude-User/i,
|
|
41
|
+
// Google
|
|
42
|
+
/Googlebot/i,
|
|
43
|
+
/Google-Extended/i,
|
|
44
|
+
/AdsBot-Google/i,
|
|
45
|
+
// Microsoft / Bing
|
|
46
|
+
/bingbot/i,
|
|
47
|
+
/msnbot/i,
|
|
48
|
+
// AI search engines
|
|
49
|
+
/PerplexityBot/i,
|
|
50
|
+
/YouBot/i,
|
|
51
|
+
// Common HTTP automation libraries
|
|
52
|
+
/python-requests/i,
|
|
53
|
+
/node-fetch/i,
|
|
54
|
+
/\baxios\b/i,
|
|
55
|
+
/\bgot\b\//i,
|
|
56
|
+
/\bundici\b/i,
|
|
57
|
+
/\bcurl\b/i,
|
|
58
|
+
/\bwget\b/i,
|
|
59
|
+
/\bhttpie\b/i,
|
|
60
|
+
// Generic crawler signals
|
|
61
|
+
/\bbot\b/i,
|
|
62
|
+
/\bcrawler\b/i,
|
|
63
|
+
/\bspider\b/i,
|
|
64
|
+
/\bscraper\b/i,
|
|
65
|
+
/\bfetcher\b/i,
|
|
66
|
+
// MCP / AgentID clients
|
|
67
|
+
/mcp-client/i,
|
|
68
|
+
/agentid-client/i
|
|
69
|
+
];
|
|
70
|
+
var BROWSER_UA_RE = /Mozilla\/5\.0/i;
|
|
71
|
+
function single(value) {
|
|
72
|
+
return Array.isArray(value) ? value[0] : value;
|
|
73
|
+
}
|
|
74
|
+
function isAgentRequest(headers) {
|
|
75
|
+
const ua = single(headers["user-agent"]) ?? "";
|
|
76
|
+
if (!ua) return true;
|
|
77
|
+
if (BOT_UA_PATTERNS.some((p) => p.test(ua))) return true;
|
|
78
|
+
const cfRaw = single(headers["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["accept-language"]) return true;
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
function extractToken(headers) {
|
|
90
|
+
const auth = single(headers["authorization"]) ?? "";
|
|
91
|
+
if (auth.startsWith("Bearer ")) {
|
|
92
|
+
const token = auth.slice(7).trim();
|
|
93
|
+
if (token) return token;
|
|
94
|
+
}
|
|
95
|
+
const custom = single(headers["x-agentid-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 verifyAgentIDToken(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: "agentid",
|
|
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-agentid-verified",
|
|
148
|
+
"x-agentid-sub",
|
|
149
|
+
"x-agentid-claims"
|
|
150
|
+
];
|
|
151
|
+
function agentID(options = {}) {
|
|
152
|
+
const {
|
|
153
|
+
jwksUrl,
|
|
154
|
+
blockUnauthorizedAgents = true,
|
|
155
|
+
clockTolerance = 30,
|
|
156
|
+
onUnauthorizedAgent
|
|
157
|
+
} = options;
|
|
158
|
+
return async function agentIDMiddleware(req, res, next) {
|
|
159
|
+
for (const header of MANAGED_HEADERS) {
|
|
160
|
+
delete req.headers[header];
|
|
161
|
+
}
|
|
162
|
+
if (!isAgentRequest(req.headers)) {
|
|
163
|
+
req.agentId = { verified: false, reason: "not_agent" };
|
|
164
|
+
return next();
|
|
165
|
+
}
|
|
166
|
+
const token = extractToken(req.headers);
|
|
167
|
+
if (!token) {
|
|
168
|
+
return handleUnauthorized(
|
|
169
|
+
req,
|
|
170
|
+
res,
|
|
171
|
+
next,
|
|
172
|
+
"no_token",
|
|
173
|
+
'Agent request missing AgentID token. Provide "Authorization: Bearer <token>".',
|
|
174
|
+
blockUnauthorizedAgents,
|
|
175
|
+
onUnauthorizedAgent
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
let claims;
|
|
179
|
+
try {
|
|
180
|
+
claims = await verifyAgentIDToken(token, {
|
|
181
|
+
...jwksUrl !== void 0 && { jwksUrl },
|
|
182
|
+
clockTolerance
|
|
183
|
+
});
|
|
184
|
+
} catch (err) {
|
|
185
|
+
console.warn(
|
|
186
|
+
"[agent-id] Token verification failed:",
|
|
187
|
+
err instanceof Error ? err.message : String(err)
|
|
188
|
+
);
|
|
189
|
+
return handleUnauthorized(
|
|
190
|
+
req,
|
|
191
|
+
res,
|
|
192
|
+
next,
|
|
193
|
+
"invalid_token",
|
|
194
|
+
"Invalid or expired AgentID token.",
|
|
195
|
+
blockUnauthorizedAgents,
|
|
196
|
+
onUnauthorizedAgent
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
req.agentId = { verified: true, claims };
|
|
200
|
+
return next();
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function handleUnauthorized(req, res, next, reason, message, block, onUnauthorized) {
|
|
204
|
+
req.agentId = { verified: false, reason };
|
|
205
|
+
if (onUnauthorized) {
|
|
206
|
+
onUnauthorized(req, res, next, message);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (block) {
|
|
210
|
+
res.status(403).json({ error: "AGENT_UNAUTHORIZED", message });
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
next();
|
|
214
|
+
}
|
|
215
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
216
|
+
0 && (module.exports = {
|
|
217
|
+
agentID,
|
|
218
|
+
extractToken,
|
|
219
|
+
isAgentRequest,
|
|
220
|
+
verifyAgentIDToken
|
|
221
|
+
});
|
|
222
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/detect.ts","../src/verify.ts","../src/middleware.ts"],"sourcesContent":["export { agentID } 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 * Bot / AI-agent detection for Express.js (Node.js).\n *\n * Mirrors the logic in @agent-id/nextjs but uses `IncomingHttpHeaders`\n * from Node's `http` module instead of the Web `Headers` API.\n */\nimport type { IncomingHttpHeaders } from \"node:http\";\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\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\nconst BROWSER_UA_RE = /Mozilla\\/5\\.0/i;\n\n/** Normalise a potentially multi-value header to a single string. */\nfunction single(\n value: string | string[] | undefined\n): string | undefined {\n return Array.isArray(value) ? value[0] : value;\n}\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 - `req.headers` from an Express `Request`\n */\nexport function isAgentRequest(headers: IncomingHttpHeaders): boolean {\n const ua = single(headers[\"user-agent\"]) ?? \"\";\n\n if (!ua) return true;\n\n if (BOT_UA_PATTERNS.some((p) => p.test(ua))) return true;\n\n // Cloudflare Bot Management score\n const cfRaw = single(headers[\"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\n }\n }\n\n // Non-browser UA + no Accept-Language → likely automated\n if (!BROWSER_UA_RE.test(ua) && !headers[\"accept-language\"]) return true;\n\n return false;\n}\n\n/**\n * Extract the AgentID JWT from request headers.\n *\n * Checks:\n * 1. `Authorization: Bearer <token>` (preferred)\n * 2. `X-AgentID-Token` (fallback)\n *\n * @returns Raw JWT string or `null`.\n */\nexport function extractToken(headers: IncomingHttpHeaders): string | null {\n const auth = single(headers[\"authorization\"]) ?? \"\";\n if (auth.startsWith(\"Bearer \")) {\n const token = auth.slice(7).trim();\n if (token) return token;\n }\n\n const custom = single(headers[\"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","import type { Request, Response, NextFunction } from \"express\";\nimport { isAgentRequest, extractToken } from \"./detect.js\";\nimport { verifyAgentIDToken } from \"./verify.js\";\nimport type { VerifierOptions, AgentIDResult, AgentIDClaims } from \"./types.js\";\n\n// ── TypeScript augmentation ──────────────────────────────────────────────────\n// Adds `req.agentId` to Express's Request interface.\n// Consumers get full type-safety without any extra setup.\ndeclare global {\n // eslint-disable-next-line @typescript-eslint/no-namespace\n namespace Express {\n interface Request {\n /** AgentID verification result set by the agentID() middleware. */\n agentId?: AgentIDResult;\n }\n }\n}\n\nexport type AgentIDMiddlewareOptions = VerifierOptions & {\n /**\n * Custom handler invoked when an agent is detected but has no valid token.\n * Call `next()` to pass through, or `res.status(403).json(...)` to block.\n * If omitted, the default behaviour is controlled by `blockUnauthorizedAgents`.\n */\n onUnauthorizedAgent?: (\n req: Request,\n res: Response,\n next: NextFunction,\n reason: string\n ) => void;\n};\n\n/** Headers managed by this middleware — stripped from incoming requests. */\nconst MANAGED_HEADERS = [\n \"x-agentid-verified\",\n \"x-agentid-sub\",\n \"x-agentid-claims\",\n] as const;\n\n/**\n * Express middleware that detects AI-agent traffic and enforces AgentID\n * JWT authentication.\n *\n * Sets `req.agentId` on every request:\n * - `{ verified: false, reason: 'not_agent' }` — human traffic (always passes through)\n * - `{ verified: true, claims }` — agent with valid JWT\n * - `{ verified: false, reason: 'no_token' | 'invalid_token' }` — blocked agent\n * (returns 403 when `blockUnauthorizedAgents: true`)\n *\n * @example\n * ```ts\n * import express from 'express';\n * import { agentID } from '@agent-id/express';\n *\n * const app = express();\n * app.use(agentID({ blockUnauthorizedAgents: true }));\n *\n * app.get('/api/data', (req, res) => {\n * if (req.agentId?.verified) {\n * res.json({ sub: req.agentId.claims.sub });\n * } else {\n * res.status(403).json({ error: 'Unauthorized' });\n * }\n * });\n * ```\n */\nexport function agentID(options: AgentIDMiddlewareOptions = {}) {\n const {\n jwksUrl,\n blockUnauthorizedAgents = true,\n clockTolerance = 30,\n onUnauthorizedAgent,\n } = options;\n\n return async function agentIDMiddleware(\n req: Request,\n res: Response,\n next: NextFunction\n ): Promise<void> {\n // ── Security: strip client-supplied AgentID headers ──────────────────\n // Prevents a malicious client from pre-setting x-agentid-verified: true\n // to fool any downstream code that checks the header directly.\n for (const header of MANAGED_HEADERS) {\n delete req.headers[header];\n }\n\n // ── Non-agent traffic ──────────────────────────────────────────────────\n if (!isAgentRequest(req.headers)) {\n req.agentId = { verified: false, reason: \"not_agent\" };\n return next();\n }\n\n // ── Agent detected — require a valid JWT ───────────────────────────────\n const token = extractToken(req.headers);\n\n if (!token) {\n return handleUnauthorized(\n req,\n res,\n next,\n \"no_token\",\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 console.warn(\n \"[agent-id] Token verification failed:\",\n err instanceof Error ? err.message : String(err)\n );\n return handleUnauthorized(\n req,\n res,\n next,\n \"invalid_token\",\n \"Invalid or expired AgentID token.\",\n blockUnauthorizedAgents,\n onUnauthorizedAgent\n );\n }\n\n // ── Verified ───────────────────────────────────────────────────────────\n req.agentId = { verified: true, claims };\n return next();\n };\n}\n\n// ── Internal helper ──────────────────────────────────────────────────────────\n\nfunction handleUnauthorized(\n req: Request,\n res: Response,\n next: NextFunction,\n reason: \"no_token\" | \"invalid_token\",\n message: string,\n block: boolean,\n onUnauthorized: AgentIDMiddlewareOptions[\"onUnauthorizedAgent\"]\n): void {\n req.agentId = { verified: false, reason };\n\n if (onUnauthorized) {\n onUnauthorized(req, res, next, message);\n return;\n }\n\n if (block) {\n res.status(403).json({ error: \"AGENT_UNAUTHORIZED\", message });\n return;\n }\n\n next();\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// src/detect.ts
|
|
2
|
+
var BOT_UA_PATTERNS = [
|
|
3
|
+
// OpenAI
|
|
4
|
+
/GPTBot/i,
|
|
5
|
+
/ChatGPT-User/i,
|
|
6
|
+
/OAI-SearchBot/i,
|
|
7
|
+
// Anthropic / Claude
|
|
8
|
+
/ClaudeBot/i,
|
|
9
|
+
/Claude-Web/i,
|
|
10
|
+
/anthropic-ai/i,
|
|
11
|
+
/Claude-User/i,
|
|
12
|
+
// Google
|
|
13
|
+
/Googlebot/i,
|
|
14
|
+
/Google-Extended/i,
|
|
15
|
+
/AdsBot-Google/i,
|
|
16
|
+
// Microsoft / Bing
|
|
17
|
+
/bingbot/i,
|
|
18
|
+
/msnbot/i,
|
|
19
|
+
// AI search engines
|
|
20
|
+
/PerplexityBot/i,
|
|
21
|
+
/YouBot/i,
|
|
22
|
+
// Common HTTP automation libraries
|
|
23
|
+
/python-requests/i,
|
|
24
|
+
/node-fetch/i,
|
|
25
|
+
/\baxios\b/i,
|
|
26
|
+
/\bgot\b\//i,
|
|
27
|
+
/\bundici\b/i,
|
|
28
|
+
/\bcurl\b/i,
|
|
29
|
+
/\bwget\b/i,
|
|
30
|
+
/\bhttpie\b/i,
|
|
31
|
+
// Generic crawler signals
|
|
32
|
+
/\bbot\b/i,
|
|
33
|
+
/\bcrawler\b/i,
|
|
34
|
+
/\bspider\b/i,
|
|
35
|
+
/\bscraper\b/i,
|
|
36
|
+
/\bfetcher\b/i,
|
|
37
|
+
// MCP / AgentID clients
|
|
38
|
+
/mcp-client/i,
|
|
39
|
+
/agentid-client/i
|
|
40
|
+
];
|
|
41
|
+
var BROWSER_UA_RE = /Mozilla\/5\.0/i;
|
|
42
|
+
function single(value) {
|
|
43
|
+
return Array.isArray(value) ? value[0] : value;
|
|
44
|
+
}
|
|
45
|
+
function isAgentRequest(headers) {
|
|
46
|
+
const ua = single(headers["user-agent"]) ?? "";
|
|
47
|
+
if (!ua) return true;
|
|
48
|
+
if (BOT_UA_PATTERNS.some((p) => p.test(ua))) return true;
|
|
49
|
+
const cfRaw = single(headers["cf-bot-management"]);
|
|
50
|
+
if (cfRaw) {
|
|
51
|
+
try {
|
|
52
|
+
const parsed = JSON.parse(cfRaw);
|
|
53
|
+
if (typeof parsed.score === "number" && parsed.score < 30) return true;
|
|
54
|
+
} catch {
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (!BROWSER_UA_RE.test(ua) && !headers["accept-language"]) return true;
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
function extractToken(headers) {
|
|
61
|
+
const auth = single(headers["authorization"]) ?? "";
|
|
62
|
+
if (auth.startsWith("Bearer ")) {
|
|
63
|
+
const token = auth.slice(7).trim();
|
|
64
|
+
if (token) return token;
|
|
65
|
+
}
|
|
66
|
+
const custom = single(headers["x-agentid-token"])?.trim();
|
|
67
|
+
if (custom) return custom;
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/verify.ts
|
|
72
|
+
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
73
|
+
var DEFAULT_JWKS_URL = "https://agentpass.vercel.app/api/jwks";
|
|
74
|
+
var jwksSets = /* @__PURE__ */ new Map();
|
|
75
|
+
function getJwks(rawUrl) {
|
|
76
|
+
if (!jwksSets.has(rawUrl)) {
|
|
77
|
+
const url = new URL(rawUrl);
|
|
78
|
+
const isLocal = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1";
|
|
79
|
+
if (url.protocol !== "https:" && !isLocal) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`[agent-id] jwksUrl must use HTTPS, received: ${rawUrl}`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
jwksSets.set(
|
|
85
|
+
rawUrl,
|
|
86
|
+
createRemoteJWKSet(url, {
|
|
87
|
+
cacheMaxAge: 60 * 60 * 1e3
|
|
88
|
+
// 1 hour in ms
|
|
89
|
+
})
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return jwksSets.get(rawUrl);
|
|
93
|
+
}
|
|
94
|
+
async function verifyAgentIDToken(token, options = {}) {
|
|
95
|
+
const jwksUrl = options.jwksUrl ?? DEFAULT_JWKS_URL;
|
|
96
|
+
const JWKS = getJwks(jwksUrl);
|
|
97
|
+
const { payload } = await jwtVerify(token, JWKS, {
|
|
98
|
+
issuer: "agentid",
|
|
99
|
+
// ↓ Critical — explicit allowlist prevents algorithm-confusion attacks.
|
|
100
|
+
// Any token claiming alg:"none", alg:"HS256", or anything else is
|
|
101
|
+
// rejected before signature verification.
|
|
102
|
+
algorithms: ["RS256"],
|
|
103
|
+
clockTolerance: options.clockTolerance ?? 30
|
|
104
|
+
});
|
|
105
|
+
if (payload.auth_method !== "bankid") {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`[agent-id] JWT has invalid auth_method: expected "bankid", got "${payload.auth_method ?? "undefined"}"`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
if (typeof payload.sub !== "string" || payload.sub.length === 0) {
|
|
111
|
+
throw new Error("[agent-id] JWT is missing the sub claim");
|
|
112
|
+
}
|
|
113
|
+
return payload;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/middleware.ts
|
|
117
|
+
var MANAGED_HEADERS = [
|
|
118
|
+
"x-agentid-verified",
|
|
119
|
+
"x-agentid-sub",
|
|
120
|
+
"x-agentid-claims"
|
|
121
|
+
];
|
|
122
|
+
function agentID(options = {}) {
|
|
123
|
+
const {
|
|
124
|
+
jwksUrl,
|
|
125
|
+
blockUnauthorizedAgents = true,
|
|
126
|
+
clockTolerance = 30,
|
|
127
|
+
onUnauthorizedAgent
|
|
128
|
+
} = options;
|
|
129
|
+
return async function agentIDMiddleware(req, res, next) {
|
|
130
|
+
for (const header of MANAGED_HEADERS) {
|
|
131
|
+
delete req.headers[header];
|
|
132
|
+
}
|
|
133
|
+
if (!isAgentRequest(req.headers)) {
|
|
134
|
+
req.agentId = { verified: false, reason: "not_agent" };
|
|
135
|
+
return next();
|
|
136
|
+
}
|
|
137
|
+
const token = extractToken(req.headers);
|
|
138
|
+
if (!token) {
|
|
139
|
+
return handleUnauthorized(
|
|
140
|
+
req,
|
|
141
|
+
res,
|
|
142
|
+
next,
|
|
143
|
+
"no_token",
|
|
144
|
+
'Agent request missing AgentID token. Provide "Authorization: Bearer <token>".',
|
|
145
|
+
blockUnauthorizedAgents,
|
|
146
|
+
onUnauthorizedAgent
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
let claims;
|
|
150
|
+
try {
|
|
151
|
+
claims = await verifyAgentIDToken(token, {
|
|
152
|
+
...jwksUrl !== void 0 && { jwksUrl },
|
|
153
|
+
clockTolerance
|
|
154
|
+
});
|
|
155
|
+
} catch (err) {
|
|
156
|
+
console.warn(
|
|
157
|
+
"[agent-id] Token verification failed:",
|
|
158
|
+
err instanceof Error ? err.message : String(err)
|
|
159
|
+
);
|
|
160
|
+
return handleUnauthorized(
|
|
161
|
+
req,
|
|
162
|
+
res,
|
|
163
|
+
next,
|
|
164
|
+
"invalid_token",
|
|
165
|
+
"Invalid or expired AgentID token.",
|
|
166
|
+
blockUnauthorizedAgents,
|
|
167
|
+
onUnauthorizedAgent
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
req.agentId = { verified: true, claims };
|
|
171
|
+
return next();
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function handleUnauthorized(req, res, next, reason, message, block, onUnauthorized) {
|
|
175
|
+
req.agentId = { verified: false, reason };
|
|
176
|
+
if (onUnauthorized) {
|
|
177
|
+
onUnauthorized(req, res, next, message);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (block) {
|
|
181
|
+
res.status(403).json({ error: "AGENT_UNAUTHORIZED", message });
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
next();
|
|
185
|
+
}
|
|
186
|
+
export {
|
|
187
|
+
agentID,
|
|
188
|
+
extractToken,
|
|
189
|
+
isAgentRequest,
|
|
190
|
+
verifyAgentIDToken
|
|
191
|
+
};
|
|
192
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/detect.ts","../src/verify.ts","../src/middleware.ts"],"sourcesContent":["/**\n * Bot / AI-agent detection for Express.js (Node.js).\n *\n * Mirrors the logic in @agent-id/nextjs but uses `IncomingHttpHeaders`\n * from Node's `http` module instead of the Web `Headers` API.\n */\nimport type { IncomingHttpHeaders } from \"node:http\";\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\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\nconst BROWSER_UA_RE = /Mozilla\\/5\\.0/i;\n\n/** Normalise a potentially multi-value header to a single string. */\nfunction single(\n value: string | string[] | undefined\n): string | undefined {\n return Array.isArray(value) ? value[0] : value;\n}\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 - `req.headers` from an Express `Request`\n */\nexport function isAgentRequest(headers: IncomingHttpHeaders): boolean {\n const ua = single(headers[\"user-agent\"]) ?? \"\";\n\n if (!ua) return true;\n\n if (BOT_UA_PATTERNS.some((p) => p.test(ua))) return true;\n\n // Cloudflare Bot Management score\n const cfRaw = single(headers[\"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\n }\n }\n\n // Non-browser UA + no Accept-Language → likely automated\n if (!BROWSER_UA_RE.test(ua) && !headers[\"accept-language\"]) return true;\n\n return false;\n}\n\n/**\n * Extract the AgentID JWT from request headers.\n *\n * Checks:\n * 1. `Authorization: Bearer <token>` (preferred)\n * 2. `X-AgentID-Token` (fallback)\n *\n * @returns Raw JWT string or `null`.\n */\nexport function extractToken(headers: IncomingHttpHeaders): string | null {\n const auth = single(headers[\"authorization\"]) ?? \"\";\n if (auth.startsWith(\"Bearer \")) {\n const token = auth.slice(7).trim();\n if (token) return token;\n }\n\n const custom = single(headers[\"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","import type { Request, Response, NextFunction } from \"express\";\nimport { isAgentRequest, extractToken } from \"./detect.js\";\nimport { verifyAgentIDToken } from \"./verify.js\";\nimport type { VerifierOptions, AgentIDResult, AgentIDClaims } from \"./types.js\";\n\n// ── TypeScript augmentation ──────────────────────────────────────────────────\n// Adds `req.agentId` to Express's Request interface.\n// Consumers get full type-safety without any extra setup.\ndeclare global {\n // eslint-disable-next-line @typescript-eslint/no-namespace\n namespace Express {\n interface Request {\n /** AgentID verification result set by the agentID() middleware. */\n agentId?: AgentIDResult;\n }\n }\n}\n\nexport type AgentIDMiddlewareOptions = VerifierOptions & {\n /**\n * Custom handler invoked when an agent is detected but has no valid token.\n * Call `next()` to pass through, or `res.status(403).json(...)` to block.\n * If omitted, the default behaviour is controlled by `blockUnauthorizedAgents`.\n */\n onUnauthorizedAgent?: (\n req: Request,\n res: Response,\n next: NextFunction,\n reason: string\n ) => void;\n};\n\n/** Headers managed by this middleware — stripped from incoming requests. */\nconst MANAGED_HEADERS = [\n \"x-agentid-verified\",\n \"x-agentid-sub\",\n \"x-agentid-claims\",\n] as const;\n\n/**\n * Express middleware that detects AI-agent traffic and enforces AgentID\n * JWT authentication.\n *\n * Sets `req.agentId` on every request:\n * - `{ verified: false, reason: 'not_agent' }` — human traffic (always passes through)\n * - `{ verified: true, claims }` — agent with valid JWT\n * - `{ verified: false, reason: 'no_token' | 'invalid_token' }` — blocked agent\n * (returns 403 when `blockUnauthorizedAgents: true`)\n *\n * @example\n * ```ts\n * import express from 'express';\n * import { agentID } from '@agent-id/express';\n *\n * const app = express();\n * app.use(agentID({ blockUnauthorizedAgents: true }));\n *\n * app.get('/api/data', (req, res) => {\n * if (req.agentId?.verified) {\n * res.json({ sub: req.agentId.claims.sub });\n * } else {\n * res.status(403).json({ error: 'Unauthorized' });\n * }\n * });\n * ```\n */\nexport function agentID(options: AgentIDMiddlewareOptions = {}) {\n const {\n jwksUrl,\n blockUnauthorizedAgents = true,\n clockTolerance = 30,\n onUnauthorizedAgent,\n } = options;\n\n return async function agentIDMiddleware(\n req: Request,\n res: Response,\n next: NextFunction\n ): Promise<void> {\n // ── Security: strip client-supplied AgentID headers ──────────────────\n // Prevents a malicious client from pre-setting x-agentid-verified: true\n // to fool any downstream code that checks the header directly.\n for (const header of MANAGED_HEADERS) {\n delete req.headers[header];\n }\n\n // ── Non-agent traffic ──────────────────────────────────────────────────\n if (!isAgentRequest(req.headers)) {\n req.agentId = { verified: false, reason: \"not_agent\" };\n return next();\n }\n\n // ── Agent detected — require a valid JWT ───────────────────────────────\n const token = extractToken(req.headers);\n\n if (!token) {\n return handleUnauthorized(\n req,\n res,\n next,\n \"no_token\",\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 console.warn(\n \"[agent-id] Token verification failed:\",\n err instanceof Error ? err.message : String(err)\n );\n return handleUnauthorized(\n req,\n res,\n next,\n \"invalid_token\",\n \"Invalid or expired AgentID token.\",\n blockUnauthorizedAgents,\n onUnauthorizedAgent\n );\n }\n\n // ── Verified ───────────────────────────────────────────────────────────\n req.agentId = { verified: true, claims };\n return next();\n };\n}\n\n// ── Internal helper ──────────────────────────────────────────────────────────\n\nfunction handleUnauthorized(\n req: Request,\n res: Response,\n next: NextFunction,\n reason: \"no_token\" | \"invalid_token\",\n message: string,\n block: boolean,\n onUnauthorized: AgentIDMiddlewareOptions[\"onUnauthorizedAgent\"]\n): void {\n req.agentId = { verified: false, reason };\n\n if (onUnauthorized) {\n onUnauthorized(req, res, next, message);\n return;\n }\n\n if (block) {\n res.status(403).json({ error: \"AGENT_UNAUTHORIZED\", message });\n return;\n }\n\n next();\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agent-id/express",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Agent-ID verifier middleware for Express.js — blocks unauthorized AI agents from your API routes",
|
|
5
|
+
"keywords": ["agent-id", "agentpass", "bankid", "jwt", "express", "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
|
+
"express": ">=4.0.0"
|
|
33
|
+
},
|
|
34
|
+
"peerDependenciesMeta": {
|
|
35
|
+
"express": {
|
|
36
|
+
"optional": false
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"jose": "^6.0.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/express": "^5.0.0",
|
|
44
|
+
"@types/node": "^22.0.0",
|
|
45
|
+
"express": "^4.21.0",
|
|
46
|
+
"tsup": "^8.0.0",
|
|
47
|
+
"typescript": "^5.6.0",
|
|
48
|
+
"vitest": "^3.0.0"
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=18.0.0"
|
|
52
|
+
},
|
|
53
|
+
"repository": {
|
|
54
|
+
"type": "git",
|
|
55
|
+
"url": "https://github.com/your-org/agentpass"
|
|
56
|
+
}
|
|
57
|
+
}
|