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