@agentix-security/express 0.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/dist/index.cjs ADDED
@@ -0,0 +1,414 @@
1
+ 'use strict';
2
+
3
+ var crypto = require('crypto');
4
+
5
+ // src/index.ts
6
+
7
+ // ../core/src/policy.ts
8
+ function isValidIntent(intent, validIntents) {
9
+ if (!intent || typeof intent !== "string") return false;
10
+ if (validIntents) return validIntents.includes(intent);
11
+ return /^[a-z][a-z0-9_]{1,63}$/.test(intent);
12
+ }
13
+ function licenseActive(lease, now = Date.now()) {
14
+ if (!lease) return false;
15
+ return Date.parse(lease.expires_at) > now;
16
+ }
17
+ function decision(partial) {
18
+ return {
19
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
20
+ ...partial
21
+ };
22
+ }
23
+ function deriveTokenSecret(licenseKey) {
24
+ return crypto.createHmac("sha256", licenseKey).update("agentix-v1-token-secret").digest("hex");
25
+ }
26
+ function b64url(value) {
27
+ return Buffer.from(value).toString("base64url");
28
+ }
29
+ function sign(secret, data) {
30
+ return crypto.createHmac("sha256", secret).update(data).digest("base64url");
31
+ }
32
+ function issueToken(secret, payload) {
33
+ const header = b64url(JSON.stringify({ alg: "HS256", typ: "AGX" }));
34
+ const body = b64url(JSON.stringify({ jti: `tok_${crypto.randomUUID()}`, ...payload }));
35
+ const sig = sign(secret, `${header}.${body}`);
36
+ return `${header}.${body}.${sig}`;
37
+ }
38
+ function tokenId(raw) {
39
+ const parts = raw.split(".");
40
+ if (parts.length !== 3) return null;
41
+ try {
42
+ const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
43
+ return payload.jti;
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ // ../sdk/src/rate-limit.ts
50
+ var RateLimiter = class {
51
+ constructor(limit, windowMs) {
52
+ this.limit = limit;
53
+ this.windowMs = windowMs;
54
+ }
55
+ limit;
56
+ windowMs;
57
+ buckets = /* @__PURE__ */ new Map();
58
+ consume(key) {
59
+ const now = Date.now();
60
+ const bucket = this.buckets.get(key);
61
+ if (!bucket || now - bucket.windowStart > this.windowMs) {
62
+ this.buckets.set(key, { windowStart: now, count: 1 });
63
+ return true;
64
+ }
65
+ bucket.count += 1;
66
+ this.buckets.set(key, bucket);
67
+ return bucket.count <= this.limit;
68
+ }
69
+ };
70
+
71
+ // ../sdk/src/engine.ts
72
+ function fingerprint(ip, userAgent, acceptLanguage) {
73
+ return crypto.createHash("sha256").update([ip, userAgent, acceptLanguage].join("|")).digest("hex");
74
+ }
75
+ async function shipAudit(controlPlaneUrl, adminKey, row) {
76
+ if (!adminKey) return;
77
+ try {
78
+ await fetch(`${controlPlaneUrl.replace(/\/$/, "")}/v1/audit/decisions`, {
79
+ method: "POST",
80
+ headers: { authorization: `Bearer ${adminKey}`, "content-type": "application/json" },
81
+ body: JSON.stringify(row)
82
+ });
83
+ } catch {
84
+ }
85
+ }
86
+ var Engine = class {
87
+ constructor(opts) {
88
+ this.opts = opts;
89
+ this.declareLimiter = new RateLimiter(opts.declareLimit, opts.declareWindowMs);
90
+ }
91
+ opts;
92
+ declareLimiter;
93
+ async handle(req) {
94
+ const { method, path } = req;
95
+ if (method === "GET" && path === "/.well-known/ai-agent.json") {
96
+ return this.handleDiscovery(req);
97
+ }
98
+ if (method === "POST" && path === "/agent/v1/declare_intent") {
99
+ return this.handleDeclareIntent(req);
100
+ }
101
+ return { type: "passthrough" };
102
+ }
103
+ handleDiscovery(req) {
104
+ const lease = this.opts.getLease();
105
+ const allIntents = this.opts.getRegisteredIntents ? [...this.opts.getRegisteredIntents().keys()] : [];
106
+ const body = {
107
+ service: "agentix-intent-sdk",
108
+ version: "0.2.0",
109
+ tenant_id: this.opts.tenantId,
110
+ deployment_id: this.opts.deploymentId,
111
+ discovery: {
112
+ well_known: `${req.baseUrl}/.well-known/ai-agent.json`,
113
+ token_endpoint: `${req.baseUrl}/agent/v1/declare_intent`
114
+ },
115
+ license: {
116
+ active: licenseActive(lease),
117
+ expires_at: lease?.expires_at ?? null,
118
+ policy_pack_version: lease?.policy_pack_version ?? null
119
+ },
120
+ intents: allIntents
121
+ };
122
+ void this.audit(req, "/.well-known/ai-agent.json", 200, {
123
+ trustMode: "unknown",
124
+ intentScope: "none",
125
+ tokenId: null,
126
+ decisionValue: "allow",
127
+ decisionReason: "agent_discovery_served",
128
+ policyId: "agent-discovery"
129
+ });
130
+ return {
131
+ type: "response",
132
+ status: 200,
133
+ headers: { "cache-control": "no-store", "content-type": "application/json" },
134
+ body
135
+ };
136
+ }
137
+ async handleDeclareIntent(req) {
138
+ const lease = this.opts.getLease();
139
+ const leaseOk = licenseActive(lease) || this.opts.devMode === true;
140
+ if (!leaseOk) {
141
+ void this.audit(req, "/agent/v1/declare_intent", 403, {
142
+ trustMode: "unmanaged_automation",
143
+ intentScope: "none",
144
+ tokenId: null,
145
+ decisionValue: "deny",
146
+ decisionReason: "license_expired_agent_endpoint_disabled",
147
+ policyId: "license-lease"
148
+ });
149
+ return { type: "response", status: 403, body: { error: "license_not_active" } };
150
+ }
151
+ const fp = this.fingerprint(req);
152
+ if (!this.declareLimiter.consume(fp)) {
153
+ void this.audit(req, "/agent/v1/declare_intent", 429, {
154
+ trustMode: "unmanaged_automation",
155
+ intentScope: "none",
156
+ tokenId: null,
157
+ decisionValue: "throttle",
158
+ decisionReason: "declare_intent_rate_limited",
159
+ policyId: "declare-intent-rate-limit"
160
+ });
161
+ return { type: "response", status: 429, body: { error: "declare_intent_rate_limited" } };
162
+ }
163
+ const registeredIntents = this.opts.getRegisteredIntents ? [...this.opts.getRegisteredIntents().keys()] : [];
164
+ const body = req.body ?? {};
165
+ if (!body.intent || !isValidIntent(body.intent, registeredIntents)) {
166
+ void this.audit(req, "/agent/v1/declare_intent", 400, {
167
+ trustMode: "unmanaged_automation",
168
+ intentScope: "none",
169
+ tokenId: null,
170
+ decisionValue: "deny",
171
+ decisionReason: "invalid_intent",
172
+ policyId: "intent-validation",
173
+ metadata: { supplied_intent: body.intent ?? null }
174
+ });
175
+ return { type: "response", status: 400, body: { error: "invalid_intent" } };
176
+ }
177
+ const intent = body.intent;
178
+ const ttl = intent === "checkout_intent" ? this.opts.checkoutTokenTtlMs : this.opts.tokenTtlMs;
179
+ const iat = Math.floor(Date.now() / 1e3);
180
+ const exp = iat + Math.floor(ttl / 1e3);
181
+ const binding = this.fingerprint(req);
182
+ const raw = issueToken(this.opts.tokenSecret, { intent, domain: this.opts.domain, binding, iat, exp });
183
+ const jti = tokenId(raw);
184
+ void this.audit(req, "/agent/v1/declare_intent", 200, {
185
+ trustMode: "managed_agent",
186
+ intentScope: intent,
187
+ tokenId: jti,
188
+ decisionValue: "allow",
189
+ decisionReason: "intent_token_issued",
190
+ policyId: "intent-token-issuer",
191
+ metadata: { subject: body.subject ?? null, constraints: body.constraints ?? null }
192
+ });
193
+ return {
194
+ type: "response",
195
+ status: 200,
196
+ body: { access_token: raw, token_type: "Bearer", token_id: jti, intent, expires_in: exp - iat }
197
+ };
198
+ }
199
+ fingerprint(req) {
200
+ return fingerprint(
201
+ req.ip,
202
+ req.headers["user-agent"] ?? req.headers["User-Agent"] ?? "unknown",
203
+ req.headers["accept-language"] ?? req.headers["Accept-Language"] ?? ""
204
+ );
205
+ }
206
+ async audit(req, route, statusCode, input) {
207
+ const row = decision({
208
+ tenant_id: this.opts.tenantId,
209
+ deployment_id: this.opts.deploymentId,
210
+ domain: this.opts.domain,
211
+ route,
212
+ method: req.method,
213
+ client_fingerprint_hash: this.fingerprint(req),
214
+ trust_mode: input.trustMode,
215
+ intent_scope: input.intentScope,
216
+ token_id: input.tokenId,
217
+ decision: input.decisionValue,
218
+ decision_reason: input.decisionReason,
219
+ policy_id: input.policyId,
220
+ status_code: statusCode,
221
+ metadata: input.metadata ?? {}
222
+ });
223
+ void shipAudit(this.opts.controlPlaneUrl, this.opts.controlPlaneAdminKey, row);
224
+ }
225
+ };
226
+
227
+ // ../sdk/src/index.ts
228
+ var DEFAULT_CONTROL_PLANE = "https://agentix-control-plane.onrender.com";
229
+ var AgentixSDK = class {
230
+ constructor(config) {
231
+ this.config = config;
232
+ }
233
+ config;
234
+ lease = null;
235
+ engine = null;
236
+ initPromise = null;
237
+ intentRegistry = /* @__PURE__ */ new Map();
238
+ // Resolved at init from the server:
239
+ _tenantId = "";
240
+ _domain = "";
241
+ _tokenSecret = "";
242
+ _deploymentId = "";
243
+ _proxyUrl = "";
244
+ async ensureInitialized() {
245
+ if (!this.initPromise) this.initPromise = this._init();
246
+ return this.initPromise;
247
+ }
248
+ async initialize() {
249
+ return this.ensureInitialized();
250
+ }
251
+ async _init() {
252
+ const cp = (this.config.controlPlaneUrl ?? DEFAULT_CONTROL_PLANE).replace(/\/$/, "");
253
+ try {
254
+ const res = await fetch(`${cp}/v1/deployments/register`, {
255
+ method: "POST",
256
+ headers: { "content-type": "application/json" },
257
+ body: JSON.stringify({
258
+ license_key: this.config.licenseKey,
259
+ deployment_id: this.config.deploymentId
260
+ })
261
+ });
262
+ if (res.ok) {
263
+ const body = await res.json();
264
+ this._deploymentId = body.deployment_id;
265
+ this._tenantId = body.tenant_id;
266
+ this._domain = body.domain;
267
+ this._tokenSecret = body.token_secret;
268
+ this._proxyUrl = body.proxy_url;
269
+ } else {
270
+ console.warn(`[agentix] Registration failed (${res.status}) \u2014 operating in degraded mode`);
271
+ this._tokenSecret = deriveTokenSecret(this.config.licenseKey);
272
+ this._domain = "localhost";
273
+ this._deploymentId = this.config.deploymentId ?? "unknown";
274
+ this._proxyUrl = cp;
275
+ }
276
+ } catch (err) {
277
+ console.warn("[agentix] Registration error (control plane unreachable) \u2014 operating in degraded mode:", err.message);
278
+ this._tokenSecret = deriveTokenSecret(this.config.licenseKey);
279
+ this._domain = "localhost";
280
+ this._deploymentId = this.config.deploymentId ?? "unknown";
281
+ this._proxyUrl = cp;
282
+ }
283
+ this.engine = new Engine({
284
+ tenantId: this._tenantId,
285
+ deploymentId: this._deploymentId,
286
+ domain: this._domain,
287
+ tokenSecret: this._tokenSecret,
288
+ tokenTtlMs: this.config.tokenTtlMs ?? 15 * 60 * 1e3,
289
+ checkoutTokenTtlMs: this.config.checkoutTokenTtlMs ?? 5 * 60 * 1e3,
290
+ declareLimit: this.config.declareLimit ?? 8,
291
+ declareWindowMs: this.config.declareWindowMs ?? 6e4,
292
+ controlPlaneUrl: cp,
293
+ controlPlaneAdminKey: this.config.controlPlaneAdminKey,
294
+ getLease: () => this.lease,
295
+ getRegisteredIntents: () => this.intentRegistry,
296
+ devMode: this.config.devMode
297
+ });
298
+ }
299
+ getEngine() {
300
+ if (!this.engine) throw new Error("[agentix] SDK not initialized \u2014 await sdk.initialize() first");
301
+ return this.engine;
302
+ }
303
+ registerIntent(intent, mode = "enforce") {
304
+ this.intentRegistry.set(intent, mode);
305
+ }
306
+ getIntentRegistry() {
307
+ return this.intentRegistry;
308
+ }
309
+ getResolvedDomain() {
310
+ return this._domain;
311
+ }
312
+ getResolvedTenantId() {
313
+ return this._tenantId;
314
+ }
315
+ getResolvedTokenSecret() {
316
+ return this._tokenSecret;
317
+ }
318
+ getDeploymentId() {
319
+ return this._deploymentId;
320
+ }
321
+ getProxyUrl() {
322
+ return this._proxyUrl;
323
+ }
324
+ getLease() {
325
+ return this.lease;
326
+ }
327
+ };
328
+
329
+ // src/index.ts
330
+ function normalize(req) {
331
+ const proto = req.get("x-forwarded-proto") ?? req.protocol ?? "http";
332
+ const host = req.get("host") ?? req.hostname ?? "localhost";
333
+ const ip = req.get("x-forwarded-for")?.split(",")[0]?.trim() ?? req.ip ?? req.socket?.remoteAddress ?? "unknown";
334
+ const headers = {};
335
+ for (const [key, value] of Object.entries(req.headers)) {
336
+ if (typeof value === "string") headers[key] = value;
337
+ else if (Array.isArray(value)) headers[key] = value[0] ?? "";
338
+ }
339
+ return { method: req.method, path: req.path, headers, ip, body: req.body, baseUrl: `${proto}://${host}` };
340
+ }
341
+ function bearerToken(req) {
342
+ const auth = req.get("authorization");
343
+ if (!auth?.startsWith("Bearer ")) return null;
344
+ return auth.slice("Bearer ".length).trim();
345
+ }
346
+ function clientFingerprint(req) {
347
+ const ip = req.get("x-forwarded-for")?.split(",")[0]?.trim() ?? req.ip ?? req.socket?.remoteAddress ?? "unknown";
348
+ const ua = req.get("user-agent") ?? "unknown";
349
+ const lang = req.get("accept-language") ?? "";
350
+ return crypto.createHash("sha256").update([ip, ua, lang].join("|")).digest("hex");
351
+ }
352
+ async function callProxy(proxyUrl, deploymentId, token, fingerprint2, intent, mode) {
353
+ const controller = new AbortController();
354
+ const id = setTimeout(() => controller.abort(), 1500);
355
+ try {
356
+ const res = await fetch(`${proxyUrl}/v1/enforce`, {
357
+ method: "POST",
358
+ headers: { "content-type": "application/json" },
359
+ body: JSON.stringify({ deployment_id: deploymentId, token, fingerprint: fingerprint2, intent, mode }),
360
+ signal: controller.signal
361
+ });
362
+ clearTimeout(id);
363
+ if (res.ok) return await res.json();
364
+ return { decision: "allow", reason: "proxy_error" };
365
+ } catch {
366
+ clearTimeout(id);
367
+ return { decision: "allow", reason: "proxy_unreachable" };
368
+ }
369
+ }
370
+ function agentixMiddleware(sdk) {
371
+ return async (req, res, next) => {
372
+ await sdk.ensureInitialized();
373
+ const result = await sdk.getEngine().handle(normalize(req));
374
+ if (result.type === "passthrough") {
375
+ next();
376
+ return;
377
+ }
378
+ if (result.headers) for (const [k, v] of Object.entries(result.headers)) res.setHeader(k, v);
379
+ res.status(result.status).json(result.body);
380
+ };
381
+ }
382
+ function secure(sdk, intent, handler, opts = {}) {
383
+ const mode = opts.mode ?? "enforce";
384
+ sdk.registerIntent(intent, mode);
385
+ return async (req, res, next) => {
386
+ await sdk.ensureInitialized();
387
+ const token = bearerToken(req);
388
+ const fp = clientFingerprint(req);
389
+ const baseUrl = `${req.get("x-forwarded-proto") ?? req.protocol ?? "http"}://${req.get("host") ?? req.hostname ?? "localhost"}`;
390
+ if (!token) {
391
+ res.setHeader("www-authenticate", 'Bearer realm="agentix", error="missing_token"');
392
+ res.status(401).json({ error: "missing_token", agent_discovery: `${baseUrl}/.well-known/ai-agent.json` });
393
+ return;
394
+ }
395
+ const verdict = await callProxy(sdk.getProxyUrl(), sdk.getDeploymentId(), token, fp, intent, mode);
396
+ if (verdict.decision === "allow") {
397
+ handler(req, res, next);
398
+ return;
399
+ }
400
+ const reason = verdict.reason ?? "denied";
401
+ if (reason === "missing_token" || reason === "invalid_token") {
402
+ res.setHeader("www-authenticate", `Bearer realm="agentix", error="${reason}"`);
403
+ res.status(401).json({ error: reason, agent_discovery: `${baseUrl}/.well-known/ai-agent.json` });
404
+ } else if (reason === "out_of_scope") {
405
+ res.status(403).json({ error: "out_of_scope", required_intent: verdict.required_intent ?? intent });
406
+ } else {
407
+ res.status(401).json({ error: reason });
408
+ }
409
+ };
410
+ }
411
+
412
+ exports.AgentixSDK = AgentixSDK;
413
+ exports.agentixMiddleware = agentixMiddleware;
414
+ exports.secure = secure;
@@ -0,0 +1,112 @@
1
+ import { RequestHandler } from 'express';
2
+
3
+ interface LicenseLease {
4
+ tenant_id: string;
5
+ deployment_id: string;
6
+ customer_id: string;
7
+ domains: string[];
8
+ features: string[];
9
+ policy_pack_version: string;
10
+ expires_at: string;
11
+ issued_at: string;
12
+ watermark: string;
13
+ signature: string;
14
+ }
15
+
16
+ interface AgentixRequest {
17
+ method: string;
18
+ path: string;
19
+ headers: Record<string, string>;
20
+ ip: string;
21
+ body: unknown;
22
+ baseUrl: string;
23
+ }
24
+ type AgentixResult = {
25
+ type: 'passthrough';
26
+ } | {
27
+ type: 'response';
28
+ status: number;
29
+ headers?: Record<string, string>;
30
+ body: unknown;
31
+ };
32
+ interface EngineOptions {
33
+ tenantId: string;
34
+ deploymentId: string;
35
+ domain: string;
36
+ tokenSecret: string;
37
+ tokenTtlMs: number;
38
+ checkoutTokenTtlMs: number;
39
+ declareLimit: number;
40
+ declareWindowMs: number;
41
+ controlPlaneUrl: string;
42
+ controlPlaneAdminKey?: string;
43
+ getLease: () => LicenseLease | null;
44
+ getRegisteredIntents?: () => ReadonlyMap<string, 'enforce' | 'shadow'>;
45
+ devMode?: boolean;
46
+ }
47
+ declare class Engine {
48
+ private readonly opts;
49
+ private readonly declareLimiter;
50
+ constructor(opts: EngineOptions);
51
+ handle(req: AgentixRequest): Promise<AgentixResult>;
52
+ private handleDiscovery;
53
+ private handleDeclareIntent;
54
+ private fingerprint;
55
+ private audit;
56
+ }
57
+
58
+ interface AgentixSDKConfig {
59
+ licenseKey: string;
60
+ /** Defaults to https://agentix-control-plane.onrender.com */
61
+ controlPlaneUrl?: string;
62
+ deploymentId?: string;
63
+ /** For local dev only — bypasses license enforcement. */
64
+ devMode?: boolean;
65
+ tokenTtlMs?: number;
66
+ checkoutTokenTtlMs?: number;
67
+ declareLimit?: number;
68
+ declareWindowMs?: number;
69
+ controlPlaneAdminKey?: string;
70
+ }
71
+
72
+ declare class AgentixSDK {
73
+ readonly config: AgentixSDKConfig;
74
+ private lease;
75
+ private engine;
76
+ private initPromise;
77
+ private readonly intentRegistry;
78
+ private _tenantId;
79
+ private _domain;
80
+ private _tokenSecret;
81
+ private _deploymentId;
82
+ private _proxyUrl;
83
+ constructor(config: AgentixSDKConfig);
84
+ ensureInitialized(): Promise<void>;
85
+ initialize(): Promise<void>;
86
+ private _init;
87
+ getEngine(): Engine;
88
+ registerIntent(intent: string, mode?: 'enforce' | 'shadow'): void;
89
+ getIntentRegistry(): ReadonlyMap<string, 'enforce' | 'shadow'>;
90
+ getResolvedDomain(): string;
91
+ getResolvedTenantId(): string;
92
+ getResolvedTokenSecret(): string;
93
+ getDeploymentId(): string;
94
+ getProxyUrl(): string;
95
+ getLease(): LicenseLease | null;
96
+ }
97
+
98
+ /** Global middleware — handles discovery, token issuance, and route-config enforcement. */
99
+ declare function agentixMiddleware(sdk: AgentixSDK): RequestHandler;
100
+ /**
101
+ * Per-route annotation — wrap a specific handler with intent enforcement via the Agentix proxy.
102
+ * Fail-open: if the proxy is unreachable the request passes through (reverts to pre-SDK state).
103
+ *
104
+ * @example
105
+ * app.get('/api/jobs', secure(sdk, 'browse_jobs', handler))
106
+ * app.post('/api/apply', secure(sdk, 'submit_application', handler, { mode: 'enforce' }))
107
+ */
108
+ declare function secure(sdk: AgentixSDK, intent: string, handler: RequestHandler, opts?: {
109
+ mode?: 'enforce' | 'shadow';
110
+ }): RequestHandler;
111
+
112
+ export { AgentixSDK, type AgentixSDKConfig, agentixMiddleware, secure };
@@ -0,0 +1,112 @@
1
+ import { RequestHandler } from 'express';
2
+
3
+ interface LicenseLease {
4
+ tenant_id: string;
5
+ deployment_id: string;
6
+ customer_id: string;
7
+ domains: string[];
8
+ features: string[];
9
+ policy_pack_version: string;
10
+ expires_at: string;
11
+ issued_at: string;
12
+ watermark: string;
13
+ signature: string;
14
+ }
15
+
16
+ interface AgentixRequest {
17
+ method: string;
18
+ path: string;
19
+ headers: Record<string, string>;
20
+ ip: string;
21
+ body: unknown;
22
+ baseUrl: string;
23
+ }
24
+ type AgentixResult = {
25
+ type: 'passthrough';
26
+ } | {
27
+ type: 'response';
28
+ status: number;
29
+ headers?: Record<string, string>;
30
+ body: unknown;
31
+ };
32
+ interface EngineOptions {
33
+ tenantId: string;
34
+ deploymentId: string;
35
+ domain: string;
36
+ tokenSecret: string;
37
+ tokenTtlMs: number;
38
+ checkoutTokenTtlMs: number;
39
+ declareLimit: number;
40
+ declareWindowMs: number;
41
+ controlPlaneUrl: string;
42
+ controlPlaneAdminKey?: string;
43
+ getLease: () => LicenseLease | null;
44
+ getRegisteredIntents?: () => ReadonlyMap<string, 'enforce' | 'shadow'>;
45
+ devMode?: boolean;
46
+ }
47
+ declare class Engine {
48
+ private readonly opts;
49
+ private readonly declareLimiter;
50
+ constructor(opts: EngineOptions);
51
+ handle(req: AgentixRequest): Promise<AgentixResult>;
52
+ private handleDiscovery;
53
+ private handleDeclareIntent;
54
+ private fingerprint;
55
+ private audit;
56
+ }
57
+
58
+ interface AgentixSDKConfig {
59
+ licenseKey: string;
60
+ /** Defaults to https://agentix-control-plane.onrender.com */
61
+ controlPlaneUrl?: string;
62
+ deploymentId?: string;
63
+ /** For local dev only — bypasses license enforcement. */
64
+ devMode?: boolean;
65
+ tokenTtlMs?: number;
66
+ checkoutTokenTtlMs?: number;
67
+ declareLimit?: number;
68
+ declareWindowMs?: number;
69
+ controlPlaneAdminKey?: string;
70
+ }
71
+
72
+ declare class AgentixSDK {
73
+ readonly config: AgentixSDKConfig;
74
+ private lease;
75
+ private engine;
76
+ private initPromise;
77
+ private readonly intentRegistry;
78
+ private _tenantId;
79
+ private _domain;
80
+ private _tokenSecret;
81
+ private _deploymentId;
82
+ private _proxyUrl;
83
+ constructor(config: AgentixSDKConfig);
84
+ ensureInitialized(): Promise<void>;
85
+ initialize(): Promise<void>;
86
+ private _init;
87
+ getEngine(): Engine;
88
+ registerIntent(intent: string, mode?: 'enforce' | 'shadow'): void;
89
+ getIntentRegistry(): ReadonlyMap<string, 'enforce' | 'shadow'>;
90
+ getResolvedDomain(): string;
91
+ getResolvedTenantId(): string;
92
+ getResolvedTokenSecret(): string;
93
+ getDeploymentId(): string;
94
+ getProxyUrl(): string;
95
+ getLease(): LicenseLease | null;
96
+ }
97
+
98
+ /** Global middleware — handles discovery, token issuance, and route-config enforcement. */
99
+ declare function agentixMiddleware(sdk: AgentixSDK): RequestHandler;
100
+ /**
101
+ * Per-route annotation — wrap a specific handler with intent enforcement via the Agentix proxy.
102
+ * Fail-open: if the proxy is unreachable the request passes through (reverts to pre-SDK state).
103
+ *
104
+ * @example
105
+ * app.get('/api/jobs', secure(sdk, 'browse_jobs', handler))
106
+ * app.post('/api/apply', secure(sdk, 'submit_application', handler, { mode: 'enforce' }))
107
+ */
108
+ declare function secure(sdk: AgentixSDK, intent: string, handler: RequestHandler, opts?: {
109
+ mode?: 'enforce' | 'shadow';
110
+ }): RequestHandler;
111
+
112
+ export { AgentixSDK, type AgentixSDKConfig, agentixMiddleware, secure };
package/dist/index.js ADDED
@@ -0,0 +1,410 @@
1
+ import { createHmac, createHash, randomUUID } from 'crypto';
2
+
3
+ // src/index.ts
4
+
5
+ // ../core/src/policy.ts
6
+ function isValidIntent(intent, validIntents) {
7
+ if (!intent || typeof intent !== "string") return false;
8
+ if (validIntents) return validIntents.includes(intent);
9
+ return /^[a-z][a-z0-9_]{1,63}$/.test(intent);
10
+ }
11
+ function licenseActive(lease, now = Date.now()) {
12
+ if (!lease) return false;
13
+ return Date.parse(lease.expires_at) > now;
14
+ }
15
+ function decision(partial) {
16
+ return {
17
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
18
+ ...partial
19
+ };
20
+ }
21
+ function deriveTokenSecret(licenseKey) {
22
+ return createHmac("sha256", licenseKey).update("agentix-v1-token-secret").digest("hex");
23
+ }
24
+ function b64url(value) {
25
+ return Buffer.from(value).toString("base64url");
26
+ }
27
+ function sign(secret, data) {
28
+ return createHmac("sha256", secret).update(data).digest("base64url");
29
+ }
30
+ function issueToken(secret, payload) {
31
+ const header = b64url(JSON.stringify({ alg: "HS256", typ: "AGX" }));
32
+ const body = b64url(JSON.stringify({ jti: `tok_${randomUUID()}`, ...payload }));
33
+ const sig = sign(secret, `${header}.${body}`);
34
+ return `${header}.${body}.${sig}`;
35
+ }
36
+ function tokenId(raw) {
37
+ const parts = raw.split(".");
38
+ if (parts.length !== 3) return null;
39
+ try {
40
+ const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
41
+ return payload.jti;
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+
47
+ // ../sdk/src/rate-limit.ts
48
+ var RateLimiter = class {
49
+ constructor(limit, windowMs) {
50
+ this.limit = limit;
51
+ this.windowMs = windowMs;
52
+ }
53
+ limit;
54
+ windowMs;
55
+ buckets = /* @__PURE__ */ new Map();
56
+ consume(key) {
57
+ const now = Date.now();
58
+ const bucket = this.buckets.get(key);
59
+ if (!bucket || now - bucket.windowStart > this.windowMs) {
60
+ this.buckets.set(key, { windowStart: now, count: 1 });
61
+ return true;
62
+ }
63
+ bucket.count += 1;
64
+ this.buckets.set(key, bucket);
65
+ return bucket.count <= this.limit;
66
+ }
67
+ };
68
+
69
+ // ../sdk/src/engine.ts
70
+ function fingerprint(ip, userAgent, acceptLanguage) {
71
+ return createHash("sha256").update([ip, userAgent, acceptLanguage].join("|")).digest("hex");
72
+ }
73
+ async function shipAudit(controlPlaneUrl, adminKey, row) {
74
+ if (!adminKey) return;
75
+ try {
76
+ await fetch(`${controlPlaneUrl.replace(/\/$/, "")}/v1/audit/decisions`, {
77
+ method: "POST",
78
+ headers: { authorization: `Bearer ${adminKey}`, "content-type": "application/json" },
79
+ body: JSON.stringify(row)
80
+ });
81
+ } catch {
82
+ }
83
+ }
84
+ var Engine = class {
85
+ constructor(opts) {
86
+ this.opts = opts;
87
+ this.declareLimiter = new RateLimiter(opts.declareLimit, opts.declareWindowMs);
88
+ }
89
+ opts;
90
+ declareLimiter;
91
+ async handle(req) {
92
+ const { method, path } = req;
93
+ if (method === "GET" && path === "/.well-known/ai-agent.json") {
94
+ return this.handleDiscovery(req);
95
+ }
96
+ if (method === "POST" && path === "/agent/v1/declare_intent") {
97
+ return this.handleDeclareIntent(req);
98
+ }
99
+ return { type: "passthrough" };
100
+ }
101
+ handleDiscovery(req) {
102
+ const lease = this.opts.getLease();
103
+ const allIntents = this.opts.getRegisteredIntents ? [...this.opts.getRegisteredIntents().keys()] : [];
104
+ const body = {
105
+ service: "agentix-intent-sdk",
106
+ version: "0.2.0",
107
+ tenant_id: this.opts.tenantId,
108
+ deployment_id: this.opts.deploymentId,
109
+ discovery: {
110
+ well_known: `${req.baseUrl}/.well-known/ai-agent.json`,
111
+ token_endpoint: `${req.baseUrl}/agent/v1/declare_intent`
112
+ },
113
+ license: {
114
+ active: licenseActive(lease),
115
+ expires_at: lease?.expires_at ?? null,
116
+ policy_pack_version: lease?.policy_pack_version ?? null
117
+ },
118
+ intents: allIntents
119
+ };
120
+ void this.audit(req, "/.well-known/ai-agent.json", 200, {
121
+ trustMode: "unknown",
122
+ intentScope: "none",
123
+ tokenId: null,
124
+ decisionValue: "allow",
125
+ decisionReason: "agent_discovery_served",
126
+ policyId: "agent-discovery"
127
+ });
128
+ return {
129
+ type: "response",
130
+ status: 200,
131
+ headers: { "cache-control": "no-store", "content-type": "application/json" },
132
+ body
133
+ };
134
+ }
135
+ async handleDeclareIntent(req) {
136
+ const lease = this.opts.getLease();
137
+ const leaseOk = licenseActive(lease) || this.opts.devMode === true;
138
+ if (!leaseOk) {
139
+ void this.audit(req, "/agent/v1/declare_intent", 403, {
140
+ trustMode: "unmanaged_automation",
141
+ intentScope: "none",
142
+ tokenId: null,
143
+ decisionValue: "deny",
144
+ decisionReason: "license_expired_agent_endpoint_disabled",
145
+ policyId: "license-lease"
146
+ });
147
+ return { type: "response", status: 403, body: { error: "license_not_active" } };
148
+ }
149
+ const fp = this.fingerprint(req);
150
+ if (!this.declareLimiter.consume(fp)) {
151
+ void this.audit(req, "/agent/v1/declare_intent", 429, {
152
+ trustMode: "unmanaged_automation",
153
+ intentScope: "none",
154
+ tokenId: null,
155
+ decisionValue: "throttle",
156
+ decisionReason: "declare_intent_rate_limited",
157
+ policyId: "declare-intent-rate-limit"
158
+ });
159
+ return { type: "response", status: 429, body: { error: "declare_intent_rate_limited" } };
160
+ }
161
+ const registeredIntents = this.opts.getRegisteredIntents ? [...this.opts.getRegisteredIntents().keys()] : [];
162
+ const body = req.body ?? {};
163
+ if (!body.intent || !isValidIntent(body.intent, registeredIntents)) {
164
+ void this.audit(req, "/agent/v1/declare_intent", 400, {
165
+ trustMode: "unmanaged_automation",
166
+ intentScope: "none",
167
+ tokenId: null,
168
+ decisionValue: "deny",
169
+ decisionReason: "invalid_intent",
170
+ policyId: "intent-validation",
171
+ metadata: { supplied_intent: body.intent ?? null }
172
+ });
173
+ return { type: "response", status: 400, body: { error: "invalid_intent" } };
174
+ }
175
+ const intent = body.intent;
176
+ const ttl = intent === "checkout_intent" ? this.opts.checkoutTokenTtlMs : this.opts.tokenTtlMs;
177
+ const iat = Math.floor(Date.now() / 1e3);
178
+ const exp = iat + Math.floor(ttl / 1e3);
179
+ const binding = this.fingerprint(req);
180
+ const raw = issueToken(this.opts.tokenSecret, { intent, domain: this.opts.domain, binding, iat, exp });
181
+ const jti = tokenId(raw);
182
+ void this.audit(req, "/agent/v1/declare_intent", 200, {
183
+ trustMode: "managed_agent",
184
+ intentScope: intent,
185
+ tokenId: jti,
186
+ decisionValue: "allow",
187
+ decisionReason: "intent_token_issued",
188
+ policyId: "intent-token-issuer",
189
+ metadata: { subject: body.subject ?? null, constraints: body.constraints ?? null }
190
+ });
191
+ return {
192
+ type: "response",
193
+ status: 200,
194
+ body: { access_token: raw, token_type: "Bearer", token_id: jti, intent, expires_in: exp - iat }
195
+ };
196
+ }
197
+ fingerprint(req) {
198
+ return fingerprint(
199
+ req.ip,
200
+ req.headers["user-agent"] ?? req.headers["User-Agent"] ?? "unknown",
201
+ req.headers["accept-language"] ?? req.headers["Accept-Language"] ?? ""
202
+ );
203
+ }
204
+ async audit(req, route, statusCode, input) {
205
+ const row = decision({
206
+ tenant_id: this.opts.tenantId,
207
+ deployment_id: this.opts.deploymentId,
208
+ domain: this.opts.domain,
209
+ route,
210
+ method: req.method,
211
+ client_fingerprint_hash: this.fingerprint(req),
212
+ trust_mode: input.trustMode,
213
+ intent_scope: input.intentScope,
214
+ token_id: input.tokenId,
215
+ decision: input.decisionValue,
216
+ decision_reason: input.decisionReason,
217
+ policy_id: input.policyId,
218
+ status_code: statusCode,
219
+ metadata: input.metadata ?? {}
220
+ });
221
+ void shipAudit(this.opts.controlPlaneUrl, this.opts.controlPlaneAdminKey, row);
222
+ }
223
+ };
224
+
225
+ // ../sdk/src/index.ts
226
+ var DEFAULT_CONTROL_PLANE = "https://agentix-control-plane.onrender.com";
227
+ var AgentixSDK = class {
228
+ constructor(config) {
229
+ this.config = config;
230
+ }
231
+ config;
232
+ lease = null;
233
+ engine = null;
234
+ initPromise = null;
235
+ intentRegistry = /* @__PURE__ */ new Map();
236
+ // Resolved at init from the server:
237
+ _tenantId = "";
238
+ _domain = "";
239
+ _tokenSecret = "";
240
+ _deploymentId = "";
241
+ _proxyUrl = "";
242
+ async ensureInitialized() {
243
+ if (!this.initPromise) this.initPromise = this._init();
244
+ return this.initPromise;
245
+ }
246
+ async initialize() {
247
+ return this.ensureInitialized();
248
+ }
249
+ async _init() {
250
+ const cp = (this.config.controlPlaneUrl ?? DEFAULT_CONTROL_PLANE).replace(/\/$/, "");
251
+ try {
252
+ const res = await fetch(`${cp}/v1/deployments/register`, {
253
+ method: "POST",
254
+ headers: { "content-type": "application/json" },
255
+ body: JSON.stringify({
256
+ license_key: this.config.licenseKey,
257
+ deployment_id: this.config.deploymentId
258
+ })
259
+ });
260
+ if (res.ok) {
261
+ const body = await res.json();
262
+ this._deploymentId = body.deployment_id;
263
+ this._tenantId = body.tenant_id;
264
+ this._domain = body.domain;
265
+ this._tokenSecret = body.token_secret;
266
+ this._proxyUrl = body.proxy_url;
267
+ } else {
268
+ console.warn(`[agentix] Registration failed (${res.status}) \u2014 operating in degraded mode`);
269
+ this._tokenSecret = deriveTokenSecret(this.config.licenseKey);
270
+ this._domain = "localhost";
271
+ this._deploymentId = this.config.deploymentId ?? "unknown";
272
+ this._proxyUrl = cp;
273
+ }
274
+ } catch (err) {
275
+ console.warn("[agentix] Registration error (control plane unreachable) \u2014 operating in degraded mode:", err.message);
276
+ this._tokenSecret = deriveTokenSecret(this.config.licenseKey);
277
+ this._domain = "localhost";
278
+ this._deploymentId = this.config.deploymentId ?? "unknown";
279
+ this._proxyUrl = cp;
280
+ }
281
+ this.engine = new Engine({
282
+ tenantId: this._tenantId,
283
+ deploymentId: this._deploymentId,
284
+ domain: this._domain,
285
+ tokenSecret: this._tokenSecret,
286
+ tokenTtlMs: this.config.tokenTtlMs ?? 15 * 60 * 1e3,
287
+ checkoutTokenTtlMs: this.config.checkoutTokenTtlMs ?? 5 * 60 * 1e3,
288
+ declareLimit: this.config.declareLimit ?? 8,
289
+ declareWindowMs: this.config.declareWindowMs ?? 6e4,
290
+ controlPlaneUrl: cp,
291
+ controlPlaneAdminKey: this.config.controlPlaneAdminKey,
292
+ getLease: () => this.lease,
293
+ getRegisteredIntents: () => this.intentRegistry,
294
+ devMode: this.config.devMode
295
+ });
296
+ }
297
+ getEngine() {
298
+ if (!this.engine) throw new Error("[agentix] SDK not initialized \u2014 await sdk.initialize() first");
299
+ return this.engine;
300
+ }
301
+ registerIntent(intent, mode = "enforce") {
302
+ this.intentRegistry.set(intent, mode);
303
+ }
304
+ getIntentRegistry() {
305
+ return this.intentRegistry;
306
+ }
307
+ getResolvedDomain() {
308
+ return this._domain;
309
+ }
310
+ getResolvedTenantId() {
311
+ return this._tenantId;
312
+ }
313
+ getResolvedTokenSecret() {
314
+ return this._tokenSecret;
315
+ }
316
+ getDeploymentId() {
317
+ return this._deploymentId;
318
+ }
319
+ getProxyUrl() {
320
+ return this._proxyUrl;
321
+ }
322
+ getLease() {
323
+ return this.lease;
324
+ }
325
+ };
326
+
327
+ // src/index.ts
328
+ function normalize(req) {
329
+ const proto = req.get("x-forwarded-proto") ?? req.protocol ?? "http";
330
+ const host = req.get("host") ?? req.hostname ?? "localhost";
331
+ const ip = req.get("x-forwarded-for")?.split(",")[0]?.trim() ?? req.ip ?? req.socket?.remoteAddress ?? "unknown";
332
+ const headers = {};
333
+ for (const [key, value] of Object.entries(req.headers)) {
334
+ if (typeof value === "string") headers[key] = value;
335
+ else if (Array.isArray(value)) headers[key] = value[0] ?? "";
336
+ }
337
+ return { method: req.method, path: req.path, headers, ip, body: req.body, baseUrl: `${proto}://${host}` };
338
+ }
339
+ function bearerToken(req) {
340
+ const auth = req.get("authorization");
341
+ if (!auth?.startsWith("Bearer ")) return null;
342
+ return auth.slice("Bearer ".length).trim();
343
+ }
344
+ function clientFingerprint(req) {
345
+ const ip = req.get("x-forwarded-for")?.split(",")[0]?.trim() ?? req.ip ?? req.socket?.remoteAddress ?? "unknown";
346
+ const ua = req.get("user-agent") ?? "unknown";
347
+ const lang = req.get("accept-language") ?? "";
348
+ return createHash("sha256").update([ip, ua, lang].join("|")).digest("hex");
349
+ }
350
+ async function callProxy(proxyUrl, deploymentId, token, fingerprint2, intent, mode) {
351
+ const controller = new AbortController();
352
+ const id = setTimeout(() => controller.abort(), 1500);
353
+ try {
354
+ const res = await fetch(`${proxyUrl}/v1/enforce`, {
355
+ method: "POST",
356
+ headers: { "content-type": "application/json" },
357
+ body: JSON.stringify({ deployment_id: deploymentId, token, fingerprint: fingerprint2, intent, mode }),
358
+ signal: controller.signal
359
+ });
360
+ clearTimeout(id);
361
+ if (res.ok) return await res.json();
362
+ return { decision: "allow", reason: "proxy_error" };
363
+ } catch {
364
+ clearTimeout(id);
365
+ return { decision: "allow", reason: "proxy_unreachable" };
366
+ }
367
+ }
368
+ function agentixMiddleware(sdk) {
369
+ return async (req, res, next) => {
370
+ await sdk.ensureInitialized();
371
+ const result = await sdk.getEngine().handle(normalize(req));
372
+ if (result.type === "passthrough") {
373
+ next();
374
+ return;
375
+ }
376
+ if (result.headers) for (const [k, v] of Object.entries(result.headers)) res.setHeader(k, v);
377
+ res.status(result.status).json(result.body);
378
+ };
379
+ }
380
+ function secure(sdk, intent, handler, opts = {}) {
381
+ const mode = opts.mode ?? "enforce";
382
+ sdk.registerIntent(intent, mode);
383
+ return async (req, res, next) => {
384
+ await sdk.ensureInitialized();
385
+ const token = bearerToken(req);
386
+ const fp = clientFingerprint(req);
387
+ const baseUrl = `${req.get("x-forwarded-proto") ?? req.protocol ?? "http"}://${req.get("host") ?? req.hostname ?? "localhost"}`;
388
+ if (!token) {
389
+ res.setHeader("www-authenticate", 'Bearer realm="agentix", error="missing_token"');
390
+ res.status(401).json({ error: "missing_token", agent_discovery: `${baseUrl}/.well-known/ai-agent.json` });
391
+ return;
392
+ }
393
+ const verdict = await callProxy(sdk.getProxyUrl(), sdk.getDeploymentId(), token, fp, intent, mode);
394
+ if (verdict.decision === "allow") {
395
+ handler(req, res, next);
396
+ return;
397
+ }
398
+ const reason = verdict.reason ?? "denied";
399
+ if (reason === "missing_token" || reason === "invalid_token") {
400
+ res.setHeader("www-authenticate", `Bearer realm="agentix", error="${reason}"`);
401
+ res.status(401).json({ error: reason, agent_discovery: `${baseUrl}/.well-known/ai-agent.json` });
402
+ } else if (reason === "out_of_scope") {
403
+ res.status(403).json({ error: "out_of_scope", required_intent: verdict.required_intent ?? intent });
404
+ } else {
405
+ res.status(401).json({ error: reason });
406
+ }
407
+ };
408
+ }
409
+
410
+ export { AgentixSDK, agentixMiddleware, secure };
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@agentix-security/express",
3
+ "version": "0.1.0",
4
+ "description": "Agentix Express adapter — AI agent intent-based authorization for Express apps",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsup",
21
+ "typecheck": "tsc --noEmit"
22
+ },
23
+ "peerDependencies": {
24
+ "express": "^4"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "keywords": ["agentix", "express", "ai", "agents", "authorization", "intent"],
30
+ "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/agentix-dev/agentix"
34
+ }
35
+ }