@agentix-security/nextjs 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 +570 -0
- package/dist/index.d.cts +123 -0
- package/dist/index.d.ts +123 -0
- package/dist/index.js +565 -0
- package/package.json +35 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var server_js = require('next/server.js');
|
|
4
|
+
var crypto$1 = require('crypto');
|
|
5
|
+
|
|
6
|
+
// src/index.ts
|
|
7
|
+
|
|
8
|
+
// ../core/src/policy.ts
|
|
9
|
+
function isValidIntent(intent, validIntents) {
|
|
10
|
+
if (!intent || typeof intent !== "string") return false;
|
|
11
|
+
if (validIntents) return validIntents.includes(intent);
|
|
12
|
+
return /^[a-z][a-z0-9_]{1,63}$/.test(intent);
|
|
13
|
+
}
|
|
14
|
+
function licenseActive(lease, now = Date.now()) {
|
|
15
|
+
if (!lease) return false;
|
|
16
|
+
return Date.parse(lease.expires_at) > now;
|
|
17
|
+
}
|
|
18
|
+
function decision(partial) {
|
|
19
|
+
return {
|
|
20
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
21
|
+
...partial
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function deriveTokenSecret(licenseKey) {
|
|
25
|
+
return crypto$1.createHmac("sha256", licenseKey).update("agentix-v1-token-secret").digest("hex");
|
|
26
|
+
}
|
|
27
|
+
function b64url(value) {
|
|
28
|
+
return Buffer.from(value).toString("base64url");
|
|
29
|
+
}
|
|
30
|
+
function sign(secret, data) {
|
|
31
|
+
return crypto$1.createHmac("sha256", secret).update(data).digest("base64url");
|
|
32
|
+
}
|
|
33
|
+
function issueToken(secret, payload) {
|
|
34
|
+
const header = b64url(JSON.stringify({ alg: "HS256", typ: "AGX" }));
|
|
35
|
+
const body = b64url(JSON.stringify({ jti: `tok_${crypto$1.randomUUID()}`, ...payload }));
|
|
36
|
+
const sig = sign(secret, `${header}.${body}`);
|
|
37
|
+
return `${header}.${body}.${sig}`;
|
|
38
|
+
}
|
|
39
|
+
function tokenId(raw) {
|
|
40
|
+
const parts = raw.split(".");
|
|
41
|
+
if (parts.length !== 3) return null;
|
|
42
|
+
try {
|
|
43
|
+
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
|
|
44
|
+
return payload.jti;
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ../sdk/src/rate-limit.ts
|
|
51
|
+
var RateLimiter = class {
|
|
52
|
+
constructor(limit, windowMs) {
|
|
53
|
+
this.limit = limit;
|
|
54
|
+
this.windowMs = windowMs;
|
|
55
|
+
}
|
|
56
|
+
limit;
|
|
57
|
+
windowMs;
|
|
58
|
+
buckets = /* @__PURE__ */ new Map();
|
|
59
|
+
consume(key) {
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
const bucket = this.buckets.get(key);
|
|
62
|
+
if (!bucket || now - bucket.windowStart > this.windowMs) {
|
|
63
|
+
this.buckets.set(key, { windowStart: now, count: 1 });
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
bucket.count += 1;
|
|
67
|
+
this.buckets.set(key, bucket);
|
|
68
|
+
return bucket.count <= this.limit;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// ../sdk/src/engine.ts
|
|
73
|
+
function fingerprint(ip, userAgent, acceptLanguage) {
|
|
74
|
+
return crypto$1.createHash("sha256").update([ip, userAgent, acceptLanguage].join("|")).digest("hex");
|
|
75
|
+
}
|
|
76
|
+
async function shipAudit(controlPlaneUrl, adminKey, row) {
|
|
77
|
+
if (!adminKey) return;
|
|
78
|
+
try {
|
|
79
|
+
await fetch(`${controlPlaneUrl.replace(/\/$/, "")}/v1/audit/decisions`, {
|
|
80
|
+
method: "POST",
|
|
81
|
+
headers: { authorization: `Bearer ${adminKey}`, "content-type": "application/json" },
|
|
82
|
+
body: JSON.stringify(row)
|
|
83
|
+
});
|
|
84
|
+
} catch {
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
var Engine = class {
|
|
88
|
+
constructor(opts) {
|
|
89
|
+
this.opts = opts;
|
|
90
|
+
this.declareLimiter = new RateLimiter(opts.declareLimit, opts.declareWindowMs);
|
|
91
|
+
}
|
|
92
|
+
opts;
|
|
93
|
+
declareLimiter;
|
|
94
|
+
async handle(req) {
|
|
95
|
+
const { method, path } = req;
|
|
96
|
+
if (method === "GET" && path === "/.well-known/ai-agent.json") {
|
|
97
|
+
return this.handleDiscovery(req);
|
|
98
|
+
}
|
|
99
|
+
if (method === "POST" && path === "/agent/v1/declare_intent") {
|
|
100
|
+
return this.handleDeclareIntent(req);
|
|
101
|
+
}
|
|
102
|
+
return { type: "passthrough" };
|
|
103
|
+
}
|
|
104
|
+
handleDiscovery(req) {
|
|
105
|
+
const lease = this.opts.getLease();
|
|
106
|
+
const allIntents = this.opts.getRegisteredIntents ? [...this.opts.getRegisteredIntents().keys()] : [];
|
|
107
|
+
const body = {
|
|
108
|
+
service: "agentix-intent-sdk",
|
|
109
|
+
version: "0.2.0",
|
|
110
|
+
tenant_id: this.opts.tenantId,
|
|
111
|
+
deployment_id: this.opts.deploymentId,
|
|
112
|
+
discovery: {
|
|
113
|
+
well_known: `${req.baseUrl}/.well-known/ai-agent.json`,
|
|
114
|
+
token_endpoint: `${req.baseUrl}/agent/v1/declare_intent`
|
|
115
|
+
},
|
|
116
|
+
license: {
|
|
117
|
+
active: licenseActive(lease),
|
|
118
|
+
expires_at: lease?.expires_at ?? null,
|
|
119
|
+
policy_pack_version: lease?.policy_pack_version ?? null
|
|
120
|
+
},
|
|
121
|
+
intents: allIntents
|
|
122
|
+
};
|
|
123
|
+
void this.audit(req, "/.well-known/ai-agent.json", 200, {
|
|
124
|
+
trustMode: "unknown",
|
|
125
|
+
intentScope: "none",
|
|
126
|
+
tokenId: null,
|
|
127
|
+
decisionValue: "allow",
|
|
128
|
+
decisionReason: "agent_discovery_served",
|
|
129
|
+
policyId: "agent-discovery"
|
|
130
|
+
});
|
|
131
|
+
return {
|
|
132
|
+
type: "response",
|
|
133
|
+
status: 200,
|
|
134
|
+
headers: { "cache-control": "no-store", "content-type": "application/json" },
|
|
135
|
+
body
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
async handleDeclareIntent(req) {
|
|
139
|
+
const lease = this.opts.getLease();
|
|
140
|
+
const leaseOk = licenseActive(lease) || this.opts.devMode === true;
|
|
141
|
+
if (!leaseOk) {
|
|
142
|
+
void this.audit(req, "/agent/v1/declare_intent", 403, {
|
|
143
|
+
trustMode: "unmanaged_automation",
|
|
144
|
+
intentScope: "none",
|
|
145
|
+
tokenId: null,
|
|
146
|
+
decisionValue: "deny",
|
|
147
|
+
decisionReason: "license_expired_agent_endpoint_disabled",
|
|
148
|
+
policyId: "license-lease"
|
|
149
|
+
});
|
|
150
|
+
return { type: "response", status: 403, body: { error: "license_not_active" } };
|
|
151
|
+
}
|
|
152
|
+
const fp = this.fingerprint(req);
|
|
153
|
+
if (!this.declareLimiter.consume(fp)) {
|
|
154
|
+
void this.audit(req, "/agent/v1/declare_intent", 429, {
|
|
155
|
+
trustMode: "unmanaged_automation",
|
|
156
|
+
intentScope: "none",
|
|
157
|
+
tokenId: null,
|
|
158
|
+
decisionValue: "throttle",
|
|
159
|
+
decisionReason: "declare_intent_rate_limited",
|
|
160
|
+
policyId: "declare-intent-rate-limit"
|
|
161
|
+
});
|
|
162
|
+
return { type: "response", status: 429, body: { error: "declare_intent_rate_limited" } };
|
|
163
|
+
}
|
|
164
|
+
const registeredIntents = this.opts.getRegisteredIntents ? [...this.opts.getRegisteredIntents().keys()] : [];
|
|
165
|
+
const body = req.body ?? {};
|
|
166
|
+
if (!body.intent || !isValidIntent(body.intent, registeredIntents)) {
|
|
167
|
+
void this.audit(req, "/agent/v1/declare_intent", 400, {
|
|
168
|
+
trustMode: "unmanaged_automation",
|
|
169
|
+
intentScope: "none",
|
|
170
|
+
tokenId: null,
|
|
171
|
+
decisionValue: "deny",
|
|
172
|
+
decisionReason: "invalid_intent",
|
|
173
|
+
policyId: "intent-validation",
|
|
174
|
+
metadata: { supplied_intent: body.intent ?? null }
|
|
175
|
+
});
|
|
176
|
+
return { type: "response", status: 400, body: { error: "invalid_intent" } };
|
|
177
|
+
}
|
|
178
|
+
const intent = body.intent;
|
|
179
|
+
const ttl = intent === "checkout_intent" ? this.opts.checkoutTokenTtlMs : this.opts.tokenTtlMs;
|
|
180
|
+
const iat = Math.floor(Date.now() / 1e3);
|
|
181
|
+
const exp = iat + Math.floor(ttl / 1e3);
|
|
182
|
+
const binding = this.fingerprint(req);
|
|
183
|
+
const raw = issueToken(this.opts.tokenSecret, { intent, domain: this.opts.domain, binding, iat, exp });
|
|
184
|
+
const jti = tokenId(raw);
|
|
185
|
+
void this.audit(req, "/agent/v1/declare_intent", 200, {
|
|
186
|
+
trustMode: "managed_agent",
|
|
187
|
+
intentScope: intent,
|
|
188
|
+
tokenId: jti,
|
|
189
|
+
decisionValue: "allow",
|
|
190
|
+
decisionReason: "intent_token_issued",
|
|
191
|
+
policyId: "intent-token-issuer",
|
|
192
|
+
metadata: { subject: body.subject ?? null, constraints: body.constraints ?? null }
|
|
193
|
+
});
|
|
194
|
+
return {
|
|
195
|
+
type: "response",
|
|
196
|
+
status: 200,
|
|
197
|
+
body: { access_token: raw, token_type: "Bearer", token_id: jti, intent, expires_in: exp - iat }
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
fingerprint(req) {
|
|
201
|
+
return fingerprint(
|
|
202
|
+
req.ip,
|
|
203
|
+
req.headers["user-agent"] ?? req.headers["User-Agent"] ?? "unknown",
|
|
204
|
+
req.headers["accept-language"] ?? req.headers["Accept-Language"] ?? ""
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
async audit(req, route, statusCode, input) {
|
|
208
|
+
const row = decision({
|
|
209
|
+
tenant_id: this.opts.tenantId,
|
|
210
|
+
deployment_id: this.opts.deploymentId,
|
|
211
|
+
domain: this.opts.domain,
|
|
212
|
+
route,
|
|
213
|
+
method: req.method,
|
|
214
|
+
client_fingerprint_hash: this.fingerprint(req),
|
|
215
|
+
trust_mode: input.trustMode,
|
|
216
|
+
intent_scope: input.intentScope,
|
|
217
|
+
token_id: input.tokenId,
|
|
218
|
+
decision: input.decisionValue,
|
|
219
|
+
decision_reason: input.decisionReason,
|
|
220
|
+
policy_id: input.policyId,
|
|
221
|
+
status_code: statusCode,
|
|
222
|
+
metadata: input.metadata ?? {}
|
|
223
|
+
});
|
|
224
|
+
void shipAudit(this.opts.controlPlaneUrl, this.opts.controlPlaneAdminKey, row);
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// ../sdk/src/index.ts
|
|
229
|
+
var DEFAULT_CONTROL_PLANE = "https://agentix-control-plane.onrender.com";
|
|
230
|
+
var AgentixSDK = class {
|
|
231
|
+
constructor(config) {
|
|
232
|
+
this.config = config;
|
|
233
|
+
}
|
|
234
|
+
config;
|
|
235
|
+
lease = null;
|
|
236
|
+
engine = null;
|
|
237
|
+
initPromise = null;
|
|
238
|
+
intentRegistry = /* @__PURE__ */ new Map();
|
|
239
|
+
// Resolved at init from the server:
|
|
240
|
+
_tenantId = "";
|
|
241
|
+
_domain = "";
|
|
242
|
+
_tokenSecret = "";
|
|
243
|
+
_deploymentId = "";
|
|
244
|
+
_proxyUrl = "";
|
|
245
|
+
async ensureInitialized() {
|
|
246
|
+
if (!this.initPromise) this.initPromise = this._init();
|
|
247
|
+
return this.initPromise;
|
|
248
|
+
}
|
|
249
|
+
async initialize() {
|
|
250
|
+
return this.ensureInitialized();
|
|
251
|
+
}
|
|
252
|
+
async _init() {
|
|
253
|
+
const cp = (this.config.controlPlaneUrl ?? DEFAULT_CONTROL_PLANE).replace(/\/$/, "");
|
|
254
|
+
try {
|
|
255
|
+
const res = await fetch(`${cp}/v1/deployments/register`, {
|
|
256
|
+
method: "POST",
|
|
257
|
+
headers: { "content-type": "application/json" },
|
|
258
|
+
body: JSON.stringify({
|
|
259
|
+
license_key: this.config.licenseKey,
|
|
260
|
+
deployment_id: this.config.deploymentId
|
|
261
|
+
})
|
|
262
|
+
});
|
|
263
|
+
if (res.ok) {
|
|
264
|
+
const body = await res.json();
|
|
265
|
+
this._deploymentId = body.deployment_id;
|
|
266
|
+
this._tenantId = body.tenant_id;
|
|
267
|
+
this._domain = body.domain;
|
|
268
|
+
this._tokenSecret = body.token_secret;
|
|
269
|
+
this._proxyUrl = body.proxy_url;
|
|
270
|
+
} else {
|
|
271
|
+
console.warn(`[agentix] Registration failed (${res.status}) \u2014 operating in degraded mode`);
|
|
272
|
+
this._tokenSecret = deriveTokenSecret(this.config.licenseKey);
|
|
273
|
+
this._domain = "localhost";
|
|
274
|
+
this._deploymentId = this.config.deploymentId ?? "unknown";
|
|
275
|
+
this._proxyUrl = cp;
|
|
276
|
+
}
|
|
277
|
+
} catch (err) {
|
|
278
|
+
console.warn("[agentix] Registration error (control plane unreachable) \u2014 operating in degraded mode:", err.message);
|
|
279
|
+
this._tokenSecret = deriveTokenSecret(this.config.licenseKey);
|
|
280
|
+
this._domain = "localhost";
|
|
281
|
+
this._deploymentId = this.config.deploymentId ?? "unknown";
|
|
282
|
+
this._proxyUrl = cp;
|
|
283
|
+
}
|
|
284
|
+
this.engine = new Engine({
|
|
285
|
+
tenantId: this._tenantId,
|
|
286
|
+
deploymentId: this._deploymentId,
|
|
287
|
+
domain: this._domain,
|
|
288
|
+
tokenSecret: this._tokenSecret,
|
|
289
|
+
tokenTtlMs: this.config.tokenTtlMs ?? 15 * 60 * 1e3,
|
|
290
|
+
checkoutTokenTtlMs: this.config.checkoutTokenTtlMs ?? 5 * 60 * 1e3,
|
|
291
|
+
declareLimit: this.config.declareLimit ?? 8,
|
|
292
|
+
declareWindowMs: this.config.declareWindowMs ?? 6e4,
|
|
293
|
+
controlPlaneUrl: cp,
|
|
294
|
+
controlPlaneAdminKey: this.config.controlPlaneAdminKey,
|
|
295
|
+
getLease: () => this.lease,
|
|
296
|
+
getRegisteredIntents: () => this.intentRegistry,
|
|
297
|
+
devMode: this.config.devMode
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
getEngine() {
|
|
301
|
+
if (!this.engine) throw new Error("[agentix] SDK not initialized \u2014 await sdk.initialize() first");
|
|
302
|
+
return this.engine;
|
|
303
|
+
}
|
|
304
|
+
registerIntent(intent, mode = "enforce") {
|
|
305
|
+
this.intentRegistry.set(intent, mode);
|
|
306
|
+
}
|
|
307
|
+
getIntentRegistry() {
|
|
308
|
+
return this.intentRegistry;
|
|
309
|
+
}
|
|
310
|
+
getResolvedDomain() {
|
|
311
|
+
return this._domain;
|
|
312
|
+
}
|
|
313
|
+
getResolvedTenantId() {
|
|
314
|
+
return this._tenantId;
|
|
315
|
+
}
|
|
316
|
+
getResolvedTokenSecret() {
|
|
317
|
+
return this._tokenSecret;
|
|
318
|
+
}
|
|
319
|
+
getDeploymentId() {
|
|
320
|
+
return this._deploymentId;
|
|
321
|
+
}
|
|
322
|
+
getProxyUrl() {
|
|
323
|
+
return this._proxyUrl;
|
|
324
|
+
}
|
|
325
|
+
getLease() {
|
|
326
|
+
return this.lease;
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
// ../sdk/src/token-web.ts
|
|
331
|
+
function b64url2(value) {
|
|
332
|
+
return btoa(value).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
333
|
+
}
|
|
334
|
+
function encodePayload(value) {
|
|
335
|
+
return b64url2(JSON.stringify(value));
|
|
336
|
+
}
|
|
337
|
+
async function hmac(secret, data) {
|
|
338
|
+
const key = await crypto.subtle.importKey(
|
|
339
|
+
"raw",
|
|
340
|
+
new TextEncoder().encode(secret),
|
|
341
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
342
|
+
false,
|
|
343
|
+
["sign"]
|
|
344
|
+
);
|
|
345
|
+
const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(data));
|
|
346
|
+
const bytes = Array.from(new Uint8Array(sig));
|
|
347
|
+
return b64url2(String.fromCharCode(...bytes));
|
|
348
|
+
}
|
|
349
|
+
async function issueTokenWeb(secret, payload) {
|
|
350
|
+
const jti = payload.jti ?? `tok_${crypto.randomUUID()}`;
|
|
351
|
+
const header = encodePayload({ alg: "HS256", typ: "AGX" });
|
|
352
|
+
const body = encodePayload({ jti, ...payload });
|
|
353
|
+
const sig = await hmac(secret, `${header}.${body}`);
|
|
354
|
+
return `${header}.${body}.${sig}`;
|
|
355
|
+
}
|
|
356
|
+
async function verifyTokenWeb(secret, raw) {
|
|
357
|
+
const parts = raw.split(".");
|
|
358
|
+
if (parts.length !== 3) return null;
|
|
359
|
+
const [header, body, sig] = parts;
|
|
360
|
+
const expected = await hmac(secret, `${header}.${body}`);
|
|
361
|
+
if (sig !== expected) return null;
|
|
362
|
+
try {
|
|
363
|
+
const decoded = atob(body.replace(/-/g, "+").replace(/_/g, "/"));
|
|
364
|
+
const payload = JSON.parse(decoded);
|
|
365
|
+
if (payload.exp <= Math.floor(Date.now() / 1e3)) return null;
|
|
366
|
+
return payload;
|
|
367
|
+
} catch {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
function tokenIdWeb(raw) {
|
|
372
|
+
const parts = raw.split(".");
|
|
373
|
+
if (parts.length !== 3) return null;
|
|
374
|
+
try {
|
|
375
|
+
const decoded = atob(parts[1].replace(/-/g, "+").replace(/_/g, "/"));
|
|
376
|
+
return JSON.parse(decoded).jti ?? null;
|
|
377
|
+
} catch {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// src/index.ts
|
|
383
|
+
async function callProxy(proxyUrl, deploymentId, token, fingerprint3, intent, mode) {
|
|
384
|
+
const controller = new AbortController();
|
|
385
|
+
const id = setTimeout(() => controller.abort(), 1500);
|
|
386
|
+
try {
|
|
387
|
+
const res = await fetch(`${proxyUrl}/v1/enforce`, {
|
|
388
|
+
method: "POST",
|
|
389
|
+
headers: { "content-type": "application/json" },
|
|
390
|
+
body: JSON.stringify({ deployment_id: deploymentId, token, fingerprint: fingerprint3, intent, mode }),
|
|
391
|
+
signal: controller.signal
|
|
392
|
+
});
|
|
393
|
+
clearTimeout(id);
|
|
394
|
+
if (res.ok) return await res.json();
|
|
395
|
+
return { decision: "allow", reason: "proxy_error" };
|
|
396
|
+
} catch {
|
|
397
|
+
clearTimeout(id);
|
|
398
|
+
return { decision: "allow", reason: "proxy_unreachable" };
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
function getIp(req) {
|
|
402
|
+
return req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
|
|
403
|
+
}
|
|
404
|
+
async function fingerprint2(req) {
|
|
405
|
+
const data = [getIp(req), req.headers.get("user-agent") ?? "unknown", req.headers.get("accept-language") ?? ""].join("|");
|
|
406
|
+
const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(data));
|
|
407
|
+
return Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
408
|
+
}
|
|
409
|
+
function bearer(req) {
|
|
410
|
+
const auth = req.headers.get("authorization");
|
|
411
|
+
if (!auth?.startsWith("Bearer ")) return null;
|
|
412
|
+
return auth.slice("Bearer ".length).trim();
|
|
413
|
+
}
|
|
414
|
+
async function shipAudit2(url, key, row) {
|
|
415
|
+
if (!key) return;
|
|
416
|
+
try {
|
|
417
|
+
await fetch(`${url}/v1/audit/decisions`, {
|
|
418
|
+
method: "POST",
|
|
419
|
+
headers: { authorization: `Bearer ${key}`, "content-type": "application/json" },
|
|
420
|
+
body: JSON.stringify(row)
|
|
421
|
+
});
|
|
422
|
+
} catch {
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
function auditRow(sdk, req, route, status, fp, fields) {
|
|
426
|
+
return {
|
|
427
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
428
|
+
tenant_id: sdk.getResolvedTenantId(),
|
|
429
|
+
deployment_id: sdk.config.deploymentId ?? "default",
|
|
430
|
+
domain: sdk.getResolvedDomain(),
|
|
431
|
+
route,
|
|
432
|
+
method: req.method,
|
|
433
|
+
client_fingerprint_hash: fp,
|
|
434
|
+
status_code: status,
|
|
435
|
+
metadata: {},
|
|
436
|
+
...fields
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
function agentixMiddleware(sdk) {
|
|
440
|
+
return async (req) => {
|
|
441
|
+
await sdk.ensureInitialized();
|
|
442
|
+
const { pathname } = req.nextUrl;
|
|
443
|
+
const fp = await fingerprint2(req);
|
|
444
|
+
const baseUrl = req.nextUrl.origin;
|
|
445
|
+
const cp = (sdk.config.controlPlaneUrl ?? "https://agentix-control-plane.onrender.com").replace(/\/$/, "");
|
|
446
|
+
const adminKey = sdk.config.controlPlaneAdminKey;
|
|
447
|
+
const tokenSecret = sdk.getResolvedTokenSecret();
|
|
448
|
+
if (req.method === "GET" && pathname === "/.well-known/ai-agent.json") {
|
|
449
|
+
const allIntents = [...sdk.getIntentRegistry().keys()];
|
|
450
|
+
void shipAudit2(cp, adminKey, auditRow(sdk, req, pathname, 200, fp, {
|
|
451
|
+
trust_mode: "unknown",
|
|
452
|
+
intent_scope: "none",
|
|
453
|
+
token_id: null,
|
|
454
|
+
decision: "allow",
|
|
455
|
+
decision_reason: "agent_discovery_served",
|
|
456
|
+
policy_id: "agent-discovery"
|
|
457
|
+
}));
|
|
458
|
+
return server_js.NextResponse.json({
|
|
459
|
+
service: "agentix-intent-sdk",
|
|
460
|
+
version: "0.2.0",
|
|
461
|
+
tenant_id: sdk.getResolvedTenantId(),
|
|
462
|
+
deployment_id: sdk.getDeploymentId(),
|
|
463
|
+
discovery: { well_known: `${baseUrl}/.well-known/ai-agent.json`, token_endpoint: `${baseUrl}/agent/v1/declare_intent` },
|
|
464
|
+
intents: allIntents
|
|
465
|
+
}, { headers: { "cache-control": "no-store" } });
|
|
466
|
+
}
|
|
467
|
+
if (req.method === "POST" && pathname === "/agent/v1/declare_intent") {
|
|
468
|
+
let body = {};
|
|
469
|
+
try {
|
|
470
|
+
body = await req.json();
|
|
471
|
+
} catch {
|
|
472
|
+
}
|
|
473
|
+
const validIntents = [...sdk.getIntentRegistry().keys()];
|
|
474
|
+
if (!body.intent || !isValidIntent(body.intent, validIntents)) {
|
|
475
|
+
void shipAudit2(cp, adminKey, auditRow(sdk, req, pathname, 400, fp, {
|
|
476
|
+
trust_mode: "unmanaged_automation",
|
|
477
|
+
intent_scope: "none",
|
|
478
|
+
token_id: null,
|
|
479
|
+
decision: "deny",
|
|
480
|
+
decision_reason: "invalid_intent",
|
|
481
|
+
policy_id: "intent-validation",
|
|
482
|
+
metadata: { supplied_intent: body.intent ?? null }
|
|
483
|
+
}));
|
|
484
|
+
return server_js.NextResponse.json({ error: "invalid_intent", valid_intents: validIntents }, { status: 400 });
|
|
485
|
+
}
|
|
486
|
+
const intent = body.intent;
|
|
487
|
+
const ttl = sdk.config.tokenTtlMs ?? 15 * 60 * 1e3;
|
|
488
|
+
const iat = Math.floor(Date.now() / 1e3);
|
|
489
|
+
const exp = iat + Math.floor(ttl / 1e3);
|
|
490
|
+
const raw = await issueTokenWeb(tokenSecret, { intent, domain: sdk.getResolvedDomain(), binding: fp, iat, exp });
|
|
491
|
+
const jti = tokenIdWeb(raw);
|
|
492
|
+
void shipAudit2(cp, adminKey, auditRow(sdk, req, pathname, 200, fp, {
|
|
493
|
+
trust_mode: "managed_agent",
|
|
494
|
+
intent_scope: intent,
|
|
495
|
+
token_id: jti,
|
|
496
|
+
decision: "allow",
|
|
497
|
+
decision_reason: "intent_token_issued",
|
|
498
|
+
policy_id: "intent-token-issuer",
|
|
499
|
+
metadata: { subject: body.subject ?? null, constraints: body.constraints ?? null }
|
|
500
|
+
}));
|
|
501
|
+
return server_js.NextResponse.json({
|
|
502
|
+
access_token: raw,
|
|
503
|
+
token_type: "Bearer",
|
|
504
|
+
token_id: jti,
|
|
505
|
+
intent,
|
|
506
|
+
expires_in: exp - iat
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
return server_js.NextResponse.next();
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
function secure(sdk, intent, handler, opts = {}) {
|
|
513
|
+
const mode = opts.mode ?? "enforce";
|
|
514
|
+
sdk.registerIntent(intent, mode);
|
|
515
|
+
return async (req, ctx) => {
|
|
516
|
+
await sdk.ensureInitialized();
|
|
517
|
+
const fp = await fingerprint2(req);
|
|
518
|
+
const raw = bearer(req);
|
|
519
|
+
const baseUrl = req.nextUrl.origin;
|
|
520
|
+
if (!raw) {
|
|
521
|
+
return server_js.NextResponse.json(
|
|
522
|
+
{ error: "missing_token", agent_discovery: `${baseUrl}/.well-known/ai-agent.json` },
|
|
523
|
+
{ status: 401, headers: { "www-authenticate": 'Bearer realm="agentix", error="missing_token"' } }
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
const verdict = await callProxy(sdk.getProxyUrl(), sdk.getDeploymentId(), raw, fp, intent, mode);
|
|
527
|
+
if (verdict.decision === "allow") return handler(req, ctx);
|
|
528
|
+
const reason = verdict.reason ?? "denied";
|
|
529
|
+
if (reason === "missing_token" || reason === "invalid_token") {
|
|
530
|
+
return server_js.NextResponse.json({ error: reason, agent_discovery: `${baseUrl}/.well-known/ai-agent.json` }, { status: 401 });
|
|
531
|
+
}
|
|
532
|
+
if (reason === "out_of_scope") {
|
|
533
|
+
return server_js.NextResponse.json({ error: "out_of_scope", required_intent: verdict.required_intent ?? intent }, { status: 403 });
|
|
534
|
+
}
|
|
535
|
+
return server_js.NextResponse.json({ error: reason }, { status: 401 });
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
function withAgentix(sdk, intent, mode = "enforce") {
|
|
539
|
+
sdk.registerIntent(intent, mode);
|
|
540
|
+
return function(handler) {
|
|
541
|
+
return async (req, res) => {
|
|
542
|
+
await sdk.ensureInitialized();
|
|
543
|
+
const auth = req.headers["authorization"] ?? "";
|
|
544
|
+
const raw = auth.startsWith("Bearer ") ? auth.slice("Bearer ".length).trim() : null;
|
|
545
|
+
if (!raw) {
|
|
546
|
+
res.status(401).json({ error: "missing_token" });
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const payload = await verifyTokenWeb(sdk.getResolvedTokenSecret(), raw);
|
|
550
|
+
if (!payload || payload.domain !== sdk.getResolvedDomain()) {
|
|
551
|
+
res.status(401).json({ error: "invalid_token" });
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
if (payload.intent !== intent) {
|
|
555
|
+
if (mode === "shadow") {
|
|
556
|
+
await handler(req, res);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
res.status(403).json({ error: "out_of_scope", required_intent: intent });
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
await handler(req, res);
|
|
563
|
+
};
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
exports.AgentixSDK = AgentixSDK;
|
|
568
|
+
exports.agentixMiddleware = agentixMiddleware;
|
|
569
|
+
exports.secure = secure;
|
|
570
|
+
exports.withAgentix = withAgentix;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { NextRequest, NextFetchEvent, NextResponse } from 'next/server.js';
|
|
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
|
+
type NextMiddleware = (req: NextRequest, event: NextFetchEvent) => Promise<NextResponse>;
|
|
99
|
+
declare function agentixMiddleware(sdk: AgentixSDK): NextMiddleware;
|
|
100
|
+
/**
|
|
101
|
+
* Per-route annotation for Next.js App Router — wraps a route handler with intent enforcement.
|
|
102
|
+
* The middleware only needs to handle discovery + token issuance; enforcement is per-handler.
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* // /app/api/jobs/route.ts
|
|
106
|
+
* import { secure } from '@agentix/nextjs'
|
|
107
|
+
* export const GET = secure(sdk, 'browse_jobs', async (req) => NextResponse.json(jobs))
|
|
108
|
+
* export const POST = secure(sdk, 'submit_application', handler, { mode: 'enforce' })
|
|
109
|
+
*/
|
|
110
|
+
declare function secure(sdk: AgentixSDK, intent: string, handler: (req: NextRequest, ctx?: unknown) => Promise<Response> | Response, opts?: {
|
|
111
|
+
mode?: 'enforce' | 'shadow';
|
|
112
|
+
}): (req: NextRequest, ctx?: unknown) => Promise<Response>;
|
|
113
|
+
interface PagesRes {
|
|
114
|
+
status(code: number): PagesRes;
|
|
115
|
+
json(body: unknown): void;
|
|
116
|
+
}
|
|
117
|
+
interface PagesReq {
|
|
118
|
+
method?: string;
|
|
119
|
+
headers: Record<string, string | string[] | undefined>;
|
|
120
|
+
}
|
|
121
|
+
declare function withAgentix(sdk: AgentixSDK, intent: string, mode?: 'enforce' | 'shadow'): (handler: (req: PagesReq, res: PagesRes) => Promise<void> | void) => (req: PagesReq, res: PagesRes) => Promise<void>;
|
|
122
|
+
|
|
123
|
+
export { AgentixSDK, type AgentixSDKConfig, agentixMiddleware, secure, withAgentix };
|