@agentadmit/sdk 1.0.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/.github/workflows/publish.yml +53 -0
- package/LICENSE +56 -0
- package/README.md +203 -0
- package/dist/auth.d.ts +34 -0
- package/dist/auth.js +305 -0
- package/dist/config.d.ts +64 -0
- package/dist/config.js +92 -0
- package/dist/errors.d.ts +41 -0
- package/dist/errors.js +38 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +34 -0
- package/dist/keys.d.ts +12 -0
- package/dist/keys.js +29 -0
- package/dist/routes.d.ts +26 -0
- package/dist/routes.js +209 -0
- package/dist/storage.d.ts +62 -0
- package/dist/storage.js +161 -0
- package/package.json +55 -0
- package/src/auth.ts +356 -0
- package/src/config.ts +150 -0
- package/src/errors.ts +50 -0
- package/src/index.ts +27 -0
- package/src/keys.ts +33 -0
- package/src/routes.ts +245 -0
- package/src/storage.ts +228 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Publish @agentadmit/sdk to npm with Trusted Publishing + Provenance
|
|
2
|
+
#
|
|
3
|
+
# Trusted Publishing (OIDC) eliminates long-lived npm tokens.
|
|
4
|
+
# Provenance is generated automatically with trusted publishing.
|
|
5
|
+
#
|
|
6
|
+
# Prerequisites (one-time setup on npmjs.com):
|
|
7
|
+
# 1. Create npm org: @agentadmit
|
|
8
|
+
# 2. Create package: @agentadmit/sdk
|
|
9
|
+
# 3. Configure trusted publisher:
|
|
10
|
+
# - Organization: PhoenixCo-Founder
|
|
11
|
+
# - Repository: agentadmit-sdk-node
|
|
12
|
+
# - Workflow: publish.yml
|
|
13
|
+
# 4. Restrict token access: "Require 2FA and disallow tokens"
|
|
14
|
+
#
|
|
15
|
+
# Trigger: push a version tag (e.g., git tag v0.1.0 && git push --tags)
|
|
16
|
+
|
|
17
|
+
name: Publish to npm
|
|
18
|
+
|
|
19
|
+
on:
|
|
20
|
+
push:
|
|
21
|
+
tags:
|
|
22
|
+
- 'v*'
|
|
23
|
+
|
|
24
|
+
permissions:
|
|
25
|
+
contents: read
|
|
26
|
+
id-token: write
|
|
27
|
+
|
|
28
|
+
jobs:
|
|
29
|
+
publish:
|
|
30
|
+
runs-on: ubuntu-latest
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@v4
|
|
33
|
+
|
|
34
|
+
- uses: actions/setup-node@v4
|
|
35
|
+
with:
|
|
36
|
+
node-version: '22'
|
|
37
|
+
registry-url: 'https://registry.npmjs.org'
|
|
38
|
+
|
|
39
|
+
# Install npm 11+ for trusted publishing support
|
|
40
|
+
- name: Upgrade npm for trusted publishing
|
|
41
|
+
run: npm install -g npm@latest
|
|
42
|
+
|
|
43
|
+
- name: Install dependencies
|
|
44
|
+
run: npm ci --ignore-scripts
|
|
45
|
+
|
|
46
|
+
- name: Build
|
|
47
|
+
run: npm run build
|
|
48
|
+
|
|
49
|
+
- name: Run tests
|
|
50
|
+
run: npm test --if-present
|
|
51
|
+
|
|
52
|
+
- name: Publish with trusted publishing
|
|
53
|
+
run: npm publish --access public
|
package/LICENSE
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
AgentAdmit Proprietary License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AgentAdmit LLC. All rights reserved.
|
|
4
|
+
|
|
5
|
+
Patent Pending — U.S. Application No. 19/660,916
|
|
6
|
+
|
|
7
|
+
TERMS OF USE
|
|
8
|
+
|
|
9
|
+
1. GRANT OF LICENSE. AgentAdmit LLC ("Licensor") grants you a limited,
|
|
10
|
+
non-exclusive, non-transferable, revocable license to use this software
|
|
11
|
+
development kit ("SDK") solely for the purpose of integrating with the
|
|
12
|
+
AgentAdmit hosted service (api.agentadmit.com).
|
|
13
|
+
|
|
14
|
+
2. RESTRICTIONS. You may not:
|
|
15
|
+
(a) Use this SDK with any service other than the AgentAdmit hosted service;
|
|
16
|
+
(b) Modify, adapt, or create derivative works of this SDK for the purpose
|
|
17
|
+
of building or operating a competing service;
|
|
18
|
+
(c) Reverse engineer, decompile, or disassemble the AgentAdmit protocol
|
|
19
|
+
or service architecture;
|
|
20
|
+
(d) Remove or alter any proprietary notices, labels, or marks;
|
|
21
|
+
(e) Redistribute this SDK as a standalone product or as part of a
|
|
22
|
+
competing authorization service.
|
|
23
|
+
|
|
24
|
+
3. PERMITTED USES. You may:
|
|
25
|
+
(a) Install and use this SDK in your applications;
|
|
26
|
+
(b) Include this SDK as a dependency in your projects;
|
|
27
|
+
(c) Distribute your applications that incorporate this SDK, provided
|
|
28
|
+
those applications connect to the AgentAdmit hosted service.
|
|
29
|
+
|
|
30
|
+
4. AGENTADMIT SERVICE REQUIRED. This SDK is designed exclusively for use
|
|
31
|
+
with the AgentAdmit hosted service. An AgentAdmit account and valid API
|
|
32
|
+
keys are required. Sign up at https://agentadmit.com.
|
|
33
|
+
|
|
34
|
+
5. INTELLECTUAL PROPERTY. The AgentAdmit protocol, user-mediated token
|
|
35
|
+
delivery mechanism, mandatory introspection architecture, and related
|
|
36
|
+
inventions are protected by pending U.S. patent(s) and other intellectual
|
|
37
|
+
property rights. This license does not grant any rights to the underlying
|
|
38
|
+
patents or trade secrets.
|
|
39
|
+
|
|
40
|
+
6. NO WARRANTY. THIS SDK IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND,
|
|
41
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF
|
|
42
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT.
|
|
43
|
+
|
|
44
|
+
7. LIMITATION OF LIABILITY. IN NO EVENT SHALL AGENTADMIT LLC BE LIABLE FOR
|
|
45
|
+
ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES
|
|
46
|
+
ARISING FROM YOUR USE OF THIS SDK.
|
|
47
|
+
|
|
48
|
+
8. TERMINATION. This license terminates automatically if you violate any
|
|
49
|
+
of its terms. Upon termination, you must cease all use and destroy all
|
|
50
|
+
copies of this SDK in your possession.
|
|
51
|
+
|
|
52
|
+
9. GOVERNING LAW. This license is governed by the laws of the State of
|
|
53
|
+
California, United States.
|
|
54
|
+
|
|
55
|
+
For licensing inquiries: legal@agentadmit.com
|
|
56
|
+
For developer support: https://agentadmit.com/docs
|
package/README.md
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# @agentadmit/sdk — Node.js
|
|
2
|
+
|
|
3
|
+
User-mediated AI agent authorization. Plug-and-play for Express and Next.js.
|
|
4
|
+
|
|
5
|
+
> **Get started:** Sign up at [agentadmit.com](https://agentadmit.com) → Get your test keys → Install the SDK → Build.
|
|
6
|
+
> Test keys are available immediately after signup. Live keys become available when you subscribe an app.
|
|
7
|
+
|
|
8
|
+
## Quick Start
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install @agentadmit/sdk
|
|
12
|
+
npx agentadmit init
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Edit `agentadmit.yaml` to define your scopes, then add to your Express app:
|
|
16
|
+
|
|
17
|
+
```javascript
|
|
18
|
+
const express = require('express');
|
|
19
|
+
const { loadConfig, createStorage, createAgentAdmitRouter, setStorage, requireScopeIfAgent } = require('@agentadmit/sdk');
|
|
20
|
+
|
|
21
|
+
const app = express();
|
|
22
|
+
app.use(express.json());
|
|
23
|
+
|
|
24
|
+
// Initialize AgentAdmit
|
|
25
|
+
const config = loadConfig('agentadmit.yaml');
|
|
26
|
+
const storage = createStorage(config);
|
|
27
|
+
setStorage(storage);
|
|
28
|
+
|
|
29
|
+
// Create and mount AgentAdmit routes
|
|
30
|
+
const { wellknownRouter, agentadmitRouter } = createAgentAdmitRouter({
|
|
31
|
+
storage,
|
|
32
|
+
getCurrentUser: async (req) => { /* your auth logic */ },
|
|
33
|
+
});
|
|
34
|
+
app.use(wellknownRouter);
|
|
35
|
+
app.use('/agentadmit', agentadmitRouter);
|
|
36
|
+
|
|
37
|
+
// Protect your routes with scope enforcement
|
|
38
|
+
app.get('/api/orders', requireScopeIfAgent('read:orders'), (req, res) => {
|
|
39
|
+
const user = req.agentAdmit?.user;
|
|
40
|
+
// Your existing logic — unchanged
|
|
41
|
+
res.json({ orders: getOrdersForUser(user.user_id) });
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Next.js API Routes
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
// pages/api/orders.ts (or app/api/orders/route.ts)
|
|
49
|
+
import { validateAgentToken, getConfig } from '@agentadmit/sdk';
|
|
50
|
+
|
|
51
|
+
export default async function handler(req, res) {
|
|
52
|
+
const token = req.headers.authorization?.replace('Bearer ', '');
|
|
53
|
+
const config = getConfig();
|
|
54
|
+
|
|
55
|
+
if (token?.startsWith(config.token_prefix_access)) {
|
|
56
|
+
const ctx = await validateAgentToken(token);
|
|
57
|
+
if (!ctx.scopes.includes('read:orders')) {
|
|
58
|
+
return res.status(403).json({ error: 'insufficient_scope' });
|
|
59
|
+
}
|
|
60
|
+
// Agent path
|
|
61
|
+
return res.json({ orders: await getOrders(ctx.user.user_id) });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Regular user path
|
|
65
|
+
// ... your existing auth
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## MCP Server Integration
|
|
70
|
+
|
|
71
|
+
Building an MCP server in TypeScript/Node? AgentAdmit is the auth layer. MCP servers are app owners. Same SDK, same pricing.
|
|
72
|
+
|
|
73
|
+
For **STDIO transport** (most MCP servers), the agent includes the token in tool arguments:
|
|
74
|
+
|
|
75
|
+
```javascript
|
|
76
|
+
const { validateAgentToken } = require('@agentadmit/sdk');
|
|
77
|
+
|
|
78
|
+
async function handleToolCall(name, args) {
|
|
79
|
+
// 1. Extract token from tool arguments
|
|
80
|
+
const token = args.agentadmit_token;
|
|
81
|
+
delete args.agentadmit_token;
|
|
82
|
+
if (!token) throw new Error('agentadmit_token required');
|
|
83
|
+
|
|
84
|
+
// 2. Validate via AgentAdmit hosted service
|
|
85
|
+
const ctx = await validateAgentToken(token);
|
|
86
|
+
|
|
87
|
+
// 3. Check scope for this tool
|
|
88
|
+
const required = SCOPE_MAP[name];
|
|
89
|
+
if (required && !ctx.scopes.includes(required)) {
|
|
90
|
+
throw new Error(`Missing scope '${required}'`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 4. Run the tool
|
|
94
|
+
return TOOL_HANDLERS[name](args, ctx);
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
For **HTTP transport** (Express-based MCP servers), use the full SDK middleware. The agent sends the token via `Authorization: Bearer` header, same as any HTTP API.
|
|
99
|
+
|
|
100
|
+
Full MCP integration guide with complete before/after examples: `agentadmit.com/docs/mcp-guide`
|
|
101
|
+
|
|
102
|
+
**MCP operators:** You also get the embeddable admin panel with revoke capability, admin scopes for your own AI agent to monitor your server, and full audit trail for billing. See the Admin Revocation and Embeddable Admin Panel sections below.
|
|
103
|
+
|
|
104
|
+
## How It Works
|
|
105
|
+
|
|
106
|
+
1. User clicks "AgentAdmit" in your app
|
|
107
|
+
2. Selects scopes and connection duration
|
|
108
|
+
3. Gets a token to give to their AI agent
|
|
109
|
+
4. Agent exchanges the token for scoped API access
|
|
110
|
+
5. User revokes anytime
|
|
111
|
+
|
|
112
|
+
The token goes to the human, not the agent. No automated delivery = no prompt injection surface.
|
|
113
|
+
|
|
114
|
+
## Important
|
|
115
|
+
|
|
116
|
+
**Mandatory introspection.** All token validation goes through api.agentadmit.com. There is no self-hosted mode. No local JWT validation. No bypass. This is required for security, audit logging, and scope enforcement.
|
|
117
|
+
|
|
118
|
+
**Admin revocation.** As the app operator, you can revoke any user's agent connection via `DELETE /agentadmit/admin/connections/{connection_id}` (requires admin role or `manage:connections` scope). Your own AI agent can also revoke connections if given this scope.
|
|
119
|
+
|
|
120
|
+
**Embeddable admin panel.** Drop the `<AgentAdmitAdminPanel>` React component into your admin section to view all agent connections, usage metrics, billing status, and revoke any connection without leaving your app. See the React SDK for details.
|
|
121
|
+
|
|
122
|
+
**In-app AI scopes.** If your app has built-in AI features (analysis, plan generation, photo recognition), do not expose those as agent scopes. The user's AI agent can read the raw data and do the analysis itself. Exposing in-app AI endpoints to agents creates double cost.
|
|
123
|
+
|
|
124
|
+
## Rate Limiting
|
|
125
|
+
|
|
126
|
+
The AgentAdmit introspection endpoint enforces rate limits. The Node.js SDK handles HTTP 429 responses **automatically** with exponential backoff and jitter — no changes needed in your middleware code.
|
|
127
|
+
|
|
128
|
+
### Retry behavior
|
|
129
|
+
|
|
130
|
+
| Parameter | Default | Description |
|
|
131
|
+
|-----------|---------|-------------|
|
|
132
|
+
| Initial delay | 1 second | First retry wait |
|
|
133
|
+
| Backoff multiplier | 2× | Doubles each retry |
|
|
134
|
+
| Cap | 30 seconds | Maximum wait per retry |
|
|
135
|
+
| Jitter | 0–500 ms | Random addition to each delay |
|
|
136
|
+
| Max retries | **3** | Configurable |
|
|
137
|
+
|
|
138
|
+
The SDK also respects the `Retry-After` response header — if present, it overrides the computed backoff delay.
|
|
139
|
+
|
|
140
|
+
### Configuring max retries
|
|
141
|
+
|
|
142
|
+
In `agentadmit.yaml`:
|
|
143
|
+
|
|
144
|
+
```yaml
|
|
145
|
+
max_retries: 5 # default: 3. Set to 0 to disable retries.
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Handling exhausted retries
|
|
149
|
+
|
|
150
|
+
When all retries are exhausted, `validateAgentToken` throws `RateLimitError`:
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
import { requireScope, RateLimitError } from '@agentadmit/sdk';
|
|
154
|
+
|
|
155
|
+
app.use((err: any, req, res, next) => {
|
|
156
|
+
if (err instanceof RateLimitError) {
|
|
157
|
+
res.set('Retry-After', String(err.retryAfter ?? 60));
|
|
158
|
+
return res.status(429).json({
|
|
159
|
+
error: 'rate_limited',
|
|
160
|
+
retry_after: err.retryAfter,
|
|
161
|
+
limit: err.limit,
|
|
162
|
+
remaining: err.remaining,
|
|
163
|
+
reset: err.reset,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
next(err);
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
`RateLimitError` properties:
|
|
171
|
+
- `retryAfter` — seconds from `Retry-After` header (or `null`)
|
|
172
|
+
- `limit` — `X-RateLimit-Limit` header value (or `null`)
|
|
173
|
+
- `remaining` — `X-RateLimit-Remaining` header value (or `null`)
|
|
174
|
+
- `reset` — `X-RateLimit-Reset` Unix timestamp (or `null`)
|
|
175
|
+
|
|
176
|
+
## Documentation
|
|
177
|
+
|
|
178
|
+
Full integration guide: https://agentadmit.com/docs/app-owner-guide
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
## Data Collection & Privacy
|
|
182
|
+
|
|
183
|
+
The AgentAdmit Node.js SDK runs server-side and does not interact with app stores or end-user devices directly.
|
|
184
|
+
|
|
185
|
+
### What the SDK does
|
|
186
|
+
- Validates AgentAdmit tokens presented by AI agents
|
|
187
|
+
- Enforces scope-based access control on your API routes
|
|
188
|
+
- Manages connection lifecycle (create, revoke, audit)
|
|
189
|
+
|
|
190
|
+
### What the SDK does NOT do
|
|
191
|
+
- Does not collect end-user data
|
|
192
|
+
- Does not send telemetry or analytics
|
|
193
|
+
- Does not phone home to AgentAdmit servers (all operations use your configured keys and storage)
|
|
194
|
+
- Does not track users or devices
|
|
195
|
+
|
|
196
|
+
### Privacy impact
|
|
197
|
+
Since this SDK runs on your server, it has no direct App Store or Play Store compliance surface. Your client-side integration (e.g., the AgentAdmit React SDK) handles privacy manifest and data safety requirements.
|
|
198
|
+
|
|
199
|
+
For complete compliance guidance, see our [compliance guide](https://agentadmit.com/docs/compliance).
|
|
200
|
+
|
|
201
|
+
## License
|
|
202
|
+
|
|
203
|
+
All rights reserved. Patent pending.
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agentadmit/auth.ts
|
|
3
|
+
* Token validation, scope enforcement, and audit logging for Express.
|
|
4
|
+
*/
|
|
5
|
+
import { Request, Response, NextFunction } from 'express';
|
|
6
|
+
import { StorageBackend } from './storage';
|
|
7
|
+
export declare function setStorage(storage: StorageBackend): void;
|
|
8
|
+
export declare function setUserVerifier(fn: (token: string) => string | Promise<string>): void;
|
|
9
|
+
export interface AgentContext {
|
|
10
|
+
auth_type: 'agent' | 'user';
|
|
11
|
+
user: Record<string, any>;
|
|
12
|
+
connection: Record<string, any> | null;
|
|
13
|
+
scopes: string[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Validate an ag_at_ token and return the agent context.
|
|
17
|
+
*/
|
|
18
|
+
export declare function validateAgentToken(token: string): Promise<Omit<AgentContext, 'auth_type'>>;
|
|
19
|
+
/**
|
|
20
|
+
* Express middleware: require a specific scope (agent-only).
|
|
21
|
+
*/
|
|
22
|
+
export declare function requireScope(scope: string): (req: Request, res: Response, next: NextFunction) => Promise<Response<any, Record<string, any>> | undefined>;
|
|
23
|
+
/**
|
|
24
|
+
* Express middleware: enforce scope only if caller is an agent.
|
|
25
|
+
*/
|
|
26
|
+
export declare function requireScopeIfAgent(scope: string): (req: Request, res: Response, next: NextFunction) => Promise<void | Response<any, Record<string, any>>>;
|
|
27
|
+
/**
|
|
28
|
+
* Express middleware: resolve user or agent from token.
|
|
29
|
+
*/
|
|
30
|
+
export declare function resolveAuth(): (req: Request, res: Response, next: NextFunction) => Promise<void | Response<any, Record<string, any>>>;
|
|
31
|
+
/**
|
|
32
|
+
* Check connection cap for tier enforcement.
|
|
33
|
+
*/
|
|
34
|
+
export declare function checkConnectionCap(userId: string, tier: string): Promise<void>;
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* agentadmit/auth.ts
|
|
4
|
+
* Token validation, scope enforcement, and audit logging for Express.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.setStorage = setStorage;
|
|
8
|
+
exports.setUserVerifier = setUserVerifier;
|
|
9
|
+
exports.validateAgentToken = validateAgentToken;
|
|
10
|
+
exports.requireScope = requireScope;
|
|
11
|
+
exports.requireScopeIfAgent = requireScopeIfAgent;
|
|
12
|
+
exports.resolveAuth = resolveAuth;
|
|
13
|
+
exports.checkConnectionCap = checkConnectionCap;
|
|
14
|
+
const config_1 = require("./config");
|
|
15
|
+
const errors_1 = require("./errors");
|
|
16
|
+
let _storage = null;
|
|
17
|
+
let _verifyUserToken = null;
|
|
18
|
+
function setStorage(storage) {
|
|
19
|
+
_storage = storage;
|
|
20
|
+
}
|
|
21
|
+
function setUserVerifier(fn) {
|
|
22
|
+
_verifyUserToken = fn;
|
|
23
|
+
}
|
|
24
|
+
function getStorage() {
|
|
25
|
+
if (!_storage)
|
|
26
|
+
throw new Error('AgentAdmit storage not initialized');
|
|
27
|
+
return _storage;
|
|
28
|
+
}
|
|
29
|
+
function getBearerToken(req) {
|
|
30
|
+
const auth = req.headers.authorization || '';
|
|
31
|
+
if (auth.startsWith('Bearer '))
|
|
32
|
+
return auth.slice(7);
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Rate-limit retry helpers
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
/** Parse an integer from an HTTP response header. Returns null if missing or invalid. */
|
|
39
|
+
function parseIntHeader(headers, name) {
|
|
40
|
+
const val = headers.get(name);
|
|
41
|
+
if (val === null)
|
|
42
|
+
return null;
|
|
43
|
+
const n = parseInt(val, 10);
|
|
44
|
+
return Number.isFinite(n) ? n : null;
|
|
45
|
+
}
|
|
46
|
+
/** Parse a float from an HTTP response header. Returns null if missing or invalid. */
|
|
47
|
+
function parseFloatHeader(headers, name) {
|
|
48
|
+
const val = headers.get(name);
|
|
49
|
+
if (val === null)
|
|
50
|
+
return null;
|
|
51
|
+
const n = parseFloat(val);
|
|
52
|
+
return Number.isFinite(n) ? n : null;
|
|
53
|
+
}
|
|
54
|
+
/** sleep for `ms` milliseconds */
|
|
55
|
+
function sleep(ms) {
|
|
56
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* POST to the AgentAdmit introspection endpoint with automatic 429 retry.
|
|
60
|
+
*
|
|
61
|
+
* Retry policy:
|
|
62
|
+
* - Initial delay: 1 second
|
|
63
|
+
* - Each retry doubles the delay, capped at 30 seconds
|
|
64
|
+
* - Each delay adds 0–500 ms of random jitter
|
|
65
|
+
* - Honors Retry-After header if present
|
|
66
|
+
* - After maxRetries exhausted, throws RateLimitError
|
|
67
|
+
*/
|
|
68
|
+
async function introspectWithRetry(verifyUrl, token, appId, apiKey, maxRetries) {
|
|
69
|
+
let delay = 1000; // ms
|
|
70
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
71
|
+
let response;
|
|
72
|
+
try {
|
|
73
|
+
response = await fetch(verifyUrl, {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: {
|
|
76
|
+
Authorization: `Bearer ${apiKey}`,
|
|
77
|
+
'Content-Type': 'application/json',
|
|
78
|
+
},
|
|
79
|
+
body: JSON.stringify({ token }),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
throw new Error(`AgentAdmit introspection failed (network): ${err.message}`);
|
|
84
|
+
}
|
|
85
|
+
if (response.status !== 429) {
|
|
86
|
+
return response;
|
|
87
|
+
}
|
|
88
|
+
// --- 429 handling ---
|
|
89
|
+
const retryAfter = parseFloatHeader(response.headers, 'Retry-After');
|
|
90
|
+
const limit = parseIntHeader(response.headers, 'X-RateLimit-Limit');
|
|
91
|
+
const remaining = parseIntHeader(response.headers, 'X-RateLimit-Remaining');
|
|
92
|
+
const reset = parseIntHeader(response.headers, 'X-RateLimit-Reset');
|
|
93
|
+
if (attempt >= maxRetries) {
|
|
94
|
+
throw new errors_1.RateLimitError({
|
|
95
|
+
message: `AgentAdmit rate limit exceeded. Max retries (${maxRetries}) exhausted.`,
|
|
96
|
+
retryAfter,
|
|
97
|
+
limit,
|
|
98
|
+
remaining,
|
|
99
|
+
reset,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
const waitMs = retryAfter !== null ? retryAfter * 1000 : Math.min(delay, 30000);
|
|
103
|
+
const jitterMs = Math.random() * 500; // 0–500 ms
|
|
104
|
+
const totalWaitMs = waitMs + jitterMs;
|
|
105
|
+
console.warn(`[AgentAdmit] Rate-limited (attempt ${attempt + 1}/${maxRetries}). ` +
|
|
106
|
+
`Retrying in ${(totalWaitMs / 1000).toFixed(2)}s.`);
|
|
107
|
+
await sleep(totalWaitMs);
|
|
108
|
+
delay = Math.min(delay * 2, 30000);
|
|
109
|
+
}
|
|
110
|
+
// Should never be reached
|
|
111
|
+
throw new Error('Unexpected exit from retry loop');
|
|
112
|
+
}
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
/**
|
|
115
|
+
* Validate an ag_at_ token and return the agent context.
|
|
116
|
+
*/
|
|
117
|
+
async function validateAgentToken(token) {
|
|
118
|
+
const config = (0, config_1.getConfig)();
|
|
119
|
+
if (!token.startsWith(config.token_prefix_access)) {
|
|
120
|
+
throw new Error('Not an AgentAdmit access token');
|
|
121
|
+
}
|
|
122
|
+
// MANDATORY INTROSPECTION — validate via AgentAdmit hosted service
|
|
123
|
+
// No local JWT decode. Every verification call goes through AgentAdmit.
|
|
124
|
+
const verifyUrl = config.agentadmit_verify_url || 'https://api.agentadmit.com/v1/verify';
|
|
125
|
+
const appId = config.app_id;
|
|
126
|
+
const apiKey = config.api_key || '';
|
|
127
|
+
const maxRetries = config.max_retries ?? 3;
|
|
128
|
+
// introspectWithRetry handles 429 with exponential backoff + jitter.
|
|
129
|
+
// RateLimitError propagates to the caller when retries are exhausted.
|
|
130
|
+
const response = await introspectWithRetry(verifyUrl, token, appId, apiKey, maxRetries);
|
|
131
|
+
if (response.status === 401) {
|
|
132
|
+
const errData = (await response.json().catch(() => ({})));
|
|
133
|
+
throw new Error(errData.error_description || 'Token validation failed');
|
|
134
|
+
}
|
|
135
|
+
if (response.status !== 200) {
|
|
136
|
+
throw new Error(`Verification service returned ${response.status}`);
|
|
137
|
+
}
|
|
138
|
+
const data = (await response.json());
|
|
139
|
+
// Check active flag (RFC 7662 introspection pattern).
|
|
140
|
+
// The verify endpoint returns {active: false} with HTTP 200 for invalid/
|
|
141
|
+
// expired/revoked tokens. Without this check, we'd read empty scopes.
|
|
142
|
+
if (!data.active) {
|
|
143
|
+
const reason = data.error || 'invalid_token';
|
|
144
|
+
throw new Error(`Token is not active: ${reason}`);
|
|
145
|
+
}
|
|
146
|
+
const scopes = data.scopes || [];
|
|
147
|
+
const userId = data.user_id;
|
|
148
|
+
const connectionId = data.connection_id;
|
|
149
|
+
if (!userId) {
|
|
150
|
+
throw new Error('Introspection returned no user');
|
|
151
|
+
}
|
|
152
|
+
// User lookup from app's local database (if storage is configured)
|
|
153
|
+
let user = { [config.user_lookup_field]: userId };
|
|
154
|
+
try {
|
|
155
|
+
const storage = getStorage();
|
|
156
|
+
const localUser = await storage.getUser(userId, config.user_lookup_field);
|
|
157
|
+
if (localUser)
|
|
158
|
+
user = localUser;
|
|
159
|
+
}
|
|
160
|
+
catch { }
|
|
161
|
+
const connection = {
|
|
162
|
+
connection_id: connectionId,
|
|
163
|
+
scopes,
|
|
164
|
+
agent_label: data.agent_label || 'Unknown Agent',
|
|
165
|
+
};
|
|
166
|
+
return { user, connection, scopes };
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Express middleware: require a specific scope (agent-only).
|
|
170
|
+
*/
|
|
171
|
+
function requireScope(scope) {
|
|
172
|
+
return async (req, res, next) => {
|
|
173
|
+
const token = getBearerToken(req);
|
|
174
|
+
const config = (0, config_1.getConfig)();
|
|
175
|
+
if (!token || !token.startsWith(config.token_prefix_access)) {
|
|
176
|
+
return res.status(401).json({ error: 'invalid_token', error_description: 'AgentAdmit token required' });
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
const ctx = await validateAgentToken(token);
|
|
180
|
+
if (!ctx.scopes.includes(scope)) {
|
|
181
|
+
return res.status(403).json({
|
|
182
|
+
error: 'insufficient_scope',
|
|
183
|
+
required_scope: scope,
|
|
184
|
+
granted_scopes: ctx.scopes,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
await logAccess(ctx, scope, req);
|
|
188
|
+
req.agentAdmit = { auth_type: 'agent', ...ctx };
|
|
189
|
+
next();
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
return res.status(401).json({ error: 'invalid_token', error_description: err.message });
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Express middleware: enforce scope only if caller is an agent.
|
|
198
|
+
*/
|
|
199
|
+
function requireScopeIfAgent(scope) {
|
|
200
|
+
return async (req, res, next) => {
|
|
201
|
+
const token = getBearerToken(req);
|
|
202
|
+
const config = (0, config_1.getConfig)();
|
|
203
|
+
if (!token || !token.startsWith(config.token_prefix_access)) {
|
|
204
|
+
return next(); // Not an agent — pass through
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
const ctx = await validateAgentToken(token);
|
|
208
|
+
if (!ctx.scopes.includes(scope)) {
|
|
209
|
+
return res.status(403).json({
|
|
210
|
+
error: 'insufficient_scope',
|
|
211
|
+
required_scope: scope,
|
|
212
|
+
granted_scopes: ctx.scopes,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
await logAccess(ctx, scope, req);
|
|
216
|
+
req.agentAdmit = { auth_type: 'agent', ...ctx };
|
|
217
|
+
next();
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
return res.status(401).json({ error: 'invalid_token', error_description: err.message });
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Express middleware: resolve user or agent from token.
|
|
226
|
+
*/
|
|
227
|
+
function resolveAuth() {
|
|
228
|
+
return async (req, res, next) => {
|
|
229
|
+
const token = getBearerToken(req);
|
|
230
|
+
const config = (0, config_1.getConfig)();
|
|
231
|
+
if (!token) {
|
|
232
|
+
return res.status(401).json({ error: 'not_authenticated' });
|
|
233
|
+
}
|
|
234
|
+
if (token.startsWith(config.token_prefix_access)) {
|
|
235
|
+
try {
|
|
236
|
+
const ctx = await validateAgentToken(token);
|
|
237
|
+
req.agentAdmit = { auth_type: 'agent', ...ctx };
|
|
238
|
+
return next();
|
|
239
|
+
}
|
|
240
|
+
catch (err) {
|
|
241
|
+
return res.status(401).json({ error: 'invalid_token', error_description: err.message });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// Regular user token
|
|
245
|
+
if (!_verifyUserToken) {
|
|
246
|
+
return res.status(500).json({ error: 'server_error', error_description: 'User token verifier not configured' });
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
const userId = await _verifyUserToken(token);
|
|
250
|
+
const storage = getStorage();
|
|
251
|
+
const user = await storage.getUser(userId, config.user_lookup_field);
|
|
252
|
+
if (!user) {
|
|
253
|
+
return res.status(404).json({ error: 'user_not_found' });
|
|
254
|
+
}
|
|
255
|
+
req.agentAdmit = { auth_type: 'user', user, scopes: ['*'], connection: null };
|
|
256
|
+
next();
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return res.status(401).json({ error: 'invalid_token' });
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Write audit log entry.
|
|
265
|
+
*/
|
|
266
|
+
async function logAccess(ctx, scope, req) {
|
|
267
|
+
try {
|
|
268
|
+
const config = (0, config_1.getConfig)();
|
|
269
|
+
const storage = getStorage();
|
|
270
|
+
await storage.logAccess({
|
|
271
|
+
timestamp: new Date(),
|
|
272
|
+
connection_id: ctx.connection?.connection_id || 'unknown',
|
|
273
|
+
user_id: ctx.user?.[config.user_lookup_field] || 'unknown',
|
|
274
|
+
scope_used: scope,
|
|
275
|
+
resource: req.path,
|
|
276
|
+
method: req.method,
|
|
277
|
+
agent_label: ctx.connection?.agent_label || 'Unknown Agent',
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
catch (err) {
|
|
281
|
+
console.error('[AgentAdmit] Audit log failed:', err);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Check connection cap for tier enforcement.
|
|
286
|
+
*/
|
|
287
|
+
async function checkConnectionCap(userId, tier) {
|
|
288
|
+
const { getTierLimits } = require('./config');
|
|
289
|
+
const limits = getTierLimits(tier);
|
|
290
|
+
if (!limits?.hard_cap)
|
|
291
|
+
return;
|
|
292
|
+
const storage = getStorage();
|
|
293
|
+
const count = await storage.countActiveConnections(userId);
|
|
294
|
+
if (count >= limits.connections_limit) {
|
|
295
|
+
const err = new Error(`Connection limit reached (${count}/${limits.connections_limit})`);
|
|
296
|
+
err.statusCode = 429;
|
|
297
|
+
err.detail = {
|
|
298
|
+
error: 'connection_limit_reached',
|
|
299
|
+
connections_used: count,
|
|
300
|
+
connections_limit: limits.connections_limit,
|
|
301
|
+
tier,
|
|
302
|
+
};
|
|
303
|
+
throw err;
|
|
304
|
+
}
|
|
305
|
+
}
|