@agentadmit/sdk 1.0.0 → 1.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/src/errors.ts DELETED
@@ -1,50 +0,0 @@
1
- /**
2
- * agentadmit/errors.ts
3
- * Custom error types for the AgentAdmit Node.js SDK.
4
- */
5
-
6
- /**
7
- * Thrown when the AgentAdmit introspection endpoint returns HTTP 429
8
- * and all retry attempts (with exponential backoff) have been exhausted.
9
- *
10
- * @example
11
- * ```ts
12
- * import { validateAgentToken, RateLimitError } from '@agentadmit/sdk';
13
- *
14
- * try {
15
- * await validateAgentToken(token);
16
- * } catch (err) {
17
- * if (err instanceof RateLimitError) {
18
- * res.status(429).json({
19
- * error: 'rate_limited',
20
- * retry_after: err.retryAfter,
21
- * });
22
- * }
23
- * }
24
- * ```
25
- */
26
- export class RateLimitError extends Error {
27
- /** Seconds to wait before retrying (from Retry-After header), or null. */
28
- readonly retryAfter: number | null;
29
- /** Total request limit for the current window (X-RateLimit-Limit), or null. */
30
- readonly limit: number | null;
31
- /** Requests remaining in the current window (X-RateLimit-Remaining), or null. */
32
- readonly remaining: number | null;
33
- /** Unix timestamp when the rate limit window resets (X-RateLimit-Reset), or null. */
34
- readonly reset: number | null;
35
-
36
- constructor(options: {
37
- message?: string;
38
- retryAfter?: number | null;
39
- limit?: number | null;
40
- remaining?: number | null;
41
- reset?: number | null;
42
- } = {}) {
43
- super(options.message ?? 'AgentAdmit rate limit exceeded. Max retries exhausted.');
44
- this.name = 'RateLimitError';
45
- this.retryAfter = options.retryAfter ?? null;
46
- this.limit = options.limit ?? null;
47
- this.remaining = options.remaining ?? null;
48
- this.reset = options.reset ?? null;
49
- }
50
- }
package/src/index.ts DELETED
@@ -1,27 +0,0 @@
1
- /**
2
- * @agentadmit/sdk — Node.js SDK for AgentAdmit
3
- *
4
- * User-mediated AI agent authorization. Plug-and-play for Express and Next.js.
5
- */
6
-
7
- export { loadConfig, getConfig, getScopeMetadata, getDurationOptions, getTierLimits } from './config';
8
- export type { AgentAdmitConfig, ScopeDefinition, DurationOption, TierDefinition, StorageConfig } from './config';
9
-
10
- export { generateKeyPair, loadPrivateKey, loadPublicKey } from './keys';
11
-
12
- export { StorageBackend, MongoDBStorage, MemoryStorage, createStorage } from './storage';
13
-
14
- export {
15
- validateAgentToken,
16
- requireScope,
17
- requireScopeIfAgent,
18
- resolveAuth,
19
- checkConnectionCap,
20
- setStorage,
21
- setUserVerifier,
22
- } from './auth';
23
- export type { AgentContext } from './auth';
24
-
25
- export { createAgentAdmitRouter } from './routes';
26
-
27
- export { RateLimitError } from './errors';
package/src/keys.ts DELETED
@@ -1,33 +0,0 @@
1
- /**
2
- * agentadmit/keys.ts
3
- *
4
- * DEPRECATED — AgentAdmit is a hosted service.
5
- *
6
- * All cryptographic operations (key generation, JWT signing, JWKS serving)
7
- * are performed by the AgentAdmit hosted service. The SDK does NOT generate
8
- * or load RSA key pairs. Calling any function in this module will throw.
9
- */
10
-
11
- export function generateKeyPair(_outputDir?: string): never {
12
- throw new Error(
13
- '[AgentAdmit] generateKeyPair() is not supported. ' +
14
- 'AgentAdmit is a hosted service — all key management is handled server-side. ' +
15
- 'Remove any calls to generateKeyPair() from your codebase.',
16
- );
17
- }
18
-
19
- export function loadPrivateKey(_keyPath?: string): never {
20
- throw new Error(
21
- '[AgentAdmit] loadPrivateKey() is not supported. ' +
22
- 'AgentAdmit is a hosted service — JWT signing is handled by the hosted service. ' +
23
- 'Remove any calls to loadPrivateKey() from your codebase.',
24
- );
25
- }
26
-
27
- export function loadPublicKey(_keyPath?: string): never {
28
- throw new Error(
29
- '[AgentAdmit] loadPublicKey() is not supported. ' +
30
- 'AgentAdmit is a hosted service — token verification uses hosted introspection. ' +
31
- 'Remove any calls to loadPublicKey() from your codebase.',
32
- );
33
- }
package/src/routes.ts DELETED
@@ -1,245 +0,0 @@
1
- /**
2
- * agentadmit/routes.ts
3
- * Express router with all AgentAdmit endpoints.
4
- *
5
- * ALL token operations go through the AgentAdmit hosted service. The SDK does
6
- * NOT sign JWTs, generate RSA keys, or serve JWKS endpoints. The hosted service
7
- * owns all cryptographic operations.
8
- */
9
-
10
- import { Router, Request, Response } from 'express';
11
- import { randomBytes } from 'crypto';
12
- import { getConfig, getScopeMetadata, getDurationOptions } from './config';
13
- import { StorageBackend } from './storage';
14
- import { checkConnectionCap } from './auth';
15
-
16
- const AGENTADMIT_VERSION = '0.1';
17
-
18
- interface RouterOptions {
19
- storage: StorageBackend;
20
- getCurrentUser: (req: Request) => Promise<Record<string, any> | null>;
21
- determineRole?: (user: Record<string, any>) => string;
22
- getUserTier?: (user: Record<string, any>) => string;
23
- validateScopes?: (scopes: string[], user: Record<string, any>) => { valid: boolean; invalid: string[] };
24
- getEndpointsForScopes?: (scopes: string[]) => Record<string, any>[];
25
- }
26
-
27
- /**
28
- * Make an authenticated request to the AgentAdmit hosted service.
29
- */
30
- async function callHostedService(
31
- path: string,
32
- body: Record<string, any>,
33
- ): Promise<{ status: number; data: any }> {
34
- const config = getConfig();
35
- const url = `${config.agentadmit_api_url.replace(/\/$/, '')}${path}`;
36
-
37
- const resp = await fetch(url, {
38
- method: 'POST',
39
- headers: {
40
- 'Authorization': `Bearer ${config.api_key}`,
41
- 'Content-Type': 'application/json',
42
- 'X-App-Id': config.app_id,
43
- },
44
- body: JSON.stringify(body),
45
- });
46
-
47
- const data = await resp.json().catch(() => ({}));
48
- return { status: resp.status, data };
49
- }
50
-
51
- export function createAgentAdmitRouter(options: RouterOptions): { wellknownRouter: Router; agentadmitRouter: Router } {
52
- const config = getConfig();
53
- const { storage, getCurrentUser } = options;
54
- const determineRole = options.determineRole || (() => 'user');
55
- const getUserTier = options.getUserTier || (() => config.default_tier);
56
- const getEndpointsForScopes = options.getEndpointsForScopes || (() => []);
57
-
58
- const validateScopes = options.validateScopes || ((scopes: string[]) => {
59
- const validNames = new Set(config.scopes.map(s => s.name));
60
- const invalid = scopes.filter(s => !validNames.has(s));
61
- return { valid: invalid.length === 0, invalid };
62
- });
63
-
64
- const wellknownRouter = Router();
65
- const agentadmitRouter = Router();
66
-
67
- // Discovery
68
- wellknownRouter.get('/.well-known/agentadmit', (_req: Request, res: Response) => {
69
- const base = config.api_base_url.replace(/\/$/, '');
70
- res.json({
71
- agentadmit_version: AGENTADMIT_VERSION,
72
- issuer: base,
73
- app_name: config.app_name,
74
- app_id: config.app_id,
75
- api_base_url: base,
76
- agentadmit_service_url: config.agentadmit_api_url,
77
- scopes_endpoint: `${base}${config.route_prefix}/scopes`,
78
- discovery_endpoint: `${base}${config.route_prefix}/discovery`,
79
- connections_endpoint: `${base}${config.route_prefix}/connections`,
80
- scopes_supported: config.scopes.map(s => s.name),
81
- roles_supported: [...new Set(config.scopes.map(s => s.role || 'user'))],
82
- duration_options: getDurationOptions(),
83
- });
84
- });
85
-
86
- // Scopes
87
- agentadmitRouter.get('/scopes', (_req: Request, res: Response) => {
88
- res.json({
89
- scopes: getScopeMetadata(),
90
- roles: [...new Set(config.scopes.map(s => s.role || 'user'))],
91
- });
92
- });
93
-
94
- // Durations
95
- agentadmitRouter.get('/durations', (_req: Request, res: Response) => {
96
- res.json({ durations: getDurationOptions() });
97
- });
98
-
99
- // Generate connection token — calls hosted service
100
- agentadmitRouter.post('/connections/generate-token', async (req: Request, res: Response) => {
101
- try {
102
- const currentUser = await getCurrentUser(req);
103
- if (!currentUser) return res.status(401).json({ error: 'unauthorized' });
104
-
105
- const { scopes, duration_seconds, label } = req.body;
106
- if (!scopes || !Array.isArray(scopes)) {
107
- return res.status(400).json({ error: 'invalid_request', error_description: 'scopes array required' });
108
- }
109
-
110
- const validation = validateScopes(scopes, currentUser);
111
- if (!validation.valid) {
112
- return res.status(400).json({ error: 'invalid_scope', invalid_scopes: validation.invalid });
113
- }
114
-
115
- const duration = duration_seconds || config.connection_token_ttl;
116
- const userId = currentUser[config.user_lookup_field];
117
- const role = determineRole(currentUser);
118
- const userTier = getUserTier(currentUser);
119
-
120
- await checkConnectionCap(userId, userTier);
121
-
122
- // Call AgentAdmit hosted service
123
- const { status, data } = await callHostedService(`/api/v1/apps/${config.app_id}/token`, {
124
- user_id: String(userId),
125
- scopes,
126
- duration_hours: Math.max(1, Math.floor(duration / 3600)),
127
- label: label ?? null,
128
- user_role: role,
129
- metadata: { subscription_tier: userTier, app_name: config.app_name },
130
- });
131
-
132
- if (status !== 200 && status !== 201) {
133
- console.error('[AgentAdmit] Hosted token generation failed:', status, data);
134
- return res.status(502).json({ error: 'token_generation_failed', error_description: 'Authorization service could not generate token' });
135
- }
136
-
137
- // Store local record
138
- // Use hosted service's connection_id if provided; generate a local fallback
139
- // to prevent duplicate-key errors when hosting service omits it.
140
- await storage.storeConnection({
141
- connection_id: data.connection_id || `conn_${randomBytes(16).toString('base64url')}`,
142
- user_id: String(userId),
143
- scopes,
144
- role,
145
- agent_label: label,
146
- duration_seconds: duration,
147
- status: 'active',
148
- });
149
-
150
- res.json({
151
- connection_token: data.token || data.connection_token,
152
- expires_in: duration,
153
- scopes,
154
- });
155
- } catch (err: any) {
156
- console.error('[AgentAdmit] Generate token error:', err);
157
- res.status(500).json({ error: 'server_error', error_description: err.message });
158
- }
159
- });
160
-
161
- // Token exchange — forwards to hosted service
162
- agentadmitRouter.post('/token', async (req: Request, res: Response) => {
163
- try {
164
- const { grant_type, connection_token, agent_id, agent_label, agent_metadata } = req.body;
165
-
166
- if (grant_type !== 'connection_token') {
167
- return res.status(400).json({ error: 'unsupported_grant_type' });
168
- }
169
- if (!connection_token) {
170
- return res.status(400).json({ error: 'invalid_request', error_description: 'connection_token required' });
171
- }
172
-
173
- // Forward to AgentAdmit hosted service
174
- const { status, data } = await callHostedService('/api/v1/exchange', {
175
- token: connection_token,
176
- agent_label: agent_label ?? null,
177
- agent_id: agent_id ?? null,
178
- agent_metadata: agent_metadata ?? null,
179
- });
180
-
181
- if (status !== 200) {
182
- return res.status(status < 500 ? status : 502).json(data);
183
- }
184
-
185
- // Add endpoint map if available
186
- if (getEndpointsForScopes && data.scopes) {
187
- data.endpoints = getEndpointsForScopes(data.scopes);
188
- }
189
-
190
- res.json(data);
191
- } catch (err: any) {
192
- const status = err.statusCode || 500;
193
- res.status(status).json(err.detail || { error: 'server_error', error_description: err.message });
194
- }
195
- });
196
-
197
- // List connections
198
- agentadmitRouter.get('/connections', async (req: Request, res: Response) => {
199
- try {
200
- const currentUser = await getCurrentUser(req);
201
- if (!currentUser) return res.status(401).json({ error: 'unauthorized' });
202
-
203
- const userId = currentUser[config.user_lookup_field];
204
- const connections = await storage.listConnections(userId);
205
- res.json({ connections, total: connections.length });
206
- } catch {
207
- res.status(500).json({ error: 'server_error' });
208
- }
209
- });
210
-
211
- // Delete connection — also notifies hosted service
212
- agentadmitRouter.delete('/connections/:connectionId', async (req: Request, res: Response) => {
213
- try {
214
- const currentUser = await getCurrentUser(req);
215
- if (!currentUser) return res.status(401).json({ error: 'unauthorized' });
216
-
217
- const userId = currentUser[config.user_lookup_field];
218
- const conn = await storage.getConnection(req.params.connectionId);
219
-
220
- if (!conn || conn.user_id !== String(userId)) {
221
- return res.status(404).json({ error: 'not_found' });
222
- }
223
- if (conn.status !== 'active') {
224
- return res.status(400).json({ error: 'already_revoked' });
225
- }
226
-
227
- // Notify hosted service
228
- try {
229
- await callHostedService('/api/v1/revoke', {
230
- connection_id: req.params.connectionId,
231
- reason: 'user_requested',
232
- });
233
- } catch (e) {
234
- console.warn('[AgentAdmit] Hosted revoke failed, revoking locally:', e);
235
- }
236
-
237
- await storage.revokeConnection(req.params.connectionId);
238
- res.json({ revoked: true, connection_id: req.params.connectionId });
239
- } catch {
240
- res.status(500).json({ error: 'server_error' });
241
- }
242
- });
243
-
244
- return { wellknownRouter, agentadmitRouter };
245
- }
package/src/storage.ts DELETED
@@ -1,228 +0,0 @@
1
- /**
2
- * agentadmit/storage.ts
3
- * Abstract storage interface + MongoDB + Memory implementations.
4
- */
5
-
6
- export interface StorageBackend {
7
- storeConnection(connection: Record<string, any>): Promise<void>;
8
- getConnection(connectionId: string): Promise<Record<string, any> | null>;
9
- getActiveConnection(connectionId: string): Promise<Record<string, any> | null>;
10
- updateConnection(connectionId: string, updates: Record<string, any>): Promise<boolean>;
11
- revokeConnection(connectionId: string): Promise<boolean>;
12
- listConnections(userId: string): Promise<Record<string, any>[]>;
13
- countActiveConnections(userId: string): Promise<number>;
14
- storeToken(tokenRecord: Record<string, any>): Promise<void>;
15
- getToken(tokenHash: string): Promise<Record<string, any> | null>;
16
- markTokenUsed(tokenHash: string): Promise<boolean>;
17
- logAccess(entry: Record<string, any>): Promise<void>;
18
- countAuditCalls(userId: string, periodStart: Date, periodEnd: Date): Promise<number>;
19
- getUser(userId: string, lookupField: string): Promise<Record<string, any> | null>;
20
- }
21
-
22
- export class MongoDBStorage implements StorageBackend {
23
- private db: any;
24
- private connections: any;
25
- private auditLog: any;
26
- private tokens: any;
27
- private users: any = null;
28
-
29
- constructor(
30
- uri: string,
31
- database: string,
32
- connectionsCollection: string,
33
- auditLogCollection: string,
34
- tokensCollection: string,
35
- ) {
36
- // Lazy import to keep mongodb as optional peer dep
37
- const { MongoClient } = require('mongodb');
38
- const client = new MongoClient(uri);
39
- this.db = client.db(database);
40
- this.connections = this.db.collection(connectionsCollection);
41
- this.auditLog = this.db.collection(auditLogCollection);
42
- this.tokens = this.db.collection(tokensCollection);
43
-
44
- // Create indexes
45
- this.connections.createIndex({ connection_id: 1 }, { unique: true }).catch(() => {});
46
- this.connections.createIndex({ user_id: 1, status: 1 }).catch(() => {});
47
- this.tokens.createIndex({ token_hash: 1 }, { unique: true }).catch(() => {});
48
- this.auditLog.createIndex({ user_id: 1, timestamp: -1 }).catch(() => {});
49
-
50
- console.log(`[AgentAdmit] MongoDB storage initialized: ${database}`);
51
- }
52
-
53
- setUsersCollection(name: string) {
54
- this.users = this.db.collection(name);
55
- }
56
-
57
- async storeConnection(connection: Record<string, any>): Promise<void> {
58
- await this.connections.insertOne(connection);
59
- }
60
-
61
- async getConnection(connectionId: string): Promise<Record<string, any> | null> {
62
- return this.connections.findOne({ connection_id: connectionId });
63
- }
64
-
65
- async getActiveConnection(connectionId: string): Promise<Record<string, any> | null> {
66
- return this.connections.findOne({ connection_id: connectionId, status: 'active' });
67
- }
68
-
69
- async updateConnection(connectionId: string, updates: Record<string, any>): Promise<boolean> {
70
- const result = await this.connections.updateOne(
71
- { connection_id: connectionId },
72
- { $set: updates },
73
- );
74
- return result.modifiedCount > 0;
75
- }
76
-
77
- async revokeConnection(connectionId: string): Promise<boolean> {
78
- const result = await this.connections.updateOne(
79
- { connection_id: connectionId, status: 'active' },
80
- { $set: { status: 'revoked', revoked_at: new Date() } },
81
- );
82
- return result.modifiedCount > 0;
83
- }
84
-
85
- async listConnections(userId: string): Promise<Record<string, any>[]> {
86
- return this.connections.find({ user_id: userId }, { projection: { _id: 0 } }).sort({ created_at: -1 }).toArray();
87
- }
88
-
89
- async countActiveConnections(userId: string): Promise<number> {
90
- return this.connections.countDocuments({ user_id: userId, status: 'active' });
91
- }
92
-
93
- async storeToken(tokenRecord: Record<string, any>): Promise<void> {
94
- await this.tokens.insertOne(tokenRecord);
95
- }
96
-
97
- async getToken(tokenHash: string): Promise<Record<string, any> | null> {
98
- return this.tokens.findOne({ token_hash: tokenHash });
99
- }
100
-
101
- async markTokenUsed(tokenHash: string): Promise<boolean> {
102
- const result = await this.tokens.updateOne(
103
- { token_hash: tokenHash, used: false },
104
- { $set: { used: true, used_at: new Date() } },
105
- );
106
- return result.modifiedCount > 0;
107
- }
108
-
109
- async logAccess(entry: Record<string, any>): Promise<void> {
110
- try {
111
- await this.auditLog.insertOne(entry);
112
- } catch (err) {
113
- console.error('[AgentAdmit] Audit log failed:', err);
114
- }
115
- }
116
-
117
- async countAuditCalls(userId: string, periodStart: Date, periodEnd: Date): Promise<number> {
118
- return this.auditLog.countDocuments({
119
- user_id: userId,
120
- timestamp: { $gte: periodStart, $lt: periodEnd },
121
- });
122
- }
123
-
124
- async getUser(userId: string, lookupField: string = 'user_id'): Promise<Record<string, any> | null> {
125
- if (!this.users) return null;
126
- return this.users.findOne({ [lookupField]: userId });
127
- }
128
- }
129
-
130
- export class MemoryStorage implements StorageBackend {
131
- private _connections: Map<string, Record<string, any>> = new Map();
132
- private _tokens: Map<string, Record<string, any>> = new Map();
133
- private _auditLog: Record<string, any>[] = [];
134
- private _users: Map<string, Record<string, any>> = new Map();
135
-
136
- async storeConnection(connection: Record<string, any>): Promise<void> {
137
- this._connections.set(connection.connection_id, connection);
138
- }
139
-
140
- async getConnection(connectionId: string): Promise<Record<string, any> | null> {
141
- return this._connections.get(connectionId) || null;
142
- }
143
-
144
- async getActiveConnection(connectionId: string): Promise<Record<string, any> | null> {
145
- const conn = this._connections.get(connectionId);
146
- return conn?.status === 'active' ? conn : null;
147
- }
148
-
149
- async updateConnection(connectionId: string, updates: Record<string, any>): Promise<boolean> {
150
- const conn = this._connections.get(connectionId);
151
- if (!conn) return false;
152
- Object.assign(conn, updates);
153
- return true;
154
- }
155
-
156
- async revokeConnection(connectionId: string): Promise<boolean> {
157
- const conn = this._connections.get(connectionId);
158
- if (!conn || conn.status !== 'active') return false;
159
- conn.status = 'revoked';
160
- conn.revoked_at = new Date();
161
- return true;
162
- }
163
-
164
- async listConnections(userId: string): Promise<Record<string, any>[]> {
165
- return Array.from(this._connections.values()).filter(c => c.user_id === userId);
166
- }
167
-
168
- async countActiveConnections(userId: string): Promise<number> {
169
- return Array.from(this._connections.values()).filter(c => c.user_id === userId && c.status === 'active').length;
170
- }
171
-
172
- async storeToken(tokenRecord: Record<string, any>): Promise<void> {
173
- this._tokens.set(tokenRecord.token_hash, tokenRecord);
174
- }
175
-
176
- async getToken(tokenHash: string): Promise<Record<string, any> | null> {
177
- return this._tokens.get(tokenHash) || null;
178
- }
179
-
180
- async markTokenUsed(tokenHash: string): Promise<boolean> {
181
- const token = this._tokens.get(tokenHash);
182
- if (!token || token.used) return false;
183
- token.used = true;
184
- token.used_at = new Date();
185
- return true;
186
- }
187
-
188
- async logAccess(entry: Record<string, any>): Promise<void> {
189
- this._auditLog.push(entry);
190
- }
191
-
192
- async countAuditCalls(userId: string, periodStart: Date, periodEnd: Date): Promise<number> {
193
- return this._auditLog.filter(e =>
194
- e.user_id === userId &&
195
- e.timestamp >= periodStart &&
196
- e.timestamp < periodEnd
197
- ).length;
198
- }
199
-
200
- async getUser(userId: string, lookupField: string = 'user_id'): Promise<Record<string, any> | null> {
201
- return this._users.get(userId) || null;
202
- }
203
-
204
- addTestUser(userId: string, data: Record<string, any>): void {
205
- this._users.set(userId, data);
206
- }
207
- }
208
-
209
- export function createStorage(config: any): StorageBackend {
210
- const backend = config.storage?.backend || 'mongodb';
211
-
212
- if (backend === 'mongodb') {
213
- const s = new MongoDBStorage(
214
- config.storage.uri,
215
- config.storage.database,
216
- config.storage.connections_collection || 'agentadmit_connections',
217
- config.storage.audit_log_collection || 'agentadmit_audit_log',
218
- config.storage.tokens_collection || 'agentadmit_tokens',
219
- );
220
- return s;
221
- }
222
-
223
- if (backend === 'memory') {
224
- return new MemoryStorage();
225
- }
226
-
227
- throw new Error(`Unsupported storage backend: ${backend}`);
228
- }
package/tsconfig.json DELETED
@@ -1,19 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2020",
4
- "module": "commonjs",
5
- "lib": ["ES2020"],
6
- "declaration": true,
7
- "strict": true,
8
- "noImplicitAny": true,
9
- "strictNullChecks": true,
10
- "outDir": "./dist",
11
- "rootDir": "./src",
12
- "esModuleInterop": true,
13
- "skipLibCheck": true,
14
- "forceConsistentCasingInFileNames": true,
15
- "resolveJsonModule": true
16
- },
17
- "include": ["src/**/*"],
18
- "exclude": ["node_modules", "dist", "tests"]
19
- }