@astrasyncai/verification-gateway 2.4.12 → 2.4.14

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 +125 -35
  6. package/dist/adapters/express.js.map +1 -1
  7. package/dist/adapters/express.mjs +125 -35
  8. package/dist/adapters/express.mjs.map +1 -1
  9. package/dist/adapters/mcp.d.mts +26 -4
  10. package/dist/adapters/mcp.d.ts +26 -4
  11. package/dist/adapters/mcp.js +94 -28
  12. package/dist/adapters/mcp.js.map +1 -1
  13. package/dist/adapters/mcp.mjs +94 -28
  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 +71 -28
  18. package/dist/adapters/nextjs.js.map +1 -1
  19. package/dist/adapters/nextjs.mjs +71 -28
  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 +45 -22
  24. package/dist/adapters/sdk.js.map +1 -1
  25. package/dist/adapters/sdk.mjs +45 -22
  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 +86 -24
  34. package/dist/browser/background.js.map +1 -1
  35. package/dist/browser/background.mjs +86 -24
  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 +86 -24
  46. package/dist/cursor/extension.js.map +1 -1
  47. package/dist/cursor/extension.mjs +86 -24
  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 +86 -24
  54. package/dist/gateway/gateway.js.map +1 -1
  55. package/dist/gateway/gateway.mjs +86 -24
  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 +7 -7
  64. package/dist/index.d.ts +7 -7
  65. package/dist/index.js +336 -71
  66. package/dist/index.js.map +1 -1
  67. package/dist/index.mjs +336 -71
  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
package/dist/index.mjs CHANGED
@@ -126,7 +126,7 @@ function getCapabilities(accessLevel) {
126
126
  }
127
127
 
128
128
  // src/version.ts
129
- var SDK_VERSION = "2.4.12";
129
+ var SDK_VERSION = "2.4.13";
130
130
 
131
131
  // src/verify.ts
132
132
  var DEFAULT_CONFIG = {
@@ -145,22 +145,27 @@ var DEFAULT_CONFIG = {
145
145
  };
146
146
  var initCheckPerformed = false;
147
147
  var deprecationWarningShown = false;
148
- async function performInitCheck(apiBaseUrl, debug) {
148
+ async function performInitCheck(apiBaseUrl, debug, strictInit) {
149
149
  initCheckPerformed = true;
150
150
  try {
151
151
  const probeUrl = `${apiBaseUrl}/agents/verify-access`;
152
152
  const response = await fetch(probeUrl, { method: "HEAD" });
153
153
  const contentType = response.headers.get("content-type") ?? "";
154
154
  if (contentType.startsWith("text/html")) {
155
- console.warn(
156
- `[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.`
157
- );
155
+ 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).`;
156
+ if (strictInit) {
157
+ throw new Error(`${message} (strictInit=true)`);
158
+ }
159
+ console.warn(`${message} Set disableInitChecks: true on GatewayConfig to silence.`);
158
160
  } else if (debug) {
159
161
  console.log(
160
162
  `[VerificationGateway] init check passed for ${apiBaseUrl} (content-type: ${contentType})`
161
163
  );
162
164
  }
163
165
  } catch (err) {
166
+ if (strictInit) {
167
+ throw err;
168
+ }
164
169
  if (debug) {
165
170
  console.log(`[VerificationGateway] init check failed (non-blocking): ${String(err)}`);
166
171
  }
@@ -184,7 +189,23 @@ function getCacheKey(request) {
184
189
  request.counterpartyType || "",
185
190
  request.isSubAgentRequest ? "1" : "0",
186
191
  request.parentAgentId || "",
187
- request.subAgentDepth ?? ""
192
+ request.subAgentDepth ?? "",
193
+ // Audit F-A1-07: previously-missing dimensions that DO affect the
194
+ // backend verdict. Without these, two requests with different
195
+ // durations (e.g. 60s vs 86400s) collided on the same cache key and
196
+ // the shorter-duration allow served the longer-duration request.
197
+ request.durationRequired ?? "",
198
+ request.invocationProtocol || "",
199
+ request.enableRuntimeChallenge ? "1" : "0",
200
+ // callerMetadata fields contribute to risk model; include the ones
201
+ // backend reads. sourceIp/userAgent/forwardedFor change per-request
202
+ // so their inclusion effectively forces a re-check for any varying
203
+ // client (the right behavior — IP-driven anomaly scoring shouldn't
204
+ // be cached across IPs).
205
+ request.callerMetadata?.sourceIp || "",
206
+ request.callerMetadata?.userAgent || "",
207
+ request.callerMetadata?.forwardedFor || "",
208
+ request.callerMetadata?.agentCardUrl || ""
188
209
  ].join("|");
189
210
  }
190
211
  function getCachedResult(request) {
@@ -213,9 +234,13 @@ function clearCache() {
213
234
  }
214
235
  function extractCredentials(headers, query) {
215
236
  const credentials = {};
237
+ const ASTRA_ID_PATTERN = /^ASTRAE?-[A-Za-z0-9_-]{1,64}$/;
216
238
  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"];
217
239
  if (astraIdHeader) {
218
- credentials.astraId = Array.isArray(astraIdHeader) ? astraIdHeader[0] : astraIdHeader;
240
+ const raw = Array.isArray(astraIdHeader) ? astraIdHeader[0] : typeof astraIdHeader === "string" ? astraIdHeader : void 0;
241
+ if (typeof raw === "string" && ASTRA_ID_PATTERN.test(raw)) {
242
+ credentials.astraId = raw;
243
+ }
219
244
  }
220
245
  const apiKeyHeader = headers["x-api-key"] || headers["X-Api-Key"] || headers["X-API-KEY"];
221
246
  if (apiKeyHeader) {
@@ -224,9 +249,11 @@ function extractCredentials(headers, query) {
224
249
  const authHeader = headers["authorization"] || headers["Authorization"];
225
250
  if (authHeader) {
226
251
  const authValue = Array.isArray(authHeader) ? authHeader[0] : authHeader;
227
- credentials.authorizationHeader = authValue;
228
- if (authValue.startsWith("Bearer ")) {
229
- credentials.jwt = authValue.slice(7);
252
+ if (typeof authValue === "string") {
253
+ credentials.authorizationHeader = authValue;
254
+ if (authValue.startsWith("Bearer ")) {
255
+ credentials.jwt = authValue.slice(7);
256
+ }
230
257
  }
231
258
  }
232
259
  if (query) {
@@ -247,7 +274,7 @@ function createGuidanceResponse(config, reason, options = {}) {
247
274
  const isApiError = source === "api_error";
248
275
  const guidance = isApiError ? {
249
276
  message: "Verification is temporarily unavailable. Retry with exponential backoff; if the issue persists, contact support with the correlationId.",
250
- registrationUrl: `${config.apiBaseUrl.replace("/api", "")}/register`,
277
+ registrationUrl: `${config.apiBaseUrl.replace("/api", "")}/agents/register`,
251
278
  documentationUrl: `${config.apiBaseUrl.replace("/api", "")}/docs/agent-access`,
252
279
  steps: [
253
280
  "Retry the request with exponential backoff",
@@ -255,7 +282,7 @@ function createGuidanceResponse(config, reason, options = {}) {
255
282
  ]
256
283
  } : {
257
284
  message: "This service verifies AI agents before granting access. Please register your agent with AstraSync.",
258
- registrationUrl: `${config.apiBaseUrl.replace("/api", "")}/register`,
285
+ registrationUrl: `${config.apiBaseUrl.replace("/api", "")}/agents/register`,
259
286
  documentationUrl: `${config.apiBaseUrl.replace("/api", "")}/docs/agent-access`,
260
287
  steps: [
261
288
  "Register for an AstraSync account",
@@ -332,12 +359,8 @@ async function callVerifyAccessAPI(config, request) {
332
359
  "Content-Type": "application/json",
333
360
  ...config.customHeaders
334
361
  };
335
- if (credentials.authorizationHeader) {
336
- headers["Authorization"] = credentials.authorizationHeader;
337
- } else if (config.apiKey) {
338
- headers["Authorization"] = `Bearer ${config.apiKey}`;
339
- }
340
362
  if (config.apiKey) {
363
+ headers["Authorization"] = `Bearer ${config.apiKey}`;
341
364
  headers["X-API-Key"] = config.apiKey;
342
365
  }
343
366
  try {
@@ -383,7 +406,11 @@ async function callVerifyAccessAPI(config, request) {
383
406
  async function verify(config, request) {
384
407
  const mergedConfig = { ...DEFAULT_CONFIG, ...config };
385
408
  if (!initCheckPerformed && !mergedConfig.disableInitChecks && mergedConfig.apiBaseUrl) {
386
- void performInitCheck(mergedConfig.apiBaseUrl, mergedConfig.debug);
409
+ if (mergedConfig.strictInit) {
410
+ await performInitCheck(mergedConfig.apiBaseUrl, mergedConfig.debug, true);
411
+ } else {
412
+ void performInitCheck(mergedConfig.apiBaseUrl, mergedConfig.debug, false);
413
+ }
387
414
  }
388
415
  if (!deprecationWarningShown && (config.minTrustScore !== void 0 || config.minTrustScoreForFull !== void 0)) {
389
416
  deprecationWarningShown = true;
@@ -437,7 +464,7 @@ async function verify(config, request) {
437
464
  requiresApproval: apiResponse.access?.requiresApproval,
438
465
  guidance: {
439
466
  message: apiResponse.access?.reason || "Access denied by PDLSS policy",
440
- registrationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/register`,
467
+ registrationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/agents/register`,
441
468
  documentationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/docs/pdlss`
442
469
  },
443
470
  verifiedAt: /* @__PURE__ */ new Date(),
@@ -507,13 +534,15 @@ async function verify(config, request) {
507
534
  result.denialReasons = result.recommendationReasons || [
508
535
  "Access denied by AstraSync recommendation"
509
536
  ];
510
- if (result.runtimeChallenge) {
511
- result.guidance = {
512
- message: `Verification failed: ${result.runtimeChallenge.reason || "runtime challenge failed"}`,
513
- registrationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/register`,
514
- documentationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/docs/runtime-challenge`
515
- };
516
- }
537
+ result.guidance = result.runtimeChallenge ? {
538
+ message: `Verification failed: ${result.runtimeChallenge.reason || "runtime challenge failed"}`,
539
+ registrationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/agents/register`,
540
+ documentationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/docs/runtime-challenge`
541
+ } : {
542
+ message: result.recommendationReasons?.[0] || "Access denied by AstraSync recommendation",
543
+ registrationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/agents/register`,
544
+ documentationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/docs/pdlss`
545
+ };
517
546
  } else if (result.recommendation === "step_up_required") {
518
547
  result.requiresStepUp = true;
519
548
  if (ACCESS_LEVEL_HIERARCHY[result.accessLevel] > ACCESS_LEVEL_HIERARCHY["read-only"]) {
@@ -762,18 +791,40 @@ function defaultExtractPurpose(req) {
762
791
  return "general";
763
792
  }
764
793
  }
765
- function matchRoute(pattern, path) {
794
+ function matchRoute(pattern, path, opts) {
766
795
  const regexPattern = pattern.replace(/\*/g, ".*").replace(/\//g, "\\/");
767
- const regex = new RegExp(`^${regexPattern}$`);
768
- return regex.test(path);
796
+ const caseSensitiveRegex = new RegExp(`^${regexPattern}$`);
797
+ const caseSensitiveResult = caseSensitiveRegex.test(path);
798
+ if (!opts?.caseInsensitive && !opts?.logShadowDivergence) {
799
+ return caseSensitiveResult;
800
+ }
801
+ const caseInsensitiveRegex = new RegExp(`^${regexPattern}$`, "i");
802
+ const caseInsensitiveResult = caseInsensitiveRegex.test(path);
803
+ if (opts?.logShadowDivergence && caseSensitiveResult !== caseInsensitiveResult) {
804
+ console.warn(
805
+ `[SHADOW] matchRoute case-insensitive would change result: pattern=${pattern} path=${path} caseSensitive=${caseSensitiveResult} caseInsensitive=${caseInsensitiveResult} correlationId=${opts.correlationId ?? "unknown"}`
806
+ );
807
+ }
808
+ return opts?.caseInsensitive ? caseInsensitiveResult : caseSensitiveResult;
769
809
  }
770
- function findRouteConfig(routes, path, method) {
810
+ function findRouteConfig(routes, path, method, opts) {
771
811
  return routes.find((route) => {
772
812
  const methodMatches = route.method === "*" || route.method.toUpperCase() === method.toUpperCase();
773
- const pathMatches = matchRoute(route.pattern, path);
813
+ const pathMatches = matchRoute(route.pattern, path, opts);
774
814
  return methodMatches && pathMatches;
775
815
  });
776
816
  }
817
+ function dedupeFailures(result) {
818
+ if (result.failures && result.failures.length > 1) {
819
+ const seen = /* @__PURE__ */ new Set();
820
+ result.failures = result.failures.filter((f) => {
821
+ const key = `${f.dimension}|${f.message}|${f.guidance ?? ""}`;
822
+ if (seen.has(key)) return false;
823
+ seen.add(key);
824
+ return true;
825
+ });
826
+ }
827
+ }
777
828
  function defaultOnDenied(result, _req, res) {
778
829
  const statusCode = !result.identityVerified ? 401 : 403;
779
830
  res.setHeader("X-Astra-Gateway-Mode", "enforced");
@@ -800,6 +851,8 @@ function createMiddleware(options) {
800
851
  recordDecisions,
801
852
  enableRuntimeChallenge = true,
802
853
  routesRefreshMs = DEFAULT_ROUTES_REFRESH_MS,
854
+ failOnError = "open",
855
+ caseInsensitiveRouteMatch = false,
803
856
  ...config
804
857
  } = options;
805
858
  let cachedRoutes = [];
@@ -822,7 +875,7 @@ function createMiddleware(options) {
822
875
  cachedRoutes = fetched;
823
876
  lastFetchAt = Date.now();
824
877
  if (cachedRoutes.length === 0 && !warnedEmptyRoutes) {
825
- const dashboard = config.dashboardUrl ?? "https://app.astrasync.ai";
878
+ const dashboard = config.dashboardUrl ?? "https://astrasync.ai/dashboard";
826
879
  console.warn(
827
880
  `[VerificationGateway] No route policy configured for ${config.counterpartyId}. Gateway is in pass-through mode for ALL traffic until you add at least one route. Configure at ${dashboard}/dashboard/endpoints/${config.counterpartyId}/routes`
828
881
  );
@@ -848,7 +901,12 @@ function createMiddleware(options) {
848
901
  refreshing = null;
849
902
  });
850
903
  }
851
- const routeConfig = findRouteConfig(cachedRoutes, req.path, req.method);
904
+ const correlationId = req.headers["x-request-id"] || req.headers["x-correlation-id"];
905
+ const routeConfig = findRouteConfig(cachedRoutes, req.path, req.method, {
906
+ caseInsensitive: caseInsensitiveRouteMatch,
907
+ logShadowDivergence: true,
908
+ correlationId
909
+ });
852
910
  if (!routeConfig) {
853
911
  if (config.setPassThroughHeader) {
854
912
  res.setHeader("X-Astra-Gateway-Mode", "unenforced");
@@ -880,7 +938,7 @@ function createMiddleware(options) {
880
938
  denialReasons: preCheckFailures.map((f) => f.message),
881
939
  guidance: {
882
940
  message: "Request exceeds counterparty-defined PDLSS limits.",
883
- registrationUrl: `${config.apiBaseUrl?.replace("/api", "")}/register`,
941
+ registrationUrl: `${config.apiBaseUrl?.replace("/api", "")}/agents/register`,
884
942
  documentationUrl: `${config.apiBaseUrl?.replace("/api", "")}/docs/pdlss`
885
943
  },
886
944
  verifiedAt: /* @__PURE__ */ new Date()
@@ -895,13 +953,19 @@ function createMiddleware(options) {
895
953
  requestMethod: req.method
896
954
  }).catch(() => {
897
955
  });
956
+ dedupeFailures(result2);
898
957
  onDenied(result2, req, res);
899
958
  return;
900
959
  }
901
960
  const shouldRecordDecisions = recordDecisions !== false;
902
961
  const forwardedFor = req.headers["x-forwarded-for"];
903
962
  const forwardedForStr = Array.isArray(forwardedFor) ? forwardedFor.join(", ") : forwardedFor;
904
- const originalClientIp = forwardedForStr ? forwardedForStr.split(",")[0].trim() : req.ip;
963
+ const originalClientIp = req.ip ?? (forwardedForStr ? forwardedForStr.split(",")[0].trim() : void 0);
964
+ if (!req.ip && forwardedForStr) {
965
+ console.warn(
966
+ "[VerificationGateway] req.ip unset \u2014 falling back to leftmost X-Forwarded-For. Configure Express trust proxy correctly to avoid spoofable client IPs in audit logs."
967
+ );
968
+ }
905
969
  const agentCardUrl = typeof req.headers["x-astrasync-agent-card"] === "string" ? req.headers["x-astrasync-agent-card"] : void 0;
906
970
  const result = await verify(config, {
907
971
  credentials,
@@ -932,6 +996,7 @@ function createMiddleware(options) {
932
996
  recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
933
997
  });
934
998
  }
999
+ dedupeFailures(result);
935
1000
  onDenied(result, req, res);
936
1001
  return;
937
1002
  }
@@ -958,6 +1023,7 @@ function createMiddleware(options) {
958
1023
  recordDecision(config, sessionId, "denied", insufficientFailure.message).catch(() => {
959
1024
  });
960
1025
  }
1026
+ dedupeFailures(result);
961
1027
  onDenied(result, req, res);
962
1028
  return;
963
1029
  }
@@ -974,6 +1040,7 @@ function createMiddleware(options) {
974
1040
  recordDecision(config, sessionId, "denied", trustFailure.message).catch(() => {
975
1041
  });
976
1042
  }
1043
+ dedupeFailures(result);
977
1044
  onDenied(result, req, res);
978
1045
  return;
979
1046
  }
@@ -988,7 +1055,30 @@ function createMiddleware(options) {
988
1055
  }
989
1056
  next();
990
1057
  } catch (error) {
1058
+ const errorClass = error instanceof Error ? error.constructor.name : typeof error;
1059
+ const correlationId = req.headers["x-request-id"] || req.headers["x-correlation-id"] || `gen-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
991
1060
  console.error("[VerificationGateway] Middleware error:", error);
1061
+ console.warn(
1062
+ `[SHADOW] would-have-denied: errorClass=${errorClass} route=${req.method}:${req.path} merchantId=${config.counterpartyId ?? "unknown"} correlationId=${correlationId}`
1063
+ );
1064
+ if (failOnError === "closed") {
1065
+ const result = {
1066
+ identityVerified: false,
1067
+ policyAllowed: false,
1068
+ accessLevel: "none",
1069
+ denialReasons: [`Verification middleware internal error: ${errorClass}`],
1070
+ failures: [
1071
+ {
1072
+ dimension: "middleware.internal_error",
1073
+ message: `Middleware threw ${errorClass} \u2014 failing closed`
1074
+ }
1075
+ ],
1076
+ verifiedAt: /* @__PURE__ */ new Date(),
1077
+ correlationId
1078
+ };
1079
+ dedupeFailures(result);
1080
+ return onDenied(result, req, res);
1081
+ }
992
1082
  next();
993
1083
  }
994
1084
  };
@@ -1000,6 +1090,18 @@ __export(nextjs_exports, {
1000
1090
  createMatcherConfig: () => createMatcherConfig,
1001
1091
  createMiddleware: () => createMiddleware2
1002
1092
  });
1093
+ function escapeHtml(value) {
1094
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1095
+ }
1096
+ function sanitizeUrl(value, fallback) {
1097
+ if (typeof value !== "string" || value.length === 0) return escapeHtml(fallback);
1098
+ const trimmed = value.trim();
1099
+ if (/^javascript:|^data:|^vbscript:/i.test(trimmed)) return escapeHtml(fallback);
1100
+ if (/^https?:\/\//i.test(trimmed) || trimmed.startsWith("/")) {
1101
+ return escapeHtml(trimmed);
1102
+ }
1103
+ return escapeHtml(fallback);
1104
+ }
1003
1105
  function extractCredentialsFromNextRequest(request) {
1004
1106
  const credentials = {};
1005
1107
  const astraId = request.headers.get("x-astra-id") || request.headers.get("X-Astra-Id");
@@ -1071,10 +1173,18 @@ function extractPurpose(request) {
1071
1173
  }
1072
1174
  }
1073
1175
  function generateCommerceShieldHtml(result, options) {
1074
- const title = options.commerceShield?.title || "AstraSync Agent Verification";
1075
- const message = options.commerceShield?.message || result.guidance?.message || "This site verifies AI agents before granting access. We noticed you're visiting without AstraSync credentials.";
1076
- const registrationUrl = result.guidance?.registrationUrl || "https://astrasync.ai/register";
1077
- const docsUrl = result.guidance?.documentationUrl || "https://astrasync.ai/docs/agent-access";
1176
+ const title = escapeHtml(options.commerceShield?.title || "AstraSync Agent Verification");
1177
+ const message = escapeHtml(
1178
+ options.commerceShield?.message || result.guidance?.message || "This site verifies AI agents before granting access. We noticed you're visiting without AstraSync credentials."
1179
+ );
1180
+ const registrationUrl = sanitizeUrl(
1181
+ result.guidance?.registrationUrl,
1182
+ "https://astrasync.ai/register"
1183
+ );
1184
+ const docsUrl = sanitizeUrl(
1185
+ result.guidance?.documentationUrl,
1186
+ "https://astrasync.ai/docs/agent-access"
1187
+ );
1078
1188
  const allowGuest = options.commerceShield?.allowGuestAccess ?? true;
1079
1189
  return `
1080
1190
  <!DOCTYPE html>
@@ -1196,7 +1306,7 @@ function generateCommerceShieldHtml(result, options) {
1196
1306
  <div class="shield-steps">
1197
1307
  <h3>To get verified access:</h3>
1198
1308
  <ol>
1199
- <li>Register at <a href="${registrationUrl}">astrasync.ai/register</a></li>
1309
+ <li>Register at <a href="${registrationUrl}">astrasync.ai/agents/register</a></li>
1200
1310
  <li>Create and register your agent</li>
1201
1311
  <li>Add your ASTRA-ID to request headers</li>
1202
1312
  <li>Refresh this page</li>
@@ -1284,7 +1394,7 @@ function createMiddleware2(options) {
1284
1394
  denialReasons: preCheckFailures.map((f) => f.message),
1285
1395
  guidance: {
1286
1396
  message: "Request exceeds counterparty-defined PDLSS limits.",
1287
- registrationUrl: `${config.apiBaseUrl?.replace("/api", "")}/register`,
1397
+ registrationUrl: `${config.apiBaseUrl?.replace("/api", "")}/agents/register`,
1288
1398
  documentationUrl: `${config.apiBaseUrl?.replace("/api", "")}/docs/pdlss`
1289
1399
  },
1290
1400
  verifiedAt: /* @__PURE__ */ new Date()
@@ -1945,12 +2055,45 @@ function bufferToBase64(bytes) {
1945
2055
 
1946
2056
  // src/transport/rfc9421-verify.ts
1947
2057
  import { httpbis } from "http-message-signatures";
2058
+
2059
+ // src/transport/nonce-store.ts
2060
+ var InMemoryNonceStore = class {
2061
+ constructor(capacity = 1e4) {
2062
+ this.entries = /* @__PURE__ */ new Map();
2063
+ this.lastSweepMs = 0;
2064
+ this.capacity = capacity;
2065
+ }
2066
+ seen(key, expiresAtMs) {
2067
+ const nowMs = Date.now();
2068
+ if (nowMs - this.lastSweepMs > 1e3) {
2069
+ for (const [k, exp] of this.entries) {
2070
+ if (exp <= nowMs) this.entries.delete(k);
2071
+ }
2072
+ this.lastSweepMs = nowMs;
2073
+ }
2074
+ const existing = this.entries.get(key);
2075
+ if (existing !== void 0 && existing > nowMs) {
2076
+ return true;
2077
+ }
2078
+ if (this.entries.size >= this.capacity) {
2079
+ const oldest = this.entries.keys().next().value;
2080
+ if (oldest !== void 0) this.entries.delete(oldest);
2081
+ }
2082
+ this.entries.set(key, expiresAtMs);
2083
+ return false;
2084
+ }
2085
+ };
2086
+ var defaultNonceStore = new InMemoryNonceStore();
2087
+
2088
+ // src/transport/rfc9421-verify.ts
1948
2089
  async function verifyRFC9421(request, options) {
1949
2090
  const { resolver } = options;
1950
- const tolerance = options.clockSkewSec ?? 300;
2091
+ const tolerance = options.clockSkewSec ?? 60;
1951
2092
  const nowSec = options.now ? options.now() : Math.floor(Date.now() / 1e3);
2093
+ const nonceStore = options.nonceStore ?? defaultNonceStore;
1952
2094
  let resolvedKid;
1953
2095
  let resolvedAlg;
2096
+ let replayDetected = false;
1954
2097
  const keyLookup = async (parameters) => {
1955
2098
  const kid = typeof parameters.keyid === "string" ? parameters.keyid : void 0;
1956
2099
  if (!kid) return null;
@@ -1964,6 +2107,14 @@ async function verifyRFC9421(request, options) {
1964
2107
  const expires = toUnixSeconds(parameters.expires);
1965
2108
  if (created !== void 0 && Math.abs(nowSec - created) > tolerance) return null;
1966
2109
  if (expires !== void 0 && nowSec > expires + tolerance) return null;
2110
+ const nonce = typeof parameters.nonce === "string" ? parameters.nonce : void 0;
2111
+ if (nonce) {
2112
+ const expiresAtMs = (expires !== void 0 ? expires + tolerance : nowSec + tolerance) * 1e3;
2113
+ if (nonceStore.seen(`rfc9421:${kid}:${nonce}`, expiresAtMs)) {
2114
+ replayDetected = true;
2115
+ return null;
2116
+ }
2117
+ }
1967
2118
  return jwkToVerifyingKey(kid, jwk, alg);
1968
2119
  };
1969
2120
  try {
@@ -1986,7 +2137,7 @@ async function verifyRFC9421(request, options) {
1986
2137
  kid: resolvedKid,
1987
2138
  registry: resolver.name,
1988
2139
  algorithm: resolvedAlg,
1989
- error: result === false ? "signature invalid" : "no signature found"
2140
+ error: replayDetected ? "RFC9421 signature replay \u2014 already seen within tolerance window" : result === false ? "signature invalid" : "no signature found"
1990
2141
  };
1991
2142
  } catch (err) {
1992
2143
  return {
@@ -2811,14 +2962,26 @@ function sha256Sync2(data) {
2811
2962
  function verifyAP2Chain(input) {
2812
2963
  const { triple } = input;
2813
2964
  const errors = [];
2965
+ const toleranceSec = input.clockSkewSec ?? 60;
2966
+ const nonceStore = input.nonceStore ?? defaultNonceStore;
2814
2967
  const intentPresent = triple.intent !== void 0;
2815
2968
  const cartRefOk = checkCartRef(triple, errors);
2816
2969
  const paymentRefOk = checkPaymentRef(triple, errors);
2817
2970
  const { ok: agentIdContinuity, agentId } = checkAgentContinuity(triple, errors);
2818
2971
  const paymentMethodAllowed = checkPaymentMethod(triple, errors);
2819
2972
  const totalsConsistent = checkTotals(triple, errors);
2820
- const expiryOk = checkExpiries(triple, input.clockSkewSec ?? 300, input.now, errors);
2821
- const ok = cartRefOk && paymentRefOk && agentIdContinuity && paymentMethodAllowed && totalsConsistent && expiryOk;
2973
+ const expiryOk = checkExpiries(triple, toleranceSec, input.now, errors);
2974
+ let replayOk = true;
2975
+ const replayId = triple.payment?.raw?.id ?? triple.cart?.raw?.id;
2976
+ if (typeof replayId === "string" && replayId.length > 0) {
2977
+ const now = input.now ? input.now() : Math.floor(Date.now() / 1e3);
2978
+ const expiresAt = (now + toleranceSec) * 1e3;
2979
+ if (nonceStore.seen(`ap2:${replayId}`, expiresAt)) {
2980
+ errors.push(`AP2 chain replay \u2014 mandate ${replayId} already seen within tolerance window`);
2981
+ replayOk = false;
2982
+ }
2983
+ }
2984
+ const ok = cartRefOk && paymentRefOk && agentIdContinuity && paymentMethodAllowed && totalsConsistent && expiryOk && replayOk;
2822
2985
  return {
2823
2986
  ok,
2824
2987
  checks: {
@@ -2864,7 +3027,10 @@ function checkAgentContinuity(triple, errors) {
2864
3027
  const ids = [triple.intent?.agent_id, triple.cart?.agent_id, triple.payment?.agent_id].filter(
2865
3028
  (id) => typeof id === "string" && id.length > 0
2866
3029
  );
2867
- if (ids.length === 0) return { ok: true };
3030
+ if (ids.length === 0) {
3031
+ errors.push("agent_id missing across all three mandates (intent/cart/payment)");
3032
+ return { ok: false };
3033
+ }
2868
3034
  const unique = new Set(ids);
2869
3035
  if (unique.size > 1) {
2870
3036
  errors.push(`agent_id mismatch across mandates: ${Array.from(unique).join(", ")}`);
@@ -2873,9 +3039,16 @@ function checkAgentContinuity(triple, errors) {
2873
3039
  return { ok: true, agentId: ids[0] };
2874
3040
  }
2875
3041
  function checkPaymentMethod(triple, errors) {
2876
- const paymentMethod = triple.payment?.payment_method;
2877
3042
  const allowed = triple.intent?.paymentMethods;
2878
- if (!paymentMethod || !allowed || allowed.length === 0) return true;
3043
+ if (!allowed || allowed.length === 0) return true;
3044
+ if (!triple.payment) return true;
3045
+ const paymentMethod = triple.payment.payment_method;
3046
+ if (!paymentMethod) {
3047
+ errors.push(
3048
+ `payment.payment_method missing but intent declares allowlist [${allowed.join(", ")}]`
3049
+ );
3050
+ return false;
3051
+ }
2879
3052
  if (!allowed.includes(paymentMethod)) {
2880
3053
  errors.push(
2881
3054
  `payment_method "${paymentMethod}" not in intent.paymentMethods [${allowed.join(", ")}]`
@@ -2909,19 +3082,24 @@ function checkTotals(triple, errors) {
2909
3082
  function checkExpiries(triple, toleranceSec, nowFn, errors) {
2910
3083
  const now = nowFn ? nowFn() : Math.floor(Date.now() / 1e3);
2911
3084
  let ok = true;
2912
- for (const [name, mandate] of [
2913
- ["intent", triple.intent],
2914
- ["cart", triple.cart]
2915
- ]) {
2916
- if (!mandate?.expires) continue;
2917
- const parsed = parseExpiry(mandate.expires);
3085
+ const layers = [
3086
+ ["intent", triple.intent?.expires],
3087
+ ["cart", triple.cart?.expires],
3088
+ [
3089
+ "payment",
3090
+ typeof triple.payment?.raw?.expires === "string" ? triple.payment.raw.expires : typeof triple.payment?.raw?.exp === "string" ? triple.payment.raw.exp : void 0
3091
+ ]
3092
+ ];
3093
+ for (const [name, expires] of layers) {
3094
+ if (!expires) continue;
3095
+ const parsed = parseExpiry(expires);
2918
3096
  if (parsed === null) {
2919
3097
  errors.push(`${name}.expires unparseable`);
2920
3098
  ok = false;
2921
3099
  continue;
2922
3100
  }
2923
3101
  if (now > parsed + toleranceSec) {
2924
- errors.push(`${name} mandate expired at ${mandate.expires}`);
3102
+ errors.push(`${name} mandate expired at ${expires}`);
2925
3103
  ok = false;
2926
3104
  }
2927
3105
  }
@@ -2948,10 +3126,21 @@ async function verifyACPSignature(input) {
2948
3126
  if (!input.signatureHeader) {
2949
3127
  return { ok: false, error: "missing Signature header" };
2950
3128
  }
2951
- const freshness = checkTimestamp(input.timestampHeader, input.clockSkewSec ?? 300, input.now);
3129
+ const tolerance = input.clockSkewSec ?? 60;
3130
+ const nonceStore = input.nonceStore ?? defaultNonceStore;
3131
+ const freshness = checkTimestamp(input.timestampHeader, tolerance, input.now);
2952
3132
  if (!freshness.ok) {
2953
3133
  return { ok: false, error: freshness.error, timestampStale: true };
2954
3134
  }
3135
+ const nowSec = input.now ? input.now() : Math.floor(Date.now() / 1e3);
3136
+ const expiresAtMs = (nowSec + tolerance) * 1e3;
3137
+ const replayKey = `acp:${input.signatureHeader}:${input.timestampHeader ?? ""}`;
3138
+ if (nonceStore.seen(replayKey, expiresAtMs)) {
3139
+ return {
3140
+ ok: false,
3141
+ error: "ACP signature replay \u2014 already seen within tolerance window"
3142
+ };
3143
+ }
2955
3144
  const signatureBytes = decodeBase64(input.signatureHeader);
2956
3145
  if (!signatureBytes) {
2957
3146
  return { ok: false, error: "signature header is not valid base64" };
@@ -3169,8 +3358,9 @@ function coerceString6(v) {
3169
3358
  import { BodyDigest } from "mppx";
3170
3359
  function verifyMPP(input) {
3171
3360
  const { context } = input;
3172
- const tolerance = input.clockSkewSec ?? 300;
3361
+ const tolerance = input.clockSkewSec ?? 60;
3173
3362
  const nowSec = input.now ? input.now() : Math.floor(Date.now() / 1e3);
3363
+ const nonceStore = input.nonceStore ?? defaultNonceStore;
3174
3364
  const challenge = context.credential?.challenge ?? (context.challenges && context.challenges[0]);
3175
3365
  const source = context.credential?.source;
3176
3366
  const method = challenge?.method;
@@ -3193,21 +3383,38 @@ function verifyMPP(input) {
3193
3383
  }
3194
3384
  }
3195
3385
  let bodyDigestOk = null;
3196
- if (challenge?.digest && input.rawBody !== void 0) {
3197
- try {
3198
- if (!/^sha-256=/.test(challenge.digest)) {
3386
+ if (input.rawBody !== void 0) {
3387
+ if (!challenge?.digest) {
3388
+ bodyDigestOk = false;
3389
+ } else {
3390
+ try {
3391
+ if (!/^sha-256=/.test(challenge.digest)) {
3392
+ bodyDigestOk = false;
3393
+ } else {
3394
+ bodyDigestOk = BodyDigest.verify(challenge.digest, input.rawBody);
3395
+ }
3396
+ } catch {
3199
3397
  bodyDigestOk = false;
3200
- } else {
3201
- bodyDigestOk = BodyDigest.verify(challenge.digest, input.rawBody);
3202
3398
  }
3203
- } catch {
3204
- bodyDigestOk = false;
3205
3399
  }
3206
3400
  }
3207
- const ok = expiryOk && (bodyDigestOk === null || bodyDigestOk === true);
3401
+ let replayOk = true;
3402
+ if (challenge?.digest && expiryOk) {
3403
+ const replayKey = `mpp:${challenge.digest}:${challenge.nonce ?? ""}`;
3404
+ const expiresAt = (nowSec + tolerance) * 1e3;
3405
+ if (nonceStore.seen(replayKey, expiresAt)) {
3406
+ replayOk = false;
3407
+ }
3408
+ }
3409
+ const ok = expiryOk && (bodyDigestOk === null || bodyDigestOk === true) && replayOk;
3208
3410
  const errors = [];
3209
3411
  if (!expiryOk) errors.push("challenge expired");
3210
- if (bodyDigestOk === false) errors.push("body digest mismatch");
3412
+ if (bodyDigestOk === false) {
3413
+ errors.push(
3414
+ input.rawBody !== void 0 && !challenge?.digest ? "body digest required when rawBody present" : "body digest mismatch"
3415
+ );
3416
+ }
3417
+ if (!replayOk) errors.push("MPP challenge replay \u2014 already seen within tolerance window");
3211
3418
  return {
3212
3419
  ok,
3213
3420
  expiryOk,
@@ -3371,14 +3578,32 @@ function readHeader4(headers, name) {
3371
3578
  import { createHash as createHash3, webcrypto } from "crypto";
3372
3579
  async function verifyVIChain(input) {
3373
3580
  const errors = [];
3374
- const tolerance = input.clockSkewSec ?? 300;
3581
+ const tolerance = input.clockSkewSec ?? 60;
3375
3582
  const now = input.now ? input.now() : Math.floor(Date.now() / 1e3);
3376
3583
  const { l1, l2, l3a, l3b } = input.layers;
3584
+ const nonceStore = input.nonceStore ?? defaultNonceStore;
3585
+ if (!l1) {
3586
+ if (!input.allowUnboundChain) {
3587
+ errors.push(
3588
+ "L1 missing \u2014 chain root unbound (set allowUnboundChain + expectedL2Key to override)"
3589
+ );
3590
+ } else if (!input.expectedL2Key) {
3591
+ errors.push("allowUnboundChain set but expectedL2Key missing");
3592
+ }
3593
+ }
3377
3594
  const l1SigOk = l1 ? await input.verifySignature(l1, null) : null;
3378
3595
  if (l1 && !l1SigOk) errors.push("L1 signature invalid");
3379
3596
  const l1Cnf = extractCnfJwk(l1?.payload);
3380
- const l2SigOk = await input.verifySignature(l2, l1Cnf ?? null);
3597
+ const l2ExpectedKey = l1Cnf ?? input.expectedL2Key ?? null;
3598
+ const l2SigOk = await input.verifySignature(l2, l2ExpectedKey);
3381
3599
  if (!l2SigOk) errors.push("L2 signature invalid");
3600
+ if (l2SigOk) {
3601
+ const replayKey = `vi:l2:${l2.compact}`;
3602
+ const expiresAt = now * 1e3 + tolerance * 1e3;
3603
+ if (nonceStore.seen(replayKey, expiresAt)) {
3604
+ errors.push("L2 signature replay \u2014 already seen within tolerance window");
3605
+ }
3606
+ }
3382
3607
  const l2Cnf = extractCnfJwk(l2.payload);
3383
3608
  const l3aSigOk = l3a ? await input.verifySignature(l3a, l2Cnf ?? null) : null;
3384
3609
  if (l3a && !l3aSigOk) errors.push("L3a signature invalid");
@@ -3422,7 +3647,10 @@ async function verifyVIChain(input) {
3422
3647
  }
3423
3648
  }
3424
3649
  const expiryOk = checkExpiryAcross([l1, l2, l3a, l3b], tolerance, now, errors);
3425
- const ok = l1SigOk !== false && l2SigOk && l3aSigOk !== false && l3bSigOk !== false && l1BindsL2 && l2BindsL3 && l3aL3bTxnIdMatch !== false && checkoutHashOk !== false && expiryOk;
3650
+ const noUnboundChainOrReplayErrors = !errors.some(
3651
+ (e) => e.startsWith("L1 missing") || e.startsWith("allowUnboundChain set") || e.startsWith("L2 signature replay")
3652
+ );
3653
+ const ok = l1SigOk !== false && l2SigOk && l3aSigOk !== false && l3bSigOk !== false && l1BindsL2 && l2BindsL3 && l3aL3bTxnIdMatch !== false && checkoutHashOk !== false && expiryOk && noUnboundChainOrReplayErrors;
3426
3654
  return {
3427
3655
  ok,
3428
3656
  checks: {
@@ -4077,7 +4305,7 @@ function mcpToPdlss(parsed, headerPurpose, headerAction) {
4077
4305
  action = parsed.actionFromBody;
4078
4306
  actionSource = parsed.actionSourceFromBody;
4079
4307
  } else {
4080
- action = parsed.toolName ? `${parsed.method}:${parsed.toolName}` : parsed.method;
4308
+ action = parsed.toolName ? parsed.method === "tools/call" ? parsed.toolName : `${parsed.method}:${parsed.toolName}` : parsed.method;
4081
4309
  actionSource = "transport_layer";
4082
4310
  }
4083
4311
  return { purpose, action, resource, purposeSource, actionSource };
@@ -4096,6 +4324,17 @@ function readSingleHeader(value) {
4096
4324
  if (Array.isArray(value)) return value[0];
4097
4325
  return void 0;
4098
4326
  }
4327
+ function dedupeFailures2(result) {
4328
+ if (result.failures && result.failures.length > 1) {
4329
+ const seen = /* @__PURE__ */ new Set();
4330
+ result.failures = result.failures.filter((f) => {
4331
+ const key = `${f.dimension}|${f.message}|${f.guidance ?? ""}`;
4332
+ if (seen.has(key)) return false;
4333
+ seen.add(key);
4334
+ return true;
4335
+ });
4336
+ }
4337
+ }
4099
4338
  function defaultMcpDenied(result, req, res) {
4100
4339
  const id = req.body?.id ?? null;
4101
4340
  const status = !result.identityVerified ? 401 : 403;
@@ -4135,10 +4374,11 @@ function createMcpMiddleware(options) {
4135
4374
  onAgentIdMismatch = "reject",
4136
4375
  skip = false,
4137
4376
  onDenied = defaultMcpDenied,
4138
- trustVerifiedHop = true,
4377
+ trustVerifiedHop = false,
4139
4378
  verifiedHopMaxAgeMs,
4140
4379
  recordDecisions,
4141
4380
  enableRuntimeChallenge = true,
4381
+ failOnError = "open",
4142
4382
  ...config
4143
4383
  } = options;
4144
4384
  return async (req, res, next) => {
@@ -4246,6 +4486,7 @@ function createMcpMiddleware(options) {
4246
4486
  recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
4247
4487
  });
4248
4488
  }
4489
+ dedupeFailures2(result);
4249
4490
  onDenied(result, req, res);
4250
4491
  return;
4251
4492
  }
@@ -4291,6 +4532,7 @@ function createMcpMiddleware(options) {
4291
4532
  });
4292
4533
  }
4293
4534
  }
4535
+ dedupeFailures2(result);
4294
4536
  onDenied(result, req, res);
4295
4537
  return;
4296
4538
  }
@@ -4314,7 +4556,30 @@ function createMcpMiddleware(options) {
4314
4556
  }
4315
4557
  next();
4316
4558
  } catch (error) {
4559
+ const errorClass = error instanceof Error ? error.constructor.name : typeof error;
4560
+ const correlationId = req.headers["x-request-id"] || req.headers["x-correlation-id"] || `gen-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
4317
4561
  console.error("[VerificationGateway/MCP] Middleware error:", error);
4562
+ console.warn(
4563
+ `[SHADOW] would-have-denied: errorClass=${errorClass} route=${req.method}:${req.path} merchantId=${config.counterpartyId ?? "unknown"} correlationId=${correlationId}`
4564
+ );
4565
+ if (failOnError === "closed") {
4566
+ const result = {
4567
+ identityVerified: false,
4568
+ policyAllowed: false,
4569
+ accessLevel: "none",
4570
+ denialReasons: [`MCP middleware internal error: ${errorClass}`],
4571
+ failures: [
4572
+ {
4573
+ dimension: "middleware.internal_error",
4574
+ message: `Middleware threw ${errorClass} \u2014 failing closed`
4575
+ }
4576
+ ],
4577
+ verifiedAt: /* @__PURE__ */ new Date(),
4578
+ correlationId
4579
+ };
4580
+ dedupeFailures2(result);
4581
+ return onDenied(result, req, res);
4582
+ }
4318
4583
  next();
4319
4584
  }
4320
4585
  };