@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 +414 -0
- package/dist/index.d.cts +112 -0
- package/dist/index.d.ts +112 -0
- package/dist/index.js +410 -0
- package/package.json +35 -0
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;
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|