@astrasyncai/verification-gateway 2.4.12 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/dist/adapter-interface/interface.d.mts +2 -2
  2. package/dist/adapter-interface/interface.d.ts +2 -2
  3. package/dist/adapters/express.d.mts +2 -2
  4. package/dist/adapters/express.d.ts +2 -2
  5. package/dist/adapters/express.js +224 -42
  6. package/dist/adapters/express.js.map +1 -1
  7. package/dist/adapters/express.mjs +224 -42
  8. package/dist/adapters/express.mjs.map +1 -1
  9. package/dist/adapters/mcp.d.mts +101 -57
  10. package/dist/adapters/mcp.d.ts +101 -57
  11. package/dist/adapters/mcp.js +215 -44
  12. package/dist/adapters/mcp.js.map +1 -1
  13. package/dist/adapters/mcp.mjs +215 -44
  14. package/dist/adapters/mcp.mjs.map +1 -1
  15. package/dist/adapters/nextjs.d.mts +2 -2
  16. package/dist/adapters/nextjs.d.ts +2 -2
  17. package/dist/adapters/nextjs.js +87 -34
  18. package/dist/adapters/nextjs.js.map +1 -1
  19. package/dist/adapters/nextjs.mjs +87 -34
  20. package/dist/adapters/nextjs.mjs.map +1 -1
  21. package/dist/adapters/sdk.d.mts +2 -2
  22. package/dist/adapters/sdk.d.ts +2 -2
  23. package/dist/adapters/sdk.js +61 -28
  24. package/dist/adapters/sdk.js.map +1 -1
  25. package/dist/adapters/sdk.mjs +61 -28
  26. package/dist/adapters/sdk.mjs.map +1 -1
  27. package/dist/agent/index.d.mts +2 -2
  28. package/dist/agent/index.d.ts +2 -2
  29. package/dist/agent/index.js +29 -0
  30. package/dist/agent/index.js.map +1 -1
  31. package/dist/agent/index.mjs +29 -0
  32. package/dist/agent/index.mjs.map +1 -1
  33. package/dist/browser/background.js +102 -30
  34. package/dist/browser/background.js.map +1 -1
  35. package/dist/browser/background.mjs +102 -30
  36. package/dist/browser/background.mjs.map +1 -1
  37. package/dist/browser/browser-adapter.d.mts +2 -2
  38. package/dist/browser/browser-adapter.d.ts +2 -2
  39. package/dist/cli/index.d.mts +2 -2
  40. package/dist/cli/index.d.ts +2 -2
  41. package/dist/cursor/cursor-adapter.d.mts +2 -2
  42. package/dist/cursor/cursor-adapter.d.ts +2 -2
  43. package/dist/cursor/extension.d.mts +2 -2
  44. package/dist/cursor/extension.d.ts +2 -2
  45. package/dist/cursor/extension.js +102 -30
  46. package/dist/cursor/extension.js.map +1 -1
  47. package/dist/cursor/extension.mjs +102 -30
  48. package/dist/cursor/extension.mjs.map +1 -1
  49. package/dist/{express-C1ePFB7n.d.ts → express-CrfwoNAR.d.ts} +1 -1
  50. package/dist/{express-4WStX3PV.d.mts → express-ienhAXps.d.mts} +1 -1
  51. package/dist/gateway/gateway.d.mts +2 -2
  52. package/dist/gateway/gateway.d.ts +2 -2
  53. package/dist/gateway/gateway.js +102 -30
  54. package/dist/gateway/gateway.js.map +1 -1
  55. package/dist/gateway/gateway.mjs +102 -30
  56. package/dist/gateway/gateway.mjs.map +1 -1
  57. package/dist/git-trigger/git-hooks.d.mts +2 -2
  58. package/dist/git-trigger/git-hooks.d.ts +2 -2
  59. package/dist/{index-ChPX4WHl.d.mts → index-B5e2IDWU.d.mts} +1 -1
  60. package/dist/{index-CzJMCgEy.d.ts → index-CCdZxvAr.d.ts} +71 -6
  61. package/dist/{index-D8IEntil.d.mts → index-CEg_WG6y.d.mts} +71 -6
  62. package/dist/{index-Cjm-zBeZ.d.ts → index-DC5f8eoQ.d.ts} +1 -1
  63. package/dist/index.d.mts +39 -9
  64. package/dist/index.d.ts +39 -9
  65. package/dist/index.js +500 -94
  66. package/dist/index.js.map +1 -1
  67. package/dist/index.mjs +497 -94
  68. package/dist/index.mjs.map +1 -1
  69. package/dist/local-evaluator/evaluator.d.mts +2 -2
  70. package/dist/local-evaluator/evaluator.d.ts +2 -2
  71. package/dist/local-evaluator/evaluator.js +12 -2
  72. package/dist/local-evaluator/evaluator.js.map +1 -1
  73. package/dist/local-evaluator/evaluator.mjs +12 -2
  74. package/dist/local-evaluator/evaluator.mjs.map +1 -1
  75. package/dist/{nextjs-BIORS__0.d.ts → nextjs-66R1KW8e.d.ts} +1 -1
  76. package/dist/{nextjs-CjzHdaXA.d.mts → nextjs-DSpisQst.d.mts} +1 -1
  77. package/dist/{sdk-Chhz-FcT.d.mts → sdk-5U_CBRpr.d.mts} +1 -1
  78. package/dist/{sdk-CqTEQAc6.d.ts → sdk-Bm8np66n.d.ts} +1 -1
  79. package/dist/transport/index.d.mts +2 -2
  80. package/dist/transport/index.d.ts +2 -2
  81. package/dist/transport/index.js +146 -28
  82. package/dist/transport/index.js.map +1 -1
  83. package/dist/transport/index.mjs +146 -28
  84. package/dist/transport/index.mjs.map +1 -1
  85. package/dist/{types-L15pYd2c.d.mts → types-B3USs-Kx.d.mts} +42 -1
  86. package/dist/{types-L15pYd2c.d.ts → types-B3USs-Kx.d.ts} +42 -1
  87. package/dist/{types-DNK2BgIf.d.mts → types-CgDCUfo8.d.mts} +1 -1
  88. package/dist/{types-DoWIuzfj.d.ts → types-R5N4ET6x.d.ts} +1 -1
  89. package/dist/ui/index.d.mts +1 -1
  90. package/dist/ui/index.d.ts +1 -1
  91. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -126,7 +126,66 @@ 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
+
131
+ // src/well-known.ts
132
+ var CACHE_TTL_MS = 60 * 60 * 1e3;
133
+ var cache = /* @__PURE__ */ new Map();
134
+ var inflight = /* @__PURE__ */ new Map();
135
+ function wellKnownUrl(apiBaseUrl) {
136
+ const base = apiBaseUrl.replace(/\/api\/?$/, "");
137
+ return `${base}/.well-known/agentic-commerce`;
138
+ }
139
+ async function fetchWellKnown(apiBaseUrl) {
140
+ const url = wellKnownUrl(apiBaseUrl);
141
+ const response = await fetch(url, {
142
+ method: "GET",
143
+ headers: { Accept: "application/json" },
144
+ signal: AbortSignal.timeout(5e3)
145
+ });
146
+ if (!response.ok) {
147
+ throw new Error(
148
+ `AstraSync platform must expose /.well-known/agentic-commerce; got ${response.status} from ${url}. SDK cannot initialise without it.`
149
+ );
150
+ }
151
+ const data = await response.json();
152
+ if (!data.registrationUrl || !data.documentationUrl || !data.verifyAccessUrl) {
153
+ throw new Error(
154
+ `/.well-known/agentic-commerce response missing required fields (registrationUrl, documentationUrl, verifyAccessUrl).`
155
+ );
156
+ }
157
+ return data;
158
+ }
159
+ function prefetchWellKnown(apiBaseUrl) {
160
+ const existing = inflight.get(apiBaseUrl);
161
+ if (existing) return existing;
162
+ const promise = fetchWellKnown(apiBaseUrl).then((data) => {
163
+ cache.set(apiBaseUrl, { data, fetchedAt: Date.now() });
164
+ inflight.delete(apiBaseUrl);
165
+ return data;
166
+ }).catch((err) => {
167
+ inflight.delete(apiBaseUrl);
168
+ throw err;
169
+ });
170
+ inflight.set(apiBaseUrl, promise);
171
+ return promise;
172
+ }
173
+ async function getWellKnownUrls(apiBaseUrl) {
174
+ const entry = cache.get(apiBaseUrl);
175
+ if (entry) {
176
+ if (Date.now() - entry.fetchedAt > CACHE_TTL_MS) {
177
+ prefetchWellKnown(apiBaseUrl).catch(() => {
178
+ });
179
+ }
180
+ return entry.data;
181
+ }
182
+ const pending = inflight.get(apiBaseUrl);
183
+ if (pending) return pending;
184
+ return prefetchWellKnown(apiBaseUrl);
185
+ }
186
+ function getCachedWellKnownUrls(apiBaseUrl) {
187
+ return cache.get(apiBaseUrl)?.data;
188
+ }
130
189
 
131
190
  // src/verify.ts
132
191
  var DEFAULT_CONFIG = {
@@ -145,22 +204,27 @@ var DEFAULT_CONFIG = {
145
204
  };
146
205
  var initCheckPerformed = false;
147
206
  var deprecationWarningShown = false;
148
- async function performInitCheck(apiBaseUrl, debug) {
207
+ async function performInitCheck(apiBaseUrl, debug, strictInit) {
149
208
  initCheckPerformed = true;
150
209
  try {
151
210
  const probeUrl = `${apiBaseUrl}/agents/verify-access`;
152
211
  const response = await fetch(probeUrl, { method: "HEAD" });
153
212
  const contentType = response.headers.get("content-type") ?? "";
154
213
  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
- );
214
+ 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).`;
215
+ if (strictInit) {
216
+ throw new Error(`${message} (strictInit=true)`);
217
+ }
218
+ console.warn(`${message} Set disableInitChecks: true on GatewayConfig to silence.`);
158
219
  } else if (debug) {
159
220
  console.log(
160
221
  `[VerificationGateway] init check passed for ${apiBaseUrl} (content-type: ${contentType})`
161
222
  );
162
223
  }
163
224
  } catch (err) {
225
+ if (strictInit) {
226
+ throw err;
227
+ }
164
228
  if (debug) {
165
229
  console.log(`[VerificationGateway] init check failed (non-blocking): ${String(err)}`);
166
230
  }
@@ -184,7 +248,23 @@ function getCacheKey(request) {
184
248
  request.counterpartyType || "",
185
249
  request.isSubAgentRequest ? "1" : "0",
186
250
  request.parentAgentId || "",
187
- request.subAgentDepth ?? ""
251
+ request.subAgentDepth ?? "",
252
+ // Audit F-A1-07: previously-missing dimensions that DO affect the
253
+ // backend verdict. Without these, two requests with different
254
+ // durations (e.g. 60s vs 86400s) collided on the same cache key and
255
+ // the shorter-duration allow served the longer-duration request.
256
+ request.durationRequired ?? "",
257
+ request.invocationProtocol || "",
258
+ request.enableRuntimeChallenge ? "1" : "0",
259
+ // callerMetadata fields contribute to risk model; include the ones
260
+ // backend reads. sourceIp/userAgent/forwardedFor change per-request
261
+ // so their inclusion effectively forces a re-check for any varying
262
+ // client (the right behavior — IP-driven anomaly scoring shouldn't
263
+ // be cached across IPs).
264
+ request.callerMetadata?.sourceIp || "",
265
+ request.callerMetadata?.userAgent || "",
266
+ request.callerMetadata?.forwardedFor || "",
267
+ request.callerMetadata?.agentCardUrl || ""
188
268
  ].join("|");
189
269
  }
190
270
  function getCachedResult(request) {
@@ -213,9 +293,13 @@ function clearCache() {
213
293
  }
214
294
  function extractCredentials(headers, query) {
215
295
  const credentials = {};
296
+ const ASTRA_ID_PATTERN = /^ASTRAE?-[A-Za-z0-9_-]{1,64}$/;
216
297
  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
298
  if (astraIdHeader) {
218
- credentials.astraId = Array.isArray(astraIdHeader) ? astraIdHeader[0] : astraIdHeader;
299
+ const raw = Array.isArray(astraIdHeader) ? astraIdHeader[0] : typeof astraIdHeader === "string" ? astraIdHeader : void 0;
300
+ if (typeof raw === "string" && ASTRA_ID_PATTERN.test(raw)) {
301
+ credentials.astraId = raw;
302
+ }
219
303
  }
220
304
  const apiKeyHeader = headers["x-api-key"] || headers["X-Api-Key"] || headers["X-API-KEY"];
221
305
  if (apiKeyHeader) {
@@ -224,9 +308,11 @@ function extractCredentials(headers, query) {
224
308
  const authHeader = headers["authorization"] || headers["Authorization"];
225
309
  if (authHeader) {
226
310
  const authValue = Array.isArray(authHeader) ? authHeader[0] : authHeader;
227
- credentials.authorizationHeader = authValue;
228
- if (authValue.startsWith("Bearer ")) {
229
- credentials.jwt = authValue.slice(7);
311
+ if (typeof authValue === "string") {
312
+ credentials.authorizationHeader = authValue;
313
+ if (authValue.startsWith("Bearer ")) {
314
+ credentials.jwt = authValue.slice(7);
315
+ }
230
316
  }
231
317
  }
232
318
  if (query) {
@@ -242,21 +328,22 @@ function extractCredentials(headers, query) {
242
328
  function hasCredentials(credentials) {
243
329
  return !!(credentials.astraId || credentials.apiKey || credentials.jwt);
244
330
  }
245
- function createGuidanceResponse(config, reason, options = {}) {
331
+ function createGuidanceResponse(_config, reason, options = {}) {
246
332
  const source = options.source ?? "no_credentials";
247
333
  const isApiError = source === "api_error";
334
+ const urls = options.urls;
248
335
  const guidance = isApiError ? {
249
336
  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`,
251
- documentationUrl: `${config.apiBaseUrl.replace("/api", "")}/docs/agent-access`,
337
+ registrationUrl: urls?.registrationUrl ?? "",
338
+ documentationUrl: urls?.documentationUrl ?? "",
252
339
  steps: [
253
340
  "Retry the request with exponential backoff",
254
341
  "If failures persist, share the correlationId with support"
255
342
  ]
256
343
  } : {
257
344
  message: "This service verifies AI agents before granting access. Please register your agent with AstraSync.",
258
- registrationUrl: `${config.apiBaseUrl.replace("/api", "")}/register`,
259
- documentationUrl: `${config.apiBaseUrl.replace("/api", "")}/docs/agent-access`,
345
+ registrationUrl: urls?.registrationUrl ?? "",
346
+ documentationUrl: urls?.documentationUrl ?? "",
260
347
  steps: [
261
348
  "Register for an AstraSync account",
262
349
  "Create and register your agent",
@@ -298,7 +385,7 @@ async function callVerifyAccessAPI(config, request) {
298
385
  const { credentials, ...requestData } = request;
299
386
  const body = {
300
387
  ...credentials.astraId && { agentId: credentials.astraId },
301
- purpose: requestData.purpose || "general"
388
+ ...requestData.purpose && { purpose: requestData.purpose }
302
389
  };
303
390
  if (requestData.action) body.action = requestData.action;
304
391
  if (requestData.resourceType) body.resourceType = requestData.resourceType;
@@ -332,12 +419,8 @@ async function callVerifyAccessAPI(config, request) {
332
419
  "Content-Type": "application/json",
333
420
  ...config.customHeaders
334
421
  };
335
- if (credentials.authorizationHeader) {
336
- headers["Authorization"] = credentials.authorizationHeader;
337
- } else if (config.apiKey) {
338
- headers["Authorization"] = `Bearer ${config.apiKey}`;
339
- }
340
422
  if (config.apiKey) {
423
+ headers["Authorization"] = `Bearer ${config.apiKey}`;
341
424
  headers["X-API-Key"] = config.apiKey;
342
425
  }
343
426
  try {
@@ -382,8 +465,13 @@ async function callVerifyAccessAPI(config, request) {
382
465
  }
383
466
  async function verify(config, request) {
384
467
  const mergedConfig = { ...DEFAULT_CONFIG, ...config };
468
+ const urls = mergedConfig.apiBaseUrl ? getCachedWellKnownUrls(mergedConfig.apiBaseUrl) : void 0;
385
469
  if (!initCheckPerformed && !mergedConfig.disableInitChecks && mergedConfig.apiBaseUrl) {
386
- void performInitCheck(mergedConfig.apiBaseUrl, mergedConfig.debug);
470
+ if (mergedConfig.strictInit) {
471
+ await performInitCheck(mergedConfig.apiBaseUrl, mergedConfig.debug, true);
472
+ } else {
473
+ void performInitCheck(mergedConfig.apiBaseUrl, mergedConfig.debug, false);
474
+ }
387
475
  }
388
476
  if (!deprecationWarningShown && (config.minTrustScore !== void 0 || config.minTrustScoreForFull !== void 0)) {
389
477
  deprecationWarningShown = true;
@@ -414,7 +502,8 @@ async function verify(config, request) {
414
502
  if (!apiResponse.success) {
415
503
  return createGuidanceResponse(mergedConfig, apiResponse.error, {
416
504
  source: "api_error",
417
- correlationId: apiResponse.correlationId
505
+ correlationId: apiResponse.correlationId,
506
+ urls
418
507
  });
419
508
  }
420
509
  if (!apiResponse.access?.allowed) {
@@ -437,8 +526,8 @@ async function verify(config, request) {
437
526
  requiresApproval: apiResponse.access?.requiresApproval,
438
527
  guidance: {
439
528
  message: apiResponse.access?.reason || "Access denied by PDLSS policy",
440
- registrationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/register`,
441
- documentationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/docs/pdlss`
529
+ registrationUrl: urls?.registrationUrl ?? "",
530
+ documentationUrl: urls?.documentationUrl ?? ""
442
531
  },
443
532
  verifiedAt: /* @__PURE__ */ new Date(),
444
533
  // Extract sessionId so decisions can be recorded for denials too
@@ -507,13 +596,15 @@ async function verify(config, request) {
507
596
  result.denialReasons = result.recommendationReasons || [
508
597
  "Access denied by AstraSync recommendation"
509
598
  ];
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
- }
599
+ result.guidance = result.runtimeChallenge ? {
600
+ message: `Verification failed: ${result.runtimeChallenge.reason || "runtime challenge failed"}`,
601
+ registrationUrl: urls?.registrationUrl ?? "",
602
+ documentationUrl: urls?.documentationUrl ?? ""
603
+ } : {
604
+ message: result.recommendationReasons?.[0] || "Access denied by AstraSync recommendation",
605
+ registrationUrl: urls?.registrationUrl ?? "",
606
+ documentationUrl: urls?.documentationUrl ?? ""
607
+ };
517
608
  } else if (result.recommendation === "step_up_required") {
518
609
  result.requiresStepUp = true;
519
610
  if (ACCESS_LEVEL_HIERARCHY[result.accessLevel] > ACCESS_LEVEL_HIERARCHY["read-only"]) {
@@ -762,18 +853,43 @@ function defaultExtractPurpose(req) {
762
853
  return "general";
763
854
  }
764
855
  }
765
- function matchRoute(pattern, path) {
856
+ function matchRoute(pattern, path, opts) {
766
857
  const regexPattern = pattern.replace(/\*/g, ".*").replace(/\//g, "\\/");
767
- const regex = new RegExp(`^${regexPattern}$`);
768
- return regex.test(path);
858
+ const caseSensitiveRegex = new RegExp(`^${regexPattern}$`);
859
+ const caseSensitiveResult = caseSensitiveRegex.test(path);
860
+ if (!opts?.caseInsensitive && !opts?.logShadowDivergence) {
861
+ return caseSensitiveResult;
862
+ }
863
+ const caseInsensitiveRegex = new RegExp(`^${regexPattern}$`, "i");
864
+ const caseInsensitiveResult = caseInsensitiveRegex.test(path);
865
+ if (opts?.logShadowDivergence && caseSensitiveResult !== caseInsensitiveResult) {
866
+ console.warn(
867
+ `[SHADOW] matchRoute case-insensitive would change result: pattern=${pattern} path=${path} caseSensitive=${caseSensitiveResult} caseInsensitive=${caseInsensitiveResult} correlationId=${opts.correlationId ?? "unknown"}`
868
+ );
869
+ }
870
+ return opts?.caseInsensitive ? caseInsensitiveResult : caseSensitiveResult;
769
871
  }
770
- function findRouteConfig(routes, path, method) {
872
+ function findRouteConfig(routes, path, method, opts) {
771
873
  return routes.find((route) => {
772
874
  const methodMatches = route.method === "*" || route.method.toUpperCase() === method.toUpperCase();
773
- const pathMatches = matchRoute(route.pattern, path);
875
+ const pathMatches = matchRoute(route.pattern, path, opts);
774
876
  return methodMatches && pathMatches;
775
877
  });
776
878
  }
879
+ function dedupeFailures(result) {
880
+ if (result.failures && result.failures.length > 1) {
881
+ const seen = /* @__PURE__ */ new Set();
882
+ result.failures = result.failures.filter((f) => {
883
+ const key = `${f.dimension}|${f.message}|${f.guidance ?? ""}`;
884
+ if (seen.has(key)) return false;
885
+ seen.add(key);
886
+ return true;
887
+ });
888
+ }
889
+ if (result.denialReasons && result.denialReasons.length > 1) {
890
+ result.denialReasons = [...new Set(result.denialReasons)];
891
+ }
892
+ }
777
893
  function defaultOnDenied(result, _req, res) {
778
894
  const statusCode = !result.identityVerified ? 401 : 403;
779
895
  res.setHeader("X-Astra-Gateway-Mode", "enforced");
@@ -800,8 +916,14 @@ function createMiddleware(options) {
800
916
  recordDecisions,
801
917
  enableRuntimeChallenge = true,
802
918
  routesRefreshMs = DEFAULT_ROUTES_REFRESH_MS,
919
+ failOnError = "open",
920
+ caseInsensitiveRouteMatch = false,
803
921
  ...config
804
922
  } = options;
923
+ if (config.apiBaseUrl) {
924
+ prefetchWellKnown(config.apiBaseUrl).catch(() => {
925
+ });
926
+ }
805
927
  let cachedRoutes = [];
806
928
  let lastFetchAt = 0;
807
929
  let refreshing = null;
@@ -822,7 +944,7 @@ function createMiddleware(options) {
822
944
  cachedRoutes = fetched;
823
945
  lastFetchAt = Date.now();
824
946
  if (cachedRoutes.length === 0 && !warnedEmptyRoutes) {
825
- const dashboard = config.dashboardUrl ?? "https://app.astrasync.ai";
947
+ const dashboard = config.dashboardUrl ?? "https://astrasync.ai/dashboard";
826
948
  console.warn(
827
949
  `[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
950
  );
@@ -848,7 +970,12 @@ function createMiddleware(options) {
848
970
  refreshing = null;
849
971
  });
850
972
  }
851
- const routeConfig = findRouteConfig(cachedRoutes, req.path, req.method);
973
+ const correlationId = req.headers["x-request-id"] || req.headers["x-correlation-id"];
974
+ const routeConfig = findRouteConfig(cachedRoutes, req.path, req.method, {
975
+ caseInsensitive: caseInsensitiveRouteMatch,
976
+ logShadowDivergence: true,
977
+ correlationId
978
+ });
852
979
  if (!routeConfig) {
853
980
  if (config.setPassThroughHeader) {
854
981
  res.setHeader("X-Astra-Gateway-Mode", "unenforced");
@@ -859,6 +986,7 @@ function createMiddleware(options) {
859
986
  }
860
987
  return next();
861
988
  }
989
+ const wellKnownUrls = config.apiBaseUrl ? await getWellKnownUrls(config.apiBaseUrl).catch(() => void 0) : void 0;
862
990
  const credentials = customExtractCredentials ? customExtractCredentials(req) : defaultExtractCredentials(req);
863
991
  const shouldEnforce = routeConfig.minAccessLevel !== "none";
864
992
  if (routeConfig.minAccessLevel === "none" && (!config.evaluateAlwaysIfCredentialed || !credentials.astraId)) {
@@ -880,8 +1008,8 @@ function createMiddleware(options) {
880
1008
  denialReasons: preCheckFailures.map((f) => f.message),
881
1009
  guidance: {
882
1010
  message: "Request exceeds counterparty-defined PDLSS limits.",
883
- registrationUrl: `${config.apiBaseUrl?.replace("/api", "")}/register`,
884
- documentationUrl: `${config.apiBaseUrl?.replace("/api", "")}/docs/pdlss`
1011
+ registrationUrl: wellKnownUrls?.registrationUrl ?? "",
1012
+ documentationUrl: wellKnownUrls?.documentationUrl ?? ""
885
1013
  },
886
1014
  verifiedAt: /* @__PURE__ */ new Date()
887
1015
  };
@@ -895,13 +1023,19 @@ function createMiddleware(options) {
895
1023
  requestMethod: req.method
896
1024
  }).catch(() => {
897
1025
  });
1026
+ dedupeFailures(result2);
898
1027
  onDenied(result2, req, res);
899
1028
  return;
900
1029
  }
901
1030
  const shouldRecordDecisions = recordDecisions !== false;
902
1031
  const forwardedFor = req.headers["x-forwarded-for"];
903
1032
  const forwardedForStr = Array.isArray(forwardedFor) ? forwardedFor.join(", ") : forwardedFor;
904
- const originalClientIp = forwardedForStr ? forwardedForStr.split(",")[0].trim() : req.ip;
1033
+ const originalClientIp = req.ip ?? (forwardedForStr ? forwardedForStr.split(",")[0].trim() : void 0);
1034
+ if (!req.ip && forwardedForStr) {
1035
+ console.warn(
1036
+ "[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."
1037
+ );
1038
+ }
905
1039
  const agentCardUrl = typeof req.headers["x-astrasync-agent-card"] === "string" ? req.headers["x-astrasync-agent-card"] : void 0;
906
1040
  const result = await verify(config, {
907
1041
  credentials,
@@ -932,6 +1066,7 @@ function createMiddleware(options) {
932
1066
  recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
933
1067
  });
934
1068
  }
1069
+ dedupeFailures(result);
935
1070
  onDenied(result, req, res);
936
1071
  return;
937
1072
  }
@@ -954,10 +1089,18 @@ function createMiddleware(options) {
954
1089
  };
955
1090
  result.failures = [...result.failures ?? [], insufficientFailure];
956
1091
  result.denialReasons = [...result.denialReasons ?? [], insufficientFailure.message];
1092
+ if (!result.guidance && wellKnownUrls) {
1093
+ result.guidance = {
1094
+ message: insufficientFailure.message,
1095
+ registrationUrl: wellKnownUrls.registrationUrl,
1096
+ documentationUrl: wellKnownUrls.documentationUrl
1097
+ };
1098
+ }
957
1099
  if (shouldRecordDecisions && sessionId) {
958
1100
  recordDecision(config, sessionId, "denied", insufficientFailure.message).catch(() => {
959
1101
  });
960
1102
  }
1103
+ dedupeFailures(result);
961
1104
  onDenied(result, req, res);
962
1105
  return;
963
1106
  }
@@ -970,10 +1113,18 @@ function createMiddleware(options) {
970
1113
  };
971
1114
  result.failures = [...result.failures ?? [], trustFailure];
972
1115
  result.denialReasons = [trustFailure.message];
1116
+ if (!result.guidance && wellKnownUrls) {
1117
+ result.guidance = {
1118
+ message: trustFailure.message,
1119
+ registrationUrl: wellKnownUrls.registrationUrl,
1120
+ documentationUrl: wellKnownUrls.documentationUrl
1121
+ };
1122
+ }
973
1123
  if (shouldRecordDecisions && sessionId) {
974
1124
  recordDecision(config, sessionId, "denied", trustFailure.message).catch(() => {
975
1125
  });
976
1126
  }
1127
+ dedupeFailures(result);
977
1128
  onDenied(result, req, res);
978
1129
  return;
979
1130
  }
@@ -988,7 +1139,38 @@ function createMiddleware(options) {
988
1139
  }
989
1140
  next();
990
1141
  } catch (error) {
1142
+ const errorClass = error instanceof Error ? error.constructor.name : typeof error;
1143
+ 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
1144
  console.error("[VerificationGateway] Middleware error:", error);
1145
+ console.warn(
1146
+ `[SHADOW] would-have-denied: errorClass=${errorClass} route=${req.method}:${req.path} merchantId=${config.counterpartyId ?? "unknown"} correlationId=${correlationId}`
1147
+ );
1148
+ if (failOnError === "closed") {
1149
+ const result = {
1150
+ identityVerified: false,
1151
+ policyAllowed: false,
1152
+ accessLevel: "none",
1153
+ denialReasons: [`Verification middleware internal error: ${errorClass}`],
1154
+ failures: [
1155
+ {
1156
+ dimension: "middleware.internal_error",
1157
+ message: `Middleware threw ${errorClass} \u2014 failing closed`
1158
+ }
1159
+ ],
1160
+ verifiedAt: /* @__PURE__ */ new Date(),
1161
+ correlationId
1162
+ };
1163
+ const catchUrls = config.apiBaseUrl ? await getWellKnownUrls(config.apiBaseUrl).catch(() => void 0) : void 0;
1164
+ if (catchUrls) {
1165
+ result.guidance = {
1166
+ message: `Middleware threw ${errorClass} \u2014 failing closed`,
1167
+ registrationUrl: catchUrls.registrationUrl,
1168
+ documentationUrl: catchUrls.documentationUrl
1169
+ };
1170
+ }
1171
+ dedupeFailures(result);
1172
+ return onDenied(result, req, res);
1173
+ }
992
1174
  next();
993
1175
  }
994
1176
  };
@@ -1000,6 +1182,18 @@ __export(nextjs_exports, {
1000
1182
  createMatcherConfig: () => createMatcherConfig,
1001
1183
  createMiddleware: () => createMiddleware2
1002
1184
  });
1185
+ function escapeHtml(value) {
1186
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1187
+ }
1188
+ function sanitizeUrl(value, fallback) {
1189
+ if (typeof value !== "string" || value.length === 0) return escapeHtml(fallback);
1190
+ const trimmed = value.trim();
1191
+ if (/^javascript:|^data:|^vbscript:/i.test(trimmed)) return escapeHtml(fallback);
1192
+ if (/^https?:\/\//i.test(trimmed) || trimmed.startsWith("/")) {
1193
+ return escapeHtml(trimmed);
1194
+ }
1195
+ return escapeHtml(fallback);
1196
+ }
1003
1197
  function extractCredentialsFromNextRequest(request) {
1004
1198
  const credentials = {};
1005
1199
  const astraId = request.headers.get("x-astra-id") || request.headers.get("X-Astra-Id");
@@ -1071,10 +1265,18 @@ function extractPurpose(request) {
1071
1265
  }
1072
1266
  }
1073
1267
  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";
1268
+ const title = escapeHtml(options.commerceShield?.title || "AstraSync Agent Verification");
1269
+ const message = escapeHtml(
1270
+ options.commerceShield?.message || result.guidance?.message || "This site verifies AI agents before granting access. We noticed you're visiting without AstraSync credentials."
1271
+ );
1272
+ const registrationUrl = sanitizeUrl(
1273
+ result.guidance?.registrationUrl,
1274
+ "https://astrasync.ai/register"
1275
+ );
1276
+ const docsUrl = sanitizeUrl(
1277
+ result.guidance?.documentationUrl,
1278
+ "https://astrasync.ai/docs/agent-access"
1279
+ );
1078
1280
  const allowGuest = options.commerceShield?.allowGuestAccess ?? true;
1079
1281
  return `
1080
1282
  <!DOCTYPE html>
@@ -1196,7 +1398,7 @@ function generateCommerceShieldHtml(result, options) {
1196
1398
  <div class="shield-steps">
1197
1399
  <h3>To get verified access:</h3>
1198
1400
  <ol>
1199
- <li>Register at <a href="${registrationUrl}">astrasync.ai/register</a></li>
1401
+ <li>Register at <a href="${registrationUrl}">astrasync.ai/agents/register</a></li>
1200
1402
  <li>Create and register your agent</li>
1201
1403
  <li>Add your ASTRA-ID to request headers</li>
1202
1404
  <li>Refresh this page</li>
@@ -1284,7 +1486,7 @@ function createMiddleware2(options) {
1284
1486
  denialReasons: preCheckFailures.map((f) => f.message),
1285
1487
  guidance: {
1286
1488
  message: "Request exceeds counterparty-defined PDLSS limits.",
1287
- registrationUrl: `${config.apiBaseUrl?.replace("/api", "")}/register`,
1489
+ registrationUrl: `${config.apiBaseUrl?.replace("/api", "")}/agents/register`,
1288
1490
  documentationUrl: `${config.apiBaseUrl?.replace("/api", "")}/docs/pdlss`
1289
1491
  },
1290
1492
  verifiedAt: /* @__PURE__ */ new Date()
@@ -1945,12 +2147,45 @@ function bufferToBase64(bytes) {
1945
2147
 
1946
2148
  // src/transport/rfc9421-verify.ts
1947
2149
  import { httpbis } from "http-message-signatures";
2150
+
2151
+ // src/transport/nonce-store.ts
2152
+ var InMemoryNonceStore = class {
2153
+ constructor(capacity = 1e4) {
2154
+ this.entries = /* @__PURE__ */ new Map();
2155
+ this.lastSweepMs = 0;
2156
+ this.capacity = capacity;
2157
+ }
2158
+ seen(key, expiresAtMs) {
2159
+ const nowMs = Date.now();
2160
+ if (nowMs - this.lastSweepMs > 1e3) {
2161
+ for (const [k, exp] of this.entries) {
2162
+ if (exp <= nowMs) this.entries.delete(k);
2163
+ }
2164
+ this.lastSweepMs = nowMs;
2165
+ }
2166
+ const existing = this.entries.get(key);
2167
+ if (existing !== void 0 && existing > nowMs) {
2168
+ return true;
2169
+ }
2170
+ if (this.entries.size >= this.capacity) {
2171
+ const oldest = this.entries.keys().next().value;
2172
+ if (oldest !== void 0) this.entries.delete(oldest);
2173
+ }
2174
+ this.entries.set(key, expiresAtMs);
2175
+ return false;
2176
+ }
2177
+ };
2178
+ var defaultNonceStore = new InMemoryNonceStore();
2179
+
2180
+ // src/transport/rfc9421-verify.ts
1948
2181
  async function verifyRFC9421(request, options) {
1949
2182
  const { resolver } = options;
1950
- const tolerance = options.clockSkewSec ?? 300;
2183
+ const tolerance = options.clockSkewSec ?? 60;
1951
2184
  const nowSec = options.now ? options.now() : Math.floor(Date.now() / 1e3);
2185
+ const nonceStore = options.nonceStore ?? defaultNonceStore;
1952
2186
  let resolvedKid;
1953
2187
  let resolvedAlg;
2188
+ let replayDetected = false;
1954
2189
  const keyLookup = async (parameters) => {
1955
2190
  const kid = typeof parameters.keyid === "string" ? parameters.keyid : void 0;
1956
2191
  if (!kid) return null;
@@ -1964,6 +2199,14 @@ async function verifyRFC9421(request, options) {
1964
2199
  const expires = toUnixSeconds(parameters.expires);
1965
2200
  if (created !== void 0 && Math.abs(nowSec - created) > tolerance) return null;
1966
2201
  if (expires !== void 0 && nowSec > expires + tolerance) return null;
2202
+ const nonce = typeof parameters.nonce === "string" ? parameters.nonce : void 0;
2203
+ if (nonce) {
2204
+ const expiresAtMs = (expires !== void 0 ? expires + tolerance : nowSec + tolerance) * 1e3;
2205
+ if (nonceStore.seen(`rfc9421:${kid}:${nonce}`, expiresAtMs)) {
2206
+ replayDetected = true;
2207
+ return null;
2208
+ }
2209
+ }
1967
2210
  return jwkToVerifyingKey(kid, jwk, alg);
1968
2211
  };
1969
2212
  try {
@@ -1986,7 +2229,7 @@ async function verifyRFC9421(request, options) {
1986
2229
  kid: resolvedKid,
1987
2230
  registry: resolver.name,
1988
2231
  algorithm: resolvedAlg,
1989
- error: result === false ? "signature invalid" : "no signature found"
2232
+ error: replayDetected ? "RFC9421 signature replay \u2014 already seen within tolerance window" : result === false ? "signature invalid" : "no signature found"
1990
2233
  };
1991
2234
  } catch (err) {
1992
2235
  return {
@@ -2811,14 +3054,26 @@ function sha256Sync2(data) {
2811
3054
  function verifyAP2Chain(input) {
2812
3055
  const { triple } = input;
2813
3056
  const errors = [];
3057
+ const toleranceSec = input.clockSkewSec ?? 60;
3058
+ const nonceStore = input.nonceStore ?? defaultNonceStore;
2814
3059
  const intentPresent = triple.intent !== void 0;
2815
3060
  const cartRefOk = checkCartRef(triple, errors);
2816
3061
  const paymentRefOk = checkPaymentRef(triple, errors);
2817
3062
  const { ok: agentIdContinuity, agentId } = checkAgentContinuity(triple, errors);
2818
3063
  const paymentMethodAllowed = checkPaymentMethod(triple, errors);
2819
3064
  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;
3065
+ const expiryOk = checkExpiries(triple, toleranceSec, input.now, errors);
3066
+ let replayOk = true;
3067
+ const replayId = triple.payment?.raw?.id ?? triple.cart?.raw?.id;
3068
+ if (typeof replayId === "string" && replayId.length > 0) {
3069
+ const now = input.now ? input.now() : Math.floor(Date.now() / 1e3);
3070
+ const expiresAt = (now + toleranceSec) * 1e3;
3071
+ if (nonceStore.seen(`ap2:${replayId}`, expiresAt)) {
3072
+ errors.push(`AP2 chain replay \u2014 mandate ${replayId} already seen within tolerance window`);
3073
+ replayOk = false;
3074
+ }
3075
+ }
3076
+ const ok = cartRefOk && paymentRefOk && agentIdContinuity && paymentMethodAllowed && totalsConsistent && expiryOk && replayOk;
2822
3077
  return {
2823
3078
  ok,
2824
3079
  checks: {
@@ -2864,7 +3119,10 @@ function checkAgentContinuity(triple, errors) {
2864
3119
  const ids = [triple.intent?.agent_id, triple.cart?.agent_id, triple.payment?.agent_id].filter(
2865
3120
  (id) => typeof id === "string" && id.length > 0
2866
3121
  );
2867
- if (ids.length === 0) return { ok: true };
3122
+ if (ids.length === 0) {
3123
+ errors.push("agent_id missing across all three mandates (intent/cart/payment)");
3124
+ return { ok: false };
3125
+ }
2868
3126
  const unique = new Set(ids);
2869
3127
  if (unique.size > 1) {
2870
3128
  errors.push(`agent_id mismatch across mandates: ${Array.from(unique).join(", ")}`);
@@ -2873,9 +3131,16 @@ function checkAgentContinuity(triple, errors) {
2873
3131
  return { ok: true, agentId: ids[0] };
2874
3132
  }
2875
3133
  function checkPaymentMethod(triple, errors) {
2876
- const paymentMethod = triple.payment?.payment_method;
2877
3134
  const allowed = triple.intent?.paymentMethods;
2878
- if (!paymentMethod || !allowed || allowed.length === 0) return true;
3135
+ if (!allowed || allowed.length === 0) return true;
3136
+ if (!triple.payment) return true;
3137
+ const paymentMethod = triple.payment.payment_method;
3138
+ if (!paymentMethod) {
3139
+ errors.push(
3140
+ `payment.payment_method missing but intent declares allowlist [${allowed.join(", ")}]`
3141
+ );
3142
+ return false;
3143
+ }
2879
3144
  if (!allowed.includes(paymentMethod)) {
2880
3145
  errors.push(
2881
3146
  `payment_method "${paymentMethod}" not in intent.paymentMethods [${allowed.join(", ")}]`
@@ -2909,19 +3174,24 @@ function checkTotals(triple, errors) {
2909
3174
  function checkExpiries(triple, toleranceSec, nowFn, errors) {
2910
3175
  const now = nowFn ? nowFn() : Math.floor(Date.now() / 1e3);
2911
3176
  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);
3177
+ const layers = [
3178
+ ["intent", triple.intent?.expires],
3179
+ ["cart", triple.cart?.expires],
3180
+ [
3181
+ "payment",
3182
+ typeof triple.payment?.raw?.expires === "string" ? triple.payment.raw.expires : typeof triple.payment?.raw?.exp === "string" ? triple.payment.raw.exp : void 0
3183
+ ]
3184
+ ];
3185
+ for (const [name, expires] of layers) {
3186
+ if (!expires) continue;
3187
+ const parsed = parseExpiry(expires);
2918
3188
  if (parsed === null) {
2919
3189
  errors.push(`${name}.expires unparseable`);
2920
3190
  ok = false;
2921
3191
  continue;
2922
3192
  }
2923
3193
  if (now > parsed + toleranceSec) {
2924
- errors.push(`${name} mandate expired at ${mandate.expires}`);
3194
+ errors.push(`${name} mandate expired at ${expires}`);
2925
3195
  ok = false;
2926
3196
  }
2927
3197
  }
@@ -2948,10 +3218,21 @@ async function verifyACPSignature(input) {
2948
3218
  if (!input.signatureHeader) {
2949
3219
  return { ok: false, error: "missing Signature header" };
2950
3220
  }
2951
- const freshness = checkTimestamp(input.timestampHeader, input.clockSkewSec ?? 300, input.now);
3221
+ const tolerance = input.clockSkewSec ?? 60;
3222
+ const nonceStore = input.nonceStore ?? defaultNonceStore;
3223
+ const freshness = checkTimestamp(input.timestampHeader, tolerance, input.now);
2952
3224
  if (!freshness.ok) {
2953
3225
  return { ok: false, error: freshness.error, timestampStale: true };
2954
3226
  }
3227
+ const nowSec = input.now ? input.now() : Math.floor(Date.now() / 1e3);
3228
+ const expiresAtMs = (nowSec + tolerance) * 1e3;
3229
+ const replayKey = `acp:${input.signatureHeader}:${input.timestampHeader ?? ""}`;
3230
+ if (nonceStore.seen(replayKey, expiresAtMs)) {
3231
+ return {
3232
+ ok: false,
3233
+ error: "ACP signature replay \u2014 already seen within tolerance window"
3234
+ };
3235
+ }
2955
3236
  const signatureBytes = decodeBase64(input.signatureHeader);
2956
3237
  if (!signatureBytes) {
2957
3238
  return { ok: false, error: "signature header is not valid base64" };
@@ -3169,8 +3450,9 @@ function coerceString6(v) {
3169
3450
  import { BodyDigest } from "mppx";
3170
3451
  function verifyMPP(input) {
3171
3452
  const { context } = input;
3172
- const tolerance = input.clockSkewSec ?? 300;
3453
+ const tolerance = input.clockSkewSec ?? 60;
3173
3454
  const nowSec = input.now ? input.now() : Math.floor(Date.now() / 1e3);
3455
+ const nonceStore = input.nonceStore ?? defaultNonceStore;
3174
3456
  const challenge = context.credential?.challenge ?? (context.challenges && context.challenges[0]);
3175
3457
  const source = context.credential?.source;
3176
3458
  const method = challenge?.method;
@@ -3193,21 +3475,38 @@ function verifyMPP(input) {
3193
3475
  }
3194
3476
  }
3195
3477
  let bodyDigestOk = null;
3196
- if (challenge?.digest && input.rawBody !== void 0) {
3197
- try {
3198
- if (!/^sha-256=/.test(challenge.digest)) {
3478
+ if (input.rawBody !== void 0) {
3479
+ if (!challenge?.digest) {
3480
+ bodyDigestOk = false;
3481
+ } else {
3482
+ try {
3483
+ if (!/^sha-256=/.test(challenge.digest)) {
3484
+ bodyDigestOk = false;
3485
+ } else {
3486
+ bodyDigestOk = BodyDigest.verify(challenge.digest, input.rawBody);
3487
+ }
3488
+ } catch {
3199
3489
  bodyDigestOk = false;
3200
- } else {
3201
- bodyDigestOk = BodyDigest.verify(challenge.digest, input.rawBody);
3202
3490
  }
3203
- } catch {
3204
- bodyDigestOk = false;
3205
3491
  }
3206
3492
  }
3207
- const ok = expiryOk && (bodyDigestOk === null || bodyDigestOk === true);
3493
+ let replayOk = true;
3494
+ if (challenge?.digest && expiryOk) {
3495
+ const replayKey = `mpp:${challenge.digest}:${challenge.nonce ?? ""}`;
3496
+ const expiresAt = (nowSec + tolerance) * 1e3;
3497
+ if (nonceStore.seen(replayKey, expiresAt)) {
3498
+ replayOk = false;
3499
+ }
3500
+ }
3501
+ const ok = expiryOk && (bodyDigestOk === null || bodyDigestOk === true) && replayOk;
3208
3502
  const errors = [];
3209
3503
  if (!expiryOk) errors.push("challenge expired");
3210
- if (bodyDigestOk === false) errors.push("body digest mismatch");
3504
+ if (bodyDigestOk === false) {
3505
+ errors.push(
3506
+ input.rawBody !== void 0 && !challenge?.digest ? "body digest required when rawBody present" : "body digest mismatch"
3507
+ );
3508
+ }
3509
+ if (!replayOk) errors.push("MPP challenge replay \u2014 already seen within tolerance window");
3211
3510
  return {
3212
3511
  ok,
3213
3512
  expiryOk,
@@ -3371,14 +3670,32 @@ function readHeader4(headers, name) {
3371
3670
  import { createHash as createHash3, webcrypto } from "crypto";
3372
3671
  async function verifyVIChain(input) {
3373
3672
  const errors = [];
3374
- const tolerance = input.clockSkewSec ?? 300;
3673
+ const tolerance = input.clockSkewSec ?? 60;
3375
3674
  const now = input.now ? input.now() : Math.floor(Date.now() / 1e3);
3376
3675
  const { l1, l2, l3a, l3b } = input.layers;
3676
+ const nonceStore = input.nonceStore ?? defaultNonceStore;
3677
+ if (!l1) {
3678
+ if (!input.allowUnboundChain) {
3679
+ errors.push(
3680
+ "L1 missing \u2014 chain root unbound (set allowUnboundChain + expectedL2Key to override)"
3681
+ );
3682
+ } else if (!input.expectedL2Key) {
3683
+ errors.push("allowUnboundChain set but expectedL2Key missing");
3684
+ }
3685
+ }
3377
3686
  const l1SigOk = l1 ? await input.verifySignature(l1, null) : null;
3378
3687
  if (l1 && !l1SigOk) errors.push("L1 signature invalid");
3379
3688
  const l1Cnf = extractCnfJwk(l1?.payload);
3380
- const l2SigOk = await input.verifySignature(l2, l1Cnf ?? null);
3689
+ const l2ExpectedKey = l1Cnf ?? input.expectedL2Key ?? null;
3690
+ const l2SigOk = await input.verifySignature(l2, l2ExpectedKey);
3381
3691
  if (!l2SigOk) errors.push("L2 signature invalid");
3692
+ if (l2SigOk) {
3693
+ const replayKey = `vi:l2:${l2.compact}`;
3694
+ const expiresAt = now * 1e3 + tolerance * 1e3;
3695
+ if (nonceStore.seen(replayKey, expiresAt)) {
3696
+ errors.push("L2 signature replay \u2014 already seen within tolerance window");
3697
+ }
3698
+ }
3382
3699
  const l2Cnf = extractCnfJwk(l2.payload);
3383
3700
  const l3aSigOk = l3a ? await input.verifySignature(l3a, l2Cnf ?? null) : null;
3384
3701
  if (l3a && !l3aSigOk) errors.push("L3a signature invalid");
@@ -3422,7 +3739,10 @@ async function verifyVIChain(input) {
3422
3739
  }
3423
3740
  }
3424
3741
  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;
3742
+ const noUnboundChainOrReplayErrors = !errors.some(
3743
+ (e) => e.startsWith("L1 missing") || e.startsWith("allowUnboundChain set") || e.startsWith("L2 signature replay")
3744
+ );
3745
+ const ok = l1SigOk !== false && l2SigOk && l3aSigOk !== false && l3bSigOk !== false && l1BindsL2 && l2BindsL3 && l3aL3bTxnIdMatch !== false && checkoutHashOk !== false && expiryOk && noUnboundChainOrReplayErrors;
3426
3746
  return {
3427
3747
  ok,
3428
3748
  checks: {
@@ -3855,7 +4175,7 @@ async function exportJwkFromKeyLike(keyLike) {
3855
4175
 
3856
4176
  // src/transport/registry/mastercard.ts
3857
4177
  function createMastercardRegistry(options = {}) {
3858
- const cache = /* @__PURE__ */ new Map();
4178
+ const cache2 = /* @__PURE__ */ new Map();
3859
4179
  const ttlSec = options.cacheTtlSec ?? 3600;
3860
4180
  const fetchFn = options.fetch ?? globalThis.fetch;
3861
4181
  let warned = false;
@@ -3872,7 +4192,7 @@ function createMastercardRegistry(options = {}) {
3872
4192
  }
3873
4193
  return null;
3874
4194
  }
3875
- const cached = cache.get(kid);
4195
+ const cached = cache2.get(kid);
3876
4196
  if (cached && cached.expiresAt > Date.now()) return cached.jwk;
3877
4197
  try {
3878
4198
  const res = await fetchFn(options.registryUrl);
@@ -3881,7 +4201,7 @@ function createMastercardRegistry(options = {}) {
3881
4201
  const keys = body.keys ?? [];
3882
4202
  for (const k of keys) {
3883
4203
  if (k.kid === kid) {
3884
- cache.set(kid, { jwk: k, expiresAt: Date.now() + ttlSec * 1e3 });
4204
+ cache2.set(kid, { jwk: k, expiresAt: Date.now() + ttlSec * 1e3 });
3885
4205
  return k;
3886
4206
  }
3887
4207
  }
@@ -3896,7 +4216,7 @@ function createMastercardRegistry(options = {}) {
3896
4216
  // src/transport/registry/web-bot-auth.ts
3897
4217
  var DIRECTORY_PATH = "/.well-known/http-message-signatures-directory";
3898
4218
  function createWebBotAuthRegistry(options = {}) {
3899
- const cache = /* @__PURE__ */ new Map();
4219
+ const cache2 = /* @__PURE__ */ new Map();
3900
4220
  const ttlSec = options.cacheTtlSec ?? 3600;
3901
4221
  const fetchFn = options.fetch ?? globalThis.fetch;
3902
4222
  return {
@@ -3905,7 +4225,7 @@ function createWebBotAuthRegistry(options = {}) {
3905
4225
  if (!kid) return null;
3906
4226
  const directoryUrl = resolveDirectoryUrl(options.directoryUrl, context?.origin);
3907
4227
  if (!directoryUrl) return null;
3908
- const cached = cache.get(directoryUrl);
4228
+ const cached = cache2.get(directoryUrl);
3909
4229
  const now = Date.now();
3910
4230
  if (cached && cached.expiresAt > now) {
3911
4231
  return findKeyByKid(cached.keys, kid);
@@ -3915,7 +4235,7 @@ function createWebBotAuthRegistry(options = {}) {
3915
4235
  if (!res.ok) return null;
3916
4236
  const body = await res.json();
3917
4237
  const keys = body.keys ?? [];
3918
- cache.set(directoryUrl, { keys, expiresAt: now + ttlSec * 1e3 });
4238
+ cache2.set(directoryUrl, { keys, expiresAt: now + ttlSec * 1e3 });
3919
4239
  return findKeyByKid(keys, kid);
3920
4240
  } catch {
3921
4241
  return null;
@@ -4054,19 +4374,22 @@ function extractFromMcpBody(astrasyncMeta, args, key) {
4054
4374
  }
4055
4375
  return { value: void 0, source: void 0 };
4056
4376
  }
4057
- function mcpToPdlss(parsed, headerPurpose, headerAction) {
4058
- const resource = parsed.toolName ? `mcp:tool/${parsed.toolName}` : `mcp:method/${parsed.method}`;
4377
+ function mcpToPdlss(parsed, requestPath, headerPurpose, headerAction, toolGate) {
4378
+ const resource = toolGate?.resource ?? requestPath;
4059
4379
  let purpose;
4060
4380
  let purposeSource;
4061
- if (headerPurpose) {
4381
+ if (toolGate?.purpose !== void 0) {
4382
+ purpose = toolGate.purpose;
4383
+ purposeSource = "tool_gate";
4384
+ } else if (headerPurpose) {
4062
4385
  purpose = headerPurpose;
4063
4386
  purposeSource = "header";
4064
4387
  } else if (parsed.purposeFromBody && parsed.purposeSourceFromBody) {
4065
4388
  purpose = parsed.purposeFromBody;
4066
4389
  purposeSource = parsed.purposeSourceFromBody;
4067
4390
  } else {
4068
- purpose = "mcp_invoke";
4069
- purposeSource = "default_mcp_invoke";
4391
+ purpose = void 0;
4392
+ purposeSource = void 0;
4070
4393
  }
4071
4394
  let action;
4072
4395
  let actionSource;
@@ -4077,7 +4400,7 @@ function mcpToPdlss(parsed, headerPurpose, headerAction) {
4077
4400
  action = parsed.actionFromBody;
4078
4401
  actionSource = parsed.actionSourceFromBody;
4079
4402
  } else {
4080
- action = parsed.toolName ? `${parsed.method}:${parsed.toolName}` : parsed.method;
4403
+ action = parsed.toolName ? parsed.method === "tools/call" ? parsed.toolName : `${parsed.method}:${parsed.toolName}` : parsed.method;
4081
4404
  actionSource = "transport_layer";
4082
4405
  }
4083
4406
  return { purpose, action, resource, purposeSource, actionSource };
@@ -4091,11 +4414,28 @@ function mcpRiskTier(parsed) {
4091
4414
  }
4092
4415
 
4093
4416
  // src/adapters/mcp.ts
4417
+ function normalizeToolGate(gate) {
4418
+ return typeof gate === "string" ? { minAccessLevel: gate } : gate;
4419
+ }
4094
4420
  function readSingleHeader(value) {
4095
4421
  if (typeof value === "string") return value;
4096
4422
  if (Array.isArray(value)) return value[0];
4097
4423
  return void 0;
4098
4424
  }
4425
+ function dedupeFailures2(result) {
4426
+ if (result.failures && result.failures.length > 1) {
4427
+ const seen = /* @__PURE__ */ new Set();
4428
+ result.failures = result.failures.filter((f) => {
4429
+ const key = `${f.dimension}|${f.message}|${f.guidance ?? ""}`;
4430
+ if (seen.has(key)) return false;
4431
+ seen.add(key);
4432
+ return true;
4433
+ });
4434
+ }
4435
+ if (result.denialReasons && result.denialReasons.length > 1) {
4436
+ result.denialReasons = [...new Set(result.denialReasons)];
4437
+ }
4438
+ }
4099
4439
  function defaultMcpDenied(result, req, res) {
4100
4440
  const id = req.body?.id ?? null;
4101
4441
  const status = !result.identityVerified ? 401 : 403;
@@ -4120,11 +4460,17 @@ function defaultMcpDenied(result, req, res) {
4120
4460
  });
4121
4461
  }
4122
4462
  function resolveMinAccessLevel(parsed, opts) {
4123
- if (parsed.toolName && opts.toolGates && opts.toolGates[parsed.toolName] !== void 0) {
4124
- return { level: opts.toolGates[parsed.toolName], source: "toolGate" };
4463
+ if (!parsed.toolName) {
4464
+ if (opts.methodGates && opts.methodGates[parsed.method] !== void 0) {
4465
+ return { level: opts.methodGates[parsed.method], source: "methodGate" };
4466
+ }
4467
+ return { level: "none", source: "discovery_default" };
4125
4468
  }
4126
- if (opts.methodGates && opts.methodGates[parsed.method] !== void 0) {
4127
- return { level: opts.methodGates[parsed.method], source: "methodGate" };
4469
+ if (opts.toolGates && opts.toolGates[parsed.toolName] !== void 0) {
4470
+ return {
4471
+ level: normalizeToolGate(opts.toolGates[parsed.toolName]).minAccessLevel,
4472
+ source: "toolGate"
4473
+ };
4128
4474
  }
4129
4475
  return { level: mcpRiskTier(parsed), source: "tier" };
4130
4476
  }
@@ -4135,12 +4481,17 @@ function createMcpMiddleware(options) {
4135
4481
  onAgentIdMismatch = "reject",
4136
4482
  skip = false,
4137
4483
  onDenied = defaultMcpDenied,
4138
- trustVerifiedHop = true,
4484
+ trustVerifiedHop = false,
4139
4485
  verifiedHopMaxAgeMs,
4140
4486
  recordDecisions,
4141
4487
  enableRuntimeChallenge = true,
4488
+ failOnError = "open",
4142
4489
  ...config
4143
4490
  } = options;
4491
+ if (config.apiBaseUrl) {
4492
+ prefetchWellKnown(config.apiBaseUrl).catch(() => {
4493
+ });
4494
+ }
4144
4495
  return async (req, res, next) => {
4145
4496
  try {
4146
4497
  if (skip) return next();
@@ -4153,6 +4504,7 @@ function createMcpMiddleware(options) {
4153
4504
  return next();
4154
4505
  }
4155
4506
  req.mcpRequest = parsed;
4507
+ const wellKnownUrls = config.apiBaseUrl ? await getWellKnownUrls(config.apiBaseUrl).catch(() => void 0) : void 0;
4156
4508
  const headerRaw = req.headers["x-astra-id"] ?? req.headers["x-astra-agentid"];
4157
4509
  const headerAstraId = typeof headerRaw === "string" ? headerRaw : Array.isArray(headerRaw) ? headerRaw[0] : void 0;
4158
4510
  const bodyAstraId = parsed.agentIdFromBody;
@@ -4206,9 +4558,17 @@ function createMcpMiddleware(options) {
4206
4558
  }
4207
4559
  return next();
4208
4560
  }
4561
+ const rawGate = parsed.toolName && toolGates?.[parsed.toolName];
4562
+ const gate = rawGate ? normalizeToolGate(rawGate) : void 0;
4209
4563
  const headerPurpose = readSingleHeader(req.headers["x-astra-purpose"]);
4210
4564
  const headerAction = readSingleHeader(req.headers["x-astra-action"]);
4211
- const pdlss = mcpToPdlss(parsed, headerPurpose, headerAction);
4565
+ const pdlss = mcpToPdlss(
4566
+ parsed,
4567
+ req.path,
4568
+ headerPurpose,
4569
+ headerAction,
4570
+ gate ? { purpose: gate.purpose, resource: gate.resource } : void 0
4571
+ );
4212
4572
  if (config.debug) {
4213
4573
  console.debug("[mcp-middleware] pdlss resolved", {
4214
4574
  purpose_source: pdlss.purposeSource,
@@ -4246,6 +4606,7 @@ function createMcpMiddleware(options) {
4246
4606
  recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
4247
4607
  });
4248
4608
  }
4609
+ dedupeFailures2(result);
4249
4610
  onDenied(result, req, res);
4250
4611
  return;
4251
4612
  }
@@ -4268,6 +4629,13 @@ function createMcpMiddleware(options) {
4268
4629
  };
4269
4630
  result.failures = [...result.failures ?? [], insufficientFailure];
4270
4631
  result.denialReasons = [...result.denialReasons ?? [], insufficientFailure.message];
4632
+ if (!result.guidance && wellKnownUrls) {
4633
+ result.guidance = {
4634
+ message: insufficientFailure.message,
4635
+ registrationUrl: wellKnownUrls.registrationUrl,
4636
+ documentationUrl: wellKnownUrls.documentationUrl
4637
+ };
4638
+ }
4271
4639
  if (shouldRecordDecisions) {
4272
4640
  const overrideKind = gateSource === "toolGate" ? "toolGate" : gateSource === "methodGate" ? "methodGate" : "other";
4273
4641
  const override = {
@@ -4291,6 +4659,7 @@ function createMcpMiddleware(options) {
4291
4659
  });
4292
4660
  }
4293
4661
  }
4662
+ dedupeFailures2(result);
4294
4663
  onDenied(result, req, res);
4295
4664
  return;
4296
4665
  }
@@ -4314,7 +4683,38 @@ function createMcpMiddleware(options) {
4314
4683
  }
4315
4684
  next();
4316
4685
  } catch (error) {
4686
+ const errorClass = error instanceof Error ? error.constructor.name : typeof error;
4687
+ 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
4688
  console.error("[VerificationGateway/MCP] Middleware error:", error);
4689
+ console.warn(
4690
+ `[SHADOW] would-have-denied: errorClass=${errorClass} route=${req.method}:${req.path} merchantId=${config.counterpartyId ?? "unknown"} correlationId=${correlationId}`
4691
+ );
4692
+ if (failOnError === "closed") {
4693
+ const result = {
4694
+ identityVerified: false,
4695
+ policyAllowed: false,
4696
+ accessLevel: "none",
4697
+ denialReasons: [`MCP middleware internal error: ${errorClass}`],
4698
+ failures: [
4699
+ {
4700
+ dimension: "middleware.internal_error",
4701
+ message: `Middleware threw ${errorClass} \u2014 failing closed`
4702
+ }
4703
+ ],
4704
+ verifiedAt: /* @__PURE__ */ new Date(),
4705
+ correlationId
4706
+ };
4707
+ const catchUrls = config.apiBaseUrl ? await getWellKnownUrls(config.apiBaseUrl).catch(() => void 0) : void 0;
4708
+ if (catchUrls) {
4709
+ result.guidance = {
4710
+ message: `Middleware threw ${errorClass} \u2014 failing closed`,
4711
+ registrationUrl: catchUrls.registrationUrl,
4712
+ documentationUrl: catchUrls.documentationUrl
4713
+ };
4714
+ }
4715
+ dedupeFailures2(result);
4716
+ return onDenied(result, req, res);
4717
+ }
4318
4718
  next();
4319
4719
  }
4320
4720
  };
@@ -5021,11 +5421,14 @@ export {
5021
5421
  extractCredentials,
5022
5422
  extractMcpCredentials,
5023
5423
  getAccessLevelForScore,
5424
+ getCachedWellKnownUrls,
5024
5425
  getCapabilities,
5025
5426
  getTrustLevel,
5427
+ getWellKnownUrls,
5026
5428
  hasCredentials,
5027
5429
  hasMinimumAccess,
5028
5430
  nextjs_exports as nextjs,
5431
+ prefetchWellKnown,
5029
5432
  quickVerify,
5030
5433
  recordDecision2 as recordDecision,
5031
5434
  sdk_exports as sdk,