@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.mjs CHANGED
@@ -126,7 +126,7 @@ function getCapabilities(accessLevel) {
126
126
  }
127
127
 
128
128
  // src/version.ts
129
- var SDK_VERSION = "2.4.11";
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,18 +953,27 @@ 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,
908
972
  purpose,
909
- action: req.method.toLowerCase(),
973
+ // RFC 7230 § 3.1.1 — HTTP method tokens uppercase by IANA convention.
974
+ // Backend evaluator tolerates either case as defense-in-depth
975
+ // (round-18.6 batch 2); SDK emits canonical form.
976
+ action: req.method.toUpperCase(),
910
977
  resource: req.path,
911
978
  createSession: shouldRecordDecisions,
912
979
  counterpartyUrl,
@@ -929,6 +996,7 @@ function createMiddleware(options) {
929
996
  recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
930
997
  });
931
998
  }
999
+ dedupeFailures(result);
932
1000
  onDenied(result, req, res);
933
1001
  return;
934
1002
  }
@@ -955,6 +1023,7 @@ function createMiddleware(options) {
955
1023
  recordDecision(config, sessionId, "denied", insufficientFailure.message).catch(() => {
956
1024
  });
957
1025
  }
1026
+ dedupeFailures(result);
958
1027
  onDenied(result, req, res);
959
1028
  return;
960
1029
  }
@@ -971,6 +1040,7 @@ function createMiddleware(options) {
971
1040
  recordDecision(config, sessionId, "denied", trustFailure.message).catch(() => {
972
1041
  });
973
1042
  }
1043
+ dedupeFailures(result);
974
1044
  onDenied(result, req, res);
975
1045
  return;
976
1046
  }
@@ -985,7 +1055,30 @@ function createMiddleware(options) {
985
1055
  }
986
1056
  next();
987
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)}`;
988
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
+ }
989
1082
  next();
990
1083
  }
991
1084
  };
@@ -997,6 +1090,18 @@ __export(nextjs_exports, {
997
1090
  createMatcherConfig: () => createMatcherConfig,
998
1091
  createMiddleware: () => createMiddleware2
999
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
+ }
1000
1105
  function extractCredentialsFromNextRequest(request) {
1001
1106
  const credentials = {};
1002
1107
  const astraId = request.headers.get("x-astra-id") || request.headers.get("X-Astra-Id");
@@ -1068,10 +1173,18 @@ function extractPurpose(request) {
1068
1173
  }
1069
1174
  }
1070
1175
  function generateCommerceShieldHtml(result, options) {
1071
- const title = options.commerceShield?.title || "AstraSync Agent Verification";
1072
- const message = options.commerceShield?.message || result.guidance?.message || "This site verifies AI agents before granting access. We noticed you're visiting without AstraSync credentials.";
1073
- const registrationUrl = result.guidance?.registrationUrl || "https://astrasync.ai/register";
1074
- 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
+ );
1075
1188
  const allowGuest = options.commerceShield?.allowGuestAccess ?? true;
1076
1189
  return `
1077
1190
  <!DOCTYPE html>
@@ -1193,7 +1306,7 @@ function generateCommerceShieldHtml(result, options) {
1193
1306
  <div class="shield-steps">
1194
1307
  <h3>To get verified access:</h3>
1195
1308
  <ol>
1196
- <li>Register at <a href="${registrationUrl}">astrasync.ai/register</a></li>
1309
+ <li>Register at <a href="${registrationUrl}">astrasync.ai/agents/register</a></li>
1197
1310
  <li>Create and register your agent</li>
1198
1311
  <li>Add your ASTRA-ID to request headers</li>
1199
1312
  <li>Refresh this page</li>
@@ -1281,7 +1394,7 @@ function createMiddleware2(options) {
1281
1394
  denialReasons: preCheckFailures.map((f) => f.message),
1282
1395
  guidance: {
1283
1396
  message: "Request exceeds counterparty-defined PDLSS limits.",
1284
- registrationUrl: `${config.apiBaseUrl?.replace("/api", "")}/register`,
1397
+ registrationUrl: `${config.apiBaseUrl?.replace("/api", "")}/agents/register`,
1285
1398
  documentationUrl: `${config.apiBaseUrl?.replace("/api", "")}/docs/pdlss`
1286
1399
  },
1287
1400
  verifiedAt: /* @__PURE__ */ new Date()
@@ -1324,7 +1437,10 @@ function createMiddleware2(options) {
1324
1437
  const result = await verify(config, {
1325
1438
  credentials,
1326
1439
  purpose,
1327
- action: request.method.toLowerCase(),
1440
+ // RFC 7230 § 3.1.1 — HTTP method tokens uppercase by IANA convention.
1441
+ // Backend evaluator tolerates either case as defense-in-depth
1442
+ // (round-18.6 batch 2); SDK emits canonical form.
1443
+ action: request.method.toUpperCase(),
1328
1444
  resource: pathname,
1329
1445
  counterpartyUrl,
1330
1446
  counterpartyType: config.counterpartyType || "website",
@@ -1939,12 +2055,45 @@ function bufferToBase64(bytes) {
1939
2055
 
1940
2056
  // src/transport/rfc9421-verify.ts
1941
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
1942
2089
  async function verifyRFC9421(request, options) {
1943
2090
  const { resolver } = options;
1944
- const tolerance = options.clockSkewSec ?? 300;
2091
+ const tolerance = options.clockSkewSec ?? 60;
1945
2092
  const nowSec = options.now ? options.now() : Math.floor(Date.now() / 1e3);
2093
+ const nonceStore = options.nonceStore ?? defaultNonceStore;
1946
2094
  let resolvedKid;
1947
2095
  let resolvedAlg;
2096
+ let replayDetected = false;
1948
2097
  const keyLookup = async (parameters) => {
1949
2098
  const kid = typeof parameters.keyid === "string" ? parameters.keyid : void 0;
1950
2099
  if (!kid) return null;
@@ -1958,6 +2107,14 @@ async function verifyRFC9421(request, options) {
1958
2107
  const expires = toUnixSeconds(parameters.expires);
1959
2108
  if (created !== void 0 && Math.abs(nowSec - created) > tolerance) return null;
1960
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
+ }
1961
2118
  return jwkToVerifyingKey(kid, jwk, alg);
1962
2119
  };
1963
2120
  try {
@@ -1980,7 +2137,7 @@ async function verifyRFC9421(request, options) {
1980
2137
  kid: resolvedKid,
1981
2138
  registry: resolver.name,
1982
2139
  algorithm: resolvedAlg,
1983
- 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"
1984
2141
  };
1985
2142
  } catch (err) {
1986
2143
  return {
@@ -2805,14 +2962,26 @@ function sha256Sync2(data) {
2805
2962
  function verifyAP2Chain(input) {
2806
2963
  const { triple } = input;
2807
2964
  const errors = [];
2965
+ const toleranceSec = input.clockSkewSec ?? 60;
2966
+ const nonceStore = input.nonceStore ?? defaultNonceStore;
2808
2967
  const intentPresent = triple.intent !== void 0;
2809
2968
  const cartRefOk = checkCartRef(triple, errors);
2810
2969
  const paymentRefOk = checkPaymentRef(triple, errors);
2811
2970
  const { ok: agentIdContinuity, agentId } = checkAgentContinuity(triple, errors);
2812
2971
  const paymentMethodAllowed = checkPaymentMethod(triple, errors);
2813
2972
  const totalsConsistent = checkTotals(triple, errors);
2814
- const expiryOk = checkExpiries(triple, input.clockSkewSec ?? 300, input.now, errors);
2815
- 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;
2816
2985
  return {
2817
2986
  ok,
2818
2987
  checks: {
@@ -2858,7 +3027,10 @@ function checkAgentContinuity(triple, errors) {
2858
3027
  const ids = [triple.intent?.agent_id, triple.cart?.agent_id, triple.payment?.agent_id].filter(
2859
3028
  (id) => typeof id === "string" && id.length > 0
2860
3029
  );
2861
- 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
+ }
2862
3034
  const unique = new Set(ids);
2863
3035
  if (unique.size > 1) {
2864
3036
  errors.push(`agent_id mismatch across mandates: ${Array.from(unique).join(", ")}`);
@@ -2867,9 +3039,16 @@ function checkAgentContinuity(triple, errors) {
2867
3039
  return { ok: true, agentId: ids[0] };
2868
3040
  }
2869
3041
  function checkPaymentMethod(triple, errors) {
2870
- const paymentMethod = triple.payment?.payment_method;
2871
3042
  const allowed = triple.intent?.paymentMethods;
2872
- 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
+ }
2873
3052
  if (!allowed.includes(paymentMethod)) {
2874
3053
  errors.push(
2875
3054
  `payment_method "${paymentMethod}" not in intent.paymentMethods [${allowed.join(", ")}]`
@@ -2903,19 +3082,24 @@ function checkTotals(triple, errors) {
2903
3082
  function checkExpiries(triple, toleranceSec, nowFn, errors) {
2904
3083
  const now = nowFn ? nowFn() : Math.floor(Date.now() / 1e3);
2905
3084
  let ok = true;
2906
- for (const [name, mandate] of [
2907
- ["intent", triple.intent],
2908
- ["cart", triple.cart]
2909
- ]) {
2910
- if (!mandate?.expires) continue;
2911
- 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);
2912
3096
  if (parsed === null) {
2913
3097
  errors.push(`${name}.expires unparseable`);
2914
3098
  ok = false;
2915
3099
  continue;
2916
3100
  }
2917
3101
  if (now > parsed + toleranceSec) {
2918
- errors.push(`${name} mandate expired at ${mandate.expires}`);
3102
+ errors.push(`${name} mandate expired at ${expires}`);
2919
3103
  ok = false;
2920
3104
  }
2921
3105
  }
@@ -2942,10 +3126,21 @@ async function verifyACPSignature(input) {
2942
3126
  if (!input.signatureHeader) {
2943
3127
  return { ok: false, error: "missing Signature header" };
2944
3128
  }
2945
- 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);
2946
3132
  if (!freshness.ok) {
2947
3133
  return { ok: false, error: freshness.error, timestampStale: true };
2948
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
+ }
2949
3144
  const signatureBytes = decodeBase64(input.signatureHeader);
2950
3145
  if (!signatureBytes) {
2951
3146
  return { ok: false, error: "signature header is not valid base64" };
@@ -3163,8 +3358,9 @@ function coerceString6(v) {
3163
3358
  import { BodyDigest } from "mppx";
3164
3359
  function verifyMPP(input) {
3165
3360
  const { context } = input;
3166
- const tolerance = input.clockSkewSec ?? 300;
3361
+ const tolerance = input.clockSkewSec ?? 60;
3167
3362
  const nowSec = input.now ? input.now() : Math.floor(Date.now() / 1e3);
3363
+ const nonceStore = input.nonceStore ?? defaultNonceStore;
3168
3364
  const challenge = context.credential?.challenge ?? (context.challenges && context.challenges[0]);
3169
3365
  const source = context.credential?.source;
3170
3366
  const method = challenge?.method;
@@ -3187,21 +3383,38 @@ function verifyMPP(input) {
3187
3383
  }
3188
3384
  }
3189
3385
  let bodyDigestOk = null;
3190
- if (challenge?.digest && input.rawBody !== void 0) {
3191
- try {
3192
- 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 {
3193
3397
  bodyDigestOk = false;
3194
- } else {
3195
- bodyDigestOk = BodyDigest.verify(challenge.digest, input.rawBody);
3196
3398
  }
3197
- } catch {
3198
- bodyDigestOk = false;
3199
3399
  }
3200
3400
  }
3201
- 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;
3202
3410
  const errors = [];
3203
3411
  if (!expiryOk) errors.push("challenge expired");
3204
- 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");
3205
3418
  return {
3206
3419
  ok,
3207
3420
  expiryOk,
@@ -3365,14 +3578,32 @@ function readHeader4(headers, name) {
3365
3578
  import { createHash as createHash3, webcrypto } from "crypto";
3366
3579
  async function verifyVIChain(input) {
3367
3580
  const errors = [];
3368
- const tolerance = input.clockSkewSec ?? 300;
3581
+ const tolerance = input.clockSkewSec ?? 60;
3369
3582
  const now = input.now ? input.now() : Math.floor(Date.now() / 1e3);
3370
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
+ }
3371
3594
  const l1SigOk = l1 ? await input.verifySignature(l1, null) : null;
3372
3595
  if (l1 && !l1SigOk) errors.push("L1 signature invalid");
3373
3596
  const l1Cnf = extractCnfJwk(l1?.payload);
3374
- const l2SigOk = await input.verifySignature(l2, l1Cnf ?? null);
3597
+ const l2ExpectedKey = l1Cnf ?? input.expectedL2Key ?? null;
3598
+ const l2SigOk = await input.verifySignature(l2, l2ExpectedKey);
3375
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
+ }
3376
3607
  const l2Cnf = extractCnfJwk(l2.payload);
3377
3608
  const l3aSigOk = l3a ? await input.verifySignature(l3a, l2Cnf ?? null) : null;
3378
3609
  if (l3a && !l3aSigOk) errors.push("L3a signature invalid");
@@ -3416,7 +3647,10 @@ async function verifyVIChain(input) {
3416
3647
  }
3417
3648
  }
3418
3649
  const expiryOk = checkExpiryAcross([l1, l2, l3a, l3b], tolerance, now, errors);
3419
- 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;
3420
3654
  return {
3421
3655
  ok,
3422
3656
  checks: {
@@ -4071,7 +4305,7 @@ function mcpToPdlss(parsed, headerPurpose, headerAction) {
4071
4305
  action = parsed.actionFromBody;
4072
4306
  actionSource = parsed.actionSourceFromBody;
4073
4307
  } else {
4074
- 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;
4075
4309
  actionSource = "transport_layer";
4076
4310
  }
4077
4311
  return { purpose, action, resource, purposeSource, actionSource };
@@ -4090,6 +4324,17 @@ function readSingleHeader(value) {
4090
4324
  if (Array.isArray(value)) return value[0];
4091
4325
  return void 0;
4092
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
+ }
4093
4338
  function defaultMcpDenied(result, req, res) {
4094
4339
  const id = req.body?.id ?? null;
4095
4340
  const status = !result.identityVerified ? 401 : 403;
@@ -4129,10 +4374,11 @@ function createMcpMiddleware(options) {
4129
4374
  onAgentIdMismatch = "reject",
4130
4375
  skip = false,
4131
4376
  onDenied = defaultMcpDenied,
4132
- trustVerifiedHop = true,
4377
+ trustVerifiedHop = false,
4133
4378
  verifiedHopMaxAgeMs,
4134
4379
  recordDecisions,
4135
4380
  enableRuntimeChallenge = true,
4381
+ failOnError = "open",
4136
4382
  ...config
4137
4383
  } = options;
4138
4384
  return async (req, res, next) => {
@@ -4240,6 +4486,7 @@ function createMcpMiddleware(options) {
4240
4486
  recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
4241
4487
  });
4242
4488
  }
4489
+ dedupeFailures2(result);
4243
4490
  onDenied(result, req, res);
4244
4491
  return;
4245
4492
  }
@@ -4285,6 +4532,7 @@ function createMcpMiddleware(options) {
4285
4532
  });
4286
4533
  }
4287
4534
  }
4535
+ dedupeFailures2(result);
4288
4536
  onDenied(result, req, res);
4289
4537
  return;
4290
4538
  }
@@ -4308,7 +4556,30 @@ function createMcpMiddleware(options) {
4308
4556
  }
4309
4557
  next();
4310
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)}`;
4311
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
+ }
4312
4583
  next();
4313
4584
  }
4314
4585
  };