@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/README.md +69 -0
- package/dist/alerts.d.ts +93 -0
- package/dist/alerts.js +104 -0
- package/dist/auth.d.ts +26 -3
- package/dist/auth.js +19 -2
- package/dist/config.js +5 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.js +13 -1
- package/dist/routes.js +28 -20
- package/dist/webhooks.d.ts +37 -0
- package/dist/webhooks.js +92 -0
- package/package.json +11 -4
- package/tests/webhooks.test.ts +109 -0
- package/.github/workflows/publish.yml +0 -53
- package/src/auth.ts +0 -356
- package/src/config.ts +0 -150
- package/src/errors.ts +0 -50
- package/src/index.ts +0 -27
- package/src/keys.ts +0 -33
- package/src/routes.ts +0 -245
- package/src/storage.ts +0 -228
- package/tsconfig.json +0 -19
package/README.md
CHANGED
|
@@ -201,3 +201,72 @@ For complete compliance guidance, see our [compliance guide](https://agentadmit.
|
|
|
201
201
|
## License
|
|
202
202
|
|
|
203
203
|
All rights reserved. Patent pending.
|
|
204
|
+
|
|
205
|
+
## Security Alerts
|
|
206
|
+
|
|
207
|
+
Monitor suspicious agent activity. Six alert types:
|
|
208
|
+
- `volume_spike`, `failed_scope_attempts`, `burst_pattern`,
|
|
209
|
+
- `stale_reactivation`, `new_scope_usage`, `revoked_connection_attempt`
|
|
210
|
+
|
|
211
|
+
### Configure Alert Thresholds
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
import { configureAlerts } from '@agentadmit/sdk';
|
|
215
|
+
|
|
216
|
+
await configureAlerts({
|
|
217
|
+
app_id: 'app_abc123',
|
|
218
|
+
alert_type: 'volume_spike',
|
|
219
|
+
enabled: true,
|
|
220
|
+
threshold_value: 100,
|
|
221
|
+
threshold_window_minutes: 5,
|
|
222
|
+
kill_switch_enabled: true,
|
|
223
|
+
});
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### List Alert Events
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
import { listAlerts } from '@agentadmit/sdk';
|
|
230
|
+
const { events, total } = await listAlerts({ app_id: 'app_abc123', alert_type: 'volume_spike' });
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Get Current Config
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
import { getAlertConfig } from '@agentadmit/sdk';
|
|
237
|
+
const config = await getAlertConfig({ app_id: 'app_abc123' });
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
### Notifying Your Users
|
|
242
|
+
|
|
243
|
+
AgentAdmit detects anomalies, fires alerts, and (with kill switch) auto-revokes connections. **How you notify your own users is up to you.** AgentAdmit provides the data — you deliver it through your own system (in-app notifications, email, push, etc.).
|
|
244
|
+
|
|
245
|
+
- **Poll alerts** — Use the SDK methods above from your backend to check for new events, then notify users through your existing system.
|
|
246
|
+
- **Webhook delivery** — Configure a webhook URL in your AgentAdmit dashboard. When an alert fires, AgentAdmit POSTs the payload to your server, signed with your `whsec_…` secret. Always verify the signature against the **raw** request body before trusting the payload:
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
import express from 'express';
|
|
250
|
+
import { verifyWebhookSignature, WebhookSignatureError } from '@agentadmit/sdk';
|
|
251
|
+
|
|
252
|
+
app.post('/agentadmit/alerts', express.raw({ type: 'application/json' }), (req, res) => {
|
|
253
|
+
try {
|
|
254
|
+
verifyWebhookSignature(
|
|
255
|
+
req.body, // raw Buffer
|
|
256
|
+
req.header('X-AgentAdmit-Signature') ?? '',
|
|
257
|
+
process.env.AGENTADMIT_WEBHOOK_SECRET!, // whsec_…
|
|
258
|
+
);
|
|
259
|
+
} catch (err) {
|
|
260
|
+
if (err instanceof WebhookSignatureError) {
|
|
261
|
+
return res.status(400).json({ error: 'invalid_signature' });
|
|
262
|
+
}
|
|
263
|
+
throw err;
|
|
264
|
+
}
|
|
265
|
+
const event = JSON.parse(req.body.toString('utf-8'));
|
|
266
|
+
// ...
|
|
267
|
+
res.sendStatus(200);
|
|
268
|
+
});
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
The header format is `t=<unix_ts>,v1=<hex>` — an HMAC-SHA256 of `${t}.${rawBody}` keyed with your signing secret. The helper compares in constant time and rejects timestamps more than 5 minutes off (replay protection).
|
|
272
|
+
- **React SDK** — Embed the `<AlertsPanel>` component so users can view their own alert history and tighten thresholds.
|
package/dist/alerts.d.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agentadmit/alerts.ts
|
|
3
|
+
* Alert configuration and event management for the AgentAdmit hosted service.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* import { configureAlerts, listAlerts, getAlertConfig } from '@agentadmit/sdk';
|
|
7
|
+
*
|
|
8
|
+
* // Configure a volume spike alert
|
|
9
|
+
* const result = await configureAlerts({
|
|
10
|
+
* app_id: 'app_abc123',
|
|
11
|
+
* alert_type: 'volume_spike',
|
|
12
|
+
* enabled: true,
|
|
13
|
+
* threshold_value: 100,
|
|
14
|
+
* threshold_window_minutes: 5,
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* // List alert events
|
|
18
|
+
* const events = await listAlerts({ app_id: 'app_abc123', alert_type: 'volume_spike' });
|
|
19
|
+
*
|
|
20
|
+
* // Get current config
|
|
21
|
+
* const config = await getAlertConfig({ app_id: 'app_abc123' });
|
|
22
|
+
*/
|
|
23
|
+
/** The 6 supported alert types. */
|
|
24
|
+
export declare const ALERT_TYPES: readonly ["volume_spike", "failed_scope_attempts", "burst_pattern", "stale_reactivation", "new_scope_usage", "revoked_connection_attempt"];
|
|
25
|
+
export type AlertType = typeof ALERT_TYPES[number];
|
|
26
|
+
export interface ConfigureAlertsOptions {
|
|
27
|
+
app_id: string;
|
|
28
|
+
alert_type: AlertType | string;
|
|
29
|
+
connection_id?: string;
|
|
30
|
+
enabled?: boolean;
|
|
31
|
+
threshold_value?: number;
|
|
32
|
+
threshold_window_minutes?: number;
|
|
33
|
+
threshold_rate_per_minute?: number;
|
|
34
|
+
stale_days?: number;
|
|
35
|
+
kill_switch_enabled?: boolean;
|
|
36
|
+
kill_switch_threshold_value?: number;
|
|
37
|
+
kill_switch_threshold_window_minutes?: number;
|
|
38
|
+
}
|
|
39
|
+
export interface ListAlertsOptions {
|
|
40
|
+
app_id: string;
|
|
41
|
+
connection_id?: string;
|
|
42
|
+
alert_type?: AlertType | string;
|
|
43
|
+
limit?: number;
|
|
44
|
+
offset?: number;
|
|
45
|
+
}
|
|
46
|
+
export interface GetAlertConfigOptions {
|
|
47
|
+
app_id: string;
|
|
48
|
+
connection_id?: string;
|
|
49
|
+
}
|
|
50
|
+
export interface AlertEvent {
|
|
51
|
+
id?: string;
|
|
52
|
+
app_id: string;
|
|
53
|
+
connection_id?: string;
|
|
54
|
+
alert_type: string;
|
|
55
|
+
triggered_at: string;
|
|
56
|
+
details?: Record<string, any>;
|
|
57
|
+
}
|
|
58
|
+
export interface AlertEventsResponse {
|
|
59
|
+
events: AlertEvent[];
|
|
60
|
+
total: number;
|
|
61
|
+
limit: number;
|
|
62
|
+
offset: number;
|
|
63
|
+
}
|
|
64
|
+
export interface AlertConfigResponse {
|
|
65
|
+
app_id: string;
|
|
66
|
+
app_level: Record<string, any>;
|
|
67
|
+
connection_overrides: Record<string, Record<string, any>>;
|
|
68
|
+
alert_types: string[];
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Configure alert thresholds for an app or connection.
|
|
72
|
+
*
|
|
73
|
+
* @param options - Alert configuration options.
|
|
74
|
+
* @returns { ok: true, config: {...} }
|
|
75
|
+
*/
|
|
76
|
+
export declare function configureAlerts(options: ConfigureAlertsOptions): Promise<{
|
|
77
|
+
ok: true;
|
|
78
|
+
config: Record<string, any>;
|
|
79
|
+
}>;
|
|
80
|
+
/**
|
|
81
|
+
* List alert events for an app.
|
|
82
|
+
*
|
|
83
|
+
* @param options - Filter and pagination options.
|
|
84
|
+
* @returns { events: [...], total, limit, offset }
|
|
85
|
+
*/
|
|
86
|
+
export declare function listAlerts(options: ListAlertsOptions): Promise<AlertEventsResponse>;
|
|
87
|
+
/**
|
|
88
|
+
* Get the current alert configuration for an app.
|
|
89
|
+
*
|
|
90
|
+
* @param options - App ID and optional connection ID.
|
|
91
|
+
* @returns Current alert config with app-level and connection-level settings.
|
|
92
|
+
*/
|
|
93
|
+
export declare function getAlertConfig(options: GetAlertConfigOptions): Promise<AlertConfigResponse>;
|
package/dist/alerts.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* agentadmit/alerts.ts
|
|
4
|
+
* Alert configuration and event management for the AgentAdmit hosted service.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* import { configureAlerts, listAlerts, getAlertConfig } from '@agentadmit/sdk';
|
|
8
|
+
*
|
|
9
|
+
* // Configure a volume spike alert
|
|
10
|
+
* const result = await configureAlerts({
|
|
11
|
+
* app_id: 'app_abc123',
|
|
12
|
+
* alert_type: 'volume_spike',
|
|
13
|
+
* enabled: true,
|
|
14
|
+
* threshold_value: 100,
|
|
15
|
+
* threshold_window_minutes: 5,
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* // List alert events
|
|
19
|
+
* const events = await listAlerts({ app_id: 'app_abc123', alert_type: 'volume_spike' });
|
|
20
|
+
*
|
|
21
|
+
* // Get current config
|
|
22
|
+
* const config = await getAlertConfig({ app_id: 'app_abc123' });
|
|
23
|
+
*/
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.ALERT_TYPES = void 0;
|
|
26
|
+
exports.configureAlerts = configureAlerts;
|
|
27
|
+
exports.listAlerts = listAlerts;
|
|
28
|
+
exports.getAlertConfig = getAlertConfig;
|
|
29
|
+
const config_1 = require("./config");
|
|
30
|
+
/** The 6 supported alert types. */
|
|
31
|
+
exports.ALERT_TYPES = [
|
|
32
|
+
'volume_spike',
|
|
33
|
+
'failed_scope_attempts',
|
|
34
|
+
'burst_pattern',
|
|
35
|
+
'stale_reactivation',
|
|
36
|
+
'new_scope_usage',
|
|
37
|
+
'revoked_connection_attempt',
|
|
38
|
+
];
|
|
39
|
+
/**
|
|
40
|
+
* Make an authenticated request to the AgentAdmit hosted service.
|
|
41
|
+
* Supports GET (with query string) and POST (with JSON body).
|
|
42
|
+
*/
|
|
43
|
+
async function callHostedService(method, path, body) {
|
|
44
|
+
const config = (0, config_1.getConfig)();
|
|
45
|
+
const url = `${config.agentadmit_api_url.replace(/\/$/, '')}${path}`;
|
|
46
|
+
const resp = await fetch(url, {
|
|
47
|
+
method,
|
|
48
|
+
headers: {
|
|
49
|
+
Authorization: `Bearer ${config.api_key}`,
|
|
50
|
+
'Content-Type': 'application/json',
|
|
51
|
+
'X-App-Id': config.app_id,
|
|
52
|
+
},
|
|
53
|
+
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
|
54
|
+
});
|
|
55
|
+
if (!resp.ok) {
|
|
56
|
+
const errData = await resp.json().catch(() => ({}));
|
|
57
|
+
const err = new Error(errData.error_description || errData.error || `HTTP ${resp.status}`);
|
|
58
|
+
err.status = resp.status;
|
|
59
|
+
err.data = errData;
|
|
60
|
+
throw err;
|
|
61
|
+
}
|
|
62
|
+
return resp.json();
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Configure alert thresholds for an app or connection.
|
|
66
|
+
*
|
|
67
|
+
* @param options - Alert configuration options.
|
|
68
|
+
* @returns { ok: true, config: {...} }
|
|
69
|
+
*/
|
|
70
|
+
async function configureAlerts(options) {
|
|
71
|
+
// Remove undefined values
|
|
72
|
+
const body = Object.fromEntries(Object.entries(options).filter(([, v]) => v !== undefined));
|
|
73
|
+
return callHostedService('POST', '/api/v1/alerts', body);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* List alert events for an app.
|
|
77
|
+
*
|
|
78
|
+
* @param options - Filter and pagination options.
|
|
79
|
+
* @returns { events: [...], total, limit, offset }
|
|
80
|
+
*/
|
|
81
|
+
async function listAlerts(options) {
|
|
82
|
+
const params = new URLSearchParams({ app_id: options.app_id });
|
|
83
|
+
if (options.connection_id)
|
|
84
|
+
params.set('connection_id', options.connection_id);
|
|
85
|
+
if (options.alert_type)
|
|
86
|
+
params.set('alert_type', options.alert_type);
|
|
87
|
+
if (options.limit !== undefined)
|
|
88
|
+
params.set('limit', String(options.limit));
|
|
89
|
+
if (options.offset !== undefined)
|
|
90
|
+
params.set('offset', String(options.offset));
|
|
91
|
+
return callHostedService('GET', `/api/v1/alerts?${params}`);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Get the current alert configuration for an app.
|
|
95
|
+
*
|
|
96
|
+
* @param options - App ID and optional connection ID.
|
|
97
|
+
* @returns Current alert config with app-level and connection-level settings.
|
|
98
|
+
*/
|
|
99
|
+
async function getAlertConfig(options) {
|
|
100
|
+
const params = new URLSearchParams({ app_id: options.app_id });
|
|
101
|
+
if (options.connection_id)
|
|
102
|
+
params.set('connection_id', options.connection_id);
|
|
103
|
+
return callHostedService('GET', `/api/v1/alerts/config?${params}`);
|
|
104
|
+
}
|
package/dist/auth.d.ts
CHANGED
|
@@ -12,6 +12,29 @@ export interface AgentContext {
|
|
|
12
12
|
connection: Record<string, any> | null;
|
|
13
13
|
scopes: string[];
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Error codes the hosted /api/v1/verify endpoint returns with HTTP 200 and
|
|
17
|
+
* `active: false`. Unknown codes pass through as plain strings.
|
|
18
|
+
*/
|
|
19
|
+
export declare const VERIFY_ERROR_CODES: readonly ["invalid_token", "token_expired", "token_revoked", "connection_revoked", "connection_expired", "environment_mismatch", "insufficient_scope"];
|
|
20
|
+
export type VerifyErrorCode = (typeof VERIFY_ERROR_CODES)[number];
|
|
21
|
+
/** Successful introspection result from /api/v1/verify. */
|
|
22
|
+
export interface VerifyActive {
|
|
23
|
+
active: true;
|
|
24
|
+
sub?: string;
|
|
25
|
+
user_id?: string;
|
|
26
|
+
connection_id?: string;
|
|
27
|
+
scopes?: string[];
|
|
28
|
+
role?: string;
|
|
29
|
+
app_id?: string;
|
|
30
|
+
jti?: string;
|
|
31
|
+
exp?: number;
|
|
32
|
+
}
|
|
33
|
+
/** Failed (but non-fatal) introspection result — HTTP 200, active: false. */
|
|
34
|
+
export interface VerifyInactive {
|
|
35
|
+
active: false;
|
|
36
|
+
error?: VerifyErrorCode | (string & {});
|
|
37
|
+
}
|
|
15
38
|
/**
|
|
16
39
|
* Validate an ag_at_ token and return the agent context.
|
|
17
40
|
*/
|
|
@@ -19,15 +42,15 @@ export declare function validateAgentToken(token: string): Promise<Omit<AgentCon
|
|
|
19
42
|
/**
|
|
20
43
|
* Express middleware: require a specific scope (agent-only).
|
|
21
44
|
*/
|
|
22
|
-
export declare function requireScope(scope: string): (req: Request, res: Response, next: NextFunction) => Promise<Response
|
|
45
|
+
export declare function requireScope(scope: string): (req: Request, res: Response, next: NextFunction) => Promise<Response | undefined>;
|
|
23
46
|
/**
|
|
24
47
|
* Express middleware: enforce scope only if caller is an agent.
|
|
25
48
|
*/
|
|
26
|
-
export declare function requireScopeIfAgent(scope: string): (req: Request, res: Response, next: NextFunction) => Promise<void | Response
|
|
49
|
+
export declare function requireScopeIfAgent(scope: string): (req: Request, res: Response, next: NextFunction) => Promise<void | Response>;
|
|
27
50
|
/**
|
|
28
51
|
* Express middleware: resolve user or agent from token.
|
|
29
52
|
*/
|
|
30
|
-
export declare function resolveAuth(): (req: Request, res: Response, next: NextFunction) => Promise<void | Response
|
|
53
|
+
export declare function resolveAuth(): (req: Request, res: Response, next: NextFunction) => Promise<void | Response>;
|
|
31
54
|
/**
|
|
32
55
|
* Check connection cap for tier enforcement.
|
|
33
56
|
*/
|
package/dist/auth.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Token validation, scope enforcement, and audit logging for Express.
|
|
5
5
|
*/
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.VERIFY_ERROR_CODES = void 0;
|
|
7
8
|
exports.setStorage = setStorage;
|
|
8
9
|
exports.setUserVerifier = setUserVerifier;
|
|
9
10
|
exports.validateAgentToken = validateAgentToken;
|
|
@@ -32,6 +33,19 @@ function getBearerToken(req) {
|
|
|
32
33
|
return auth.slice(7);
|
|
33
34
|
return null;
|
|
34
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Error codes the hosted /api/v1/verify endpoint returns with HTTP 200 and
|
|
38
|
+
* `active: false`. Unknown codes pass through as plain strings.
|
|
39
|
+
*/
|
|
40
|
+
exports.VERIFY_ERROR_CODES = [
|
|
41
|
+
'invalid_token',
|
|
42
|
+
'token_expired',
|
|
43
|
+
'token_revoked',
|
|
44
|
+
'connection_revoked',
|
|
45
|
+
'connection_expired',
|
|
46
|
+
'environment_mismatch',
|
|
47
|
+
'insufficient_scope',
|
|
48
|
+
];
|
|
35
49
|
// ---------------------------------------------------------------------------
|
|
36
50
|
// Rate-limit retry helpers
|
|
37
51
|
// ---------------------------------------------------------------------------
|
|
@@ -121,7 +135,7 @@ async function validateAgentToken(token) {
|
|
|
121
135
|
}
|
|
122
136
|
// MANDATORY INTROSPECTION — validate via AgentAdmit hosted service
|
|
123
137
|
// No local JWT decode. Every verification call goes through AgentAdmit.
|
|
124
|
-
const verifyUrl = config.agentadmit_verify_url || 'https://api.agentadmit.com/v1/verify';
|
|
138
|
+
const verifyUrl = config.agentadmit_verify_url || 'https://api.agentadmit.com/api/v1/verify';
|
|
125
139
|
const appId = config.app_id;
|
|
126
140
|
const apiKey = config.api_key || '';
|
|
127
141
|
const maxRetries = config.max_retries ?? 3;
|
|
@@ -135,10 +149,13 @@ async function validateAgentToken(token) {
|
|
|
135
149
|
if (response.status !== 200) {
|
|
136
150
|
throw new Error(`Verification service returned ${response.status}`);
|
|
137
151
|
}
|
|
152
|
+
// Shape: VerifyActive | VerifyInactive (kept loose for forward-compat).
|
|
138
153
|
const data = (await response.json());
|
|
139
154
|
// Check active flag (RFC 7662 introspection pattern).
|
|
140
155
|
// The verify endpoint returns {active: false} with HTTP 200 for invalid/
|
|
141
|
-
// expired/revoked tokens
|
|
156
|
+
// expired/revoked tokens (error is one of VERIFY_ERROR_CODES, e.g.
|
|
157
|
+
// token_expired, connection_expired, environment_mismatch). Without this
|
|
158
|
+
// check, we'd read empty scopes.
|
|
142
159
|
if (!data.active) {
|
|
143
160
|
const reason = data.error || 'invalid_token';
|
|
144
161
|
throw new Error(`Token is not active: ${reason}`);
|
package/dist/config.js
CHANGED
|
@@ -25,7 +25,7 @@ const DEFAULT_CONFIG = {
|
|
|
25
25
|
api_key: '',
|
|
26
26
|
api_base_url: 'http://localhost:3000',
|
|
27
27
|
agentadmit_api_url: 'https://api.agentadmit.com',
|
|
28
|
-
agentadmit_verify_url: 'https://api.agentadmit.com/v1/verify',
|
|
28
|
+
agentadmit_verify_url: 'https://api.agentadmit.com/api/v1/verify',
|
|
29
29
|
token_prefix_connection: 'ag_ct_',
|
|
30
30
|
token_prefix_access: 'ag_at_',
|
|
31
31
|
algorithm: 'RS256',
|
|
@@ -71,6 +71,10 @@ function loadConfig(configPath = 'agentadmit.yaml') {
|
|
|
71
71
|
}
|
|
72
72
|
const raw = js_yaml_1.default.load(fs_1.default.readFileSync(resolvedPath, 'utf-8')) || {};
|
|
73
73
|
_config = { ...DEFAULT_CONFIG, ...raw };
|
|
74
|
+
// Validate the key prefix without ever echoing the key itself.
|
|
75
|
+
if (_config.api_key && !/^aa_(test|live)_/.test(_config.api_key)) {
|
|
76
|
+
throw new Error("Invalid api_key: must start with 'aa_test_' or 'aa_live_'");
|
|
77
|
+
}
|
|
74
78
|
console.log(`[AgentAdmit] Config loaded: ${resolvedPath} (${_config.scopes.length} scopes)`);
|
|
75
79
|
return _config;
|
|
76
80
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,11 @@ export { loadConfig, getConfig, getScopeMetadata, getDurationOptions, getTierLim
|
|
|
7
7
|
export type { AgentAdmitConfig, ScopeDefinition, DurationOption, TierDefinition, StorageConfig } from './config';
|
|
8
8
|
export { generateKeyPair, loadPrivateKey, loadPublicKey } from './keys';
|
|
9
9
|
export { StorageBackend, MongoDBStorage, MemoryStorage, createStorage } from './storage';
|
|
10
|
-
export { validateAgentToken, requireScope, requireScopeIfAgent, resolveAuth, checkConnectionCap, setStorage, setUserVerifier, } from './auth';
|
|
11
|
-
export type { AgentContext } from './auth';
|
|
10
|
+
export { validateAgentToken, requireScope, requireScopeIfAgent, resolveAuth, checkConnectionCap, setStorage, setUserVerifier, VERIFY_ERROR_CODES, } from './auth';
|
|
11
|
+
export type { AgentContext, VerifyErrorCode, VerifyActive, VerifyInactive } from './auth';
|
|
12
|
+
export { verifyWebhookSignature, isValidWebhookSignature, WebhookSignatureError, SIGNATURE_HEADER, DEFAULT_TOLERANCE_SECONDS, } from './webhooks';
|
|
13
|
+
export type { VerifyWebhookSignatureOptions } from './webhooks';
|
|
12
14
|
export { createAgentAdmitRouter } from './routes';
|
|
13
15
|
export { RateLimitError } from './errors';
|
|
16
|
+
export { configureAlerts, listAlerts, getAlertConfig, ALERT_TYPES, } from './alerts';
|
|
17
|
+
export type { AlertType, ConfigureAlertsOptions, ListAlertsOptions, GetAlertConfigOptions, AlertEvent, AlertEventsResponse, AlertConfigResponse, } from './alerts';
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* User-mediated AI agent authorization. Plug-and-play for Express and Next.js.
|
|
6
6
|
*/
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
-
exports.RateLimitError = exports.createAgentAdmitRouter = exports.setUserVerifier = exports.setStorage = exports.checkConnectionCap = exports.resolveAuth = exports.requireScopeIfAgent = exports.requireScope = exports.validateAgentToken = exports.createStorage = exports.MemoryStorage = exports.MongoDBStorage = exports.loadPublicKey = exports.loadPrivateKey = exports.generateKeyPair = exports.getTierLimits = exports.getDurationOptions = exports.getScopeMetadata = exports.getConfig = exports.loadConfig = void 0;
|
|
8
|
+
exports.ALERT_TYPES = exports.getAlertConfig = exports.listAlerts = exports.configureAlerts = exports.RateLimitError = exports.createAgentAdmitRouter = exports.DEFAULT_TOLERANCE_SECONDS = exports.SIGNATURE_HEADER = exports.WebhookSignatureError = exports.isValidWebhookSignature = exports.verifyWebhookSignature = exports.VERIFY_ERROR_CODES = exports.setUserVerifier = exports.setStorage = exports.checkConnectionCap = exports.resolveAuth = exports.requireScopeIfAgent = exports.requireScope = exports.validateAgentToken = exports.createStorage = exports.MemoryStorage = exports.MongoDBStorage = exports.loadPublicKey = exports.loadPrivateKey = exports.generateKeyPair = exports.getTierLimits = exports.getDurationOptions = exports.getScopeMetadata = exports.getConfig = exports.loadConfig = void 0;
|
|
9
9
|
var config_1 = require("./config");
|
|
10
10
|
Object.defineProperty(exports, "loadConfig", { enumerable: true, get: function () { return config_1.loadConfig; } });
|
|
11
11
|
Object.defineProperty(exports, "getConfig", { enumerable: true, get: function () { return config_1.getConfig; } });
|
|
@@ -28,7 +28,19 @@ Object.defineProperty(exports, "resolveAuth", { enumerable: true, get: function
|
|
|
28
28
|
Object.defineProperty(exports, "checkConnectionCap", { enumerable: true, get: function () { return auth_1.checkConnectionCap; } });
|
|
29
29
|
Object.defineProperty(exports, "setStorage", { enumerable: true, get: function () { return auth_1.setStorage; } });
|
|
30
30
|
Object.defineProperty(exports, "setUserVerifier", { enumerable: true, get: function () { return auth_1.setUserVerifier; } });
|
|
31
|
+
Object.defineProperty(exports, "VERIFY_ERROR_CODES", { enumerable: true, get: function () { return auth_1.VERIFY_ERROR_CODES; } });
|
|
32
|
+
var webhooks_1 = require("./webhooks");
|
|
33
|
+
Object.defineProperty(exports, "verifyWebhookSignature", { enumerable: true, get: function () { return webhooks_1.verifyWebhookSignature; } });
|
|
34
|
+
Object.defineProperty(exports, "isValidWebhookSignature", { enumerable: true, get: function () { return webhooks_1.isValidWebhookSignature; } });
|
|
35
|
+
Object.defineProperty(exports, "WebhookSignatureError", { enumerable: true, get: function () { return webhooks_1.WebhookSignatureError; } });
|
|
36
|
+
Object.defineProperty(exports, "SIGNATURE_HEADER", { enumerable: true, get: function () { return webhooks_1.SIGNATURE_HEADER; } });
|
|
37
|
+
Object.defineProperty(exports, "DEFAULT_TOLERANCE_SECONDS", { enumerable: true, get: function () { return webhooks_1.DEFAULT_TOLERANCE_SECONDS; } });
|
|
31
38
|
var routes_1 = require("./routes");
|
|
32
39
|
Object.defineProperty(exports, "createAgentAdmitRouter", { enumerable: true, get: function () { return routes_1.createAgentAdmitRouter; } });
|
|
33
40
|
var errors_1 = require("./errors");
|
|
34
41
|
Object.defineProperty(exports, "RateLimitError", { enumerable: true, get: function () { return errors_1.RateLimitError; } });
|
|
42
|
+
var alerts_1 = require("./alerts");
|
|
43
|
+
Object.defineProperty(exports, "configureAlerts", { enumerable: true, get: function () { return alerts_1.configureAlerts; } });
|
|
44
|
+
Object.defineProperty(exports, "listAlerts", { enumerable: true, get: function () { return alerts_1.listAlerts; } });
|
|
45
|
+
Object.defineProperty(exports, "getAlertConfig", { enumerable: true, get: function () { return alerts_1.getAlertConfig; } });
|
|
46
|
+
Object.defineProperty(exports, "ALERT_TYPES", { enumerable: true, get: function () { return alerts_1.ALERT_TYPES; } });
|
package/dist/routes.js
CHANGED
|
@@ -15,18 +15,23 @@ const config_1 = require("./config");
|
|
|
15
15
|
const auth_1 = require("./auth");
|
|
16
16
|
const AGENTADMIT_VERSION = '0.1';
|
|
17
17
|
/**
|
|
18
|
-
* Make
|
|
18
|
+
* Make a request to the AgentAdmit hosted service. Authenticated with the
|
|
19
|
+
* operator API key, except for /exchange (authenticated: false) where the
|
|
20
|
+
* connection token itself is the credential.
|
|
19
21
|
*/
|
|
20
|
-
async function callHostedService(path, body) {
|
|
22
|
+
async function callHostedService(path, body, options = {}) {
|
|
21
23
|
const config = (0, config_1.getConfig)();
|
|
22
24
|
const url = `${config.agentadmit_api_url.replace(/\/$/, '')}${path}`;
|
|
25
|
+
const headers = {
|
|
26
|
+
'Content-Type': 'application/json',
|
|
27
|
+
'X-App-Id': config.app_id,
|
|
28
|
+
};
|
|
29
|
+
if (options.authenticated !== false) {
|
|
30
|
+
headers['Authorization'] = `Bearer ${config.api_key}`;
|
|
31
|
+
}
|
|
23
32
|
const resp = await fetch(url, {
|
|
24
33
|
method: 'POST',
|
|
25
|
-
headers
|
|
26
|
-
'Authorization': `Bearer ${config.api_key}`,
|
|
27
|
-
'Content-Type': 'application/json',
|
|
28
|
-
'X-App-Id': config.app_id,
|
|
29
|
-
},
|
|
34
|
+
headers,
|
|
30
35
|
body: JSON.stringify(body),
|
|
31
36
|
});
|
|
32
37
|
const data = await resp.json().catch(() => ({}));
|
|
@@ -88,20 +93,22 @@ function createAgentAdmitRouter(options) {
|
|
|
88
93
|
if (!validation.valid) {
|
|
89
94
|
return res.status(400).json({ error: 'invalid_scope', invalid_scopes: validation.invalid });
|
|
90
95
|
}
|
|
91
|
-
const duration = duration_seconds || config.connection_token_ttl;
|
|
92
96
|
const userId = currentUser[config.user_lookup_field];
|
|
93
97
|
const role = determineRole(currentUser);
|
|
94
98
|
const userTier = getUserTier(currentUser);
|
|
95
99
|
await (0, auth_1.checkConnectionCap)(userId, userTier);
|
|
96
|
-
// Call AgentAdmit hosted service
|
|
97
|
-
|
|
100
|
+
// Call AgentAdmit hosted service. duration_seconds is tri-state:
|
|
101
|
+
// key absent → hosted default (30 days); explicit null → until
|
|
102
|
+
// revoked; integer 60–31536000 → explicit duration.
|
|
103
|
+
const issueBody = {
|
|
98
104
|
user_id: String(userId),
|
|
99
105
|
scopes,
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
106
|
+
role,
|
|
107
|
+
};
|
|
108
|
+
if ('duration_seconds' in req.body) {
|
|
109
|
+
issueBody.duration_seconds = duration_seconds ?? null;
|
|
110
|
+
}
|
|
111
|
+
const { status, data } = await callHostedService(`/api/v1/apps/${config.app_id}/token`, issueBody);
|
|
105
112
|
if (status !== 200 && status !== 201) {
|
|
106
113
|
console.error('[AgentAdmit] Hosted token generation failed:', status, data);
|
|
107
114
|
return res.status(502).json({ error: 'token_generation_failed', error_description: 'Authorization service could not generate token' });
|
|
@@ -115,12 +122,12 @@ function createAgentAdmitRouter(options) {
|
|
|
115
122
|
scopes,
|
|
116
123
|
role,
|
|
117
124
|
agent_label: label,
|
|
118
|
-
duration_seconds:
|
|
125
|
+
duration_seconds: 'duration_seconds' in req.body ? duration_seconds ?? null : null,
|
|
119
126
|
status: 'active',
|
|
120
127
|
});
|
|
121
128
|
res.json({
|
|
122
|
-
connection_token: data.token
|
|
123
|
-
expires_in:
|
|
129
|
+
connection_token: data.token,
|
|
130
|
+
expires_in: data.expires_in ?? config.connection_token_ttl,
|
|
124
131
|
scopes,
|
|
125
132
|
});
|
|
126
133
|
}
|
|
@@ -139,13 +146,14 @@ function createAgentAdmitRouter(options) {
|
|
|
139
146
|
if (!connection_token) {
|
|
140
147
|
return res.status(400).json({ error: 'invalid_request', error_description: 'connection_token required' });
|
|
141
148
|
}
|
|
142
|
-
// Forward to AgentAdmit hosted service
|
|
149
|
+
// Forward to AgentAdmit hosted service. No API key on this call —
|
|
150
|
+
// the connection token is the credential.
|
|
143
151
|
const { status, data } = await callHostedService('/api/v1/exchange', {
|
|
144
152
|
token: connection_token,
|
|
145
153
|
agent_label: agent_label ?? null,
|
|
146
154
|
agent_id: agent_id ?? null,
|
|
147
155
|
agent_metadata: agent_metadata ?? null,
|
|
148
|
-
});
|
|
156
|
+
}, { authenticated: false });
|
|
149
157
|
if (status !== 200) {
|
|
150
158
|
return res.status(status < 500 ? status : 502).json(data);
|
|
151
159
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agentadmit/webhooks.ts
|
|
3
|
+
* Verification for inbound AgentAdmit alert webhooks.
|
|
4
|
+
*
|
|
5
|
+
* AgentAdmit signs every alert webhook delivery with the app's webhook
|
|
6
|
+
* signing secret (`whsec_…`, returned once when the webhook URL is
|
|
7
|
+
* configured). The signature arrives in the `X-AgentAdmit-Signature` header:
|
|
8
|
+
*
|
|
9
|
+
* X-AgentAdmit-Signature: t=<unix_ts>,v1=<hex hmac-sha256>
|
|
10
|
+
*
|
|
11
|
+
* where the HMAC input is `${t}.${rawBody}` keyed with the full whsec_
|
|
12
|
+
* secret. Always verify against the raw request body, before JSON parsing
|
|
13
|
+
* (use `express.raw()` or capture the body with a verify hook).
|
|
14
|
+
*/
|
|
15
|
+
export declare const SIGNATURE_HEADER = "X-AgentAdmit-Signature";
|
|
16
|
+
export declare const DEFAULT_TOLERANCE_SECONDS = 300;
|
|
17
|
+
export declare class WebhookSignatureError extends Error {
|
|
18
|
+
constructor(message?: string);
|
|
19
|
+
}
|
|
20
|
+
export interface VerifyWebhookSignatureOptions {
|
|
21
|
+
/**
|
|
22
|
+
* Maximum allowed clock skew (seconds) between the signature timestamp and
|
|
23
|
+
* now; deliveries outside the window are rejected to prevent replay.
|
|
24
|
+
* Set to 0 to disable. Default: 300.
|
|
25
|
+
*/
|
|
26
|
+
toleranceSeconds?: number;
|
|
27
|
+
/** Override the current unix timestamp (for tests). */
|
|
28
|
+
now?: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Verify the X-AgentAdmit-Signature header on an inbound alert webhook.
|
|
32
|
+
* Throws WebhookSignatureError on any failure; the message never includes
|
|
33
|
+
* the secret or the payload.
|
|
34
|
+
*/
|
|
35
|
+
export declare function verifyWebhookSignature(payload: string | Buffer, signatureHeader: string, secret: string, options?: VerifyWebhookSignatureOptions): void;
|
|
36
|
+
/** Boolean form of verifyWebhookSignature(). */
|
|
37
|
+
export declare function isValidWebhookSignature(payload: string | Buffer, signatureHeader: string, secret: string, options?: VerifyWebhookSignatureOptions): boolean;
|
package/dist/webhooks.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* agentadmit/webhooks.ts
|
|
4
|
+
* Verification for inbound AgentAdmit alert webhooks.
|
|
5
|
+
*
|
|
6
|
+
* AgentAdmit signs every alert webhook delivery with the app's webhook
|
|
7
|
+
* signing secret (`whsec_…`, returned once when the webhook URL is
|
|
8
|
+
* configured). The signature arrives in the `X-AgentAdmit-Signature` header:
|
|
9
|
+
*
|
|
10
|
+
* X-AgentAdmit-Signature: t=<unix_ts>,v1=<hex hmac-sha256>
|
|
11
|
+
*
|
|
12
|
+
* where the HMAC input is `${t}.${rawBody}` keyed with the full whsec_
|
|
13
|
+
* secret. Always verify against the raw request body, before JSON parsing
|
|
14
|
+
* (use `express.raw()` or capture the body with a verify hook).
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.WebhookSignatureError = exports.DEFAULT_TOLERANCE_SECONDS = exports.SIGNATURE_HEADER = void 0;
|
|
18
|
+
exports.verifyWebhookSignature = verifyWebhookSignature;
|
|
19
|
+
exports.isValidWebhookSignature = isValidWebhookSignature;
|
|
20
|
+
const crypto_1 = require("crypto");
|
|
21
|
+
exports.SIGNATURE_HEADER = 'X-AgentAdmit-Signature';
|
|
22
|
+
exports.DEFAULT_TOLERANCE_SECONDS = 300;
|
|
23
|
+
class WebhookSignatureError extends Error {
|
|
24
|
+
constructor(message = 'Webhook signature verification failed') {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = 'WebhookSignatureError';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
exports.WebhookSignatureError = WebhookSignatureError;
|
|
30
|
+
/**
|
|
31
|
+
* Verify the X-AgentAdmit-Signature header on an inbound alert webhook.
|
|
32
|
+
* Throws WebhookSignatureError on any failure; the message never includes
|
|
33
|
+
* the secret or the payload.
|
|
34
|
+
*/
|
|
35
|
+
function verifyWebhookSignature(payload, signatureHeader, secret, options = {}) {
|
|
36
|
+
const { toleranceSeconds = exports.DEFAULT_TOLERANCE_SECONDS, now } = options;
|
|
37
|
+
if (!secret)
|
|
38
|
+
throw new WebhookSignatureError('Webhook signing secret is required');
|
|
39
|
+
if (!signatureHeader)
|
|
40
|
+
throw new WebhookSignatureError('Missing X-AgentAdmit-Signature header');
|
|
41
|
+
const body = Buffer.isBuffer(payload) ? payload : Buffer.from(payload, 'utf-8');
|
|
42
|
+
let timestamp = null;
|
|
43
|
+
const candidates = [];
|
|
44
|
+
for (const part of signatureHeader.split(',')) {
|
|
45
|
+
const eq = part.indexOf('=');
|
|
46
|
+
if (eq === -1)
|
|
47
|
+
continue;
|
|
48
|
+
const key = part.slice(0, eq).trim();
|
|
49
|
+
const value = part.slice(eq + 1).trim();
|
|
50
|
+
if (key === 't') {
|
|
51
|
+
timestamp = /^\d+$/.test(value) ? parseInt(value, 10) : null;
|
|
52
|
+
if (timestamp === null)
|
|
53
|
+
throw new WebhookSignatureError('Malformed signature header');
|
|
54
|
+
}
|
|
55
|
+
else if (key === 'v1') {
|
|
56
|
+
candidates.push(value);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (timestamp === null || candidates.length === 0) {
|
|
60
|
+
throw new WebhookSignatureError('Malformed signature header');
|
|
61
|
+
}
|
|
62
|
+
if (toleranceSeconds) {
|
|
63
|
+
const current = now ?? Math.floor(Date.now() / 1000);
|
|
64
|
+
if (Math.abs(current - timestamp) > toleranceSeconds) {
|
|
65
|
+
throw new WebhookSignatureError('Signature timestamp outside tolerance window');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const expected = (0, crypto_1.createHmac)('sha256', secret)
|
|
69
|
+
.update(`${timestamp}.`)
|
|
70
|
+
.update(body)
|
|
71
|
+
.digest('hex');
|
|
72
|
+
const expectedBuf = Buffer.from(expected, 'utf-8');
|
|
73
|
+
const matched = candidates.some(candidate => {
|
|
74
|
+
const candidateBuf = Buffer.from(candidate, 'utf-8');
|
|
75
|
+
return candidateBuf.length === expectedBuf.length && (0, crypto_1.timingSafeEqual)(candidateBuf, expectedBuf);
|
|
76
|
+
});
|
|
77
|
+
if (!matched) {
|
|
78
|
+
throw new WebhookSignatureError('Signature verification failed');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** Boolean form of verifyWebhookSignature(). */
|
|
82
|
+
function isValidWebhookSignature(payload, signatureHeader, secret, options = {}) {
|
|
83
|
+
try {
|
|
84
|
+
verifyWebhookSignature(payload, signatureHeader, secret, options);
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
if (err instanceof WebhookSignatureError)
|
|
89
|
+
return false;
|
|
90
|
+
throw err;
|
|
91
|
+
}
|
|
92
|
+
}
|