@astrasyncai/verification-gateway 2.4.12 → 2.5.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.
Files changed (91) hide show
  1. package/dist/adapter-interface/interface.d.mts +2 -2
  2. package/dist/adapter-interface/interface.d.ts +2 -2
  3. package/dist/adapters/express.d.mts +2 -2
  4. package/dist/adapters/express.d.ts +2 -2
  5. package/dist/adapters/express.js +224 -42
  6. package/dist/adapters/express.js.map +1 -1
  7. package/dist/adapters/express.mjs +224 -42
  8. package/dist/adapters/express.mjs.map +1 -1
  9. package/dist/adapters/mcp.d.mts +101 -57
  10. package/dist/adapters/mcp.d.ts +101 -57
  11. package/dist/adapters/mcp.js +215 -44
  12. package/dist/adapters/mcp.js.map +1 -1
  13. package/dist/adapters/mcp.mjs +215 -44
  14. package/dist/adapters/mcp.mjs.map +1 -1
  15. package/dist/adapters/nextjs.d.mts +2 -2
  16. package/dist/adapters/nextjs.d.ts +2 -2
  17. package/dist/adapters/nextjs.js +87 -34
  18. package/dist/adapters/nextjs.js.map +1 -1
  19. package/dist/adapters/nextjs.mjs +87 -34
  20. package/dist/adapters/nextjs.mjs.map +1 -1
  21. package/dist/adapters/sdk.d.mts +2 -2
  22. package/dist/adapters/sdk.d.ts +2 -2
  23. package/dist/adapters/sdk.js +61 -28
  24. package/dist/adapters/sdk.js.map +1 -1
  25. package/dist/adapters/sdk.mjs +61 -28
  26. package/dist/adapters/sdk.mjs.map +1 -1
  27. package/dist/agent/index.d.mts +2 -2
  28. package/dist/agent/index.d.ts +2 -2
  29. package/dist/agent/index.js +29 -0
  30. package/dist/agent/index.js.map +1 -1
  31. package/dist/agent/index.mjs +29 -0
  32. package/dist/agent/index.mjs.map +1 -1
  33. package/dist/browser/background.js +102 -30
  34. package/dist/browser/background.js.map +1 -1
  35. package/dist/browser/background.mjs +102 -30
  36. package/dist/browser/background.mjs.map +1 -1
  37. package/dist/browser/browser-adapter.d.mts +2 -2
  38. package/dist/browser/browser-adapter.d.ts +2 -2
  39. package/dist/cli/index.d.mts +2 -2
  40. package/dist/cli/index.d.ts +2 -2
  41. package/dist/cursor/cursor-adapter.d.mts +2 -2
  42. package/dist/cursor/cursor-adapter.d.ts +2 -2
  43. package/dist/cursor/extension.d.mts +2 -2
  44. package/dist/cursor/extension.d.ts +2 -2
  45. package/dist/cursor/extension.js +102 -30
  46. package/dist/cursor/extension.js.map +1 -1
  47. package/dist/cursor/extension.mjs +102 -30
  48. package/dist/cursor/extension.mjs.map +1 -1
  49. package/dist/{express-C1ePFB7n.d.ts → express-CrfwoNAR.d.ts} +1 -1
  50. package/dist/{express-4WStX3PV.d.mts → express-ienhAXps.d.mts} +1 -1
  51. package/dist/gateway/gateway.d.mts +2 -2
  52. package/dist/gateway/gateway.d.ts +2 -2
  53. package/dist/gateway/gateway.js +102 -30
  54. package/dist/gateway/gateway.js.map +1 -1
  55. package/dist/gateway/gateway.mjs +102 -30
  56. package/dist/gateway/gateway.mjs.map +1 -1
  57. package/dist/git-trigger/git-hooks.d.mts +2 -2
  58. package/dist/git-trigger/git-hooks.d.ts +2 -2
  59. package/dist/{index-ChPX4WHl.d.mts → index-B5e2IDWU.d.mts} +1 -1
  60. package/dist/{index-CzJMCgEy.d.ts → index-CCdZxvAr.d.ts} +71 -6
  61. package/dist/{index-D8IEntil.d.mts → index-CEg_WG6y.d.mts} +71 -6
  62. package/dist/{index-Cjm-zBeZ.d.ts → index-DC5f8eoQ.d.ts} +1 -1
  63. package/dist/index.d.mts +39 -9
  64. package/dist/index.d.ts +39 -9
  65. package/dist/index.js +500 -94
  66. package/dist/index.js.map +1 -1
  67. package/dist/index.mjs +497 -94
  68. package/dist/index.mjs.map +1 -1
  69. package/dist/local-evaluator/evaluator.d.mts +2 -2
  70. package/dist/local-evaluator/evaluator.d.ts +2 -2
  71. package/dist/local-evaluator/evaluator.js +12 -2
  72. package/dist/local-evaluator/evaluator.js.map +1 -1
  73. package/dist/local-evaluator/evaluator.mjs +12 -2
  74. package/dist/local-evaluator/evaluator.mjs.map +1 -1
  75. package/dist/{nextjs-BIORS__0.d.ts → nextjs-66R1KW8e.d.ts} +1 -1
  76. package/dist/{nextjs-CjzHdaXA.d.mts → nextjs-DSpisQst.d.mts} +1 -1
  77. package/dist/{sdk-Chhz-FcT.d.mts → sdk-5U_CBRpr.d.mts} +1 -1
  78. package/dist/{sdk-CqTEQAc6.d.ts → sdk-Bm8np66n.d.ts} +1 -1
  79. package/dist/transport/index.d.mts +2 -2
  80. package/dist/transport/index.d.ts +2 -2
  81. package/dist/transport/index.js +146 -28
  82. package/dist/transport/index.js.map +1 -1
  83. package/dist/transport/index.mjs +146 -28
  84. package/dist/transport/index.mjs.map +1 -1
  85. package/dist/{types-L15pYd2c.d.mts → types-B3USs-Kx.d.mts} +42 -1
  86. package/dist/{types-L15pYd2c.d.ts → types-B3USs-Kx.d.ts} +42 -1
  87. package/dist/{types-DNK2BgIf.d.mts → types-CgDCUfo8.d.mts} +1 -1
  88. package/dist/{types-DoWIuzfj.d.ts → types-R5N4ET6x.d.ts} +1 -1
  89. package/dist/ui/index.d.mts +1 -1
  90. package/dist/ui/index.d.ts +1 -1
  91. package/package.json +1 -1
@@ -18,7 +18,66 @@ function hasMinimumAccess(actual, required) {
18
18
  }
19
19
 
20
20
  // src/version.ts
21
- var SDK_VERSION = "2.4.12";
21
+ var SDK_VERSION = "2.4.13";
22
+
23
+ // src/well-known.ts
24
+ var CACHE_TTL_MS = 60 * 60 * 1e3;
25
+ var cache = /* @__PURE__ */ new Map();
26
+ var inflight = /* @__PURE__ */ new Map();
27
+ function wellKnownUrl(apiBaseUrl) {
28
+ const base = apiBaseUrl.replace(/\/api\/?$/, "");
29
+ return `${base}/.well-known/agentic-commerce`;
30
+ }
31
+ async function fetchWellKnown(apiBaseUrl) {
32
+ const url = wellKnownUrl(apiBaseUrl);
33
+ const response = await fetch(url, {
34
+ method: "GET",
35
+ headers: { Accept: "application/json" },
36
+ signal: AbortSignal.timeout(5e3)
37
+ });
38
+ if (!response.ok) {
39
+ throw new Error(
40
+ `AstraSync platform must expose /.well-known/agentic-commerce; got ${response.status} from ${url}. SDK cannot initialise without it.`
41
+ );
42
+ }
43
+ const data = await response.json();
44
+ if (!data.registrationUrl || !data.documentationUrl || !data.verifyAccessUrl) {
45
+ throw new Error(
46
+ `/.well-known/agentic-commerce response missing required fields (registrationUrl, documentationUrl, verifyAccessUrl).`
47
+ );
48
+ }
49
+ return data;
50
+ }
51
+ function prefetchWellKnown(apiBaseUrl) {
52
+ const existing = inflight.get(apiBaseUrl);
53
+ if (existing) return existing;
54
+ const promise = fetchWellKnown(apiBaseUrl).then((data) => {
55
+ cache.set(apiBaseUrl, { data, fetchedAt: Date.now() });
56
+ inflight.delete(apiBaseUrl);
57
+ return data;
58
+ }).catch((err) => {
59
+ inflight.delete(apiBaseUrl);
60
+ throw err;
61
+ });
62
+ inflight.set(apiBaseUrl, promise);
63
+ return promise;
64
+ }
65
+ async function getWellKnownUrls(apiBaseUrl) {
66
+ const entry = cache.get(apiBaseUrl);
67
+ if (entry) {
68
+ if (Date.now() - entry.fetchedAt > CACHE_TTL_MS) {
69
+ prefetchWellKnown(apiBaseUrl).catch(() => {
70
+ });
71
+ }
72
+ return entry.data;
73
+ }
74
+ const pending = inflight.get(apiBaseUrl);
75
+ if (pending) return pending;
76
+ return prefetchWellKnown(apiBaseUrl);
77
+ }
78
+ function getCachedWellKnownUrls(apiBaseUrl) {
79
+ return cache.get(apiBaseUrl)?.data;
80
+ }
22
81
 
23
82
  // src/verify.ts
24
83
  var DEFAULT_CONFIG = {
@@ -37,22 +96,27 @@ var DEFAULT_CONFIG = {
37
96
  };
38
97
  var initCheckPerformed = false;
39
98
  var deprecationWarningShown = false;
40
- async function performInitCheck(apiBaseUrl, debug) {
99
+ async function performInitCheck(apiBaseUrl, debug, strictInit) {
41
100
  initCheckPerformed = true;
42
101
  try {
43
102
  const probeUrl = `${apiBaseUrl}/agents/verify-access`;
44
103
  const response = await fetch(probeUrl, { method: "HEAD" });
45
104
  const contentType = response.headers.get("content-type") ?? "";
46
105
  if (contentType.startsWith("text/html")) {
47
- console.warn(
48
- `[VerificationGateway] apiBaseUrl '${apiBaseUrl}' returned HTML (content-type: ${contentType}). This usually means apiBaseUrl is pointing at a marketing site instead of the API. Expected: 'https://astrasync.ai/api' (prod) or 'https://staging.astrasync.ai/api' (staging). Set disableInitChecks: true on GatewayConfig to silence this warning.`
49
- );
106
+ const message = `[VerificationGateway] apiBaseUrl '${apiBaseUrl}' returned HTML (content-type: ${contentType}). This usually means apiBaseUrl is pointing at a marketing site instead of the API. Expected: 'https://astrasync.ai/api' (prod) or 'https://staging.astrasync.ai/api' (staging).`;
107
+ if (strictInit) {
108
+ throw new Error(`${message} (strictInit=true)`);
109
+ }
110
+ console.warn(`${message} Set disableInitChecks: true on GatewayConfig to silence.`);
50
111
  } else if (debug) {
51
112
  console.log(
52
113
  `[VerificationGateway] init check passed for ${apiBaseUrl} (content-type: ${contentType})`
53
114
  );
54
115
  }
55
116
  } catch (err) {
117
+ if (strictInit) {
118
+ throw err;
119
+ }
56
120
  if (debug) {
57
121
  console.log(`[VerificationGateway] init check failed (non-blocking): ${String(err)}`);
58
122
  }
@@ -76,7 +140,23 @@ function getCacheKey(request) {
76
140
  request.counterpartyType || "",
77
141
  request.isSubAgentRequest ? "1" : "0",
78
142
  request.parentAgentId || "",
79
- request.subAgentDepth ?? ""
143
+ request.subAgentDepth ?? "",
144
+ // Audit F-A1-07: previously-missing dimensions that DO affect the
145
+ // backend verdict. Without these, two requests with different
146
+ // durations (e.g. 60s vs 86400s) collided on the same cache key and
147
+ // the shorter-duration allow served the longer-duration request.
148
+ request.durationRequired ?? "",
149
+ request.invocationProtocol || "",
150
+ request.enableRuntimeChallenge ? "1" : "0",
151
+ // callerMetadata fields contribute to risk model; include the ones
152
+ // backend reads. sourceIp/userAgent/forwardedFor change per-request
153
+ // so their inclusion effectively forces a re-check for any varying
154
+ // client (the right behavior — IP-driven anomaly scoring shouldn't
155
+ // be cached across IPs).
156
+ request.callerMetadata?.sourceIp || "",
157
+ request.callerMetadata?.userAgent || "",
158
+ request.callerMetadata?.forwardedFor || "",
159
+ request.callerMetadata?.agentCardUrl || ""
80
160
  ].join("|");
81
161
  }
82
162
  function getCachedResult(request) {
@@ -102,9 +182,13 @@ function cacheResult(request, result, configuredTtl) {
102
182
  }
103
183
  function extractCredentials(headers, query) {
104
184
  const credentials = {};
185
+ const ASTRA_ID_PATTERN = /^ASTRAE?-[A-Za-z0-9_-]{1,64}$/;
105
186
  const astraIdHeader = headers["x-astra-id"] || headers["X-Astra-Id"] || headers["X-ASTRA-ID"] || headers["x-astra-agentid"] || headers["X-Astra-AgentId"] || headers["x-astra-agent-id"] || headers["X-Astra-Agent-Id"] || headers["X-ASTRA-AGENT-ID"];
106
187
  if (astraIdHeader) {
107
- credentials.astraId = Array.isArray(astraIdHeader) ? astraIdHeader[0] : astraIdHeader;
188
+ const raw = Array.isArray(astraIdHeader) ? astraIdHeader[0] : typeof astraIdHeader === "string" ? astraIdHeader : void 0;
189
+ if (typeof raw === "string" && ASTRA_ID_PATTERN.test(raw)) {
190
+ credentials.astraId = raw;
191
+ }
108
192
  }
109
193
  const apiKeyHeader = headers["x-api-key"] || headers["X-Api-Key"] || headers["X-API-KEY"];
110
194
  if (apiKeyHeader) {
@@ -113,9 +197,11 @@ function extractCredentials(headers, query) {
113
197
  const authHeader = headers["authorization"] || headers["Authorization"];
114
198
  if (authHeader) {
115
199
  const authValue = Array.isArray(authHeader) ? authHeader[0] : authHeader;
116
- credentials.authorizationHeader = authValue;
117
- if (authValue.startsWith("Bearer ")) {
118
- credentials.jwt = authValue.slice(7);
200
+ if (typeof authValue === "string") {
201
+ credentials.authorizationHeader = authValue;
202
+ if (authValue.startsWith("Bearer ")) {
203
+ credentials.jwt = authValue.slice(7);
204
+ }
119
205
  }
120
206
  }
121
207
  if (query) {
@@ -128,21 +214,22 @@ function extractCredentials(headers, query) {
128
214
  }
129
215
  return credentials;
130
216
  }
131
- function createGuidanceResponse(config, reason, options = {}) {
217
+ function createGuidanceResponse(_config, reason, options = {}) {
132
218
  const source = options.source ?? "no_credentials";
133
219
  const isApiError = source === "api_error";
220
+ const urls = options.urls;
134
221
  const guidance = isApiError ? {
135
222
  message: "Verification is temporarily unavailable. Retry with exponential backoff; if the issue persists, contact support with the correlationId.",
136
- registrationUrl: `${config.apiBaseUrl.replace("/api", "")}/register`,
137
- documentationUrl: `${config.apiBaseUrl.replace("/api", "")}/docs/agent-access`,
223
+ registrationUrl: urls?.registrationUrl ?? "",
224
+ documentationUrl: urls?.documentationUrl ?? "",
138
225
  steps: [
139
226
  "Retry the request with exponential backoff",
140
227
  "If failures persist, share the correlationId with support"
141
228
  ]
142
229
  } : {
143
230
  message: "This service verifies AI agents before granting access. Please register your agent with AstraSync.",
144
- registrationUrl: `${config.apiBaseUrl.replace("/api", "")}/register`,
145
- documentationUrl: `${config.apiBaseUrl.replace("/api", "")}/docs/agent-access`,
231
+ registrationUrl: urls?.registrationUrl ?? "",
232
+ documentationUrl: urls?.documentationUrl ?? "",
146
233
  steps: [
147
234
  "Register for an AstraSync account",
148
235
  "Create and register your agent",
@@ -184,7 +271,7 @@ async function callVerifyAccessAPI(config, request) {
184
271
  const { credentials, ...requestData } = request;
185
272
  const body = {
186
273
  ...credentials.astraId && { agentId: credentials.astraId },
187
- purpose: requestData.purpose || "general"
274
+ ...requestData.purpose && { purpose: requestData.purpose }
188
275
  };
189
276
  if (requestData.action) body.action = requestData.action;
190
277
  if (requestData.resourceType) body.resourceType = requestData.resourceType;
@@ -218,12 +305,8 @@ async function callVerifyAccessAPI(config, request) {
218
305
  "Content-Type": "application/json",
219
306
  ...config.customHeaders
220
307
  };
221
- if (credentials.authorizationHeader) {
222
- headers["Authorization"] = credentials.authorizationHeader;
223
- } else if (config.apiKey) {
224
- headers["Authorization"] = `Bearer ${config.apiKey}`;
225
- }
226
308
  if (config.apiKey) {
309
+ headers["Authorization"] = `Bearer ${config.apiKey}`;
227
310
  headers["X-API-Key"] = config.apiKey;
228
311
  }
229
312
  try {
@@ -268,8 +351,13 @@ async function callVerifyAccessAPI(config, request) {
268
351
  }
269
352
  async function verify(config, request) {
270
353
  const mergedConfig = { ...DEFAULT_CONFIG, ...config };
354
+ const urls = mergedConfig.apiBaseUrl ? getCachedWellKnownUrls(mergedConfig.apiBaseUrl) : void 0;
271
355
  if (!initCheckPerformed && !mergedConfig.disableInitChecks && mergedConfig.apiBaseUrl) {
272
- void performInitCheck(mergedConfig.apiBaseUrl, mergedConfig.debug);
356
+ if (mergedConfig.strictInit) {
357
+ await performInitCheck(mergedConfig.apiBaseUrl, mergedConfig.debug, true);
358
+ } else {
359
+ void performInitCheck(mergedConfig.apiBaseUrl, mergedConfig.debug, false);
360
+ }
273
361
  }
274
362
  if (!deprecationWarningShown && (config.minTrustScore !== void 0 || config.minTrustScoreForFull !== void 0)) {
275
363
  deprecationWarningShown = true;
@@ -300,7 +388,8 @@ async function verify(config, request) {
300
388
  if (!apiResponse.success) {
301
389
  return createGuidanceResponse(mergedConfig, apiResponse.error, {
302
390
  source: "api_error",
303
- correlationId: apiResponse.correlationId
391
+ correlationId: apiResponse.correlationId,
392
+ urls
304
393
  });
305
394
  }
306
395
  if (!apiResponse.access?.allowed) {
@@ -323,8 +412,8 @@ async function verify(config, request) {
323
412
  requiresApproval: apiResponse.access?.requiresApproval,
324
413
  guidance: {
325
414
  message: apiResponse.access?.reason || "Access denied by PDLSS policy",
326
- registrationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/register`,
327
- documentationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/docs/pdlss`
415
+ registrationUrl: urls?.registrationUrl ?? "",
416
+ documentationUrl: urls?.documentationUrl ?? ""
328
417
  },
329
418
  verifiedAt: /* @__PURE__ */ new Date(),
330
419
  // Extract sessionId so decisions can be recorded for denials too
@@ -393,13 +482,15 @@ async function verify(config, request) {
393
482
  result.denialReasons = result.recommendationReasons || [
394
483
  "Access denied by AstraSync recommendation"
395
484
  ];
396
- if (result.runtimeChallenge) {
397
- result.guidance = {
398
- message: `Verification failed: ${result.runtimeChallenge.reason || "runtime challenge failed"}`,
399
- registrationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/register`,
400
- documentationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/docs/runtime-challenge`
401
- };
402
- }
485
+ result.guidance = result.runtimeChallenge ? {
486
+ message: `Verification failed: ${result.runtimeChallenge.reason || "runtime challenge failed"}`,
487
+ registrationUrl: urls?.registrationUrl ?? "",
488
+ documentationUrl: urls?.documentationUrl ?? ""
489
+ } : {
490
+ message: result.recommendationReasons?.[0] || "Access denied by AstraSync recommendation",
491
+ registrationUrl: urls?.registrationUrl ?? "",
492
+ documentationUrl: urls?.documentationUrl ?? ""
493
+ };
403
494
  } else if (result.recommendation === "step_up_required") {
404
495
  result.requiresStepUp = true;
405
496
  if (ACCESS_LEVEL_HIERARCHY[result.accessLevel] > ACCESS_LEVEL_HIERARCHY["read-only"]) {
@@ -534,19 +625,22 @@ function extractFromMcpBody(astrasyncMeta, args, key) {
534
625
  }
535
626
  return { value: void 0, source: void 0 };
536
627
  }
537
- function mcpToPdlss(parsed, headerPurpose, headerAction) {
538
- const resource = parsed.toolName ? `mcp:tool/${parsed.toolName}` : `mcp:method/${parsed.method}`;
628
+ function mcpToPdlss(parsed, requestPath, headerPurpose, headerAction, toolGate) {
629
+ const resource = toolGate?.resource ?? requestPath;
539
630
  let purpose;
540
631
  let purposeSource;
541
- if (headerPurpose) {
632
+ if (toolGate?.purpose !== void 0) {
633
+ purpose = toolGate.purpose;
634
+ purposeSource = "tool_gate";
635
+ } else if (headerPurpose) {
542
636
  purpose = headerPurpose;
543
637
  purposeSource = "header";
544
638
  } else if (parsed.purposeFromBody && parsed.purposeSourceFromBody) {
545
639
  purpose = parsed.purposeFromBody;
546
640
  purposeSource = parsed.purposeSourceFromBody;
547
641
  } else {
548
- purpose = "mcp_invoke";
549
- purposeSource = "default_mcp_invoke";
642
+ purpose = void 0;
643
+ purposeSource = void 0;
550
644
  }
551
645
  let action;
552
646
  let actionSource;
@@ -557,7 +651,7 @@ function mcpToPdlss(parsed, headerPurpose, headerAction) {
557
651
  action = parsed.actionFromBody;
558
652
  actionSource = parsed.actionSourceFromBody;
559
653
  } else {
560
- action = parsed.toolName ? `${parsed.method}:${parsed.toolName}` : parsed.method;
654
+ action = parsed.toolName ? parsed.method === "tools/call" ? parsed.toolName : `${parsed.method}:${parsed.toolName}` : parsed.method;
561
655
  actionSource = "transport_layer";
562
656
  }
563
657
  return { purpose, action, resource, purposeSource, actionSource };
@@ -571,11 +665,28 @@ function mcpRiskTier(parsed) {
571
665
  }
572
666
 
573
667
  // src/adapters/mcp.ts
668
+ function normalizeToolGate(gate) {
669
+ return typeof gate === "string" ? { minAccessLevel: gate } : gate;
670
+ }
574
671
  function readSingleHeader(value) {
575
672
  if (typeof value === "string") return value;
576
673
  if (Array.isArray(value)) return value[0];
577
674
  return void 0;
578
675
  }
676
+ function dedupeFailures(result) {
677
+ if (result.failures && result.failures.length > 1) {
678
+ const seen = /* @__PURE__ */ new Set();
679
+ result.failures = result.failures.filter((f) => {
680
+ const key = `${f.dimension}|${f.message}|${f.guidance ?? ""}`;
681
+ if (seen.has(key)) return false;
682
+ seen.add(key);
683
+ return true;
684
+ });
685
+ }
686
+ if (result.denialReasons && result.denialReasons.length > 1) {
687
+ result.denialReasons = [...new Set(result.denialReasons)];
688
+ }
689
+ }
579
690
  function defaultMcpDenied(result, req, res) {
580
691
  const id = req.body?.id ?? null;
581
692
  const status = !result.identityVerified ? 401 : 403;
@@ -600,11 +711,17 @@ function defaultMcpDenied(result, req, res) {
600
711
  });
601
712
  }
602
713
  function resolveMinAccessLevel(parsed, opts) {
603
- if (parsed.toolName && opts.toolGates && opts.toolGates[parsed.toolName] !== void 0) {
604
- return { level: opts.toolGates[parsed.toolName], source: "toolGate" };
714
+ if (!parsed.toolName) {
715
+ if (opts.methodGates && opts.methodGates[parsed.method] !== void 0) {
716
+ return { level: opts.methodGates[parsed.method], source: "methodGate" };
717
+ }
718
+ return { level: "none", source: "discovery_default" };
605
719
  }
606
- if (opts.methodGates && opts.methodGates[parsed.method] !== void 0) {
607
- return { level: opts.methodGates[parsed.method], source: "methodGate" };
720
+ if (opts.toolGates && opts.toolGates[parsed.toolName] !== void 0) {
721
+ return {
722
+ level: normalizeToolGate(opts.toolGates[parsed.toolName]).minAccessLevel,
723
+ source: "toolGate"
724
+ };
608
725
  }
609
726
  return { level: mcpRiskTier(parsed), source: "tier" };
610
727
  }
@@ -615,12 +732,17 @@ function createMcpMiddleware(options) {
615
732
  onAgentIdMismatch = "reject",
616
733
  skip = false,
617
734
  onDenied = defaultMcpDenied,
618
- trustVerifiedHop = true,
735
+ trustVerifiedHop = false,
619
736
  verifiedHopMaxAgeMs,
620
737
  recordDecisions,
621
738
  enableRuntimeChallenge = true,
739
+ failOnError = "open",
622
740
  ...config
623
741
  } = options;
742
+ if (config.apiBaseUrl) {
743
+ prefetchWellKnown(config.apiBaseUrl).catch(() => {
744
+ });
745
+ }
624
746
  return async (req, res, next) => {
625
747
  try {
626
748
  if (skip) return next();
@@ -633,6 +755,7 @@ function createMcpMiddleware(options) {
633
755
  return next();
634
756
  }
635
757
  req.mcpRequest = parsed;
758
+ const wellKnownUrls = config.apiBaseUrl ? await getWellKnownUrls(config.apiBaseUrl).catch(() => void 0) : void 0;
636
759
  const headerRaw = req.headers["x-astra-id"] ?? req.headers["x-astra-agentid"];
637
760
  const headerAstraId = typeof headerRaw === "string" ? headerRaw : Array.isArray(headerRaw) ? headerRaw[0] : void 0;
638
761
  const bodyAstraId = parsed.agentIdFromBody;
@@ -686,9 +809,17 @@ function createMcpMiddleware(options) {
686
809
  }
687
810
  return next();
688
811
  }
812
+ const rawGate = parsed.toolName && toolGates?.[parsed.toolName];
813
+ const gate = rawGate ? normalizeToolGate(rawGate) : void 0;
689
814
  const headerPurpose = readSingleHeader(req.headers["x-astra-purpose"]);
690
815
  const headerAction = readSingleHeader(req.headers["x-astra-action"]);
691
- const pdlss = mcpToPdlss(parsed, headerPurpose, headerAction);
816
+ const pdlss = mcpToPdlss(
817
+ parsed,
818
+ req.path,
819
+ headerPurpose,
820
+ headerAction,
821
+ gate ? { purpose: gate.purpose, resource: gate.resource } : void 0
822
+ );
692
823
  if (config.debug) {
693
824
  console.debug("[mcp-middleware] pdlss resolved", {
694
825
  purpose_source: pdlss.purposeSource,
@@ -726,6 +857,7 @@ function createMcpMiddleware(options) {
726
857
  recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
727
858
  });
728
859
  }
860
+ dedupeFailures(result);
729
861
  onDenied(result, req, res);
730
862
  return;
731
863
  }
@@ -748,6 +880,13 @@ function createMcpMiddleware(options) {
748
880
  };
749
881
  result.failures = [...result.failures ?? [], insufficientFailure];
750
882
  result.denialReasons = [...result.denialReasons ?? [], insufficientFailure.message];
883
+ if (!result.guidance && wellKnownUrls) {
884
+ result.guidance = {
885
+ message: insufficientFailure.message,
886
+ registrationUrl: wellKnownUrls.registrationUrl,
887
+ documentationUrl: wellKnownUrls.documentationUrl
888
+ };
889
+ }
751
890
  if (shouldRecordDecisions) {
752
891
  const overrideKind = gateSource === "toolGate" ? "toolGate" : gateSource === "methodGate" ? "methodGate" : "other";
753
892
  const override = {
@@ -771,6 +910,7 @@ function createMcpMiddleware(options) {
771
910
  });
772
911
  }
773
912
  }
913
+ dedupeFailures(result);
774
914
  onDenied(result, req, res);
775
915
  return;
776
916
  }
@@ -794,7 +934,38 @@ function createMcpMiddleware(options) {
794
934
  }
795
935
  next();
796
936
  } catch (error) {
937
+ const errorClass = error instanceof Error ? error.constructor.name : typeof error;
938
+ const correlationId = req.headers["x-request-id"] || req.headers["x-correlation-id"] || `gen-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
797
939
  console.error("[VerificationGateway/MCP] Middleware error:", error);
940
+ console.warn(
941
+ `[SHADOW] would-have-denied: errorClass=${errorClass} route=${req.method}:${req.path} merchantId=${config.counterpartyId ?? "unknown"} correlationId=${correlationId}`
942
+ );
943
+ if (failOnError === "closed") {
944
+ const result = {
945
+ identityVerified: false,
946
+ policyAllowed: false,
947
+ accessLevel: "none",
948
+ denialReasons: [`MCP middleware internal error: ${errorClass}`],
949
+ failures: [
950
+ {
951
+ dimension: "middleware.internal_error",
952
+ message: `Middleware threw ${errorClass} \u2014 failing closed`
953
+ }
954
+ ],
955
+ verifiedAt: /* @__PURE__ */ new Date(),
956
+ correlationId
957
+ };
958
+ const catchUrls = config.apiBaseUrl ? await getWellKnownUrls(config.apiBaseUrl).catch(() => void 0) : void 0;
959
+ if (catchUrls) {
960
+ result.guidance = {
961
+ message: `Middleware threw ${errorClass} \u2014 failing closed`,
962
+ registrationUrl: catchUrls.registrationUrl,
963
+ documentationUrl: catchUrls.documentationUrl
964
+ };
965
+ }
966
+ dedupeFailures(result);
967
+ return onDenied(result, req, res);
968
+ }
798
969
  next();
799
970
  }
800
971
  };