@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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@agentadmit/sdk",
3
- "version": "1.0.0",
4
- "description": "AgentAdmit SDK \u2014 User-mediated AI agent authorization for Node.js apps",
3
+ "version": "1.1.0",
4
+ "description": "AgentAdmit SDK User-mediated AI agent authorization for Node.js apps",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "scripts": {
@@ -16,8 +16,8 @@
16
16
  "mongodb": "6.0.0"
17
17
  },
18
18
  "devDependencies": {
19
- "typescript": "5.3.0",
20
- "@types/node": "20.0.0",
19
+ "typescript": "5.9.3",
20
+ "@types/node": "22.19.20",
21
21
  "@types/jsonwebtoken": "9.0.0",
22
22
  "@types/js-yaml": "4.0.0",
23
23
  "@types/uuid": "9.0.0",
@@ -34,6 +34,13 @@
34
34
  "optional": true
35
35
  }
36
36
  },
37
+ "jest": {
38
+ "preset": "ts-jest",
39
+ "testEnvironment": "node",
40
+ "roots": [
41
+ "<rootDir>/tests"
42
+ ]
43
+ },
37
44
  "license": "SEE LICENSE IN LICENSE",
38
45
  "author": "Christopher Emerson",
39
46
  "repository": {
@@ -0,0 +1,109 @@
1
+ /** Tests for src/webhooks.ts — X-AgentAdmit-Signature verification. */
2
+
3
+ import { createHmac } from 'crypto';
4
+ import {
5
+ isValidWebhookSignature,
6
+ verifyWebhookSignature,
7
+ WebhookSignatureError,
8
+ } from '../src/webhooks';
9
+
10
+ const SECRET = 'whsec_test123';
11
+ const PAYLOAD = Buffer.from('{"event":"agentadmit.alert","alert_type":"usage_spike"}');
12
+ const NOW = 1750000000;
13
+
14
+ function sign(payload: Buffer, secret = SECRET, timestamp = NOW): string {
15
+ const digest = createHmac('sha256', secret)
16
+ .update(`${timestamp}.`)
17
+ .update(payload)
18
+ .digest('hex');
19
+ return `t=${timestamp},v1=${digest}`;
20
+ }
21
+
22
+ describe('verifyWebhookSignature', () => {
23
+ it('accepts a valid signature', () => {
24
+ expect(() =>
25
+ verifyWebhookSignature(PAYLOAD, sign(PAYLOAD), SECRET, { now: NOW }),
26
+ ).not.toThrow();
27
+ });
28
+
29
+ it('accepts a string payload', () => {
30
+ expect(() =>
31
+ verifyWebhookSignature(PAYLOAD.toString(), sign(PAYLOAD), SECRET, { now: NOW }),
32
+ ).not.toThrow();
33
+ });
34
+
35
+ it('rejects a tampered payload', () => {
36
+ expect(() =>
37
+ verifyWebhookSignature(Buffer.concat([PAYLOAD, Buffer.from(' ')]), sign(PAYLOAD), SECRET, { now: NOW }),
38
+ ).toThrow(WebhookSignatureError);
39
+ });
40
+
41
+ it('rejects a signature made with the wrong secret', () => {
42
+ expect(() =>
43
+ verifyWebhookSignature(PAYLOAD, sign(PAYLOAD, 'whsec_other456'), SECRET, { now: NOW }),
44
+ ).toThrow('verification failed');
45
+ });
46
+
47
+ it('rejects a stale timestamp', () => {
48
+ expect(() =>
49
+ verifyWebhookSignature(PAYLOAD, sign(PAYLOAD, SECRET, NOW - 600), SECRET, { now: NOW }),
50
+ ).toThrow('tolerance');
51
+ });
52
+
53
+ it('rejects a future timestamp', () => {
54
+ expect(() =>
55
+ verifyWebhookSignature(PAYLOAD, sign(PAYLOAD, SECRET, NOW + 600), SECRET, { now: NOW }),
56
+ ).toThrow('tolerance');
57
+ });
58
+
59
+ it('accepts a timestamp within tolerance', () => {
60
+ expect(() =>
61
+ verifyWebhookSignature(PAYLOAD, sign(PAYLOAD, SECRET, NOW - 200), SECRET, { now: NOW }),
62
+ ).not.toThrow();
63
+ });
64
+
65
+ it('skips the timestamp check when tolerance is 0', () => {
66
+ expect(() =>
67
+ verifyWebhookSignature(PAYLOAD, sign(PAYLOAD, SECRET, NOW - 99999), SECRET, {
68
+ toleranceSeconds: 0,
69
+ now: NOW,
70
+ }),
71
+ ).not.toThrow();
72
+ });
73
+
74
+ it('rejects a missing header', () => {
75
+ expect(() => verifyWebhookSignature(PAYLOAD, '', SECRET, { now: NOW })).toThrow('Missing');
76
+ });
77
+
78
+ it.each(['nonsense', 't=abc,v1=def', 't=123', 'v1=abc'])(
79
+ 'rejects malformed header %p',
80
+ header => {
81
+ expect(() => verifyWebhookSignature(PAYLOAD, header, SECRET, { now: NOW })).toThrow(
82
+ 'Malformed',
83
+ );
84
+ },
85
+ );
86
+
87
+ it('rejects a missing secret', () => {
88
+ expect(() => verifyWebhookSignature(PAYLOAD, sign(PAYLOAD), '', { now: NOW })).toThrow(
89
+ 'secret',
90
+ );
91
+ });
92
+
93
+ it('accepts when any v1 candidate matches', () => {
94
+ expect(() =>
95
+ verifyWebhookSignature(PAYLOAD, `${sign(PAYLOAD)},v1=deadbeef`, SECRET, { now: NOW }),
96
+ ).not.toThrow();
97
+ });
98
+ });
99
+
100
+ describe('isValidWebhookSignature', () => {
101
+ it('returns booleans instead of throwing', () => {
102
+ expect(isValidWebhookSignature(PAYLOAD, sign(PAYLOAD), SECRET, { now: NOW })).toBe(true);
103
+ expect(
104
+ isValidWebhookSignature(Buffer.concat([PAYLOAD, Buffer.from('x')]), sign(PAYLOAD), SECRET, {
105
+ now: NOW,
106
+ }),
107
+ ).toBe(false);
108
+ });
109
+ });
@@ -1,53 +0,0 @@
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/src/auth.ts DELETED
@@ -1,356 +0,0 @@
1
- /**
2
- * agentadmit/auth.ts
3
- * Token validation, scope enforcement, and audit logging for Express.
4
- */
5
-
6
- import { Request, Response, NextFunction } from 'express';
7
- import jwt from 'jsonwebtoken';
8
- import { getConfig } from './config';
9
- import { loadPublicKey } from './keys';
10
- import { StorageBackend } from './storage';
11
- import { RateLimitError } from './errors';
12
-
13
- let _storage: StorageBackend | null = null;
14
- let _verifyUserToken: ((token: string) => string | Promise<string>) | null = null;
15
-
16
- export function setStorage(storage: StorageBackend) {
17
- _storage = storage;
18
- }
19
-
20
- export function setUserVerifier(fn: (token: string) => string | Promise<string>) {
21
- _verifyUserToken = fn;
22
- }
23
-
24
- function getStorage(): StorageBackend {
25
- if (!_storage) throw new Error('AgentAdmit storage not initialized');
26
- return _storage;
27
- }
28
-
29
- function getBearerToken(req: Request): string | null {
30
- const auth = req.headers.authorization || '';
31
- if (auth.startsWith('Bearer ')) return auth.slice(7);
32
- return null;
33
- }
34
-
35
- export interface AgentContext {
36
- auth_type: 'agent' | 'user';
37
- user: Record<string, any>;
38
- connection: Record<string, any> | null;
39
- scopes: string[];
40
- }
41
-
42
- // ---------------------------------------------------------------------------
43
- // Rate-limit retry helpers
44
- // ---------------------------------------------------------------------------
45
-
46
- /** Parse an integer from an HTTP response header. Returns null if missing or invalid. */
47
- function parseIntHeader(headers: Headers, name: string): number | null {
48
- const val = headers.get(name);
49
- if (val === null) return null;
50
- const n = parseInt(val, 10);
51
- return Number.isFinite(n) ? n : null;
52
- }
53
-
54
- /** Parse a float from an HTTP response header. Returns null if missing or invalid. */
55
- function parseFloatHeader(headers: Headers, name: string): number | null {
56
- const val = headers.get(name);
57
- if (val === null) return null;
58
- const n = parseFloat(val);
59
- return Number.isFinite(n) ? n : null;
60
- }
61
-
62
- /** sleep for `ms` milliseconds */
63
- function sleep(ms: number): Promise<void> {
64
- return new Promise(resolve => setTimeout(resolve, ms));
65
- }
66
-
67
- /**
68
- * POST to the AgentAdmit introspection endpoint with automatic 429 retry.
69
- *
70
- * Retry policy:
71
- * - Initial delay: 1 second
72
- * - Each retry doubles the delay, capped at 30 seconds
73
- * - Each delay adds 0–500 ms of random jitter
74
- * - Honors Retry-After header if present
75
- * - After maxRetries exhausted, throws RateLimitError
76
- */
77
- async function introspectWithRetry(
78
- verifyUrl: string,
79
- token: string,
80
- appId: string,
81
- apiKey: string,
82
- maxRetries: number,
83
- ): Promise<globalThis.Response> {
84
- let delay = 1000; // ms
85
-
86
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
87
- let response: globalThis.Response;
88
- try {
89
- response = await fetch(verifyUrl, {
90
- method: 'POST',
91
- headers: {
92
- Authorization: `Bearer ${apiKey}`,
93
- 'Content-Type': 'application/json',
94
- },
95
- body: JSON.stringify({ token }),
96
- });
97
- } catch (err: any) {
98
- throw new Error(`AgentAdmit introspection failed (network): ${err.message}`);
99
- }
100
-
101
- if (response.status !== 429) {
102
- return response;
103
- }
104
-
105
- // --- 429 handling ---
106
- const retryAfter = parseFloatHeader(response.headers, 'Retry-After');
107
- const limit = parseIntHeader(response.headers, 'X-RateLimit-Limit');
108
- const remaining = parseIntHeader(response.headers, 'X-RateLimit-Remaining');
109
- const reset = parseIntHeader(response.headers, 'X-RateLimit-Reset');
110
-
111
- if (attempt >= maxRetries) {
112
- throw new RateLimitError({
113
- message: `AgentAdmit rate limit exceeded. Max retries (${maxRetries}) exhausted.`,
114
- retryAfter,
115
- limit,
116
- remaining,
117
- reset,
118
- });
119
- }
120
-
121
- const waitMs = retryAfter !== null ? retryAfter * 1000 : Math.min(delay, 30_000);
122
- const jitterMs = Math.random() * 500; // 0–500 ms
123
- const totalWaitMs = waitMs + jitterMs;
124
-
125
- console.warn(
126
- `[AgentAdmit] Rate-limited (attempt ${attempt + 1}/${maxRetries}). ` +
127
- `Retrying in ${(totalWaitMs / 1000).toFixed(2)}s.`,
128
- );
129
-
130
- await sleep(totalWaitMs);
131
- delay = Math.min(delay * 2, 30_000);
132
- }
133
-
134
- // Should never be reached
135
- throw new Error('Unexpected exit from retry loop');
136
- }
137
-
138
- // ---------------------------------------------------------------------------
139
-
140
- /**
141
- * Validate an ag_at_ token and return the agent context.
142
- */
143
- export async function validateAgentToken(token: string): Promise<Omit<AgentContext, 'auth_type'>> {
144
- const config = getConfig();
145
-
146
- if (!token.startsWith(config.token_prefix_access)) {
147
- throw new Error('Not an AgentAdmit access token');
148
- }
149
-
150
- // MANDATORY INTROSPECTION — validate via AgentAdmit hosted service
151
- // No local JWT decode. Every verification call goes through AgentAdmit.
152
- const verifyUrl = (config as any).agentadmit_verify_url || 'https://api.agentadmit.com/v1/verify';
153
- const appId = config.app_id;
154
- const apiKey = (config as any).api_key || '';
155
- const maxRetries = (config as any).max_retries ?? 3;
156
-
157
- // introspectWithRetry handles 429 with exponential backoff + jitter.
158
- // RateLimitError propagates to the caller when retries are exhausted.
159
- const response = await introspectWithRetry(verifyUrl, token, appId, apiKey, maxRetries);
160
-
161
- if (response.status === 401) {
162
- const errData = (await response.json().catch(() => ({}))) as Record<string, string>;
163
- throw new Error(errData.error_description || 'Token validation failed');
164
- }
165
-
166
- if (response.status !== 200) {
167
- throw new Error(`Verification service returned ${response.status}`);
168
- }
169
-
170
- const data = (await response.json()) as Record<string, any>;
171
-
172
- // Check active flag (RFC 7662 introspection pattern).
173
- // The verify endpoint returns {active: false} with HTTP 200 for invalid/
174
- // expired/revoked tokens. Without this check, we'd read empty scopes.
175
- if (!data.active) {
176
- const reason = data.error || 'invalid_token';
177
- throw new Error(`Token is not active: ${reason}`);
178
- }
179
-
180
- const scopes: string[] = data.scopes || [];
181
- const userId: string = data.user_id;
182
- const connectionId: string = data.connection_id;
183
-
184
- if (!userId) {
185
- throw new Error('Introspection returned no user');
186
- }
187
-
188
- // User lookup from app's local database (if storage is configured)
189
- let user: Record<string, any> = { [config.user_lookup_field]: userId };
190
- try {
191
- const storage = getStorage();
192
- const localUser = await storage.getUser(userId, config.user_lookup_field);
193
- if (localUser) user = localUser;
194
- } catch {}
195
-
196
- const connection = {
197
- connection_id: connectionId,
198
- scopes,
199
- agent_label: data.agent_label || 'Unknown Agent',
200
- };
201
-
202
- return { user, connection, scopes };
203
- }
204
-
205
- /**
206
- * Express middleware: require a specific scope (agent-only).
207
- */
208
- export function requireScope(scope: string) {
209
- return async (req: Request, res: Response, next: NextFunction) => {
210
- const token = getBearerToken(req);
211
- const config = getConfig();
212
-
213
- if (!token || !token.startsWith(config.token_prefix_access)) {
214
- return res.status(401).json({ error: 'invalid_token', error_description: 'AgentAdmit token required' });
215
- }
216
-
217
- try {
218
- const ctx = await validateAgentToken(token);
219
- if (!ctx.scopes.includes(scope)) {
220
- return res.status(403).json({
221
- error: 'insufficient_scope',
222
- required_scope: scope,
223
- granted_scopes: ctx.scopes,
224
- });
225
- }
226
-
227
- await logAccess(ctx, scope, req);
228
- (req as any).agentAdmit = { auth_type: 'agent', ...ctx };
229
- next();
230
- } catch (err: any) {
231
- return res.status(401).json({ error: 'invalid_token', error_description: err.message });
232
- }
233
- };
234
- }
235
-
236
- /**
237
- * Express middleware: enforce scope only if caller is an agent.
238
- */
239
- export function requireScopeIfAgent(scope: string) {
240
- return async (req: Request, res: Response, next: NextFunction) => {
241
- const token = getBearerToken(req);
242
- const config = getConfig();
243
-
244
- if (!token || !token.startsWith(config.token_prefix_access)) {
245
- return next(); // Not an agent — pass through
246
- }
247
-
248
- try {
249
- const ctx = await validateAgentToken(token);
250
- if (!ctx.scopes.includes(scope)) {
251
- return res.status(403).json({
252
- error: 'insufficient_scope',
253
- required_scope: scope,
254
- granted_scopes: ctx.scopes,
255
- });
256
- }
257
-
258
- await logAccess(ctx, scope, req);
259
- (req as any).agentAdmit = { auth_type: 'agent', ...ctx };
260
- next();
261
- } catch (err: any) {
262
- return res.status(401).json({ error: 'invalid_token', error_description: err.message });
263
- }
264
- };
265
- }
266
-
267
- /**
268
- * Express middleware: resolve user or agent from token.
269
- */
270
- export function resolveAuth() {
271
- return async (req: Request, res: Response, next: NextFunction) => {
272
- const token = getBearerToken(req);
273
- const config = getConfig();
274
-
275
- if (!token) {
276
- return res.status(401).json({ error: 'not_authenticated' });
277
- }
278
-
279
- if (token.startsWith(config.token_prefix_access)) {
280
- try {
281
- const ctx = await validateAgentToken(token);
282
- (req as any).agentAdmit = { auth_type: 'agent', ...ctx };
283
- return next();
284
- } catch (err: any) {
285
- return res.status(401).json({ error: 'invalid_token', error_description: err.message });
286
- }
287
- }
288
-
289
- // Regular user token
290
- if (!_verifyUserToken) {
291
- return res.status(500).json({ error: 'server_error', error_description: 'User token verifier not configured' });
292
- }
293
-
294
- try {
295
- const userId = await _verifyUserToken(token);
296
- const storage = getStorage();
297
- const user = await storage.getUser(userId, config.user_lookup_field);
298
- if (!user) {
299
- return res.status(404).json({ error: 'user_not_found' });
300
- }
301
- (req as any).agentAdmit = { auth_type: 'user', user, scopes: ['*'], connection: null };
302
- next();
303
- } catch {
304
- return res.status(401).json({ error: 'invalid_token' });
305
- }
306
- };
307
- }
308
-
309
- /**
310
- * Write audit log entry.
311
- */
312
- async function logAccess(
313
- ctx: { connection: Record<string, any> | null; user: Record<string, any> },
314
- scope: string,
315
- req: Request,
316
- ): Promise<void> {
317
- try {
318
- const config = getConfig();
319
- const storage = getStorage();
320
- await storage.logAccess({
321
- timestamp: new Date(),
322
- connection_id: ctx.connection?.connection_id || 'unknown',
323
- user_id: ctx.user?.[config.user_lookup_field] || 'unknown',
324
- scope_used: scope,
325
- resource: req.path,
326
- method: req.method,
327
- agent_label: ctx.connection?.agent_label || 'Unknown Agent',
328
- });
329
- } catch (err) {
330
- console.error('[AgentAdmit] Audit log failed:', err);
331
- }
332
- }
333
-
334
- /**
335
- * Check connection cap for tier enforcement.
336
- */
337
- export async function checkConnectionCap(userId: string, tier: string): Promise<void> {
338
- const { getTierLimits } = require('./config');
339
- const limits = getTierLimits(tier);
340
- if (!limits?.hard_cap) return;
341
-
342
- const storage = getStorage();
343
- const count = await storage.countActiveConnections(userId);
344
-
345
- if (count >= limits.connections_limit) {
346
- const err: any = new Error(`Connection limit reached (${count}/${limits.connections_limit})`);
347
- err.statusCode = 429;
348
- err.detail = {
349
- error: 'connection_limit_reached',
350
- connections_used: count,
351
- connections_limit: limits.connections_limit,
352
- tier,
353
- };
354
- throw err;
355
- }
356
- }
package/src/config.ts DELETED
@@ -1,150 +0,0 @@
1
- /**
2
- * agentadmit/config.ts
3
- * Configuration loader for AgentAdmit Node.js SDK.
4
- *
5
- * IMPORTANT: AgentAdmit uses MANDATORY hosted introspection.
6
- * All token validation goes through api.agentadmit.com.
7
- * There is no self-hosted mode. No local JWT validation. No bypass.
8
- * This is required for security, audit logging, and scope enforcement.
9
- */
10
-
11
- import fs from 'fs';
12
- import path from 'path';
13
- import yaml from 'js-yaml';
14
-
15
- export interface ScopeDefinition {
16
- name: string;
17
- description: string;
18
- category?: string;
19
- role?: string;
20
- }
21
-
22
- export interface DurationOption {
23
- label: string;
24
- seconds: number | null; // null = "until revoked"
25
- }
26
-
27
- export interface TierDefinition {
28
- name: string;
29
- connections_limit: number;
30
- api_calls_monthly?: number;
31
- hard_cap: boolean;
32
- overage_per_thousand?: number;
33
- }
34
-
35
- export interface StorageConfig {
36
- backend: 'mongodb' | 'memory';
37
- uri?: string;
38
- database?: string;
39
- connections_collection?: string;
40
- audit_log_collection?: string;
41
- tokens_collection?: string;
42
- }
43
-
44
- export interface AgentAdmitConfig {
45
- app_name: string;
46
- app_id: string;
47
- api_key: string;
48
- api_base_url: string;
49
- agentadmit_api_url: string;
50
- agentadmit_verify_url: string;
51
- token_prefix_connection: string;
52
- token_prefix_access: string;
53
- algorithm: string;
54
- audience: string;
55
- connection_token_ttl: number;
56
- scopes: ScopeDefinition[];
57
- durations: DurationOption[];
58
- tiers: TierDefinition[];
59
- default_tier: string;
60
- storage: StorageConfig;
61
- route_prefix: string;
62
- discovery_path: string;
63
- user_lookup_field: string;
64
- private_key_path: string;
65
- public_key_path: string;
66
- /** Max retries on 429 before throwing RateLimitError. Default: 3. */
67
- max_retries: number;
68
- }
69
-
70
- const DEFAULT_CONFIG: Partial<AgentAdmitConfig> = {
71
- app_name: 'My App',
72
- app_id: '',
73
- api_key: '',
74
- api_base_url: 'http://localhost:3000',
75
- agentadmit_api_url: 'https://api.agentadmit.com',
76
- agentadmit_verify_url: 'https://api.agentadmit.com/v1/verify',
77
- token_prefix_connection: 'ag_ct_',
78
- token_prefix_access: 'ag_at_',
79
- algorithm: 'RS256',
80
- audience: 'agentadmit',
81
- connection_token_ttl: 900,
82
- scopes: [],
83
- durations: [
84
- { label: '1 Hour', seconds: 3600 },
85
- { label: '24 Hours', seconds: 86400 },
86
- { label: '7 Days', seconds: 604800 },
87
- { label: '30 Days', seconds: 2592000 },
88
- { label: 'Until I Revoke', seconds: null },
89
- ],
90
- tiers: [
91
- { name: 'trial', connections_limit: 3, hard_cap: true },
92
- { name: 'standard', connections_limit: 100, api_calls_monthly: 2000000, hard_cap: false },
93
- ],
94
- default_tier: 'standard',
95
- storage: {
96
- backend: 'mongodb',
97
- uri: 'mongodb://localhost:27017',
98
- database: 'agentadmit',
99
- connections_collection: 'agentadmit_connections',
100
- audit_log_collection: 'agentadmit_audit_log',
101
- tokens_collection: 'agentadmit_tokens',
102
- },
103
- route_prefix: '/agentadmit',
104
- discovery_path: '/.well-known/agentadmit',
105
- user_lookup_field: 'user_id',
106
- max_retries: 3,
107
- };
108
-
109
- let _config: AgentAdmitConfig | null = null;
110
-
111
- export function loadConfig(configPath: string = 'agentadmit.yaml'): AgentAdmitConfig {
112
- let resolvedPath = configPath;
113
-
114
- if (!fs.existsSync(resolvedPath)) {
115
- const envPath = process.env.AGENTADMIT_CONFIG;
116
- if (envPath && fs.existsSync(envPath)) {
117
- resolvedPath = envPath;
118
- } else {
119
- throw new Error(
120
- `Config file not found: ${configPath}. Run 'agentadmit init' to generate one.`
121
- );
122
- }
123
- }
124
-
125
- const raw = yaml.load(fs.readFileSync(resolvedPath, 'utf-8')) as Record<string, any> || {};
126
- _config = { ...DEFAULT_CONFIG, ...raw } as AgentAdmitConfig;
127
-
128
- console.log(`[AgentAdmit] Config loaded: ${resolvedPath} (${_config.scopes.length} scopes)`);
129
- return _config;
130
- }
131
-
132
- export function getConfig(): AgentAdmitConfig {
133
- if (!_config) {
134
- throw new Error('AgentAdmit config not loaded. Call loadConfig() first.');
135
- }
136
- return _config;
137
- }
138
-
139
- export function getScopeMetadata(): ScopeDefinition[] {
140
- return getConfig().scopes;
141
- }
142
-
143
- export function getDurationOptions(): DurationOption[] {
144
- return getConfig().durations;
145
- }
146
-
147
- export function getTierLimits(tierName: string): TierDefinition | undefined {
148
- const config = getConfig();
149
- return config.tiers.find(t => t.name === tierName) || config.tiers.find(t => t.name === config.default_tier);
150
- }