@agentix-security/nextjs 0.1.10 → 0.1.12

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 CHANGED
@@ -459,6 +459,30 @@ function auditRow(sdk, req, route, status, fp, fields) {
459
459
  ...fields
460
460
  };
461
461
  }
462
+ function buildTools(sdk, baseUrl) {
463
+ const registry = sdk.getIntentRegistry();
464
+ const descriptions = sdk.config.descriptions ?? {};
465
+ return Object.fromEntries(
466
+ [...registry.entries()].map(([intent, entry]) => {
467
+ const tool = {
468
+ routes: [...entry.routes],
469
+ token_url: `${baseUrl}/agent/v1/declare_intent?intent=${encodeURIComponent(intent)}`
470
+ };
471
+ if (descriptions[intent]) tool.description = descriptions[intent];
472
+ return [intent, tool];
473
+ })
474
+ );
475
+ }
476
+ function agentInstructions(ttlSec) {
477
+ return [
478
+ "1. Pick the tool below that matches what you need to do.",
479
+ `2. Fetch the tool's token_url with a GET request (no body, no auth required). You will receive JSON containing an access_token. Tokens are valid for ${ttlSec} seconds.`,
480
+ '3. Include the token in the Authorization header on every API request: "Authorization: Bearer <access_token>"',
481
+ "4. If you receive HTTP 401 (missing_token or invalid_token), your token is missing or expired \u2014 fetch a new one from the same token_url.",
482
+ "5. If you receive HTTP 403 (out_of_scope), your token's intent does not cover this route. Get a new token from the token_url of the correct tool.",
483
+ "6. Each token is scoped to exactly one intent. Use a different token for each capability."
484
+ ];
485
+ }
462
486
  function agentixMiddleware(sdk) {
463
487
  return async (req) => {
464
488
  await sdk.ensureInitialized();
@@ -468,11 +492,8 @@ function agentixMiddleware(sdk) {
468
492
  const cp = (sdk.config.controlPlaneUrl ?? "https://agentix-control-plane.onrender.com").replace(/\/$/, "");
469
493
  const licenseKey = sdk.config.licenseKey;
470
494
  const tokenSecret = sdk.getResolvedTokenSecret();
495
+ const ttlSec = Math.floor((sdk.config.tokenTtlMs ?? 15 * 60 * 1e3) / 1e3);
471
496
  if (req.method === "GET" && pathname === "/.well-known/ai-agent.json") {
472
- const registry = sdk.getIntentRegistry();
473
- const tools = Object.fromEntries(
474
- [...registry.entries()].map(([intent, entry]) => [intent, { routes: [...entry.routes] }])
475
- );
476
497
  void shipAudit2(cp, licenseKey, auditRow(sdk, req, pathname, 200, fp, {
477
498
  trust_mode: "unknown",
478
499
  intent_scope: "none",
@@ -486,19 +507,34 @@ function agentixMiddleware(sdk) {
486
507
  version: "0.2.0",
487
508
  tenant_id: sdk.getResolvedTenantId(),
488
509
  deployment_id: sdk.getDeploymentId(),
489
- discovery: { well_known: `${baseUrl}/.well-known/ai-agent.json`, token_endpoint: `${baseUrl}/agent/v1/declare_intent` },
490
- intents: [...registry.keys()],
491
- tools
510
+ instructions: agentInstructions(ttlSec),
511
+ token_ttl_seconds: ttlSec,
512
+ discovery: {
513
+ well_known: `${baseUrl}/.well-known/ai-agent.json`,
514
+ token_endpoint: `${baseUrl}/agent/v1/declare_intent`,
515
+ note: "Both GET (?intent=<intent>) and POST (JSON body) are accepted at token_endpoint."
516
+ },
517
+ tools: buildTools(sdk, baseUrl)
492
518
  }, { headers: { "cache-control": "no-store" } });
493
519
  }
494
- if (req.method === "POST" && pathname === "/agent/v1/declare_intent") {
495
- let body = {};
496
- try {
497
- body = await req.json();
498
- } catch {
520
+ if (pathname === "/agent/v1/declare_intent" && (req.method === "POST" || req.method === "GET")) {
521
+ let intentVal;
522
+ let subject = null;
523
+ let constraints = null;
524
+ if (req.method === "GET") {
525
+ intentVal = req.nextUrl.searchParams.get("intent") ?? void 0;
526
+ } else {
527
+ let body = {};
528
+ try {
529
+ body = await req.json();
530
+ } catch {
531
+ }
532
+ intentVal = body.intent;
533
+ subject = body.subject ?? null;
534
+ constraints = body.constraints ?? null;
499
535
  }
500
536
  const validIntents = [...sdk.getIntentRegistry().keys()];
501
- if (!body.intent || !isValidIntent(body.intent, validIntents)) {
537
+ if (!intentVal || !isValidIntent(intentVal, validIntents)) {
502
538
  void shipAudit2(cp, licenseKey, auditRow(sdk, req, pathname, 400, fp, {
503
539
  trust_mode: "unmanaged_automation",
504
540
  intent_scope: "none",
@@ -506,14 +542,17 @@ function agentixMiddleware(sdk) {
506
542
  decision: "deny",
507
543
  decision_reason: "invalid_intent",
508
544
  policy_id: "intent-validation",
509
- metadata: { supplied_intent: body.intent ?? null }
545
+ metadata: { supplied_intent: intentVal ?? null }
510
546
  }));
511
- return server_js.NextResponse.json({ error: "invalid_intent", valid_intents: validIntents }, { status: 400 });
547
+ return server_js.NextResponse.json({
548
+ error: "invalid_intent",
549
+ valid_intents: validIntents,
550
+ hint: `Use one of the valid_intents above. Example: GET ${baseUrl}/agent/v1/declare_intent?intent=${validIntents[0] ?? "<intent>"}`
551
+ }, { status: 400 });
512
552
  }
513
- const intent = body.intent;
514
- const ttl = sdk.config.tokenTtlMs ?? 15 * 60 * 1e3;
553
+ const intent = intentVal;
515
554
  const iat = Math.floor(Date.now() / 1e3);
516
- const exp = iat + Math.floor(ttl / 1e3);
555
+ const exp = iat + ttlSec;
517
556
  const raw = await issueTokenWeb(tokenSecret, { intent, domain: sdk.getResolvedDomain(), binding: fp, iat, exp });
518
557
  const jti = tokenIdWeb(raw);
519
558
  void shipAudit2(cp, licenseKey, auditRow(sdk, req, pathname, 200, fp, {
@@ -523,23 +562,20 @@ function agentixMiddleware(sdk) {
523
562
  decision: "allow",
524
563
  decision_reason: "intent_token_issued",
525
564
  policy_id: "intent-token-issuer",
526
- metadata: { subject: body.subject ?? null, constraints: body.constraints ?? null }
565
+ metadata: { subject, constraints }
527
566
  }));
528
567
  return server_js.NextResponse.json({
529
568
  access_token: raw,
530
569
  token_type: "Bearer",
531
570
  token_id: jti,
532
571
  intent,
533
- expires_in: exp - iat
572
+ expires_in: ttlSec,
573
+ usage: `Set header: Authorization: Bearer ${raw.slice(0, 20)}... on requests to the ${intent} routes.`
534
574
  });
535
575
  }
536
576
  const isHumanPage = !pathname.startsWith("/api/") && !pathname.startsWith("/agent/") && !pathname.startsWith("/_next/") && !pathname.startsWith("/favicon");
537
577
  const score = agentScore(req);
538
578
  if (isHumanPage && score >= 0.5) {
539
- const registry = sdk.getIntentRegistry();
540
- const tools = Object.fromEntries(
541
- [...registry.entries()].map(([intent, entry]) => [intent, { routes: [...entry.routes] }])
542
- );
543
579
  void shipAudit2(cp, licenseKey, auditRow(sdk, req, pathname, 200, fp, {
544
580
  trust_mode: "unmanaged_automation",
545
581
  intent_scope: "none",
@@ -558,12 +594,15 @@ function agentixMiddleware(sdk) {
558
594
  return server_js.NextResponse.json({
559
595
  service: "agentix-intent-sdk",
560
596
  version: "0.2.0",
561
- message: "AI agent detected. Use the agent API instead of the human-facing site.",
562
- tenant_id: sdk.getResolvedTenantId(),
563
- deployment_id: sdk.getDeploymentId(),
564
- discovery: { well_known: `${baseUrl}/.well-known/ai-agent.json`, token_endpoint: `${baseUrl}/agent/v1/declare_intent` },
565
- intents: [...registry.keys()],
566
- tools
597
+ message: "You are an AI agent accessing a human-facing page. This site exposes a structured agent API \u2014 use it instead of scraping HTML.",
598
+ instructions: agentInstructions(ttlSec),
599
+ token_ttl_seconds: ttlSec,
600
+ discovery: {
601
+ well_known: `${baseUrl}/.well-known/ai-agent.json`,
602
+ token_endpoint: `${baseUrl}/agent/v1/declare_intent`,
603
+ note: "Both GET (?intent=<intent>) and POST (JSON body) are accepted at token_endpoint."
604
+ },
605
+ tools: buildTools(sdk, baseUrl)
567
606
  }, { headers: { "cache-control": "no-store" } });
568
607
  }
569
608
  if (req.method === "GET" && pathname === "/robots.txt") {
@@ -597,7 +636,12 @@ function secure(sdk, intent, handler, opts = {}) {
597
636
  const baseUrl = req.nextUrl.origin;
598
637
  if (!raw) {
599
638
  return server_js.NextResponse.json(
600
- { error: "missing_token", agent_discovery: `${baseUrl}/.well-known/ai-agent.json` },
639
+ {
640
+ error: "missing_token",
641
+ message: "This route requires an intent token. Visit agent_discovery to see available tools and their token_url.",
642
+ agent_discovery: `${baseUrl}/.well-known/ai-agent.json`,
643
+ token_url: `${baseUrl}/agent/v1/declare_intent?intent=${encodeURIComponent(intent)}`
644
+ },
601
645
  { status: 401, headers: { "www-authenticate": 'Bearer realm="agentix", error="missing_token"' } }
602
646
  );
603
647
  }
@@ -605,10 +649,21 @@ function secure(sdk, intent, handler, opts = {}) {
605
649
  if (verdict.decision === "allow") return handler(req, ctx);
606
650
  const reason = verdict.reason ?? "denied";
607
651
  if (reason === "missing_token" || reason === "invalid_token") {
608
- return server_js.NextResponse.json({ error: reason, agent_discovery: `${baseUrl}/.well-known/ai-agent.json` }, { status: 401 });
652
+ return server_js.NextResponse.json({
653
+ error: reason,
654
+ message: "Token is missing or expired. Fetch a new one from token_url.",
655
+ agent_discovery: `${baseUrl}/.well-known/ai-agent.json`,
656
+ token_url: `${baseUrl}/agent/v1/declare_intent?intent=${encodeURIComponent(intent)}`
657
+ }, { status: 401 });
609
658
  }
610
659
  if (reason === "out_of_scope") {
611
- return server_js.NextResponse.json({ error: "out_of_scope", required_intent: verdict.required_intent ?? intent }, { status: 403 });
660
+ const required = verdict.required_intent ?? intent;
661
+ return server_js.NextResponse.json({
662
+ error: "out_of_scope",
663
+ message: `Your token is scoped to a different intent. Get a new token for "${required}" from token_url.`,
664
+ required_intent: required,
665
+ token_url: `${baseUrl}/agent/v1/declare_intent?intent=${encodeURIComponent(required)}`
666
+ }, { status: 403 });
612
667
  }
613
668
  return server_js.NextResponse.json({ error: reason }, { status: 401 });
614
669
  };
package/dist/index.d.cts CHANGED
@@ -73,6 +73,9 @@ interface AgentixSDKConfig {
73
73
  /** Map of intent → route string (e.g. { read_jobs: 'GET /api/jobs' }).
74
74
  * Registered at init time so routes appear in discovery before any request. */
75
75
  routes?: Record<string, string>;
76
+ /** Human-readable description per intent, shown in agent discovery responses.
77
+ * e.g. { read_jobs: 'Browse open job listings', read_cvs: 'Access candidate CVs' } */
78
+ descriptions?: Record<string, string>;
76
79
  }
77
80
 
78
81
  declare class AgentixSDK {
package/dist/index.d.ts CHANGED
@@ -73,6 +73,9 @@ interface AgentixSDKConfig {
73
73
  /** Map of intent → route string (e.g. { read_jobs: 'GET /api/jobs' }).
74
74
  * Registered at init time so routes appear in discovery before any request. */
75
75
  routes?: Record<string, string>;
76
+ /** Human-readable description per intent, shown in agent discovery responses.
77
+ * e.g. { read_jobs: 'Browse open job listings', read_cvs: 'Access candidate CVs' } */
78
+ descriptions?: Record<string, string>;
76
79
  }
77
80
 
78
81
  declare class AgentixSDK {
package/dist/index.js CHANGED
@@ -457,6 +457,30 @@ function auditRow(sdk, req, route, status, fp, fields) {
457
457
  ...fields
458
458
  };
459
459
  }
460
+ function buildTools(sdk, baseUrl) {
461
+ const registry = sdk.getIntentRegistry();
462
+ const descriptions = sdk.config.descriptions ?? {};
463
+ return Object.fromEntries(
464
+ [...registry.entries()].map(([intent, entry]) => {
465
+ const tool = {
466
+ routes: [...entry.routes],
467
+ token_url: `${baseUrl}/agent/v1/declare_intent?intent=${encodeURIComponent(intent)}`
468
+ };
469
+ if (descriptions[intent]) tool.description = descriptions[intent];
470
+ return [intent, tool];
471
+ })
472
+ );
473
+ }
474
+ function agentInstructions(ttlSec) {
475
+ return [
476
+ "1. Pick the tool below that matches what you need to do.",
477
+ `2. Fetch the tool's token_url with a GET request (no body, no auth required). You will receive JSON containing an access_token. Tokens are valid for ${ttlSec} seconds.`,
478
+ '3. Include the token in the Authorization header on every API request: "Authorization: Bearer <access_token>"',
479
+ "4. If you receive HTTP 401 (missing_token or invalid_token), your token is missing or expired \u2014 fetch a new one from the same token_url.",
480
+ "5. If you receive HTTP 403 (out_of_scope), your token's intent does not cover this route. Get a new token from the token_url of the correct tool.",
481
+ "6. Each token is scoped to exactly one intent. Use a different token for each capability."
482
+ ];
483
+ }
460
484
  function agentixMiddleware(sdk) {
461
485
  return async (req) => {
462
486
  await sdk.ensureInitialized();
@@ -466,11 +490,8 @@ function agentixMiddleware(sdk) {
466
490
  const cp = (sdk.config.controlPlaneUrl ?? "https://agentix-control-plane.onrender.com").replace(/\/$/, "");
467
491
  const licenseKey = sdk.config.licenseKey;
468
492
  const tokenSecret = sdk.getResolvedTokenSecret();
493
+ const ttlSec = Math.floor((sdk.config.tokenTtlMs ?? 15 * 60 * 1e3) / 1e3);
469
494
  if (req.method === "GET" && pathname === "/.well-known/ai-agent.json") {
470
- const registry = sdk.getIntentRegistry();
471
- const tools = Object.fromEntries(
472
- [...registry.entries()].map(([intent, entry]) => [intent, { routes: [...entry.routes] }])
473
- );
474
495
  void shipAudit2(cp, licenseKey, auditRow(sdk, req, pathname, 200, fp, {
475
496
  trust_mode: "unknown",
476
497
  intent_scope: "none",
@@ -484,19 +505,34 @@ function agentixMiddleware(sdk) {
484
505
  version: "0.2.0",
485
506
  tenant_id: sdk.getResolvedTenantId(),
486
507
  deployment_id: sdk.getDeploymentId(),
487
- discovery: { well_known: `${baseUrl}/.well-known/ai-agent.json`, token_endpoint: `${baseUrl}/agent/v1/declare_intent` },
488
- intents: [...registry.keys()],
489
- tools
508
+ instructions: agentInstructions(ttlSec),
509
+ token_ttl_seconds: ttlSec,
510
+ discovery: {
511
+ well_known: `${baseUrl}/.well-known/ai-agent.json`,
512
+ token_endpoint: `${baseUrl}/agent/v1/declare_intent`,
513
+ note: "Both GET (?intent=<intent>) and POST (JSON body) are accepted at token_endpoint."
514
+ },
515
+ tools: buildTools(sdk, baseUrl)
490
516
  }, { headers: { "cache-control": "no-store" } });
491
517
  }
492
- if (req.method === "POST" && pathname === "/agent/v1/declare_intent") {
493
- let body = {};
494
- try {
495
- body = await req.json();
496
- } catch {
518
+ if (pathname === "/agent/v1/declare_intent" && (req.method === "POST" || req.method === "GET")) {
519
+ let intentVal;
520
+ let subject = null;
521
+ let constraints = null;
522
+ if (req.method === "GET") {
523
+ intentVal = req.nextUrl.searchParams.get("intent") ?? void 0;
524
+ } else {
525
+ let body = {};
526
+ try {
527
+ body = await req.json();
528
+ } catch {
529
+ }
530
+ intentVal = body.intent;
531
+ subject = body.subject ?? null;
532
+ constraints = body.constraints ?? null;
497
533
  }
498
534
  const validIntents = [...sdk.getIntentRegistry().keys()];
499
- if (!body.intent || !isValidIntent(body.intent, validIntents)) {
535
+ if (!intentVal || !isValidIntent(intentVal, validIntents)) {
500
536
  void shipAudit2(cp, licenseKey, auditRow(sdk, req, pathname, 400, fp, {
501
537
  trust_mode: "unmanaged_automation",
502
538
  intent_scope: "none",
@@ -504,14 +540,17 @@ function agentixMiddleware(sdk) {
504
540
  decision: "deny",
505
541
  decision_reason: "invalid_intent",
506
542
  policy_id: "intent-validation",
507
- metadata: { supplied_intent: body.intent ?? null }
543
+ metadata: { supplied_intent: intentVal ?? null }
508
544
  }));
509
- return NextResponse.json({ error: "invalid_intent", valid_intents: validIntents }, { status: 400 });
545
+ return NextResponse.json({
546
+ error: "invalid_intent",
547
+ valid_intents: validIntents,
548
+ hint: `Use one of the valid_intents above. Example: GET ${baseUrl}/agent/v1/declare_intent?intent=${validIntents[0] ?? "<intent>"}`
549
+ }, { status: 400 });
510
550
  }
511
- const intent = body.intent;
512
- const ttl = sdk.config.tokenTtlMs ?? 15 * 60 * 1e3;
551
+ const intent = intentVal;
513
552
  const iat = Math.floor(Date.now() / 1e3);
514
- const exp = iat + Math.floor(ttl / 1e3);
553
+ const exp = iat + ttlSec;
515
554
  const raw = await issueTokenWeb(tokenSecret, { intent, domain: sdk.getResolvedDomain(), binding: fp, iat, exp });
516
555
  const jti = tokenIdWeb(raw);
517
556
  void shipAudit2(cp, licenseKey, auditRow(sdk, req, pathname, 200, fp, {
@@ -521,23 +560,20 @@ function agentixMiddleware(sdk) {
521
560
  decision: "allow",
522
561
  decision_reason: "intent_token_issued",
523
562
  policy_id: "intent-token-issuer",
524
- metadata: { subject: body.subject ?? null, constraints: body.constraints ?? null }
563
+ metadata: { subject, constraints }
525
564
  }));
526
565
  return NextResponse.json({
527
566
  access_token: raw,
528
567
  token_type: "Bearer",
529
568
  token_id: jti,
530
569
  intent,
531
- expires_in: exp - iat
570
+ expires_in: ttlSec,
571
+ usage: `Set header: Authorization: Bearer ${raw.slice(0, 20)}... on requests to the ${intent} routes.`
532
572
  });
533
573
  }
534
574
  const isHumanPage = !pathname.startsWith("/api/") && !pathname.startsWith("/agent/") && !pathname.startsWith("/_next/") && !pathname.startsWith("/favicon");
535
575
  const score = agentScore(req);
536
576
  if (isHumanPage && score >= 0.5) {
537
- const registry = sdk.getIntentRegistry();
538
- const tools = Object.fromEntries(
539
- [...registry.entries()].map(([intent, entry]) => [intent, { routes: [...entry.routes] }])
540
- );
541
577
  void shipAudit2(cp, licenseKey, auditRow(sdk, req, pathname, 200, fp, {
542
578
  trust_mode: "unmanaged_automation",
543
579
  intent_scope: "none",
@@ -556,12 +592,15 @@ function agentixMiddleware(sdk) {
556
592
  return NextResponse.json({
557
593
  service: "agentix-intent-sdk",
558
594
  version: "0.2.0",
559
- message: "AI agent detected. Use the agent API instead of the human-facing site.",
560
- tenant_id: sdk.getResolvedTenantId(),
561
- deployment_id: sdk.getDeploymentId(),
562
- discovery: { well_known: `${baseUrl}/.well-known/ai-agent.json`, token_endpoint: `${baseUrl}/agent/v1/declare_intent` },
563
- intents: [...registry.keys()],
564
- tools
595
+ message: "You are an AI agent accessing a human-facing page. This site exposes a structured agent API \u2014 use it instead of scraping HTML.",
596
+ instructions: agentInstructions(ttlSec),
597
+ token_ttl_seconds: ttlSec,
598
+ discovery: {
599
+ well_known: `${baseUrl}/.well-known/ai-agent.json`,
600
+ token_endpoint: `${baseUrl}/agent/v1/declare_intent`,
601
+ note: "Both GET (?intent=<intent>) and POST (JSON body) are accepted at token_endpoint."
602
+ },
603
+ tools: buildTools(sdk, baseUrl)
565
604
  }, { headers: { "cache-control": "no-store" } });
566
605
  }
567
606
  if (req.method === "GET" && pathname === "/robots.txt") {
@@ -595,7 +634,12 @@ function secure(sdk, intent, handler, opts = {}) {
595
634
  const baseUrl = req.nextUrl.origin;
596
635
  if (!raw) {
597
636
  return NextResponse.json(
598
- { error: "missing_token", agent_discovery: `${baseUrl}/.well-known/ai-agent.json` },
637
+ {
638
+ error: "missing_token",
639
+ message: "This route requires an intent token. Visit agent_discovery to see available tools and their token_url.",
640
+ agent_discovery: `${baseUrl}/.well-known/ai-agent.json`,
641
+ token_url: `${baseUrl}/agent/v1/declare_intent?intent=${encodeURIComponent(intent)}`
642
+ },
599
643
  { status: 401, headers: { "www-authenticate": 'Bearer realm="agentix", error="missing_token"' } }
600
644
  );
601
645
  }
@@ -603,10 +647,21 @@ function secure(sdk, intent, handler, opts = {}) {
603
647
  if (verdict.decision === "allow") return handler(req, ctx);
604
648
  const reason = verdict.reason ?? "denied";
605
649
  if (reason === "missing_token" || reason === "invalid_token") {
606
- return NextResponse.json({ error: reason, agent_discovery: `${baseUrl}/.well-known/ai-agent.json` }, { status: 401 });
650
+ return NextResponse.json({
651
+ error: reason,
652
+ message: "Token is missing or expired. Fetch a new one from token_url.",
653
+ agent_discovery: `${baseUrl}/.well-known/ai-agent.json`,
654
+ token_url: `${baseUrl}/agent/v1/declare_intent?intent=${encodeURIComponent(intent)}`
655
+ }, { status: 401 });
607
656
  }
608
657
  if (reason === "out_of_scope") {
609
- return NextResponse.json({ error: "out_of_scope", required_intent: verdict.required_intent ?? intent }, { status: 403 });
658
+ const required = verdict.required_intent ?? intent;
659
+ return NextResponse.json({
660
+ error: "out_of_scope",
661
+ message: `Your token is scoped to a different intent. Get a new token for "${required}" from token_url.`,
662
+ required_intent: required,
663
+ token_url: `${baseUrl}/agent/v1/declare_intent?intent=${encodeURIComponent(required)}`
664
+ }, { status: 403 });
610
665
  }
611
666
  return NextResponse.json({ error: reason }, { status: 401 });
612
667
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentix-security/nextjs",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "Agentix Next.js adapter — AI agent intent-based authorization for Next.js apps",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",