@astrasyncai/verification-gateway 2.4.11 → 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 +129 -36
  6. package/dist/adapters/express.js.map +1 -1
  7. package/dist/adapters/express.mjs +129 -36
  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 +75 -29
  18. package/dist/adapters/nextjs.js.map +1 -1
  19. package/dist/adapters/nextjs.mjs +75 -29
  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 +344 -73
  66. package/dist/index.js.map +1 -1
  67. package/dist/index.mjs +344 -73
  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.js CHANGED
@@ -189,7 +189,7 @@ function getCapabilities(accessLevel) {
189
189
  }
190
190
 
191
191
  // src/version.ts
192
- var SDK_VERSION = "2.4.11";
192
+ var SDK_VERSION = "2.4.13";
193
193
 
194
194
  // src/verify.ts
195
195
  var DEFAULT_CONFIG = {
@@ -208,22 +208,27 @@ var DEFAULT_CONFIG = {
208
208
  };
209
209
  var initCheckPerformed = false;
210
210
  var deprecationWarningShown = false;
211
- async function performInitCheck(apiBaseUrl, debug) {
211
+ async function performInitCheck(apiBaseUrl, debug, strictInit) {
212
212
  initCheckPerformed = true;
213
213
  try {
214
214
  const probeUrl = `${apiBaseUrl}/agents/verify-access`;
215
215
  const response = await fetch(probeUrl, { method: "HEAD" });
216
216
  const contentType = response.headers.get("content-type") ?? "";
217
217
  if (contentType.startsWith("text/html")) {
218
- console.warn(
219
- `[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.`
220
- );
218
+ 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).`;
219
+ if (strictInit) {
220
+ throw new Error(`${message} (strictInit=true)`);
221
+ }
222
+ console.warn(`${message} Set disableInitChecks: true on GatewayConfig to silence.`);
221
223
  } else if (debug) {
222
224
  console.log(
223
225
  `[VerificationGateway] init check passed for ${apiBaseUrl} (content-type: ${contentType})`
224
226
  );
225
227
  }
226
228
  } catch (err) {
229
+ if (strictInit) {
230
+ throw err;
231
+ }
227
232
  if (debug) {
228
233
  console.log(`[VerificationGateway] init check failed (non-blocking): ${String(err)}`);
229
234
  }
@@ -247,7 +252,23 @@ function getCacheKey(request) {
247
252
  request.counterpartyType || "",
248
253
  request.isSubAgentRequest ? "1" : "0",
249
254
  request.parentAgentId || "",
250
- request.subAgentDepth ?? ""
255
+ request.subAgentDepth ?? "",
256
+ // Audit F-A1-07: previously-missing dimensions that DO affect the
257
+ // backend verdict. Without these, two requests with different
258
+ // durations (e.g. 60s vs 86400s) collided on the same cache key and
259
+ // the shorter-duration allow served the longer-duration request.
260
+ request.durationRequired ?? "",
261
+ request.invocationProtocol || "",
262
+ request.enableRuntimeChallenge ? "1" : "0",
263
+ // callerMetadata fields contribute to risk model; include the ones
264
+ // backend reads. sourceIp/userAgent/forwardedFor change per-request
265
+ // so their inclusion effectively forces a re-check for any varying
266
+ // client (the right behavior — IP-driven anomaly scoring shouldn't
267
+ // be cached across IPs).
268
+ request.callerMetadata?.sourceIp || "",
269
+ request.callerMetadata?.userAgent || "",
270
+ request.callerMetadata?.forwardedFor || "",
271
+ request.callerMetadata?.agentCardUrl || ""
251
272
  ].join("|");
252
273
  }
253
274
  function getCachedResult(request) {
@@ -276,9 +297,13 @@ function clearCache() {
276
297
  }
277
298
  function extractCredentials(headers, query) {
278
299
  const credentials = {};
300
+ const ASTRA_ID_PATTERN = /^ASTRAE?-[A-Za-z0-9_-]{1,64}$/;
279
301
  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"];
280
302
  if (astraIdHeader) {
281
- credentials.astraId = Array.isArray(astraIdHeader) ? astraIdHeader[0] : astraIdHeader;
303
+ const raw = Array.isArray(astraIdHeader) ? astraIdHeader[0] : typeof astraIdHeader === "string" ? astraIdHeader : void 0;
304
+ if (typeof raw === "string" && ASTRA_ID_PATTERN.test(raw)) {
305
+ credentials.astraId = raw;
306
+ }
282
307
  }
283
308
  const apiKeyHeader = headers["x-api-key"] || headers["X-Api-Key"] || headers["X-API-KEY"];
284
309
  if (apiKeyHeader) {
@@ -287,9 +312,11 @@ function extractCredentials(headers, query) {
287
312
  const authHeader = headers["authorization"] || headers["Authorization"];
288
313
  if (authHeader) {
289
314
  const authValue = Array.isArray(authHeader) ? authHeader[0] : authHeader;
290
- credentials.authorizationHeader = authValue;
291
- if (authValue.startsWith("Bearer ")) {
292
- credentials.jwt = authValue.slice(7);
315
+ if (typeof authValue === "string") {
316
+ credentials.authorizationHeader = authValue;
317
+ if (authValue.startsWith("Bearer ")) {
318
+ credentials.jwt = authValue.slice(7);
319
+ }
293
320
  }
294
321
  }
295
322
  if (query) {
@@ -310,7 +337,7 @@ function createGuidanceResponse(config, reason, options = {}) {
310
337
  const isApiError = source === "api_error";
311
338
  const guidance = isApiError ? {
312
339
  message: "Verification is temporarily unavailable. Retry with exponential backoff; if the issue persists, contact support with the correlationId.",
313
- registrationUrl: `${config.apiBaseUrl.replace("/api", "")}/register`,
340
+ registrationUrl: `${config.apiBaseUrl.replace("/api", "")}/agents/register`,
314
341
  documentationUrl: `${config.apiBaseUrl.replace("/api", "")}/docs/agent-access`,
315
342
  steps: [
316
343
  "Retry the request with exponential backoff",
@@ -318,7 +345,7 @@ function createGuidanceResponse(config, reason, options = {}) {
318
345
  ]
319
346
  } : {
320
347
  message: "This service verifies AI agents before granting access. Please register your agent with AstraSync.",
321
- registrationUrl: `${config.apiBaseUrl.replace("/api", "")}/register`,
348
+ registrationUrl: `${config.apiBaseUrl.replace("/api", "")}/agents/register`,
322
349
  documentationUrl: `${config.apiBaseUrl.replace("/api", "")}/docs/agent-access`,
323
350
  steps: [
324
351
  "Register for an AstraSync account",
@@ -395,12 +422,8 @@ async function callVerifyAccessAPI(config, request) {
395
422
  "Content-Type": "application/json",
396
423
  ...config.customHeaders
397
424
  };
398
- if (credentials.authorizationHeader) {
399
- headers["Authorization"] = credentials.authorizationHeader;
400
- } else if (config.apiKey) {
401
- headers["Authorization"] = `Bearer ${config.apiKey}`;
402
- }
403
425
  if (config.apiKey) {
426
+ headers["Authorization"] = `Bearer ${config.apiKey}`;
404
427
  headers["X-API-Key"] = config.apiKey;
405
428
  }
406
429
  try {
@@ -446,7 +469,11 @@ async function callVerifyAccessAPI(config, request) {
446
469
  async function verify(config, request) {
447
470
  const mergedConfig = { ...DEFAULT_CONFIG, ...config };
448
471
  if (!initCheckPerformed && !mergedConfig.disableInitChecks && mergedConfig.apiBaseUrl) {
449
- void performInitCheck(mergedConfig.apiBaseUrl, mergedConfig.debug);
472
+ if (mergedConfig.strictInit) {
473
+ await performInitCheck(mergedConfig.apiBaseUrl, mergedConfig.debug, true);
474
+ } else {
475
+ void performInitCheck(mergedConfig.apiBaseUrl, mergedConfig.debug, false);
476
+ }
450
477
  }
451
478
  if (!deprecationWarningShown && (config.minTrustScore !== void 0 || config.minTrustScoreForFull !== void 0)) {
452
479
  deprecationWarningShown = true;
@@ -500,7 +527,7 @@ async function verify(config, request) {
500
527
  requiresApproval: apiResponse.access?.requiresApproval,
501
528
  guidance: {
502
529
  message: apiResponse.access?.reason || "Access denied by PDLSS policy",
503
- registrationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/register`,
530
+ registrationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/agents/register`,
504
531
  documentationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/docs/pdlss`
505
532
  },
506
533
  verifiedAt: /* @__PURE__ */ new Date(),
@@ -570,13 +597,15 @@ async function verify(config, request) {
570
597
  result.denialReasons = result.recommendationReasons || [
571
598
  "Access denied by AstraSync recommendation"
572
599
  ];
573
- if (result.runtimeChallenge) {
574
- result.guidance = {
575
- message: `Verification failed: ${result.runtimeChallenge.reason || "runtime challenge failed"}`,
576
- registrationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/register`,
577
- documentationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/docs/runtime-challenge`
578
- };
579
- }
600
+ result.guidance = result.runtimeChallenge ? {
601
+ message: `Verification failed: ${result.runtimeChallenge.reason || "runtime challenge failed"}`,
602
+ registrationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/agents/register`,
603
+ documentationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/docs/runtime-challenge`
604
+ } : {
605
+ message: result.recommendationReasons?.[0] || "Access denied by AstraSync recommendation",
606
+ registrationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/agents/register`,
607
+ documentationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/docs/pdlss`
608
+ };
580
609
  } else if (result.recommendation === "step_up_required") {
581
610
  result.requiresStepUp = true;
582
611
  if (ACCESS_LEVEL_HIERARCHY[result.accessLevel] > ACCESS_LEVEL_HIERARCHY["read-only"]) {
@@ -825,18 +854,40 @@ function defaultExtractPurpose(req) {
825
854
  return "general";
826
855
  }
827
856
  }
828
- function matchRoute(pattern, path) {
857
+ function matchRoute(pattern, path, opts) {
829
858
  const regexPattern = pattern.replace(/\*/g, ".*").replace(/\//g, "\\/");
830
- const regex = new RegExp(`^${regexPattern}$`);
831
- return regex.test(path);
859
+ const caseSensitiveRegex = new RegExp(`^${regexPattern}$`);
860
+ const caseSensitiveResult = caseSensitiveRegex.test(path);
861
+ if (!opts?.caseInsensitive && !opts?.logShadowDivergence) {
862
+ return caseSensitiveResult;
863
+ }
864
+ const caseInsensitiveRegex = new RegExp(`^${regexPattern}$`, "i");
865
+ const caseInsensitiveResult = caseInsensitiveRegex.test(path);
866
+ if (opts?.logShadowDivergence && caseSensitiveResult !== caseInsensitiveResult) {
867
+ console.warn(
868
+ `[SHADOW] matchRoute case-insensitive would change result: pattern=${pattern} path=${path} caseSensitive=${caseSensitiveResult} caseInsensitive=${caseInsensitiveResult} correlationId=${opts.correlationId ?? "unknown"}`
869
+ );
870
+ }
871
+ return opts?.caseInsensitive ? caseInsensitiveResult : caseSensitiveResult;
832
872
  }
833
- function findRouteConfig(routes, path, method) {
873
+ function findRouteConfig(routes, path, method, opts) {
834
874
  return routes.find((route) => {
835
875
  const methodMatches = route.method === "*" || route.method.toUpperCase() === method.toUpperCase();
836
- const pathMatches = matchRoute(route.pattern, path);
876
+ const pathMatches = matchRoute(route.pattern, path, opts);
837
877
  return methodMatches && pathMatches;
838
878
  });
839
879
  }
880
+ function dedupeFailures(result) {
881
+ if (result.failures && result.failures.length > 1) {
882
+ const seen = /* @__PURE__ */ new Set();
883
+ result.failures = result.failures.filter((f) => {
884
+ const key = `${f.dimension}|${f.message}|${f.guidance ?? ""}`;
885
+ if (seen.has(key)) return false;
886
+ seen.add(key);
887
+ return true;
888
+ });
889
+ }
890
+ }
840
891
  function defaultOnDenied(result, _req, res) {
841
892
  const statusCode = !result.identityVerified ? 401 : 403;
842
893
  res.setHeader("X-Astra-Gateway-Mode", "enforced");
@@ -863,6 +914,8 @@ function createMiddleware(options) {
863
914
  recordDecisions,
864
915
  enableRuntimeChallenge = true,
865
916
  routesRefreshMs = DEFAULT_ROUTES_REFRESH_MS,
917
+ failOnError = "open",
918
+ caseInsensitiveRouteMatch = false,
866
919
  ...config
867
920
  } = options;
868
921
  let cachedRoutes = [];
@@ -885,7 +938,7 @@ function createMiddleware(options) {
885
938
  cachedRoutes = fetched;
886
939
  lastFetchAt = Date.now();
887
940
  if (cachedRoutes.length === 0 && !warnedEmptyRoutes) {
888
- const dashboard = config.dashboardUrl ?? "https://app.astrasync.ai";
941
+ const dashboard = config.dashboardUrl ?? "https://astrasync.ai/dashboard";
889
942
  console.warn(
890
943
  `[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`
891
944
  );
@@ -911,7 +964,12 @@ function createMiddleware(options) {
911
964
  refreshing = null;
912
965
  });
913
966
  }
914
- const routeConfig = findRouteConfig(cachedRoutes, req.path, req.method);
967
+ const correlationId = req.headers["x-request-id"] || req.headers["x-correlation-id"];
968
+ const routeConfig = findRouteConfig(cachedRoutes, req.path, req.method, {
969
+ caseInsensitive: caseInsensitiveRouteMatch,
970
+ logShadowDivergence: true,
971
+ correlationId
972
+ });
915
973
  if (!routeConfig) {
916
974
  if (config.setPassThroughHeader) {
917
975
  res.setHeader("X-Astra-Gateway-Mode", "unenforced");
@@ -943,7 +1001,7 @@ function createMiddleware(options) {
943
1001
  denialReasons: preCheckFailures.map((f) => f.message),
944
1002
  guidance: {
945
1003
  message: "Request exceeds counterparty-defined PDLSS limits.",
946
- registrationUrl: `${config.apiBaseUrl?.replace("/api", "")}/register`,
1004
+ registrationUrl: `${config.apiBaseUrl?.replace("/api", "")}/agents/register`,
947
1005
  documentationUrl: `${config.apiBaseUrl?.replace("/api", "")}/docs/pdlss`
948
1006
  },
949
1007
  verifiedAt: /* @__PURE__ */ new Date()
@@ -958,18 +1016,27 @@ function createMiddleware(options) {
958
1016
  requestMethod: req.method
959
1017
  }).catch(() => {
960
1018
  });
1019
+ dedupeFailures(result2);
961
1020
  onDenied(result2, req, res);
962
1021
  return;
963
1022
  }
964
1023
  const shouldRecordDecisions = recordDecisions !== false;
965
1024
  const forwardedFor = req.headers["x-forwarded-for"];
966
1025
  const forwardedForStr = Array.isArray(forwardedFor) ? forwardedFor.join(", ") : forwardedFor;
967
- const originalClientIp = forwardedForStr ? forwardedForStr.split(",")[0].trim() : req.ip;
1026
+ const originalClientIp = req.ip ?? (forwardedForStr ? forwardedForStr.split(",")[0].trim() : void 0);
1027
+ if (!req.ip && forwardedForStr) {
1028
+ console.warn(
1029
+ "[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."
1030
+ );
1031
+ }
968
1032
  const agentCardUrl = typeof req.headers["x-astrasync-agent-card"] === "string" ? req.headers["x-astrasync-agent-card"] : void 0;
969
1033
  const result = await verify(config, {
970
1034
  credentials,
971
1035
  purpose,
972
- action: req.method.toLowerCase(),
1036
+ // RFC 7230 § 3.1.1 — HTTP method tokens uppercase by IANA convention.
1037
+ // Backend evaluator tolerates either case as defense-in-depth
1038
+ // (round-18.6 batch 2); SDK emits canonical form.
1039
+ action: req.method.toUpperCase(),
973
1040
  resource: req.path,
974
1041
  createSession: shouldRecordDecisions,
975
1042
  counterpartyUrl,
@@ -992,6 +1059,7 @@ function createMiddleware(options) {
992
1059
  recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
993
1060
  });
994
1061
  }
1062
+ dedupeFailures(result);
995
1063
  onDenied(result, req, res);
996
1064
  return;
997
1065
  }
@@ -1018,6 +1086,7 @@ function createMiddleware(options) {
1018
1086
  recordDecision(config, sessionId, "denied", insufficientFailure.message).catch(() => {
1019
1087
  });
1020
1088
  }
1089
+ dedupeFailures(result);
1021
1090
  onDenied(result, req, res);
1022
1091
  return;
1023
1092
  }
@@ -1034,6 +1103,7 @@ function createMiddleware(options) {
1034
1103
  recordDecision(config, sessionId, "denied", trustFailure.message).catch(() => {
1035
1104
  });
1036
1105
  }
1106
+ dedupeFailures(result);
1037
1107
  onDenied(result, req, res);
1038
1108
  return;
1039
1109
  }
@@ -1048,7 +1118,30 @@ function createMiddleware(options) {
1048
1118
  }
1049
1119
  next();
1050
1120
  } catch (error) {
1121
+ const errorClass = error instanceof Error ? error.constructor.name : typeof error;
1122
+ const correlationId = req.headers["x-request-id"] || req.headers["x-correlation-id"] || `gen-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
1051
1123
  console.error("[VerificationGateway] Middleware error:", error);
1124
+ console.warn(
1125
+ `[SHADOW] would-have-denied: errorClass=${errorClass} route=${req.method}:${req.path} merchantId=${config.counterpartyId ?? "unknown"} correlationId=${correlationId}`
1126
+ );
1127
+ if (failOnError === "closed") {
1128
+ const result = {
1129
+ identityVerified: false,
1130
+ policyAllowed: false,
1131
+ accessLevel: "none",
1132
+ denialReasons: [`Verification middleware internal error: ${errorClass}`],
1133
+ failures: [
1134
+ {
1135
+ dimension: "middleware.internal_error",
1136
+ message: `Middleware threw ${errorClass} \u2014 failing closed`
1137
+ }
1138
+ ],
1139
+ verifiedAt: /* @__PURE__ */ new Date(),
1140
+ correlationId
1141
+ };
1142
+ dedupeFailures(result);
1143
+ return onDenied(result, req, res);
1144
+ }
1052
1145
  next();
1053
1146
  }
1054
1147
  };
@@ -1060,6 +1153,18 @@ __export(nextjs_exports, {
1060
1153
  createMatcherConfig: () => createMatcherConfig,
1061
1154
  createMiddleware: () => createMiddleware2
1062
1155
  });
1156
+ function escapeHtml(value) {
1157
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1158
+ }
1159
+ function sanitizeUrl(value, fallback) {
1160
+ if (typeof value !== "string" || value.length === 0) return escapeHtml(fallback);
1161
+ const trimmed = value.trim();
1162
+ if (/^javascript:|^data:|^vbscript:/i.test(trimmed)) return escapeHtml(fallback);
1163
+ if (/^https?:\/\//i.test(trimmed) || trimmed.startsWith("/")) {
1164
+ return escapeHtml(trimmed);
1165
+ }
1166
+ return escapeHtml(fallback);
1167
+ }
1063
1168
  function extractCredentialsFromNextRequest(request) {
1064
1169
  const credentials = {};
1065
1170
  const astraId = request.headers.get("x-astra-id") || request.headers.get("X-Astra-Id");
@@ -1131,10 +1236,18 @@ function extractPurpose(request) {
1131
1236
  }
1132
1237
  }
1133
1238
  function generateCommerceShieldHtml(result, options) {
1134
- const title = options.commerceShield?.title || "AstraSync Agent Verification";
1135
- const message = options.commerceShield?.message || result.guidance?.message || "This site verifies AI agents before granting access. We noticed you're visiting without AstraSync credentials.";
1136
- const registrationUrl = result.guidance?.registrationUrl || "https://astrasync.ai/register";
1137
- const docsUrl = result.guidance?.documentationUrl || "https://astrasync.ai/docs/agent-access";
1239
+ const title = escapeHtml(options.commerceShield?.title || "AstraSync Agent Verification");
1240
+ const message = escapeHtml(
1241
+ options.commerceShield?.message || result.guidance?.message || "This site verifies AI agents before granting access. We noticed you're visiting without AstraSync credentials."
1242
+ );
1243
+ const registrationUrl = sanitizeUrl(
1244
+ result.guidance?.registrationUrl,
1245
+ "https://astrasync.ai/register"
1246
+ );
1247
+ const docsUrl = sanitizeUrl(
1248
+ result.guidance?.documentationUrl,
1249
+ "https://astrasync.ai/docs/agent-access"
1250
+ );
1138
1251
  const allowGuest = options.commerceShield?.allowGuestAccess ?? true;
1139
1252
  return `
1140
1253
  <!DOCTYPE html>
@@ -1256,7 +1369,7 @@ function generateCommerceShieldHtml(result, options) {
1256
1369
  <div class="shield-steps">
1257
1370
  <h3>To get verified access:</h3>
1258
1371
  <ol>
1259
- <li>Register at <a href="${registrationUrl}">astrasync.ai/register</a></li>
1372
+ <li>Register at <a href="${registrationUrl}">astrasync.ai/agents/register</a></li>
1260
1373
  <li>Create and register your agent</li>
1261
1374
  <li>Add your ASTRA-ID to request headers</li>
1262
1375
  <li>Refresh this page</li>
@@ -1344,7 +1457,7 @@ function createMiddleware2(options) {
1344
1457
  denialReasons: preCheckFailures.map((f) => f.message),
1345
1458
  guidance: {
1346
1459
  message: "Request exceeds counterparty-defined PDLSS limits.",
1347
- registrationUrl: `${config.apiBaseUrl?.replace("/api", "")}/register`,
1460
+ registrationUrl: `${config.apiBaseUrl?.replace("/api", "")}/agents/register`,
1348
1461
  documentationUrl: `${config.apiBaseUrl?.replace("/api", "")}/docs/pdlss`
1349
1462
  },
1350
1463
  verifiedAt: /* @__PURE__ */ new Date()
@@ -1387,7 +1500,10 @@ function createMiddleware2(options) {
1387
1500
  const result = await verify(config, {
1388
1501
  credentials,
1389
1502
  purpose,
1390
- action: request.method.toLowerCase(),
1503
+ // RFC 7230 § 3.1.1 — HTTP method tokens uppercase by IANA convention.
1504
+ // Backend evaluator tolerates either case as defense-in-depth
1505
+ // (round-18.6 batch 2); SDK emits canonical form.
1506
+ action: request.method.toUpperCase(),
1391
1507
  resource: pathname,
1392
1508
  counterpartyUrl,
1393
1509
  counterpartyType: config.counterpartyType || "website",
@@ -2002,12 +2118,45 @@ function bufferToBase64(bytes) {
2002
2118
 
2003
2119
  // src/transport/rfc9421-verify.ts
2004
2120
  var import_http_message_signatures = require("http-message-signatures");
2121
+
2122
+ // src/transport/nonce-store.ts
2123
+ var InMemoryNonceStore = class {
2124
+ constructor(capacity = 1e4) {
2125
+ this.entries = /* @__PURE__ */ new Map();
2126
+ this.lastSweepMs = 0;
2127
+ this.capacity = capacity;
2128
+ }
2129
+ seen(key, expiresAtMs) {
2130
+ const nowMs = Date.now();
2131
+ if (nowMs - this.lastSweepMs > 1e3) {
2132
+ for (const [k, exp] of this.entries) {
2133
+ if (exp <= nowMs) this.entries.delete(k);
2134
+ }
2135
+ this.lastSweepMs = nowMs;
2136
+ }
2137
+ const existing = this.entries.get(key);
2138
+ if (existing !== void 0 && existing > nowMs) {
2139
+ return true;
2140
+ }
2141
+ if (this.entries.size >= this.capacity) {
2142
+ const oldest = this.entries.keys().next().value;
2143
+ if (oldest !== void 0) this.entries.delete(oldest);
2144
+ }
2145
+ this.entries.set(key, expiresAtMs);
2146
+ return false;
2147
+ }
2148
+ };
2149
+ var defaultNonceStore = new InMemoryNonceStore();
2150
+
2151
+ // src/transport/rfc9421-verify.ts
2005
2152
  async function verifyRFC9421(request, options) {
2006
2153
  const { resolver } = options;
2007
- const tolerance = options.clockSkewSec ?? 300;
2154
+ const tolerance = options.clockSkewSec ?? 60;
2008
2155
  const nowSec = options.now ? options.now() : Math.floor(Date.now() / 1e3);
2156
+ const nonceStore = options.nonceStore ?? defaultNonceStore;
2009
2157
  let resolvedKid;
2010
2158
  let resolvedAlg;
2159
+ let replayDetected = false;
2011
2160
  const keyLookup = async (parameters) => {
2012
2161
  const kid = typeof parameters.keyid === "string" ? parameters.keyid : void 0;
2013
2162
  if (!kid) return null;
@@ -2021,6 +2170,14 @@ async function verifyRFC9421(request, options) {
2021
2170
  const expires = toUnixSeconds(parameters.expires);
2022
2171
  if (created !== void 0 && Math.abs(nowSec - created) > tolerance) return null;
2023
2172
  if (expires !== void 0 && nowSec > expires + tolerance) return null;
2173
+ const nonce = typeof parameters.nonce === "string" ? parameters.nonce : void 0;
2174
+ if (nonce) {
2175
+ const expiresAtMs = (expires !== void 0 ? expires + tolerance : nowSec + tolerance) * 1e3;
2176
+ if (nonceStore.seen(`rfc9421:${kid}:${nonce}`, expiresAtMs)) {
2177
+ replayDetected = true;
2178
+ return null;
2179
+ }
2180
+ }
2024
2181
  return jwkToVerifyingKey(kid, jwk, alg);
2025
2182
  };
2026
2183
  try {
@@ -2043,7 +2200,7 @@ async function verifyRFC9421(request, options) {
2043
2200
  kid: resolvedKid,
2044
2201
  registry: resolver.name,
2045
2202
  algorithm: resolvedAlg,
2046
- error: result === false ? "signature invalid" : "no signature found"
2203
+ error: replayDetected ? "RFC9421 signature replay \u2014 already seen within tolerance window" : result === false ? "signature invalid" : "no signature found"
2047
2204
  };
2048
2205
  } catch (err) {
2049
2206
  return {
@@ -2868,14 +3025,26 @@ function sha256Sync2(data) {
2868
3025
  function verifyAP2Chain(input) {
2869
3026
  const { triple } = input;
2870
3027
  const errors = [];
3028
+ const toleranceSec = input.clockSkewSec ?? 60;
3029
+ const nonceStore = input.nonceStore ?? defaultNonceStore;
2871
3030
  const intentPresent = triple.intent !== void 0;
2872
3031
  const cartRefOk = checkCartRef(triple, errors);
2873
3032
  const paymentRefOk = checkPaymentRef(triple, errors);
2874
3033
  const { ok: agentIdContinuity, agentId } = checkAgentContinuity(triple, errors);
2875
3034
  const paymentMethodAllowed = checkPaymentMethod(triple, errors);
2876
3035
  const totalsConsistent = checkTotals(triple, errors);
2877
- const expiryOk = checkExpiries(triple, input.clockSkewSec ?? 300, input.now, errors);
2878
- const ok = cartRefOk && paymentRefOk && agentIdContinuity && paymentMethodAllowed && totalsConsistent && expiryOk;
3036
+ const expiryOk = checkExpiries(triple, toleranceSec, input.now, errors);
3037
+ let replayOk = true;
3038
+ const replayId = triple.payment?.raw?.id ?? triple.cart?.raw?.id;
3039
+ if (typeof replayId === "string" && replayId.length > 0) {
3040
+ const now = input.now ? input.now() : Math.floor(Date.now() / 1e3);
3041
+ const expiresAt = (now + toleranceSec) * 1e3;
3042
+ if (nonceStore.seen(`ap2:${replayId}`, expiresAt)) {
3043
+ errors.push(`AP2 chain replay \u2014 mandate ${replayId} already seen within tolerance window`);
3044
+ replayOk = false;
3045
+ }
3046
+ }
3047
+ const ok = cartRefOk && paymentRefOk && agentIdContinuity && paymentMethodAllowed && totalsConsistent && expiryOk && replayOk;
2879
3048
  return {
2880
3049
  ok,
2881
3050
  checks: {
@@ -2921,7 +3090,10 @@ function checkAgentContinuity(triple, errors) {
2921
3090
  const ids = [triple.intent?.agent_id, triple.cart?.agent_id, triple.payment?.agent_id].filter(
2922
3091
  (id) => typeof id === "string" && id.length > 0
2923
3092
  );
2924
- if (ids.length === 0) return { ok: true };
3093
+ if (ids.length === 0) {
3094
+ errors.push("agent_id missing across all three mandates (intent/cart/payment)");
3095
+ return { ok: false };
3096
+ }
2925
3097
  const unique = new Set(ids);
2926
3098
  if (unique.size > 1) {
2927
3099
  errors.push(`agent_id mismatch across mandates: ${Array.from(unique).join(", ")}`);
@@ -2930,9 +3102,16 @@ function checkAgentContinuity(triple, errors) {
2930
3102
  return { ok: true, agentId: ids[0] };
2931
3103
  }
2932
3104
  function checkPaymentMethod(triple, errors) {
2933
- const paymentMethod = triple.payment?.payment_method;
2934
3105
  const allowed = triple.intent?.paymentMethods;
2935
- if (!paymentMethod || !allowed || allowed.length === 0) return true;
3106
+ if (!allowed || allowed.length === 0) return true;
3107
+ if (!triple.payment) return true;
3108
+ const paymentMethod = triple.payment.payment_method;
3109
+ if (!paymentMethod) {
3110
+ errors.push(
3111
+ `payment.payment_method missing but intent declares allowlist [${allowed.join(", ")}]`
3112
+ );
3113
+ return false;
3114
+ }
2936
3115
  if (!allowed.includes(paymentMethod)) {
2937
3116
  errors.push(
2938
3117
  `payment_method "${paymentMethod}" not in intent.paymentMethods [${allowed.join(", ")}]`
@@ -2966,19 +3145,24 @@ function checkTotals(triple, errors) {
2966
3145
  function checkExpiries(triple, toleranceSec, nowFn, errors) {
2967
3146
  const now = nowFn ? nowFn() : Math.floor(Date.now() / 1e3);
2968
3147
  let ok = true;
2969
- for (const [name, mandate] of [
2970
- ["intent", triple.intent],
2971
- ["cart", triple.cart]
2972
- ]) {
2973
- if (!mandate?.expires) continue;
2974
- const parsed = parseExpiry(mandate.expires);
3148
+ const layers = [
3149
+ ["intent", triple.intent?.expires],
3150
+ ["cart", triple.cart?.expires],
3151
+ [
3152
+ "payment",
3153
+ typeof triple.payment?.raw?.expires === "string" ? triple.payment.raw.expires : typeof triple.payment?.raw?.exp === "string" ? triple.payment.raw.exp : void 0
3154
+ ]
3155
+ ];
3156
+ for (const [name, expires] of layers) {
3157
+ if (!expires) continue;
3158
+ const parsed = parseExpiry(expires);
2975
3159
  if (parsed === null) {
2976
3160
  errors.push(`${name}.expires unparseable`);
2977
3161
  ok = false;
2978
3162
  continue;
2979
3163
  }
2980
3164
  if (now > parsed + toleranceSec) {
2981
- errors.push(`${name} mandate expired at ${mandate.expires}`);
3165
+ errors.push(`${name} mandate expired at ${expires}`);
2982
3166
  ok = false;
2983
3167
  }
2984
3168
  }
@@ -3005,10 +3189,21 @@ async function verifyACPSignature(input) {
3005
3189
  if (!input.signatureHeader) {
3006
3190
  return { ok: false, error: "missing Signature header" };
3007
3191
  }
3008
- const freshness = checkTimestamp(input.timestampHeader, input.clockSkewSec ?? 300, input.now);
3192
+ const tolerance = input.clockSkewSec ?? 60;
3193
+ const nonceStore = input.nonceStore ?? defaultNonceStore;
3194
+ const freshness = checkTimestamp(input.timestampHeader, tolerance, input.now);
3009
3195
  if (!freshness.ok) {
3010
3196
  return { ok: false, error: freshness.error, timestampStale: true };
3011
3197
  }
3198
+ const nowSec = input.now ? input.now() : Math.floor(Date.now() / 1e3);
3199
+ const expiresAtMs = (nowSec + tolerance) * 1e3;
3200
+ const replayKey = `acp:${input.signatureHeader}:${input.timestampHeader ?? ""}`;
3201
+ if (nonceStore.seen(replayKey, expiresAtMs)) {
3202
+ return {
3203
+ ok: false,
3204
+ error: "ACP signature replay \u2014 already seen within tolerance window"
3205
+ };
3206
+ }
3012
3207
  const signatureBytes = decodeBase64(input.signatureHeader);
3013
3208
  if (!signatureBytes) {
3014
3209
  return { ok: false, error: "signature header is not valid base64" };
@@ -3226,8 +3421,9 @@ function coerceString6(v) {
3226
3421
  var import_mppx2 = require("mppx");
3227
3422
  function verifyMPP(input) {
3228
3423
  const { context } = input;
3229
- const tolerance = input.clockSkewSec ?? 300;
3424
+ const tolerance = input.clockSkewSec ?? 60;
3230
3425
  const nowSec = input.now ? input.now() : Math.floor(Date.now() / 1e3);
3426
+ const nonceStore = input.nonceStore ?? defaultNonceStore;
3231
3427
  const challenge = context.credential?.challenge ?? (context.challenges && context.challenges[0]);
3232
3428
  const source = context.credential?.source;
3233
3429
  const method = challenge?.method;
@@ -3250,21 +3446,38 @@ function verifyMPP(input) {
3250
3446
  }
3251
3447
  }
3252
3448
  let bodyDigestOk = null;
3253
- if (challenge?.digest && input.rawBody !== void 0) {
3254
- try {
3255
- if (!/^sha-256=/.test(challenge.digest)) {
3449
+ if (input.rawBody !== void 0) {
3450
+ if (!challenge?.digest) {
3451
+ bodyDigestOk = false;
3452
+ } else {
3453
+ try {
3454
+ if (!/^sha-256=/.test(challenge.digest)) {
3455
+ bodyDigestOk = false;
3456
+ } else {
3457
+ bodyDigestOk = import_mppx2.BodyDigest.verify(challenge.digest, input.rawBody);
3458
+ }
3459
+ } catch {
3256
3460
  bodyDigestOk = false;
3257
- } else {
3258
- bodyDigestOk = import_mppx2.BodyDigest.verify(challenge.digest, input.rawBody);
3259
3461
  }
3260
- } catch {
3261
- bodyDigestOk = false;
3262
3462
  }
3263
3463
  }
3264
- const ok = expiryOk && (bodyDigestOk === null || bodyDigestOk === true);
3464
+ let replayOk = true;
3465
+ if (challenge?.digest && expiryOk) {
3466
+ const replayKey = `mpp:${challenge.digest}:${challenge.nonce ?? ""}`;
3467
+ const expiresAt = (nowSec + tolerance) * 1e3;
3468
+ if (nonceStore.seen(replayKey, expiresAt)) {
3469
+ replayOk = false;
3470
+ }
3471
+ }
3472
+ const ok = expiryOk && (bodyDigestOk === null || bodyDigestOk === true) && replayOk;
3265
3473
  const errors = [];
3266
3474
  if (!expiryOk) errors.push("challenge expired");
3267
- if (bodyDigestOk === false) errors.push("body digest mismatch");
3475
+ if (bodyDigestOk === false) {
3476
+ errors.push(
3477
+ input.rawBody !== void 0 && !challenge?.digest ? "body digest required when rawBody present" : "body digest mismatch"
3478
+ );
3479
+ }
3480
+ if (!replayOk) errors.push("MPP challenge replay \u2014 already seen within tolerance window");
3268
3481
  return {
3269
3482
  ok,
3270
3483
  expiryOk,
@@ -3425,14 +3638,32 @@ function readHeader4(headers, name) {
3425
3638
  var import_node_crypto4 = require("crypto");
3426
3639
  async function verifyVIChain(input) {
3427
3640
  const errors = [];
3428
- const tolerance = input.clockSkewSec ?? 300;
3641
+ const tolerance = input.clockSkewSec ?? 60;
3429
3642
  const now = input.now ? input.now() : Math.floor(Date.now() / 1e3);
3430
3643
  const { l1, l2, l3a, l3b } = input.layers;
3644
+ const nonceStore = input.nonceStore ?? defaultNonceStore;
3645
+ if (!l1) {
3646
+ if (!input.allowUnboundChain) {
3647
+ errors.push(
3648
+ "L1 missing \u2014 chain root unbound (set allowUnboundChain + expectedL2Key to override)"
3649
+ );
3650
+ } else if (!input.expectedL2Key) {
3651
+ errors.push("allowUnboundChain set but expectedL2Key missing");
3652
+ }
3653
+ }
3431
3654
  const l1SigOk = l1 ? await input.verifySignature(l1, null) : null;
3432
3655
  if (l1 && !l1SigOk) errors.push("L1 signature invalid");
3433
3656
  const l1Cnf = extractCnfJwk(l1?.payload);
3434
- const l2SigOk = await input.verifySignature(l2, l1Cnf ?? null);
3657
+ const l2ExpectedKey = l1Cnf ?? input.expectedL2Key ?? null;
3658
+ const l2SigOk = await input.verifySignature(l2, l2ExpectedKey);
3435
3659
  if (!l2SigOk) errors.push("L2 signature invalid");
3660
+ if (l2SigOk) {
3661
+ const replayKey = `vi:l2:${l2.compact}`;
3662
+ const expiresAt = now * 1e3 + tolerance * 1e3;
3663
+ if (nonceStore.seen(replayKey, expiresAt)) {
3664
+ errors.push("L2 signature replay \u2014 already seen within tolerance window");
3665
+ }
3666
+ }
3436
3667
  const l2Cnf = extractCnfJwk(l2.payload);
3437
3668
  const l3aSigOk = l3a ? await input.verifySignature(l3a, l2Cnf ?? null) : null;
3438
3669
  if (l3a && !l3aSigOk) errors.push("L3a signature invalid");
@@ -3476,7 +3707,10 @@ async function verifyVIChain(input) {
3476
3707
  }
3477
3708
  }
3478
3709
  const expiryOk = checkExpiryAcross([l1, l2, l3a, l3b], tolerance, now, errors);
3479
- const ok = l1SigOk !== false && l2SigOk && l3aSigOk !== false && l3bSigOk !== false && l1BindsL2 && l2BindsL3 && l3aL3bTxnIdMatch !== false && checkoutHashOk !== false && expiryOk;
3710
+ const noUnboundChainOrReplayErrors = !errors.some(
3711
+ (e) => e.startsWith("L1 missing") || e.startsWith("allowUnboundChain set") || e.startsWith("L2 signature replay")
3712
+ );
3713
+ const ok = l1SigOk !== false && l2SigOk && l3aSigOk !== false && l3bSigOk !== false && l1BindsL2 && l2BindsL3 && l3aL3bTxnIdMatch !== false && checkoutHashOk !== false && expiryOk && noUnboundChainOrReplayErrors;
3480
3714
  return {
3481
3715
  ok,
3482
3716
  checks: {
@@ -4131,7 +4365,7 @@ function mcpToPdlss(parsed, headerPurpose, headerAction) {
4131
4365
  action = parsed.actionFromBody;
4132
4366
  actionSource = parsed.actionSourceFromBody;
4133
4367
  } else {
4134
- action = parsed.toolName ? `${parsed.method}:${parsed.toolName}` : parsed.method;
4368
+ action = parsed.toolName ? parsed.method === "tools/call" ? parsed.toolName : `${parsed.method}:${parsed.toolName}` : parsed.method;
4135
4369
  actionSource = "transport_layer";
4136
4370
  }
4137
4371
  return { purpose, action, resource, purposeSource, actionSource };
@@ -4150,6 +4384,17 @@ function readSingleHeader(value) {
4150
4384
  if (Array.isArray(value)) return value[0];
4151
4385
  return void 0;
4152
4386
  }
4387
+ function dedupeFailures2(result) {
4388
+ if (result.failures && result.failures.length > 1) {
4389
+ const seen = /* @__PURE__ */ new Set();
4390
+ result.failures = result.failures.filter((f) => {
4391
+ const key = `${f.dimension}|${f.message}|${f.guidance ?? ""}`;
4392
+ if (seen.has(key)) return false;
4393
+ seen.add(key);
4394
+ return true;
4395
+ });
4396
+ }
4397
+ }
4153
4398
  function defaultMcpDenied(result, req, res) {
4154
4399
  const id = req.body?.id ?? null;
4155
4400
  const status = !result.identityVerified ? 401 : 403;
@@ -4189,10 +4434,11 @@ function createMcpMiddleware(options) {
4189
4434
  onAgentIdMismatch = "reject",
4190
4435
  skip = false,
4191
4436
  onDenied = defaultMcpDenied,
4192
- trustVerifiedHop = true,
4437
+ trustVerifiedHop = false,
4193
4438
  verifiedHopMaxAgeMs,
4194
4439
  recordDecisions,
4195
4440
  enableRuntimeChallenge = true,
4441
+ failOnError = "open",
4196
4442
  ...config
4197
4443
  } = options;
4198
4444
  return async (req, res, next) => {
@@ -4300,6 +4546,7 @@ function createMcpMiddleware(options) {
4300
4546
  recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
4301
4547
  });
4302
4548
  }
4549
+ dedupeFailures2(result);
4303
4550
  onDenied(result, req, res);
4304
4551
  return;
4305
4552
  }
@@ -4345,6 +4592,7 @@ function createMcpMiddleware(options) {
4345
4592
  });
4346
4593
  }
4347
4594
  }
4595
+ dedupeFailures2(result);
4348
4596
  onDenied(result, req, res);
4349
4597
  return;
4350
4598
  }
@@ -4368,7 +4616,30 @@ function createMcpMiddleware(options) {
4368
4616
  }
4369
4617
  next();
4370
4618
  } catch (error) {
4619
+ const errorClass = error instanceof Error ? error.constructor.name : typeof error;
4620
+ const correlationId = req.headers["x-request-id"] || req.headers["x-correlation-id"] || `gen-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
4371
4621
  console.error("[VerificationGateway/MCP] Middleware error:", error);
4622
+ console.warn(
4623
+ `[SHADOW] would-have-denied: errorClass=${errorClass} route=${req.method}:${req.path} merchantId=${config.counterpartyId ?? "unknown"} correlationId=${correlationId}`
4624
+ );
4625
+ if (failOnError === "closed") {
4626
+ const result = {
4627
+ identityVerified: false,
4628
+ policyAllowed: false,
4629
+ accessLevel: "none",
4630
+ denialReasons: [`MCP middleware internal error: ${errorClass}`],
4631
+ failures: [
4632
+ {
4633
+ dimension: "middleware.internal_error",
4634
+ message: `Middleware threw ${errorClass} \u2014 failing closed`
4635
+ }
4636
+ ],
4637
+ verifiedAt: /* @__PURE__ */ new Date(),
4638
+ correlationId
4639
+ };
4640
+ dedupeFailures2(result);
4641
+ return onDenied(result, req, res);
4642
+ }
4372
4643
  next();
4373
4644
  }
4374
4645
  };