@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 +88 -33
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +88 -33
- package/package.json +1 -1
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
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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"
|
|
495
|
-
let
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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 (!
|
|
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:
|
|
545
|
+
metadata: { supplied_intent: intentVal ?? null }
|
|
510
546
|
}));
|
|
511
|
-
return server_js.NextResponse.json({
|
|
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 =
|
|
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 +
|
|
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
|
|
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:
|
|
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
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
discovery: {
|
|
565
|
-
|
|
566
|
-
|
|
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
|
-
{
|
|
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({
|
|
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
|
-
|
|
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
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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"
|
|
493
|
-
let
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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 (!
|
|
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:
|
|
543
|
+
metadata: { supplied_intent: intentVal ?? null }
|
|
508
544
|
}));
|
|
509
|
-
return NextResponse.json({
|
|
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 =
|
|
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 +
|
|
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
|
|
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:
|
|
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
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
discovery: {
|
|
563
|
-
|
|
564
|
-
|
|
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
|
-
{
|
|
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({
|
|
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
|
-
|
|
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
|
};
|