@agentix-security/nextjs 0.1.11 → 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 +65 -30
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +65 -30
- 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,14 +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]) => [
|
|
475
|
-
intent,
|
|
476
|
-
{ routes: [...entry.routes], token_url: `${baseUrl}/agent/v1/declare_intent?intent=${encodeURIComponent(intent)}` }
|
|
477
|
-
])
|
|
478
|
-
);
|
|
479
497
|
void shipAudit2(cp, licenseKey, auditRow(sdk, req, pathname, 200, fp, {
|
|
480
498
|
trust_mode: "unknown",
|
|
481
499
|
intent_scope: "none",
|
|
@@ -489,13 +507,14 @@ function agentixMiddleware(sdk) {
|
|
|
489
507
|
version: "0.2.0",
|
|
490
508
|
tenant_id: sdk.getResolvedTenantId(),
|
|
491
509
|
deployment_id: sdk.getDeploymentId(),
|
|
510
|
+
instructions: agentInstructions(ttlSec),
|
|
511
|
+
token_ttl_seconds: 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
|
|
515
|
+
note: "Both GET (?intent=<intent>) and POST (JSON body) are accepted at token_endpoint."
|
|
496
516
|
},
|
|
497
|
-
|
|
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({
|
|
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 +
|
|
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:
|
|
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,15 @@ 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
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
discovery: {
|
|
585
|
-
|
|
586
|
-
|
|
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)
|
|
587
606
|
}, { headers: { "cache-control": "no-store" } });
|
|
588
607
|
}
|
|
589
608
|
if (req.method === "GET" && pathname === "/robots.txt") {
|
|
@@ -617,7 +636,12 @@ function secure(sdk, intent, handler, opts = {}) {
|
|
|
617
636
|
const baseUrl = req.nextUrl.origin;
|
|
618
637
|
if (!raw) {
|
|
619
638
|
return server_js.NextResponse.json(
|
|
620
|
-
{
|
|
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
|
+
},
|
|
621
645
|
{ status: 401, headers: { "www-authenticate": 'Bearer realm="agentix", error="missing_token"' } }
|
|
622
646
|
);
|
|
623
647
|
}
|
|
@@ -625,10 +649,21 @@ function secure(sdk, intent, handler, opts = {}) {
|
|
|
625
649
|
if (verdict.decision === "allow") return handler(req, ctx);
|
|
626
650
|
const reason = verdict.reason ?? "denied";
|
|
627
651
|
if (reason === "missing_token" || reason === "invalid_token") {
|
|
628
|
-
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 });
|
|
629
658
|
}
|
|
630
659
|
if (reason === "out_of_scope") {
|
|
631
|
-
|
|
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 });
|
|
632
667
|
}
|
|
633
668
|
return server_js.NextResponse.json({ error: reason }, { status: 401 });
|
|
634
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,14 +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]) => [
|
|
473
|
-
intent,
|
|
474
|
-
{ routes: [...entry.routes], token_url: `${baseUrl}/agent/v1/declare_intent?intent=${encodeURIComponent(intent)}` }
|
|
475
|
-
])
|
|
476
|
-
);
|
|
477
495
|
void shipAudit2(cp, licenseKey, auditRow(sdk, req, pathname, 200, fp, {
|
|
478
496
|
trust_mode: "unknown",
|
|
479
497
|
intent_scope: "none",
|
|
@@ -487,13 +505,14 @@ function agentixMiddleware(sdk) {
|
|
|
487
505
|
version: "0.2.0",
|
|
488
506
|
tenant_id: sdk.getResolvedTenantId(),
|
|
489
507
|
deployment_id: sdk.getDeploymentId(),
|
|
508
|
+
instructions: agentInstructions(ttlSec),
|
|
509
|
+
token_ttl_seconds: 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
|
|
513
|
+
note: "Both GET (?intent=<intent>) and POST (JSON body) are accepted at token_endpoint."
|
|
494
514
|
},
|
|
495
|
-
|
|
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({
|
|
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 +
|
|
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:
|
|
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,15 @@ function agentixMiddleware(sdk) {
|
|
|
576
592
|
return NextResponse.json({
|
|
577
593
|
service: "agentix-intent-sdk",
|
|
578
594
|
version: "0.2.0",
|
|
579
|
-
message: "AI agent
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
discovery: {
|
|
583
|
-
|
|
584
|
-
|
|
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)
|
|
585
604
|
}, { headers: { "cache-control": "no-store" } });
|
|
586
605
|
}
|
|
587
606
|
if (req.method === "GET" && pathname === "/robots.txt") {
|
|
@@ -615,7 +634,12 @@ function secure(sdk, intent, handler, opts = {}) {
|
|
|
615
634
|
const baseUrl = req.nextUrl.origin;
|
|
616
635
|
if (!raw) {
|
|
617
636
|
return NextResponse.json(
|
|
618
|
-
{
|
|
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
|
+
},
|
|
619
643
|
{ status: 401, headers: { "www-authenticate": 'Bearer realm="agentix", error="missing_token"' } }
|
|
620
644
|
);
|
|
621
645
|
}
|
|
@@ -623,10 +647,21 @@ function secure(sdk, intent, handler, opts = {}) {
|
|
|
623
647
|
if (verdict.decision === "allow") return handler(req, ctx);
|
|
624
648
|
const reason = verdict.reason ?? "denied";
|
|
625
649
|
if (reason === "missing_token" || reason === "invalid_token") {
|
|
626
|
-
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 });
|
|
627
656
|
}
|
|
628
657
|
if (reason === "out_of_scope") {
|
|
629
|
-
|
|
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 });
|
|
630
665
|
}
|
|
631
666
|
return NextResponse.json({ error: reason }, { status: 401 });
|
|
632
667
|
};
|