@agentix-security/nextjs 0.1.11 → 0.1.13

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,31 @@ 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 buildAuthScheme(ttlSec) {
477
+ return {
478
+ type: "intent_bearer",
479
+ summary: "Routes on this service are protected by intent-scoped Bearer tokens. A caller selects the relevant tool from the tools map, obtains a short-lived token from its token_url, and presents it in the Authorization header.",
480
+ token_issuance: `GET <tool.token_url> \u2014 no credentials or request body required. Returns JSON: { access_token, token_type: "Bearer", intent, expires_in }. Tokens are valid for ${ttlSec} seconds.`,
481
+ token_usage: "Authorization: Bearer <access_token> (header on every request to the tool's routes).",
482
+ on_401_missing_or_invalid: "Token is absent or expired. A fresh token is available at the same token_url.",
483
+ on_403_out_of_scope: "Token's intent does not cover the requested route. The response body includes required_intent and a token_url for the correct scope.",
484
+ scope_model: "Each token is scoped to exactly one intent. A separate token is required for each distinct tool."
485
+ };
486
+ }
462
487
  function agentixMiddleware(sdk) {
463
488
  return async (req) => {
464
489
  await sdk.ensureInitialized();
@@ -468,14 +493,8 @@ function agentixMiddleware(sdk) {
468
493
  const cp = (sdk.config.controlPlaneUrl ?? "https://agentix-control-plane.onrender.com").replace(/\/$/, "");
469
494
  const licenseKey = sdk.config.licenseKey;
470
495
  const tokenSecret = sdk.getResolvedTokenSecret();
496
+ const ttlSec = Math.floor((sdk.config.tokenTtlMs ?? 15 * 60 * 1e3) / 1e3);
471
497
  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]) => [
475
- intent,
476
- { routes: [...entry.routes], token_url: `${baseUrl}/agent/v1/declare_intent?intent=${encodeURIComponent(intent)}` }
477
- ])
478
- );
479
498
  void shipAudit2(cp, licenseKey, auditRow(sdk, req, pathname, 200, fp, {
480
499
  trust_mode: "unknown",
481
500
  intent_scope: "none",
@@ -489,13 +508,13 @@ function agentixMiddleware(sdk) {
489
508
  version: "0.2.0",
490
509
  tenant_id: sdk.getResolvedTenantId(),
491
510
  deployment_id: sdk.getDeploymentId(),
511
+ auth_scheme: buildAuthScheme(ttlSec),
492
512
  discovery: {
493
513
  well_known: `${baseUrl}/.well-known/ai-agent.json`,
494
514
  token_endpoint: `${baseUrl}/agent/v1/declare_intent`,
495
- note: "GET /agent/v1/declare_intent?intent=<intent> also supported for non-POST clients"
515
+ note: "Both GET (?intent=<intent>) and POST (JSON body) are accepted at token_endpoint."
496
516
  },
497
- intents: [...registry.keys()],
498
- tools
517
+ tools: buildTools(sdk, baseUrl)
499
518
  }, { headers: { "cache-control": "no-store" } });
500
519
  }
501
520
  if (pathname === "/agent/v1/declare_intent" && (req.method === "POST" || req.method === "GET")) {
@@ -525,12 +544,15 @@ function agentixMiddleware(sdk) {
525
544
  policy_id: "intent-validation",
526
545
  metadata: { supplied_intent: intentVal ?? null }
527
546
  }));
528
- 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 });
529
552
  }
530
553
  const intent = intentVal;
531
- const ttl = sdk.config.tokenTtlMs ?? 15 * 60 * 1e3;
532
554
  const iat = Math.floor(Date.now() / 1e3);
533
- const exp = iat + Math.floor(ttl / 1e3);
555
+ const exp = iat + ttlSec;
534
556
  const raw = await issueTokenWeb(tokenSecret, { intent, domain: sdk.getResolvedDomain(), binding: fp, iat, exp });
535
557
  const jti = tokenIdWeb(raw);
536
558
  void shipAudit2(cp, licenseKey, auditRow(sdk, req, pathname, 200, fp, {
@@ -547,19 +569,13 @@ function agentixMiddleware(sdk) {
547
569
  token_type: "Bearer",
548
570
  token_id: jti,
549
571
  intent,
550
- expires_in: exp - iat
572
+ expires_in: ttlSec,
573
+ usage: `Set header: Authorization: Bearer ${raw.slice(0, 20)}... on requests to the ${intent} routes.`
551
574
  });
552
575
  }
553
576
  const isHumanPage = !pathname.startsWith("/api/") && !pathname.startsWith("/agent/") && !pathname.startsWith("/_next/") && !pathname.startsWith("/favicon");
554
577
  const score = agentScore(req);
555
578
  if (isHumanPage && score >= 0.5) {
556
- const registry = sdk.getIntentRegistry();
557
- const tools = Object.fromEntries(
558
- [...registry.entries()].map(([intent, entry]) => [
559
- intent,
560
- { routes: [...entry.routes], token_url: `${baseUrl}/agent/v1/declare_intent?intent=${encodeURIComponent(intent)}` }
561
- ])
562
- );
563
579
  void shipAudit2(cp, licenseKey, auditRow(sdk, req, pathname, 200, fp, {
564
580
  trust_mode: "unmanaged_automation",
565
581
  intent_scope: "none",
@@ -578,12 +594,14 @@ function agentixMiddleware(sdk) {
578
594
  return server_js.NextResponse.json({
579
595
  service: "agentix-intent-sdk",
580
596
  version: "0.2.0",
581
- message: "AI agent detected. Use the agent API instead of the human-facing site.",
582
- tenant_id: sdk.getResolvedTenantId(),
583
- deployment_id: sdk.getDeploymentId(),
584
- discovery: { well_known: `${baseUrl}/.well-known/ai-agent.json`, token_endpoint: `${baseUrl}/agent/v1/declare_intent` },
585
- intents: [...registry.keys()],
586
- tools
597
+ notice: "This response is served to automated clients. A structured agent API is available at the endpoints below.",
598
+ auth_scheme: buildAuthScheme(ttlSec),
599
+ discovery: {
600
+ well_known: `${baseUrl}/.well-known/ai-agent.json`,
601
+ token_endpoint: `${baseUrl}/agent/v1/declare_intent`,
602
+ note: "Both GET (?intent=<intent>) and POST (JSON body) are accepted at token_endpoint."
603
+ },
604
+ tools: buildTools(sdk, baseUrl)
587
605
  }, { headers: { "cache-control": "no-store" } });
588
606
  }
589
607
  if (req.method === "GET" && pathname === "/robots.txt") {
@@ -617,7 +635,12 @@ function secure(sdk, intent, handler, opts = {}) {
617
635
  const baseUrl = req.nextUrl.origin;
618
636
  if (!raw) {
619
637
  return server_js.NextResponse.json(
620
- { error: "missing_token", agent_discovery: `${baseUrl}/.well-known/ai-agent.json` },
638
+ {
639
+ error: "missing_token",
640
+ message: "This route requires an intent token. Visit agent_discovery to see available tools and their token_url.",
641
+ agent_discovery: `${baseUrl}/.well-known/ai-agent.json`,
642
+ token_url: `${baseUrl}/agent/v1/declare_intent?intent=${encodeURIComponent(intent)}`
643
+ },
621
644
  { status: 401, headers: { "www-authenticate": 'Bearer realm="agentix", error="missing_token"' } }
622
645
  );
623
646
  }
@@ -625,10 +648,21 @@ function secure(sdk, intent, handler, opts = {}) {
625
648
  if (verdict.decision === "allow") return handler(req, ctx);
626
649
  const reason = verdict.reason ?? "denied";
627
650
  if (reason === "missing_token" || reason === "invalid_token") {
628
- return server_js.NextResponse.json({ error: reason, agent_discovery: `${baseUrl}/.well-known/ai-agent.json` }, { status: 401 });
651
+ return server_js.NextResponse.json({
652
+ error: reason,
653
+ message: "Token is missing or expired. Fetch a new one from token_url.",
654
+ agent_discovery: `${baseUrl}/.well-known/ai-agent.json`,
655
+ token_url: `${baseUrl}/agent/v1/declare_intent?intent=${encodeURIComponent(intent)}`
656
+ }, { status: 401 });
629
657
  }
630
658
  if (reason === "out_of_scope") {
631
- return server_js.NextResponse.json({ error: "out_of_scope", required_intent: verdict.required_intent ?? intent }, { status: 403 });
659
+ const required = verdict.required_intent ?? intent;
660
+ return server_js.NextResponse.json({
661
+ error: "out_of_scope",
662
+ message: `Your token is scoped to a different intent. Get a new token for "${required}" from token_url.`,
663
+ required_intent: required,
664
+ token_url: `${baseUrl}/agent/v1/declare_intent?intent=${encodeURIComponent(required)}`
665
+ }, { status: 403 });
632
666
  }
633
667
  return server_js.NextResponse.json({ error: reason }, { status: 401 });
634
668
  };
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,31 @@ 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 buildAuthScheme(ttlSec) {
475
+ return {
476
+ type: "intent_bearer",
477
+ summary: "Routes on this service are protected by intent-scoped Bearer tokens. A caller selects the relevant tool from the tools map, obtains a short-lived token from its token_url, and presents it in the Authorization header.",
478
+ token_issuance: `GET <tool.token_url> \u2014 no credentials or request body required. Returns JSON: { access_token, token_type: "Bearer", intent, expires_in }. Tokens are valid for ${ttlSec} seconds.`,
479
+ token_usage: "Authorization: Bearer <access_token> (header on every request to the tool's routes).",
480
+ on_401_missing_or_invalid: "Token is absent or expired. A fresh token is available at the same token_url.",
481
+ on_403_out_of_scope: "Token's intent does not cover the requested route. The response body includes required_intent and a token_url for the correct scope.",
482
+ scope_model: "Each token is scoped to exactly one intent. A separate token is required for each distinct tool."
483
+ };
484
+ }
460
485
  function agentixMiddleware(sdk) {
461
486
  return async (req) => {
462
487
  await sdk.ensureInitialized();
@@ -466,14 +491,8 @@ function agentixMiddleware(sdk) {
466
491
  const cp = (sdk.config.controlPlaneUrl ?? "https://agentix-control-plane.onrender.com").replace(/\/$/, "");
467
492
  const licenseKey = sdk.config.licenseKey;
468
493
  const tokenSecret = sdk.getResolvedTokenSecret();
494
+ const ttlSec = Math.floor((sdk.config.tokenTtlMs ?? 15 * 60 * 1e3) / 1e3);
469
495
  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]) => [
473
- intent,
474
- { routes: [...entry.routes], token_url: `${baseUrl}/agent/v1/declare_intent?intent=${encodeURIComponent(intent)}` }
475
- ])
476
- );
477
496
  void shipAudit2(cp, licenseKey, auditRow(sdk, req, pathname, 200, fp, {
478
497
  trust_mode: "unknown",
479
498
  intent_scope: "none",
@@ -487,13 +506,13 @@ function agentixMiddleware(sdk) {
487
506
  version: "0.2.0",
488
507
  tenant_id: sdk.getResolvedTenantId(),
489
508
  deployment_id: sdk.getDeploymentId(),
509
+ auth_scheme: buildAuthScheme(ttlSec),
490
510
  discovery: {
491
511
  well_known: `${baseUrl}/.well-known/ai-agent.json`,
492
512
  token_endpoint: `${baseUrl}/agent/v1/declare_intent`,
493
- note: "GET /agent/v1/declare_intent?intent=<intent> also supported for non-POST clients"
513
+ note: "Both GET (?intent=<intent>) and POST (JSON body) are accepted at token_endpoint."
494
514
  },
495
- intents: [...registry.keys()],
496
- tools
515
+ tools: buildTools(sdk, baseUrl)
497
516
  }, { headers: { "cache-control": "no-store" } });
498
517
  }
499
518
  if (pathname === "/agent/v1/declare_intent" && (req.method === "POST" || req.method === "GET")) {
@@ -523,12 +542,15 @@ function agentixMiddleware(sdk) {
523
542
  policy_id: "intent-validation",
524
543
  metadata: { supplied_intent: intentVal ?? null }
525
544
  }));
526
- 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 });
527
550
  }
528
551
  const intent = intentVal;
529
- const ttl = sdk.config.tokenTtlMs ?? 15 * 60 * 1e3;
530
552
  const iat = Math.floor(Date.now() / 1e3);
531
- const exp = iat + Math.floor(ttl / 1e3);
553
+ const exp = iat + ttlSec;
532
554
  const raw = await issueTokenWeb(tokenSecret, { intent, domain: sdk.getResolvedDomain(), binding: fp, iat, exp });
533
555
  const jti = tokenIdWeb(raw);
534
556
  void shipAudit2(cp, licenseKey, auditRow(sdk, req, pathname, 200, fp, {
@@ -545,19 +567,13 @@ function agentixMiddleware(sdk) {
545
567
  token_type: "Bearer",
546
568
  token_id: jti,
547
569
  intent,
548
- expires_in: exp - iat
570
+ expires_in: ttlSec,
571
+ usage: `Set header: Authorization: Bearer ${raw.slice(0, 20)}... on requests to the ${intent} routes.`
549
572
  });
550
573
  }
551
574
  const isHumanPage = !pathname.startsWith("/api/") && !pathname.startsWith("/agent/") && !pathname.startsWith("/_next/") && !pathname.startsWith("/favicon");
552
575
  const score = agentScore(req);
553
576
  if (isHumanPage && score >= 0.5) {
554
- const registry = sdk.getIntentRegistry();
555
- const tools = Object.fromEntries(
556
- [...registry.entries()].map(([intent, entry]) => [
557
- intent,
558
- { routes: [...entry.routes], token_url: `${baseUrl}/agent/v1/declare_intent?intent=${encodeURIComponent(intent)}` }
559
- ])
560
- );
561
577
  void shipAudit2(cp, licenseKey, auditRow(sdk, req, pathname, 200, fp, {
562
578
  trust_mode: "unmanaged_automation",
563
579
  intent_scope: "none",
@@ -576,12 +592,14 @@ function agentixMiddleware(sdk) {
576
592
  return NextResponse.json({
577
593
  service: "agentix-intent-sdk",
578
594
  version: "0.2.0",
579
- message: "AI agent detected. Use the agent API instead of the human-facing site.",
580
- tenant_id: sdk.getResolvedTenantId(),
581
- deployment_id: sdk.getDeploymentId(),
582
- discovery: { well_known: `${baseUrl}/.well-known/ai-agent.json`, token_endpoint: `${baseUrl}/agent/v1/declare_intent` },
583
- intents: [...registry.keys()],
584
- tools
595
+ notice: "This response is served to automated clients. A structured agent API is available at the endpoints below.",
596
+ auth_scheme: buildAuthScheme(ttlSec),
597
+ discovery: {
598
+ well_known: `${baseUrl}/.well-known/ai-agent.json`,
599
+ token_endpoint: `${baseUrl}/agent/v1/declare_intent`,
600
+ note: "Both GET (?intent=<intent>) and POST (JSON body) are accepted at token_endpoint."
601
+ },
602
+ tools: buildTools(sdk, baseUrl)
585
603
  }, { headers: { "cache-control": "no-store" } });
586
604
  }
587
605
  if (req.method === "GET" && pathname === "/robots.txt") {
@@ -615,7 +633,12 @@ function secure(sdk, intent, handler, opts = {}) {
615
633
  const baseUrl = req.nextUrl.origin;
616
634
  if (!raw) {
617
635
  return NextResponse.json(
618
- { error: "missing_token", agent_discovery: `${baseUrl}/.well-known/ai-agent.json` },
636
+ {
637
+ error: "missing_token",
638
+ message: "This route requires an intent token. Visit agent_discovery to see available tools and their token_url.",
639
+ agent_discovery: `${baseUrl}/.well-known/ai-agent.json`,
640
+ token_url: `${baseUrl}/agent/v1/declare_intent?intent=${encodeURIComponent(intent)}`
641
+ },
619
642
  { status: 401, headers: { "www-authenticate": 'Bearer realm="agentix", error="missing_token"' } }
620
643
  );
621
644
  }
@@ -623,10 +646,21 @@ function secure(sdk, intent, handler, opts = {}) {
623
646
  if (verdict.decision === "allow") return handler(req, ctx);
624
647
  const reason = verdict.reason ?? "denied";
625
648
  if (reason === "missing_token" || reason === "invalid_token") {
626
- return NextResponse.json({ error: reason, agent_discovery: `${baseUrl}/.well-known/ai-agent.json` }, { status: 401 });
649
+ return NextResponse.json({
650
+ error: reason,
651
+ message: "Token is missing or expired. Fetch a new one from token_url.",
652
+ agent_discovery: `${baseUrl}/.well-known/ai-agent.json`,
653
+ token_url: `${baseUrl}/agent/v1/declare_intent?intent=${encodeURIComponent(intent)}`
654
+ }, { status: 401 });
627
655
  }
628
656
  if (reason === "out_of_scope") {
629
- return NextResponse.json({ error: "out_of_scope", required_intent: verdict.required_intent ?? intent }, { status: 403 });
657
+ const required = verdict.required_intent ?? intent;
658
+ return NextResponse.json({
659
+ error: "out_of_scope",
660
+ message: `Your token is scoped to a different intent. Get a new token for "${required}" from token_url.`,
661
+ required_intent: required,
662
+ token_url: `${baseUrl}/agent/v1/declare_intent?intent=${encodeURIComponent(required)}`
663
+ }, { status: 403 });
630
664
  }
631
665
  return NextResponse.json({ error: reason }, { status: 401 });
632
666
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentix-security/nextjs",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
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",