@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
@@ -51,7 +51,66 @@ function hasMinimumAccess(actual, required) {
51
51
  }
52
52
 
53
53
  // src/version.ts
54
- var SDK_VERSION = "2.4.12";
54
+ var SDK_VERSION = "2.4.13";
55
+
56
+ // src/well-known.ts
57
+ var CACHE_TTL_MS = 60 * 60 * 1e3;
58
+ var cache = /* @__PURE__ */ new Map();
59
+ var inflight = /* @__PURE__ */ new Map();
60
+ function wellKnownUrl(apiBaseUrl) {
61
+ const base = apiBaseUrl.replace(/\/api\/?$/, "");
62
+ return `${base}/.well-known/agentic-commerce`;
63
+ }
64
+ async function fetchWellKnown(apiBaseUrl) {
65
+ const url = wellKnownUrl(apiBaseUrl);
66
+ const response = await fetch(url, {
67
+ method: "GET",
68
+ headers: { Accept: "application/json" },
69
+ signal: AbortSignal.timeout(5e3)
70
+ });
71
+ if (!response.ok) {
72
+ throw new Error(
73
+ `AstraSync platform must expose /.well-known/agentic-commerce; got ${response.status} from ${url}. SDK cannot initialise without it.`
74
+ );
75
+ }
76
+ const data = await response.json();
77
+ if (!data.registrationUrl || !data.documentationUrl || !data.verifyAccessUrl) {
78
+ throw new Error(
79
+ `/.well-known/agentic-commerce response missing required fields (registrationUrl, documentationUrl, verifyAccessUrl).`
80
+ );
81
+ }
82
+ return data;
83
+ }
84
+ function prefetchWellKnown(apiBaseUrl) {
85
+ const existing = inflight.get(apiBaseUrl);
86
+ if (existing) return existing;
87
+ const promise = fetchWellKnown(apiBaseUrl).then((data) => {
88
+ cache.set(apiBaseUrl, { data, fetchedAt: Date.now() });
89
+ inflight.delete(apiBaseUrl);
90
+ return data;
91
+ }).catch((err) => {
92
+ inflight.delete(apiBaseUrl);
93
+ throw err;
94
+ });
95
+ inflight.set(apiBaseUrl, promise);
96
+ return promise;
97
+ }
98
+ async function getWellKnownUrls(apiBaseUrl) {
99
+ const entry = cache.get(apiBaseUrl);
100
+ if (entry) {
101
+ if (Date.now() - entry.fetchedAt > CACHE_TTL_MS) {
102
+ prefetchWellKnown(apiBaseUrl).catch(() => {
103
+ });
104
+ }
105
+ return entry.data;
106
+ }
107
+ const pending = inflight.get(apiBaseUrl);
108
+ if (pending) return pending;
109
+ return prefetchWellKnown(apiBaseUrl);
110
+ }
111
+ function getCachedWellKnownUrls(apiBaseUrl) {
112
+ return cache.get(apiBaseUrl)?.data;
113
+ }
55
114
 
56
115
  // src/verify.ts
57
116
  var DEFAULT_CONFIG = {
@@ -70,22 +129,27 @@ var DEFAULT_CONFIG = {
70
129
  };
71
130
  var initCheckPerformed = false;
72
131
  var deprecationWarningShown = false;
73
- async function performInitCheck(apiBaseUrl, debug) {
132
+ async function performInitCheck(apiBaseUrl, debug, strictInit) {
74
133
  initCheckPerformed = true;
75
134
  try {
76
135
  const probeUrl = `${apiBaseUrl}/agents/verify-access`;
77
136
  const response = await fetch(probeUrl, { method: "HEAD" });
78
137
  const contentType = response.headers.get("content-type") ?? "";
79
138
  if (contentType.startsWith("text/html")) {
80
- console.warn(
81
- `[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.`
82
- );
139
+ 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).`;
140
+ if (strictInit) {
141
+ throw new Error(`${message} (strictInit=true)`);
142
+ }
143
+ console.warn(`${message} Set disableInitChecks: true on GatewayConfig to silence.`);
83
144
  } else if (debug) {
84
145
  console.log(
85
146
  `[VerificationGateway] init check passed for ${apiBaseUrl} (content-type: ${contentType})`
86
147
  );
87
148
  }
88
149
  } catch (err) {
150
+ if (strictInit) {
151
+ throw err;
152
+ }
89
153
  if (debug) {
90
154
  console.log(`[VerificationGateway] init check failed (non-blocking): ${String(err)}`);
91
155
  }
@@ -109,7 +173,23 @@ function getCacheKey(request) {
109
173
  request.counterpartyType || "",
110
174
  request.isSubAgentRequest ? "1" : "0",
111
175
  request.parentAgentId || "",
112
- request.subAgentDepth ?? ""
176
+ request.subAgentDepth ?? "",
177
+ // Audit F-A1-07: previously-missing dimensions that DO affect the
178
+ // backend verdict. Without these, two requests with different
179
+ // durations (e.g. 60s vs 86400s) collided on the same cache key and
180
+ // the shorter-duration allow served the longer-duration request.
181
+ request.durationRequired ?? "",
182
+ request.invocationProtocol || "",
183
+ request.enableRuntimeChallenge ? "1" : "0",
184
+ // callerMetadata fields contribute to risk model; include the ones
185
+ // backend reads. sourceIp/userAgent/forwardedFor change per-request
186
+ // so their inclusion effectively forces a re-check for any varying
187
+ // client (the right behavior — IP-driven anomaly scoring shouldn't
188
+ // be cached across IPs).
189
+ request.callerMetadata?.sourceIp || "",
190
+ request.callerMetadata?.userAgent || "",
191
+ request.callerMetadata?.forwardedFor || "",
192
+ request.callerMetadata?.agentCardUrl || ""
113
193
  ].join("|");
114
194
  }
115
195
  function getCachedResult(request) {
@@ -135,9 +215,13 @@ function cacheResult(request, result, configuredTtl) {
135
215
  }
136
216
  function extractCredentials(headers, query) {
137
217
  const credentials = {};
218
+ const ASTRA_ID_PATTERN = /^ASTRAE?-[A-Za-z0-9_-]{1,64}$/;
138
219
  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"];
139
220
  if (astraIdHeader) {
140
- credentials.astraId = Array.isArray(astraIdHeader) ? astraIdHeader[0] : astraIdHeader;
221
+ const raw = Array.isArray(astraIdHeader) ? astraIdHeader[0] : typeof astraIdHeader === "string" ? astraIdHeader : void 0;
222
+ if (typeof raw === "string" && ASTRA_ID_PATTERN.test(raw)) {
223
+ credentials.astraId = raw;
224
+ }
141
225
  }
142
226
  const apiKeyHeader = headers["x-api-key"] || headers["X-Api-Key"] || headers["X-API-KEY"];
143
227
  if (apiKeyHeader) {
@@ -146,9 +230,11 @@ function extractCredentials(headers, query) {
146
230
  const authHeader = headers["authorization"] || headers["Authorization"];
147
231
  if (authHeader) {
148
232
  const authValue = Array.isArray(authHeader) ? authHeader[0] : authHeader;
149
- credentials.authorizationHeader = authValue;
150
- if (authValue.startsWith("Bearer ")) {
151
- credentials.jwt = authValue.slice(7);
233
+ if (typeof authValue === "string") {
234
+ credentials.authorizationHeader = authValue;
235
+ if (authValue.startsWith("Bearer ")) {
236
+ credentials.jwt = authValue.slice(7);
237
+ }
152
238
  }
153
239
  }
154
240
  if (query) {
@@ -161,21 +247,22 @@ function extractCredentials(headers, query) {
161
247
  }
162
248
  return credentials;
163
249
  }
164
- function createGuidanceResponse(config, reason, options = {}) {
250
+ function createGuidanceResponse(_config, reason, options = {}) {
165
251
  const source = options.source ?? "no_credentials";
166
252
  const isApiError = source === "api_error";
253
+ const urls = options.urls;
167
254
  const guidance = isApiError ? {
168
255
  message: "Verification is temporarily unavailable. Retry with exponential backoff; if the issue persists, contact support with the correlationId.",
169
- registrationUrl: `${config.apiBaseUrl.replace("/api", "")}/register`,
170
- documentationUrl: `${config.apiBaseUrl.replace("/api", "")}/docs/agent-access`,
256
+ registrationUrl: urls?.registrationUrl ?? "",
257
+ documentationUrl: urls?.documentationUrl ?? "",
171
258
  steps: [
172
259
  "Retry the request with exponential backoff",
173
260
  "If failures persist, share the correlationId with support"
174
261
  ]
175
262
  } : {
176
263
  message: "This service verifies AI agents before granting access. Please register your agent with AstraSync.",
177
- registrationUrl: `${config.apiBaseUrl.replace("/api", "")}/register`,
178
- documentationUrl: `${config.apiBaseUrl.replace("/api", "")}/docs/agent-access`,
264
+ registrationUrl: urls?.registrationUrl ?? "",
265
+ documentationUrl: urls?.documentationUrl ?? "",
179
266
  steps: [
180
267
  "Register for an AstraSync account",
181
268
  "Create and register your agent",
@@ -217,7 +304,7 @@ async function callVerifyAccessAPI(config, request) {
217
304
  const { credentials, ...requestData } = request;
218
305
  const body = {
219
306
  ...credentials.astraId && { agentId: credentials.astraId },
220
- purpose: requestData.purpose || "general"
307
+ ...requestData.purpose && { purpose: requestData.purpose }
221
308
  };
222
309
  if (requestData.action) body.action = requestData.action;
223
310
  if (requestData.resourceType) body.resourceType = requestData.resourceType;
@@ -251,12 +338,8 @@ async function callVerifyAccessAPI(config, request) {
251
338
  "Content-Type": "application/json",
252
339
  ...config.customHeaders
253
340
  };
254
- if (credentials.authorizationHeader) {
255
- headers["Authorization"] = credentials.authorizationHeader;
256
- } else if (config.apiKey) {
257
- headers["Authorization"] = `Bearer ${config.apiKey}`;
258
- }
259
341
  if (config.apiKey) {
342
+ headers["Authorization"] = `Bearer ${config.apiKey}`;
260
343
  headers["X-API-Key"] = config.apiKey;
261
344
  }
262
345
  try {
@@ -301,8 +384,13 @@ async function callVerifyAccessAPI(config, request) {
301
384
  }
302
385
  async function verify(config, request) {
303
386
  const mergedConfig = { ...DEFAULT_CONFIG, ...config };
387
+ const urls = mergedConfig.apiBaseUrl ? getCachedWellKnownUrls(mergedConfig.apiBaseUrl) : void 0;
304
388
  if (!initCheckPerformed && !mergedConfig.disableInitChecks && mergedConfig.apiBaseUrl) {
305
- void performInitCheck(mergedConfig.apiBaseUrl, mergedConfig.debug);
389
+ if (mergedConfig.strictInit) {
390
+ await performInitCheck(mergedConfig.apiBaseUrl, mergedConfig.debug, true);
391
+ } else {
392
+ void performInitCheck(mergedConfig.apiBaseUrl, mergedConfig.debug, false);
393
+ }
306
394
  }
307
395
  if (!deprecationWarningShown && (config.minTrustScore !== void 0 || config.minTrustScoreForFull !== void 0)) {
308
396
  deprecationWarningShown = true;
@@ -333,7 +421,8 @@ async function verify(config, request) {
333
421
  if (!apiResponse.success) {
334
422
  return createGuidanceResponse(mergedConfig, apiResponse.error, {
335
423
  source: "api_error",
336
- correlationId: apiResponse.correlationId
424
+ correlationId: apiResponse.correlationId,
425
+ urls
337
426
  });
338
427
  }
339
428
  if (!apiResponse.access?.allowed) {
@@ -356,8 +445,8 @@ async function verify(config, request) {
356
445
  requiresApproval: apiResponse.access?.requiresApproval,
357
446
  guidance: {
358
447
  message: apiResponse.access?.reason || "Access denied by PDLSS policy",
359
- registrationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/register`,
360
- documentationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/docs/pdlss`
448
+ registrationUrl: urls?.registrationUrl ?? "",
449
+ documentationUrl: urls?.documentationUrl ?? ""
361
450
  },
362
451
  verifiedAt: /* @__PURE__ */ new Date(),
363
452
  // Extract sessionId so decisions can be recorded for denials too
@@ -426,13 +515,15 @@ async function verify(config, request) {
426
515
  result.denialReasons = result.recommendationReasons || [
427
516
  "Access denied by AstraSync recommendation"
428
517
  ];
429
- if (result.runtimeChallenge) {
430
- result.guidance = {
431
- message: `Verification failed: ${result.runtimeChallenge.reason || "runtime challenge failed"}`,
432
- registrationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/register`,
433
- documentationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/docs/runtime-challenge`
434
- };
435
- }
518
+ result.guidance = result.runtimeChallenge ? {
519
+ message: `Verification failed: ${result.runtimeChallenge.reason || "runtime challenge failed"}`,
520
+ registrationUrl: urls?.registrationUrl ?? "",
521
+ documentationUrl: urls?.documentationUrl ?? ""
522
+ } : {
523
+ message: result.recommendationReasons?.[0] || "Access denied by AstraSync recommendation",
524
+ registrationUrl: urls?.registrationUrl ?? "",
525
+ documentationUrl: urls?.documentationUrl ?? ""
526
+ };
436
527
  } else if (result.recommendation === "step_up_required") {
437
528
  result.requiresStepUp = true;
438
529
  if (ACCESS_LEVEL_HIERARCHY[result.accessLevel] > ACCESS_LEVEL_HIERARCHY["read-only"]) {
@@ -567,19 +658,22 @@ function extractFromMcpBody(astrasyncMeta, args, key) {
567
658
  }
568
659
  return { value: void 0, source: void 0 };
569
660
  }
570
- function mcpToPdlss(parsed, headerPurpose, headerAction) {
571
- const resource = parsed.toolName ? `mcp:tool/${parsed.toolName}` : `mcp:method/${parsed.method}`;
661
+ function mcpToPdlss(parsed, requestPath, headerPurpose, headerAction, toolGate) {
662
+ const resource = toolGate?.resource ?? requestPath;
572
663
  let purpose;
573
664
  let purposeSource;
574
- if (headerPurpose) {
665
+ if (toolGate?.purpose !== void 0) {
666
+ purpose = toolGate.purpose;
667
+ purposeSource = "tool_gate";
668
+ } else if (headerPurpose) {
575
669
  purpose = headerPurpose;
576
670
  purposeSource = "header";
577
671
  } else if (parsed.purposeFromBody && parsed.purposeSourceFromBody) {
578
672
  purpose = parsed.purposeFromBody;
579
673
  purposeSource = parsed.purposeSourceFromBody;
580
674
  } else {
581
- purpose = "mcp_invoke";
582
- purposeSource = "default_mcp_invoke";
675
+ purpose = void 0;
676
+ purposeSource = void 0;
583
677
  }
584
678
  let action;
585
679
  let actionSource;
@@ -590,7 +684,7 @@ function mcpToPdlss(parsed, headerPurpose, headerAction) {
590
684
  action = parsed.actionFromBody;
591
685
  actionSource = parsed.actionSourceFromBody;
592
686
  } else {
593
- action = parsed.toolName ? `${parsed.method}:${parsed.toolName}` : parsed.method;
687
+ action = parsed.toolName ? parsed.method === "tools/call" ? parsed.toolName : `${parsed.method}:${parsed.toolName}` : parsed.method;
594
688
  actionSource = "transport_layer";
595
689
  }
596
690
  return { purpose, action, resource, purposeSource, actionSource };
@@ -604,11 +698,28 @@ function mcpRiskTier(parsed) {
604
698
  }
605
699
 
606
700
  // src/adapters/mcp.ts
701
+ function normalizeToolGate(gate) {
702
+ return typeof gate === "string" ? { minAccessLevel: gate } : gate;
703
+ }
607
704
  function readSingleHeader(value) {
608
705
  if (typeof value === "string") return value;
609
706
  if (Array.isArray(value)) return value[0];
610
707
  return void 0;
611
708
  }
709
+ function dedupeFailures(result) {
710
+ if (result.failures && result.failures.length > 1) {
711
+ const seen = /* @__PURE__ */ new Set();
712
+ result.failures = result.failures.filter((f) => {
713
+ const key = `${f.dimension}|${f.message}|${f.guidance ?? ""}`;
714
+ if (seen.has(key)) return false;
715
+ seen.add(key);
716
+ return true;
717
+ });
718
+ }
719
+ if (result.denialReasons && result.denialReasons.length > 1) {
720
+ result.denialReasons = [...new Set(result.denialReasons)];
721
+ }
722
+ }
612
723
  function defaultMcpDenied(result, req, res) {
613
724
  const id = req.body?.id ?? null;
614
725
  const status = !result.identityVerified ? 401 : 403;
@@ -633,11 +744,17 @@ function defaultMcpDenied(result, req, res) {
633
744
  });
634
745
  }
635
746
  function resolveMinAccessLevel(parsed, opts) {
636
- if (parsed.toolName && opts.toolGates && opts.toolGates[parsed.toolName] !== void 0) {
637
- return { level: opts.toolGates[parsed.toolName], source: "toolGate" };
747
+ if (!parsed.toolName) {
748
+ if (opts.methodGates && opts.methodGates[parsed.method] !== void 0) {
749
+ return { level: opts.methodGates[parsed.method], source: "methodGate" };
750
+ }
751
+ return { level: "none", source: "discovery_default" };
638
752
  }
639
- if (opts.methodGates && opts.methodGates[parsed.method] !== void 0) {
640
- return { level: opts.methodGates[parsed.method], source: "methodGate" };
753
+ if (opts.toolGates && opts.toolGates[parsed.toolName] !== void 0) {
754
+ return {
755
+ level: normalizeToolGate(opts.toolGates[parsed.toolName]).minAccessLevel,
756
+ source: "toolGate"
757
+ };
641
758
  }
642
759
  return { level: mcpRiskTier(parsed), source: "tier" };
643
760
  }
@@ -648,12 +765,17 @@ function createMcpMiddleware(options) {
648
765
  onAgentIdMismatch = "reject",
649
766
  skip = false,
650
767
  onDenied = defaultMcpDenied,
651
- trustVerifiedHop = true,
768
+ trustVerifiedHop = false,
652
769
  verifiedHopMaxAgeMs,
653
770
  recordDecisions,
654
771
  enableRuntimeChallenge = true,
772
+ failOnError = "open",
655
773
  ...config
656
774
  } = options;
775
+ if (config.apiBaseUrl) {
776
+ prefetchWellKnown(config.apiBaseUrl).catch(() => {
777
+ });
778
+ }
657
779
  return async (req, res, next) => {
658
780
  try {
659
781
  if (skip) return next();
@@ -666,6 +788,7 @@ function createMcpMiddleware(options) {
666
788
  return next();
667
789
  }
668
790
  req.mcpRequest = parsed;
791
+ const wellKnownUrls = config.apiBaseUrl ? await getWellKnownUrls(config.apiBaseUrl).catch(() => void 0) : void 0;
669
792
  const headerRaw = req.headers["x-astra-id"] ?? req.headers["x-astra-agentid"];
670
793
  const headerAstraId = typeof headerRaw === "string" ? headerRaw : Array.isArray(headerRaw) ? headerRaw[0] : void 0;
671
794
  const bodyAstraId = parsed.agentIdFromBody;
@@ -719,9 +842,17 @@ function createMcpMiddleware(options) {
719
842
  }
720
843
  return next();
721
844
  }
845
+ const rawGate = parsed.toolName && toolGates?.[parsed.toolName];
846
+ const gate = rawGate ? normalizeToolGate(rawGate) : void 0;
722
847
  const headerPurpose = readSingleHeader(req.headers["x-astra-purpose"]);
723
848
  const headerAction = readSingleHeader(req.headers["x-astra-action"]);
724
- const pdlss = mcpToPdlss(parsed, headerPurpose, headerAction);
849
+ const pdlss = mcpToPdlss(
850
+ parsed,
851
+ req.path,
852
+ headerPurpose,
853
+ headerAction,
854
+ gate ? { purpose: gate.purpose, resource: gate.resource } : void 0
855
+ );
725
856
  if (config.debug) {
726
857
  console.debug("[mcp-middleware] pdlss resolved", {
727
858
  purpose_source: pdlss.purposeSource,
@@ -759,6 +890,7 @@ function createMcpMiddleware(options) {
759
890
  recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
760
891
  });
761
892
  }
893
+ dedupeFailures(result);
762
894
  onDenied(result, req, res);
763
895
  return;
764
896
  }
@@ -781,6 +913,13 @@ function createMcpMiddleware(options) {
781
913
  };
782
914
  result.failures = [...result.failures ?? [], insufficientFailure];
783
915
  result.denialReasons = [...result.denialReasons ?? [], insufficientFailure.message];
916
+ if (!result.guidance && wellKnownUrls) {
917
+ result.guidance = {
918
+ message: insufficientFailure.message,
919
+ registrationUrl: wellKnownUrls.registrationUrl,
920
+ documentationUrl: wellKnownUrls.documentationUrl
921
+ };
922
+ }
784
923
  if (shouldRecordDecisions) {
785
924
  const overrideKind = gateSource === "toolGate" ? "toolGate" : gateSource === "methodGate" ? "methodGate" : "other";
786
925
  const override = {
@@ -804,6 +943,7 @@ function createMcpMiddleware(options) {
804
943
  });
805
944
  }
806
945
  }
946
+ dedupeFailures(result);
807
947
  onDenied(result, req, res);
808
948
  return;
809
949
  }
@@ -827,7 +967,38 @@ function createMcpMiddleware(options) {
827
967
  }
828
968
  next();
829
969
  } catch (error) {
970
+ const errorClass = error instanceof Error ? error.constructor.name : typeof error;
971
+ const correlationId = req.headers["x-request-id"] || req.headers["x-correlation-id"] || `gen-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
830
972
  console.error("[VerificationGateway/MCP] Middleware error:", error);
973
+ console.warn(
974
+ `[SHADOW] would-have-denied: errorClass=${errorClass} route=${req.method}:${req.path} merchantId=${config.counterpartyId ?? "unknown"} correlationId=${correlationId}`
975
+ );
976
+ if (failOnError === "closed") {
977
+ const result = {
978
+ identityVerified: false,
979
+ policyAllowed: false,
980
+ accessLevel: "none",
981
+ denialReasons: [`MCP middleware internal error: ${errorClass}`],
982
+ failures: [
983
+ {
984
+ dimension: "middleware.internal_error",
985
+ message: `Middleware threw ${errorClass} \u2014 failing closed`
986
+ }
987
+ ],
988
+ verifiedAt: /* @__PURE__ */ new Date(),
989
+ correlationId
990
+ };
991
+ const catchUrls = config.apiBaseUrl ? await getWellKnownUrls(config.apiBaseUrl).catch(() => void 0) : void 0;
992
+ if (catchUrls) {
993
+ result.guidance = {
994
+ message: `Middleware threw ${errorClass} \u2014 failing closed`,
995
+ registrationUrl: catchUrls.registrationUrl,
996
+ documentationUrl: catchUrls.documentationUrl
997
+ };
998
+ }
999
+ dedupeFailures(result);
1000
+ return onDenied(result, req, res);
1001
+ }
831
1002
  next();
832
1003
  }
833
1004
  };