@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 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.
@@ -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<any, Record<string, any>> | undefined>;
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<any, Record<string, any>>>;
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<any, Record<string, any>>>;
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. Without this check, we'd read empty scopes.
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 an authenticated request to the AgentAdmit hosted service.
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
- const { status, data } = await callHostedService(`/api/v1/apps/${config.app_id}/token`, {
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
- duration_hours: Math.max(1, Math.floor(duration / 3600)),
101
- label: label ?? null,
102
- user_role: role,
103
- metadata: { subscription_tier: userTier, app_name: config.app_name },
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: duration,
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 || data.connection_token,
123
- expires_in: duration,
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;
@@ -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
+ }