@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.js CHANGED
@@ -53,11 +53,14 @@ __export(src_exports, {
53
53
  extractCredentials: () => extractCredentials,
54
54
  extractMcpCredentials: () => extractMcpCredentials,
55
55
  getAccessLevelForScore: () => getAccessLevelForScore,
56
+ getCachedWellKnownUrls: () => getCachedWellKnownUrls,
56
57
  getCapabilities: () => getCapabilities,
57
58
  getTrustLevel: () => getTrustLevel,
59
+ getWellKnownUrls: () => getWellKnownUrls,
58
60
  hasCredentials: () => hasCredentials,
59
61
  hasMinimumAccess: () => hasMinimumAccess,
60
62
  nextjs: () => nextjs_exports,
63
+ prefetchWellKnown: () => prefetchWellKnown,
61
64
  quickVerify: () => quickVerify,
62
65
  recordDecision: () => recordDecision2,
63
66
  sdk: () => sdk_exports,
@@ -189,7 +192,66 @@ function getCapabilities(accessLevel) {
189
192
  }
190
193
 
191
194
  // src/version.ts
192
- var SDK_VERSION = "2.4.12";
195
+ var SDK_VERSION = "2.4.13";
196
+
197
+ // src/well-known.ts
198
+ var CACHE_TTL_MS = 60 * 60 * 1e3;
199
+ var cache = /* @__PURE__ */ new Map();
200
+ var inflight = /* @__PURE__ */ new Map();
201
+ function wellKnownUrl(apiBaseUrl) {
202
+ const base = apiBaseUrl.replace(/\/api\/?$/, "");
203
+ return `${base}/.well-known/agentic-commerce`;
204
+ }
205
+ async function fetchWellKnown(apiBaseUrl) {
206
+ const url = wellKnownUrl(apiBaseUrl);
207
+ const response = await fetch(url, {
208
+ method: "GET",
209
+ headers: { Accept: "application/json" },
210
+ signal: AbortSignal.timeout(5e3)
211
+ });
212
+ if (!response.ok) {
213
+ throw new Error(
214
+ `AstraSync platform must expose /.well-known/agentic-commerce; got ${response.status} from ${url}. SDK cannot initialise without it.`
215
+ );
216
+ }
217
+ const data = await response.json();
218
+ if (!data.registrationUrl || !data.documentationUrl || !data.verifyAccessUrl) {
219
+ throw new Error(
220
+ `/.well-known/agentic-commerce response missing required fields (registrationUrl, documentationUrl, verifyAccessUrl).`
221
+ );
222
+ }
223
+ return data;
224
+ }
225
+ function prefetchWellKnown(apiBaseUrl) {
226
+ const existing = inflight.get(apiBaseUrl);
227
+ if (existing) return existing;
228
+ const promise = fetchWellKnown(apiBaseUrl).then((data) => {
229
+ cache.set(apiBaseUrl, { data, fetchedAt: Date.now() });
230
+ inflight.delete(apiBaseUrl);
231
+ return data;
232
+ }).catch((err) => {
233
+ inflight.delete(apiBaseUrl);
234
+ throw err;
235
+ });
236
+ inflight.set(apiBaseUrl, promise);
237
+ return promise;
238
+ }
239
+ async function getWellKnownUrls(apiBaseUrl) {
240
+ const entry = cache.get(apiBaseUrl);
241
+ if (entry) {
242
+ if (Date.now() - entry.fetchedAt > CACHE_TTL_MS) {
243
+ prefetchWellKnown(apiBaseUrl).catch(() => {
244
+ });
245
+ }
246
+ return entry.data;
247
+ }
248
+ const pending = inflight.get(apiBaseUrl);
249
+ if (pending) return pending;
250
+ return prefetchWellKnown(apiBaseUrl);
251
+ }
252
+ function getCachedWellKnownUrls(apiBaseUrl) {
253
+ return cache.get(apiBaseUrl)?.data;
254
+ }
193
255
 
194
256
  // src/verify.ts
195
257
  var DEFAULT_CONFIG = {
@@ -208,22 +270,27 @@ var DEFAULT_CONFIG = {
208
270
  };
209
271
  var initCheckPerformed = false;
210
272
  var deprecationWarningShown = false;
211
- async function performInitCheck(apiBaseUrl, debug) {
273
+ async function performInitCheck(apiBaseUrl, debug, strictInit) {
212
274
  initCheckPerformed = true;
213
275
  try {
214
276
  const probeUrl = `${apiBaseUrl}/agents/verify-access`;
215
277
  const response = await fetch(probeUrl, { method: "HEAD" });
216
278
  const contentType = response.headers.get("content-type") ?? "";
217
279
  if (contentType.startsWith("text/html")) {
218
- console.warn(
219
- `[VerificationGateway] apiBaseUrl '${apiBaseUrl}' returned HTML (content-type: ${contentType}). This usually means apiBaseUrl is pointing at a marketing site instead of the API. Expected: 'https://astrasync.ai/api' (prod) or 'https://staging.astrasync.ai/api' (staging). Set disableInitChecks: true on GatewayConfig to silence this warning.`
220
- );
280
+ 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).`;
281
+ if (strictInit) {
282
+ throw new Error(`${message} (strictInit=true)`);
283
+ }
284
+ console.warn(`${message} Set disableInitChecks: true on GatewayConfig to silence.`);
221
285
  } else if (debug) {
222
286
  console.log(
223
287
  `[VerificationGateway] init check passed for ${apiBaseUrl} (content-type: ${contentType})`
224
288
  );
225
289
  }
226
290
  } catch (err) {
291
+ if (strictInit) {
292
+ throw err;
293
+ }
227
294
  if (debug) {
228
295
  console.log(`[VerificationGateway] init check failed (non-blocking): ${String(err)}`);
229
296
  }
@@ -247,7 +314,23 @@ function getCacheKey(request) {
247
314
  request.counterpartyType || "",
248
315
  request.isSubAgentRequest ? "1" : "0",
249
316
  request.parentAgentId || "",
250
- request.subAgentDepth ?? ""
317
+ request.subAgentDepth ?? "",
318
+ // Audit F-A1-07: previously-missing dimensions that DO affect the
319
+ // backend verdict. Without these, two requests with different
320
+ // durations (e.g. 60s vs 86400s) collided on the same cache key and
321
+ // the shorter-duration allow served the longer-duration request.
322
+ request.durationRequired ?? "",
323
+ request.invocationProtocol || "",
324
+ request.enableRuntimeChallenge ? "1" : "0",
325
+ // callerMetadata fields contribute to risk model; include the ones
326
+ // backend reads. sourceIp/userAgent/forwardedFor change per-request
327
+ // so their inclusion effectively forces a re-check for any varying
328
+ // client (the right behavior — IP-driven anomaly scoring shouldn't
329
+ // be cached across IPs).
330
+ request.callerMetadata?.sourceIp || "",
331
+ request.callerMetadata?.userAgent || "",
332
+ request.callerMetadata?.forwardedFor || "",
333
+ request.callerMetadata?.agentCardUrl || ""
251
334
  ].join("|");
252
335
  }
253
336
  function getCachedResult(request) {
@@ -276,9 +359,13 @@ function clearCache() {
276
359
  }
277
360
  function extractCredentials(headers, query) {
278
361
  const credentials = {};
362
+ const ASTRA_ID_PATTERN = /^ASTRAE?-[A-Za-z0-9_-]{1,64}$/;
279
363
  const astraIdHeader = headers["x-astra-id"] || headers["X-Astra-Id"] || headers["X-ASTRA-ID"] || headers["x-astra-agentid"] || headers["X-Astra-AgentId"] || headers["x-astra-agent-id"] || headers["X-Astra-Agent-Id"] || headers["X-ASTRA-AGENT-ID"];
280
364
  if (astraIdHeader) {
281
- credentials.astraId = Array.isArray(astraIdHeader) ? astraIdHeader[0] : astraIdHeader;
365
+ const raw = Array.isArray(astraIdHeader) ? astraIdHeader[0] : typeof astraIdHeader === "string" ? astraIdHeader : void 0;
366
+ if (typeof raw === "string" && ASTRA_ID_PATTERN.test(raw)) {
367
+ credentials.astraId = raw;
368
+ }
282
369
  }
283
370
  const apiKeyHeader = headers["x-api-key"] || headers["X-Api-Key"] || headers["X-API-KEY"];
284
371
  if (apiKeyHeader) {
@@ -287,9 +374,11 @@ function extractCredentials(headers, query) {
287
374
  const authHeader = headers["authorization"] || headers["Authorization"];
288
375
  if (authHeader) {
289
376
  const authValue = Array.isArray(authHeader) ? authHeader[0] : authHeader;
290
- credentials.authorizationHeader = authValue;
291
- if (authValue.startsWith("Bearer ")) {
292
- credentials.jwt = authValue.slice(7);
377
+ if (typeof authValue === "string") {
378
+ credentials.authorizationHeader = authValue;
379
+ if (authValue.startsWith("Bearer ")) {
380
+ credentials.jwt = authValue.slice(7);
381
+ }
293
382
  }
294
383
  }
295
384
  if (query) {
@@ -305,21 +394,22 @@ function extractCredentials(headers, query) {
305
394
  function hasCredentials(credentials) {
306
395
  return !!(credentials.astraId || credentials.apiKey || credentials.jwt);
307
396
  }
308
- function createGuidanceResponse(config, reason, options = {}) {
397
+ function createGuidanceResponse(_config, reason, options = {}) {
309
398
  const source = options.source ?? "no_credentials";
310
399
  const isApiError = source === "api_error";
400
+ const urls = options.urls;
311
401
  const guidance = isApiError ? {
312
402
  message: "Verification is temporarily unavailable. Retry with exponential backoff; if the issue persists, contact support with the correlationId.",
313
- registrationUrl: `${config.apiBaseUrl.replace("/api", "")}/register`,
314
- documentationUrl: `${config.apiBaseUrl.replace("/api", "")}/docs/agent-access`,
403
+ registrationUrl: urls?.registrationUrl ?? "",
404
+ documentationUrl: urls?.documentationUrl ?? "",
315
405
  steps: [
316
406
  "Retry the request with exponential backoff",
317
407
  "If failures persist, share the correlationId with support"
318
408
  ]
319
409
  } : {
320
410
  message: "This service verifies AI agents before granting access. Please register your agent with AstraSync.",
321
- registrationUrl: `${config.apiBaseUrl.replace("/api", "")}/register`,
322
- documentationUrl: `${config.apiBaseUrl.replace("/api", "")}/docs/agent-access`,
411
+ registrationUrl: urls?.registrationUrl ?? "",
412
+ documentationUrl: urls?.documentationUrl ?? "",
323
413
  steps: [
324
414
  "Register for an AstraSync account",
325
415
  "Create and register your agent",
@@ -361,7 +451,7 @@ async function callVerifyAccessAPI(config, request) {
361
451
  const { credentials, ...requestData } = request;
362
452
  const body = {
363
453
  ...credentials.astraId && { agentId: credentials.astraId },
364
- purpose: requestData.purpose || "general"
454
+ ...requestData.purpose && { purpose: requestData.purpose }
365
455
  };
366
456
  if (requestData.action) body.action = requestData.action;
367
457
  if (requestData.resourceType) body.resourceType = requestData.resourceType;
@@ -395,12 +485,8 @@ async function callVerifyAccessAPI(config, request) {
395
485
  "Content-Type": "application/json",
396
486
  ...config.customHeaders
397
487
  };
398
- if (credentials.authorizationHeader) {
399
- headers["Authorization"] = credentials.authorizationHeader;
400
- } else if (config.apiKey) {
401
- headers["Authorization"] = `Bearer ${config.apiKey}`;
402
- }
403
488
  if (config.apiKey) {
489
+ headers["Authorization"] = `Bearer ${config.apiKey}`;
404
490
  headers["X-API-Key"] = config.apiKey;
405
491
  }
406
492
  try {
@@ -445,8 +531,13 @@ async function callVerifyAccessAPI(config, request) {
445
531
  }
446
532
  async function verify(config, request) {
447
533
  const mergedConfig = { ...DEFAULT_CONFIG, ...config };
534
+ const urls = mergedConfig.apiBaseUrl ? getCachedWellKnownUrls(mergedConfig.apiBaseUrl) : void 0;
448
535
  if (!initCheckPerformed && !mergedConfig.disableInitChecks && mergedConfig.apiBaseUrl) {
449
- void performInitCheck(mergedConfig.apiBaseUrl, mergedConfig.debug);
536
+ if (mergedConfig.strictInit) {
537
+ await performInitCheck(mergedConfig.apiBaseUrl, mergedConfig.debug, true);
538
+ } else {
539
+ void performInitCheck(mergedConfig.apiBaseUrl, mergedConfig.debug, false);
540
+ }
450
541
  }
451
542
  if (!deprecationWarningShown && (config.minTrustScore !== void 0 || config.minTrustScoreForFull !== void 0)) {
452
543
  deprecationWarningShown = true;
@@ -477,7 +568,8 @@ async function verify(config, request) {
477
568
  if (!apiResponse.success) {
478
569
  return createGuidanceResponse(mergedConfig, apiResponse.error, {
479
570
  source: "api_error",
480
- correlationId: apiResponse.correlationId
571
+ correlationId: apiResponse.correlationId,
572
+ urls
481
573
  });
482
574
  }
483
575
  if (!apiResponse.access?.allowed) {
@@ -500,8 +592,8 @@ async function verify(config, request) {
500
592
  requiresApproval: apiResponse.access?.requiresApproval,
501
593
  guidance: {
502
594
  message: apiResponse.access?.reason || "Access denied by PDLSS policy",
503
- registrationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/register`,
504
- documentationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/docs/pdlss`
595
+ registrationUrl: urls?.registrationUrl ?? "",
596
+ documentationUrl: urls?.documentationUrl ?? ""
505
597
  },
506
598
  verifiedAt: /* @__PURE__ */ new Date(),
507
599
  // Extract sessionId so decisions can be recorded for denials too
@@ -570,13 +662,15 @@ async function verify(config, request) {
570
662
  result.denialReasons = result.recommendationReasons || [
571
663
  "Access denied by AstraSync recommendation"
572
664
  ];
573
- if (result.runtimeChallenge) {
574
- result.guidance = {
575
- message: `Verification failed: ${result.runtimeChallenge.reason || "runtime challenge failed"}`,
576
- registrationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/register`,
577
- documentationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/docs/runtime-challenge`
578
- };
579
- }
665
+ result.guidance = result.runtimeChallenge ? {
666
+ message: `Verification failed: ${result.runtimeChallenge.reason || "runtime challenge failed"}`,
667
+ registrationUrl: urls?.registrationUrl ?? "",
668
+ documentationUrl: urls?.documentationUrl ?? ""
669
+ } : {
670
+ message: result.recommendationReasons?.[0] || "Access denied by AstraSync recommendation",
671
+ registrationUrl: urls?.registrationUrl ?? "",
672
+ documentationUrl: urls?.documentationUrl ?? ""
673
+ };
580
674
  } else if (result.recommendation === "step_up_required") {
581
675
  result.requiresStepUp = true;
582
676
  if (ACCESS_LEVEL_HIERARCHY[result.accessLevel] > ACCESS_LEVEL_HIERARCHY["read-only"]) {
@@ -825,18 +919,43 @@ function defaultExtractPurpose(req) {
825
919
  return "general";
826
920
  }
827
921
  }
828
- function matchRoute(pattern, path) {
922
+ function matchRoute(pattern, path, opts) {
829
923
  const regexPattern = pattern.replace(/\*/g, ".*").replace(/\//g, "\\/");
830
- const regex = new RegExp(`^${regexPattern}$`);
831
- return regex.test(path);
924
+ const caseSensitiveRegex = new RegExp(`^${regexPattern}$`);
925
+ const caseSensitiveResult = caseSensitiveRegex.test(path);
926
+ if (!opts?.caseInsensitive && !opts?.logShadowDivergence) {
927
+ return caseSensitiveResult;
928
+ }
929
+ const caseInsensitiveRegex = new RegExp(`^${regexPattern}$`, "i");
930
+ const caseInsensitiveResult = caseInsensitiveRegex.test(path);
931
+ if (opts?.logShadowDivergence && caseSensitiveResult !== caseInsensitiveResult) {
932
+ console.warn(
933
+ `[SHADOW] matchRoute case-insensitive would change result: pattern=${pattern} path=${path} caseSensitive=${caseSensitiveResult} caseInsensitive=${caseInsensitiveResult} correlationId=${opts.correlationId ?? "unknown"}`
934
+ );
935
+ }
936
+ return opts?.caseInsensitive ? caseInsensitiveResult : caseSensitiveResult;
832
937
  }
833
- function findRouteConfig(routes, path, method) {
938
+ function findRouteConfig(routes, path, method, opts) {
834
939
  return routes.find((route) => {
835
940
  const methodMatches = route.method === "*" || route.method.toUpperCase() === method.toUpperCase();
836
- const pathMatches = matchRoute(route.pattern, path);
941
+ const pathMatches = matchRoute(route.pattern, path, opts);
837
942
  return methodMatches && pathMatches;
838
943
  });
839
944
  }
945
+ function dedupeFailures(result) {
946
+ if (result.failures && result.failures.length > 1) {
947
+ const seen = /* @__PURE__ */ new Set();
948
+ result.failures = result.failures.filter((f) => {
949
+ const key = `${f.dimension}|${f.message}|${f.guidance ?? ""}`;
950
+ if (seen.has(key)) return false;
951
+ seen.add(key);
952
+ return true;
953
+ });
954
+ }
955
+ if (result.denialReasons && result.denialReasons.length > 1) {
956
+ result.denialReasons = [...new Set(result.denialReasons)];
957
+ }
958
+ }
840
959
  function defaultOnDenied(result, _req, res) {
841
960
  const statusCode = !result.identityVerified ? 401 : 403;
842
961
  res.setHeader("X-Astra-Gateway-Mode", "enforced");
@@ -863,8 +982,14 @@ function createMiddleware(options) {
863
982
  recordDecisions,
864
983
  enableRuntimeChallenge = true,
865
984
  routesRefreshMs = DEFAULT_ROUTES_REFRESH_MS,
985
+ failOnError = "open",
986
+ caseInsensitiveRouteMatch = false,
866
987
  ...config
867
988
  } = options;
989
+ if (config.apiBaseUrl) {
990
+ prefetchWellKnown(config.apiBaseUrl).catch(() => {
991
+ });
992
+ }
868
993
  let cachedRoutes = [];
869
994
  let lastFetchAt = 0;
870
995
  let refreshing = null;
@@ -885,7 +1010,7 @@ function createMiddleware(options) {
885
1010
  cachedRoutes = fetched;
886
1011
  lastFetchAt = Date.now();
887
1012
  if (cachedRoutes.length === 0 && !warnedEmptyRoutes) {
888
- const dashboard = config.dashboardUrl ?? "https://app.astrasync.ai";
1013
+ const dashboard = config.dashboardUrl ?? "https://astrasync.ai/dashboard";
889
1014
  console.warn(
890
1015
  `[VerificationGateway] No route policy configured for ${config.counterpartyId}. Gateway is in pass-through mode for ALL traffic until you add at least one route. Configure at ${dashboard}/dashboard/endpoints/${config.counterpartyId}/routes`
891
1016
  );
@@ -911,7 +1036,12 @@ function createMiddleware(options) {
911
1036
  refreshing = null;
912
1037
  });
913
1038
  }
914
- const routeConfig = findRouteConfig(cachedRoutes, req.path, req.method);
1039
+ const correlationId = req.headers["x-request-id"] || req.headers["x-correlation-id"];
1040
+ const routeConfig = findRouteConfig(cachedRoutes, req.path, req.method, {
1041
+ caseInsensitive: caseInsensitiveRouteMatch,
1042
+ logShadowDivergence: true,
1043
+ correlationId
1044
+ });
915
1045
  if (!routeConfig) {
916
1046
  if (config.setPassThroughHeader) {
917
1047
  res.setHeader("X-Astra-Gateway-Mode", "unenforced");
@@ -922,6 +1052,7 @@ function createMiddleware(options) {
922
1052
  }
923
1053
  return next();
924
1054
  }
1055
+ const wellKnownUrls = config.apiBaseUrl ? await getWellKnownUrls(config.apiBaseUrl).catch(() => void 0) : void 0;
925
1056
  const credentials = customExtractCredentials ? customExtractCredentials(req) : defaultExtractCredentials(req);
926
1057
  const shouldEnforce = routeConfig.minAccessLevel !== "none";
927
1058
  if (routeConfig.minAccessLevel === "none" && (!config.evaluateAlwaysIfCredentialed || !credentials.astraId)) {
@@ -943,8 +1074,8 @@ function createMiddleware(options) {
943
1074
  denialReasons: preCheckFailures.map((f) => f.message),
944
1075
  guidance: {
945
1076
  message: "Request exceeds counterparty-defined PDLSS limits.",
946
- registrationUrl: `${config.apiBaseUrl?.replace("/api", "")}/register`,
947
- documentationUrl: `${config.apiBaseUrl?.replace("/api", "")}/docs/pdlss`
1077
+ registrationUrl: wellKnownUrls?.registrationUrl ?? "",
1078
+ documentationUrl: wellKnownUrls?.documentationUrl ?? ""
948
1079
  },
949
1080
  verifiedAt: /* @__PURE__ */ new Date()
950
1081
  };
@@ -958,13 +1089,19 @@ function createMiddleware(options) {
958
1089
  requestMethod: req.method
959
1090
  }).catch(() => {
960
1091
  });
1092
+ dedupeFailures(result2);
961
1093
  onDenied(result2, req, res);
962
1094
  return;
963
1095
  }
964
1096
  const shouldRecordDecisions = recordDecisions !== false;
965
1097
  const forwardedFor = req.headers["x-forwarded-for"];
966
1098
  const forwardedForStr = Array.isArray(forwardedFor) ? forwardedFor.join(", ") : forwardedFor;
967
- const originalClientIp = forwardedForStr ? forwardedForStr.split(",")[0].trim() : req.ip;
1099
+ const originalClientIp = req.ip ?? (forwardedForStr ? forwardedForStr.split(",")[0].trim() : void 0);
1100
+ if (!req.ip && forwardedForStr) {
1101
+ console.warn(
1102
+ "[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."
1103
+ );
1104
+ }
968
1105
  const agentCardUrl = typeof req.headers["x-astrasync-agent-card"] === "string" ? req.headers["x-astrasync-agent-card"] : void 0;
969
1106
  const result = await verify(config, {
970
1107
  credentials,
@@ -995,6 +1132,7 @@ function createMiddleware(options) {
995
1132
  recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
996
1133
  });
997
1134
  }
1135
+ dedupeFailures(result);
998
1136
  onDenied(result, req, res);
999
1137
  return;
1000
1138
  }
@@ -1017,10 +1155,18 @@ function createMiddleware(options) {
1017
1155
  };
1018
1156
  result.failures = [...result.failures ?? [], insufficientFailure];
1019
1157
  result.denialReasons = [...result.denialReasons ?? [], insufficientFailure.message];
1158
+ if (!result.guidance && wellKnownUrls) {
1159
+ result.guidance = {
1160
+ message: insufficientFailure.message,
1161
+ registrationUrl: wellKnownUrls.registrationUrl,
1162
+ documentationUrl: wellKnownUrls.documentationUrl
1163
+ };
1164
+ }
1020
1165
  if (shouldRecordDecisions && sessionId) {
1021
1166
  recordDecision(config, sessionId, "denied", insufficientFailure.message).catch(() => {
1022
1167
  });
1023
1168
  }
1169
+ dedupeFailures(result);
1024
1170
  onDenied(result, req, res);
1025
1171
  return;
1026
1172
  }
@@ -1033,10 +1179,18 @@ function createMiddleware(options) {
1033
1179
  };
1034
1180
  result.failures = [...result.failures ?? [], trustFailure];
1035
1181
  result.denialReasons = [trustFailure.message];
1182
+ if (!result.guidance && wellKnownUrls) {
1183
+ result.guidance = {
1184
+ message: trustFailure.message,
1185
+ registrationUrl: wellKnownUrls.registrationUrl,
1186
+ documentationUrl: wellKnownUrls.documentationUrl
1187
+ };
1188
+ }
1036
1189
  if (shouldRecordDecisions && sessionId) {
1037
1190
  recordDecision(config, sessionId, "denied", trustFailure.message).catch(() => {
1038
1191
  });
1039
1192
  }
1193
+ dedupeFailures(result);
1040
1194
  onDenied(result, req, res);
1041
1195
  return;
1042
1196
  }
@@ -1051,7 +1205,38 @@ function createMiddleware(options) {
1051
1205
  }
1052
1206
  next();
1053
1207
  } catch (error) {
1208
+ const errorClass = error instanceof Error ? error.constructor.name : typeof error;
1209
+ const correlationId = req.headers["x-request-id"] || req.headers["x-correlation-id"] || `gen-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
1054
1210
  console.error("[VerificationGateway] Middleware error:", error);
1211
+ console.warn(
1212
+ `[SHADOW] would-have-denied: errorClass=${errorClass} route=${req.method}:${req.path} merchantId=${config.counterpartyId ?? "unknown"} correlationId=${correlationId}`
1213
+ );
1214
+ if (failOnError === "closed") {
1215
+ const result = {
1216
+ identityVerified: false,
1217
+ policyAllowed: false,
1218
+ accessLevel: "none",
1219
+ denialReasons: [`Verification middleware internal error: ${errorClass}`],
1220
+ failures: [
1221
+ {
1222
+ dimension: "middleware.internal_error",
1223
+ message: `Middleware threw ${errorClass} \u2014 failing closed`
1224
+ }
1225
+ ],
1226
+ verifiedAt: /* @__PURE__ */ new Date(),
1227
+ correlationId
1228
+ };
1229
+ const catchUrls = config.apiBaseUrl ? await getWellKnownUrls(config.apiBaseUrl).catch(() => void 0) : void 0;
1230
+ if (catchUrls) {
1231
+ result.guidance = {
1232
+ message: `Middleware threw ${errorClass} \u2014 failing closed`,
1233
+ registrationUrl: catchUrls.registrationUrl,
1234
+ documentationUrl: catchUrls.documentationUrl
1235
+ };
1236
+ }
1237
+ dedupeFailures(result);
1238
+ return onDenied(result, req, res);
1239
+ }
1055
1240
  next();
1056
1241
  }
1057
1242
  };
@@ -1063,6 +1248,18 @@ __export(nextjs_exports, {
1063
1248
  createMatcherConfig: () => createMatcherConfig,
1064
1249
  createMiddleware: () => createMiddleware2
1065
1250
  });
1251
+ function escapeHtml(value) {
1252
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1253
+ }
1254
+ function sanitizeUrl(value, fallback) {
1255
+ if (typeof value !== "string" || value.length === 0) return escapeHtml(fallback);
1256
+ const trimmed = value.trim();
1257
+ if (/^javascript:|^data:|^vbscript:/i.test(trimmed)) return escapeHtml(fallback);
1258
+ if (/^https?:\/\//i.test(trimmed) || trimmed.startsWith("/")) {
1259
+ return escapeHtml(trimmed);
1260
+ }
1261
+ return escapeHtml(fallback);
1262
+ }
1066
1263
  function extractCredentialsFromNextRequest(request) {
1067
1264
  const credentials = {};
1068
1265
  const astraId = request.headers.get("x-astra-id") || request.headers.get("X-Astra-Id");
@@ -1134,10 +1331,18 @@ function extractPurpose(request) {
1134
1331
  }
1135
1332
  }
1136
1333
  function generateCommerceShieldHtml(result, options) {
1137
- const title = options.commerceShield?.title || "AstraSync Agent Verification";
1138
- const message = options.commerceShield?.message || result.guidance?.message || "This site verifies AI agents before granting access. We noticed you're visiting without AstraSync credentials.";
1139
- const registrationUrl = result.guidance?.registrationUrl || "https://astrasync.ai/register";
1140
- const docsUrl = result.guidance?.documentationUrl || "https://astrasync.ai/docs/agent-access";
1334
+ const title = escapeHtml(options.commerceShield?.title || "AstraSync Agent Verification");
1335
+ const message = escapeHtml(
1336
+ options.commerceShield?.message || result.guidance?.message || "This site verifies AI agents before granting access. We noticed you're visiting without AstraSync credentials."
1337
+ );
1338
+ const registrationUrl = sanitizeUrl(
1339
+ result.guidance?.registrationUrl,
1340
+ "https://astrasync.ai/register"
1341
+ );
1342
+ const docsUrl = sanitizeUrl(
1343
+ result.guidance?.documentationUrl,
1344
+ "https://astrasync.ai/docs/agent-access"
1345
+ );
1141
1346
  const allowGuest = options.commerceShield?.allowGuestAccess ?? true;
1142
1347
  return `
1143
1348
  <!DOCTYPE html>
@@ -1259,7 +1464,7 @@ function generateCommerceShieldHtml(result, options) {
1259
1464
  <div class="shield-steps">
1260
1465
  <h3>To get verified access:</h3>
1261
1466
  <ol>
1262
- <li>Register at <a href="${registrationUrl}">astrasync.ai/register</a></li>
1467
+ <li>Register at <a href="${registrationUrl}">astrasync.ai/agents/register</a></li>
1263
1468
  <li>Create and register your agent</li>
1264
1469
  <li>Add your ASTRA-ID to request headers</li>
1265
1470
  <li>Refresh this page</li>
@@ -1347,7 +1552,7 @@ function createMiddleware2(options) {
1347
1552
  denialReasons: preCheckFailures.map((f) => f.message),
1348
1553
  guidance: {
1349
1554
  message: "Request exceeds counterparty-defined PDLSS limits.",
1350
- registrationUrl: `${config.apiBaseUrl?.replace("/api", "")}/register`,
1555
+ registrationUrl: `${config.apiBaseUrl?.replace("/api", "")}/agents/register`,
1351
1556
  documentationUrl: `${config.apiBaseUrl?.replace("/api", "")}/docs/pdlss`
1352
1557
  },
1353
1558
  verifiedAt: /* @__PURE__ */ new Date()
@@ -2008,12 +2213,45 @@ function bufferToBase64(bytes) {
2008
2213
 
2009
2214
  // src/transport/rfc9421-verify.ts
2010
2215
  var import_http_message_signatures = require("http-message-signatures");
2216
+
2217
+ // src/transport/nonce-store.ts
2218
+ var InMemoryNonceStore = class {
2219
+ constructor(capacity = 1e4) {
2220
+ this.entries = /* @__PURE__ */ new Map();
2221
+ this.lastSweepMs = 0;
2222
+ this.capacity = capacity;
2223
+ }
2224
+ seen(key, expiresAtMs) {
2225
+ const nowMs = Date.now();
2226
+ if (nowMs - this.lastSweepMs > 1e3) {
2227
+ for (const [k, exp] of this.entries) {
2228
+ if (exp <= nowMs) this.entries.delete(k);
2229
+ }
2230
+ this.lastSweepMs = nowMs;
2231
+ }
2232
+ const existing = this.entries.get(key);
2233
+ if (existing !== void 0 && existing > nowMs) {
2234
+ return true;
2235
+ }
2236
+ if (this.entries.size >= this.capacity) {
2237
+ const oldest = this.entries.keys().next().value;
2238
+ if (oldest !== void 0) this.entries.delete(oldest);
2239
+ }
2240
+ this.entries.set(key, expiresAtMs);
2241
+ return false;
2242
+ }
2243
+ };
2244
+ var defaultNonceStore = new InMemoryNonceStore();
2245
+
2246
+ // src/transport/rfc9421-verify.ts
2011
2247
  async function verifyRFC9421(request, options) {
2012
2248
  const { resolver } = options;
2013
- const tolerance = options.clockSkewSec ?? 300;
2249
+ const tolerance = options.clockSkewSec ?? 60;
2014
2250
  const nowSec = options.now ? options.now() : Math.floor(Date.now() / 1e3);
2251
+ const nonceStore = options.nonceStore ?? defaultNonceStore;
2015
2252
  let resolvedKid;
2016
2253
  let resolvedAlg;
2254
+ let replayDetected = false;
2017
2255
  const keyLookup = async (parameters) => {
2018
2256
  const kid = typeof parameters.keyid === "string" ? parameters.keyid : void 0;
2019
2257
  if (!kid) return null;
@@ -2027,6 +2265,14 @@ async function verifyRFC9421(request, options) {
2027
2265
  const expires = toUnixSeconds(parameters.expires);
2028
2266
  if (created !== void 0 && Math.abs(nowSec - created) > tolerance) return null;
2029
2267
  if (expires !== void 0 && nowSec > expires + tolerance) return null;
2268
+ const nonce = typeof parameters.nonce === "string" ? parameters.nonce : void 0;
2269
+ if (nonce) {
2270
+ const expiresAtMs = (expires !== void 0 ? expires + tolerance : nowSec + tolerance) * 1e3;
2271
+ if (nonceStore.seen(`rfc9421:${kid}:${nonce}`, expiresAtMs)) {
2272
+ replayDetected = true;
2273
+ return null;
2274
+ }
2275
+ }
2030
2276
  return jwkToVerifyingKey(kid, jwk, alg);
2031
2277
  };
2032
2278
  try {
@@ -2049,7 +2295,7 @@ async function verifyRFC9421(request, options) {
2049
2295
  kid: resolvedKid,
2050
2296
  registry: resolver.name,
2051
2297
  algorithm: resolvedAlg,
2052
- error: result === false ? "signature invalid" : "no signature found"
2298
+ error: replayDetected ? "RFC9421 signature replay \u2014 already seen within tolerance window" : result === false ? "signature invalid" : "no signature found"
2053
2299
  };
2054
2300
  } catch (err) {
2055
2301
  return {
@@ -2874,14 +3120,26 @@ function sha256Sync2(data) {
2874
3120
  function verifyAP2Chain(input) {
2875
3121
  const { triple } = input;
2876
3122
  const errors = [];
3123
+ const toleranceSec = input.clockSkewSec ?? 60;
3124
+ const nonceStore = input.nonceStore ?? defaultNonceStore;
2877
3125
  const intentPresent = triple.intent !== void 0;
2878
3126
  const cartRefOk = checkCartRef(triple, errors);
2879
3127
  const paymentRefOk = checkPaymentRef(triple, errors);
2880
3128
  const { ok: agentIdContinuity, agentId } = checkAgentContinuity(triple, errors);
2881
3129
  const paymentMethodAllowed = checkPaymentMethod(triple, errors);
2882
3130
  const totalsConsistent = checkTotals(triple, errors);
2883
- const expiryOk = checkExpiries(triple, input.clockSkewSec ?? 300, input.now, errors);
2884
- const ok = cartRefOk && paymentRefOk && agentIdContinuity && paymentMethodAllowed && totalsConsistent && expiryOk;
3131
+ const expiryOk = checkExpiries(triple, toleranceSec, input.now, errors);
3132
+ let replayOk = true;
3133
+ const replayId = triple.payment?.raw?.id ?? triple.cart?.raw?.id;
3134
+ if (typeof replayId === "string" && replayId.length > 0) {
3135
+ const now = input.now ? input.now() : Math.floor(Date.now() / 1e3);
3136
+ const expiresAt = (now + toleranceSec) * 1e3;
3137
+ if (nonceStore.seen(`ap2:${replayId}`, expiresAt)) {
3138
+ errors.push(`AP2 chain replay \u2014 mandate ${replayId} already seen within tolerance window`);
3139
+ replayOk = false;
3140
+ }
3141
+ }
3142
+ const ok = cartRefOk && paymentRefOk && agentIdContinuity && paymentMethodAllowed && totalsConsistent && expiryOk && replayOk;
2885
3143
  return {
2886
3144
  ok,
2887
3145
  checks: {
@@ -2927,7 +3185,10 @@ function checkAgentContinuity(triple, errors) {
2927
3185
  const ids = [triple.intent?.agent_id, triple.cart?.agent_id, triple.payment?.agent_id].filter(
2928
3186
  (id) => typeof id === "string" && id.length > 0
2929
3187
  );
2930
- if (ids.length === 0) return { ok: true };
3188
+ if (ids.length === 0) {
3189
+ errors.push("agent_id missing across all three mandates (intent/cart/payment)");
3190
+ return { ok: false };
3191
+ }
2931
3192
  const unique = new Set(ids);
2932
3193
  if (unique.size > 1) {
2933
3194
  errors.push(`agent_id mismatch across mandates: ${Array.from(unique).join(", ")}`);
@@ -2936,9 +3197,16 @@ function checkAgentContinuity(triple, errors) {
2936
3197
  return { ok: true, agentId: ids[0] };
2937
3198
  }
2938
3199
  function checkPaymentMethod(triple, errors) {
2939
- const paymentMethod = triple.payment?.payment_method;
2940
3200
  const allowed = triple.intent?.paymentMethods;
2941
- if (!paymentMethod || !allowed || allowed.length === 0) return true;
3201
+ if (!allowed || allowed.length === 0) return true;
3202
+ if (!triple.payment) return true;
3203
+ const paymentMethod = triple.payment.payment_method;
3204
+ if (!paymentMethod) {
3205
+ errors.push(
3206
+ `payment.payment_method missing but intent declares allowlist [${allowed.join(", ")}]`
3207
+ );
3208
+ return false;
3209
+ }
2942
3210
  if (!allowed.includes(paymentMethod)) {
2943
3211
  errors.push(
2944
3212
  `payment_method "${paymentMethod}" not in intent.paymentMethods [${allowed.join(", ")}]`
@@ -2972,19 +3240,24 @@ function checkTotals(triple, errors) {
2972
3240
  function checkExpiries(triple, toleranceSec, nowFn, errors) {
2973
3241
  const now = nowFn ? nowFn() : Math.floor(Date.now() / 1e3);
2974
3242
  let ok = true;
2975
- for (const [name, mandate] of [
2976
- ["intent", triple.intent],
2977
- ["cart", triple.cart]
2978
- ]) {
2979
- if (!mandate?.expires) continue;
2980
- const parsed = parseExpiry(mandate.expires);
3243
+ const layers = [
3244
+ ["intent", triple.intent?.expires],
3245
+ ["cart", triple.cart?.expires],
3246
+ [
3247
+ "payment",
3248
+ typeof triple.payment?.raw?.expires === "string" ? triple.payment.raw.expires : typeof triple.payment?.raw?.exp === "string" ? triple.payment.raw.exp : void 0
3249
+ ]
3250
+ ];
3251
+ for (const [name, expires] of layers) {
3252
+ if (!expires) continue;
3253
+ const parsed = parseExpiry(expires);
2981
3254
  if (parsed === null) {
2982
3255
  errors.push(`${name}.expires unparseable`);
2983
3256
  ok = false;
2984
3257
  continue;
2985
3258
  }
2986
3259
  if (now > parsed + toleranceSec) {
2987
- errors.push(`${name} mandate expired at ${mandate.expires}`);
3260
+ errors.push(`${name} mandate expired at ${expires}`);
2988
3261
  ok = false;
2989
3262
  }
2990
3263
  }
@@ -3011,10 +3284,21 @@ async function verifyACPSignature(input) {
3011
3284
  if (!input.signatureHeader) {
3012
3285
  return { ok: false, error: "missing Signature header" };
3013
3286
  }
3014
- const freshness = checkTimestamp(input.timestampHeader, input.clockSkewSec ?? 300, input.now);
3287
+ const tolerance = input.clockSkewSec ?? 60;
3288
+ const nonceStore = input.nonceStore ?? defaultNonceStore;
3289
+ const freshness = checkTimestamp(input.timestampHeader, tolerance, input.now);
3015
3290
  if (!freshness.ok) {
3016
3291
  return { ok: false, error: freshness.error, timestampStale: true };
3017
3292
  }
3293
+ const nowSec = input.now ? input.now() : Math.floor(Date.now() / 1e3);
3294
+ const expiresAtMs = (nowSec + tolerance) * 1e3;
3295
+ const replayKey = `acp:${input.signatureHeader}:${input.timestampHeader ?? ""}`;
3296
+ if (nonceStore.seen(replayKey, expiresAtMs)) {
3297
+ return {
3298
+ ok: false,
3299
+ error: "ACP signature replay \u2014 already seen within tolerance window"
3300
+ };
3301
+ }
3018
3302
  const signatureBytes = decodeBase64(input.signatureHeader);
3019
3303
  if (!signatureBytes) {
3020
3304
  return { ok: false, error: "signature header is not valid base64" };
@@ -3232,8 +3516,9 @@ function coerceString6(v) {
3232
3516
  var import_mppx2 = require("mppx");
3233
3517
  function verifyMPP(input) {
3234
3518
  const { context } = input;
3235
- const tolerance = input.clockSkewSec ?? 300;
3519
+ const tolerance = input.clockSkewSec ?? 60;
3236
3520
  const nowSec = input.now ? input.now() : Math.floor(Date.now() / 1e3);
3521
+ const nonceStore = input.nonceStore ?? defaultNonceStore;
3237
3522
  const challenge = context.credential?.challenge ?? (context.challenges && context.challenges[0]);
3238
3523
  const source = context.credential?.source;
3239
3524
  const method = challenge?.method;
@@ -3256,21 +3541,38 @@ function verifyMPP(input) {
3256
3541
  }
3257
3542
  }
3258
3543
  let bodyDigestOk = null;
3259
- if (challenge?.digest && input.rawBody !== void 0) {
3260
- try {
3261
- if (!/^sha-256=/.test(challenge.digest)) {
3544
+ if (input.rawBody !== void 0) {
3545
+ if (!challenge?.digest) {
3546
+ bodyDigestOk = false;
3547
+ } else {
3548
+ try {
3549
+ if (!/^sha-256=/.test(challenge.digest)) {
3550
+ bodyDigestOk = false;
3551
+ } else {
3552
+ bodyDigestOk = import_mppx2.BodyDigest.verify(challenge.digest, input.rawBody);
3553
+ }
3554
+ } catch {
3262
3555
  bodyDigestOk = false;
3263
- } else {
3264
- bodyDigestOk = import_mppx2.BodyDigest.verify(challenge.digest, input.rawBody);
3265
3556
  }
3266
- } catch {
3267
- bodyDigestOk = false;
3268
3557
  }
3269
3558
  }
3270
- const ok = expiryOk && (bodyDigestOk === null || bodyDigestOk === true);
3559
+ let replayOk = true;
3560
+ if (challenge?.digest && expiryOk) {
3561
+ const replayKey = `mpp:${challenge.digest}:${challenge.nonce ?? ""}`;
3562
+ const expiresAt = (nowSec + tolerance) * 1e3;
3563
+ if (nonceStore.seen(replayKey, expiresAt)) {
3564
+ replayOk = false;
3565
+ }
3566
+ }
3567
+ const ok = expiryOk && (bodyDigestOk === null || bodyDigestOk === true) && replayOk;
3271
3568
  const errors = [];
3272
3569
  if (!expiryOk) errors.push("challenge expired");
3273
- if (bodyDigestOk === false) errors.push("body digest mismatch");
3570
+ if (bodyDigestOk === false) {
3571
+ errors.push(
3572
+ input.rawBody !== void 0 && !challenge?.digest ? "body digest required when rawBody present" : "body digest mismatch"
3573
+ );
3574
+ }
3575
+ if (!replayOk) errors.push("MPP challenge replay \u2014 already seen within tolerance window");
3274
3576
  return {
3275
3577
  ok,
3276
3578
  expiryOk,
@@ -3431,14 +3733,32 @@ function readHeader4(headers, name) {
3431
3733
  var import_node_crypto4 = require("crypto");
3432
3734
  async function verifyVIChain(input) {
3433
3735
  const errors = [];
3434
- const tolerance = input.clockSkewSec ?? 300;
3736
+ const tolerance = input.clockSkewSec ?? 60;
3435
3737
  const now = input.now ? input.now() : Math.floor(Date.now() / 1e3);
3436
3738
  const { l1, l2, l3a, l3b } = input.layers;
3739
+ const nonceStore = input.nonceStore ?? defaultNonceStore;
3740
+ if (!l1) {
3741
+ if (!input.allowUnboundChain) {
3742
+ errors.push(
3743
+ "L1 missing \u2014 chain root unbound (set allowUnboundChain + expectedL2Key to override)"
3744
+ );
3745
+ } else if (!input.expectedL2Key) {
3746
+ errors.push("allowUnboundChain set but expectedL2Key missing");
3747
+ }
3748
+ }
3437
3749
  const l1SigOk = l1 ? await input.verifySignature(l1, null) : null;
3438
3750
  if (l1 && !l1SigOk) errors.push("L1 signature invalid");
3439
3751
  const l1Cnf = extractCnfJwk(l1?.payload);
3440
- const l2SigOk = await input.verifySignature(l2, l1Cnf ?? null);
3752
+ const l2ExpectedKey = l1Cnf ?? input.expectedL2Key ?? null;
3753
+ const l2SigOk = await input.verifySignature(l2, l2ExpectedKey);
3441
3754
  if (!l2SigOk) errors.push("L2 signature invalid");
3755
+ if (l2SigOk) {
3756
+ const replayKey = `vi:l2:${l2.compact}`;
3757
+ const expiresAt = now * 1e3 + tolerance * 1e3;
3758
+ if (nonceStore.seen(replayKey, expiresAt)) {
3759
+ errors.push("L2 signature replay \u2014 already seen within tolerance window");
3760
+ }
3761
+ }
3442
3762
  const l2Cnf = extractCnfJwk(l2.payload);
3443
3763
  const l3aSigOk = l3a ? await input.verifySignature(l3a, l2Cnf ?? null) : null;
3444
3764
  if (l3a && !l3aSigOk) errors.push("L3a signature invalid");
@@ -3482,7 +3802,10 @@ async function verifyVIChain(input) {
3482
3802
  }
3483
3803
  }
3484
3804
  const expiryOk = checkExpiryAcross([l1, l2, l3a, l3b], tolerance, now, errors);
3485
- const ok = l1SigOk !== false && l2SigOk && l3aSigOk !== false && l3bSigOk !== false && l1BindsL2 && l2BindsL3 && l3aL3bTxnIdMatch !== false && checkoutHashOk !== false && expiryOk;
3805
+ const noUnboundChainOrReplayErrors = !errors.some(
3806
+ (e) => e.startsWith("L1 missing") || e.startsWith("allowUnboundChain set") || e.startsWith("L2 signature replay")
3807
+ );
3808
+ const ok = l1SigOk !== false && l2SigOk && l3aSigOk !== false && l3bSigOk !== false && l1BindsL2 && l2BindsL3 && l3aL3bTxnIdMatch !== false && checkoutHashOk !== false && expiryOk && noUnboundChainOrReplayErrors;
3486
3809
  return {
3487
3810
  ok,
3488
3811
  checks: {
@@ -3915,7 +4238,7 @@ async function exportJwkFromKeyLike(keyLike) {
3915
4238
 
3916
4239
  // src/transport/registry/mastercard.ts
3917
4240
  function createMastercardRegistry(options = {}) {
3918
- const cache = /* @__PURE__ */ new Map();
4241
+ const cache2 = /* @__PURE__ */ new Map();
3919
4242
  const ttlSec = options.cacheTtlSec ?? 3600;
3920
4243
  const fetchFn = options.fetch ?? globalThis.fetch;
3921
4244
  let warned = false;
@@ -3932,7 +4255,7 @@ function createMastercardRegistry(options = {}) {
3932
4255
  }
3933
4256
  return null;
3934
4257
  }
3935
- const cached = cache.get(kid);
4258
+ const cached = cache2.get(kid);
3936
4259
  if (cached && cached.expiresAt > Date.now()) return cached.jwk;
3937
4260
  try {
3938
4261
  const res = await fetchFn(options.registryUrl);
@@ -3941,7 +4264,7 @@ function createMastercardRegistry(options = {}) {
3941
4264
  const keys = body.keys ?? [];
3942
4265
  for (const k of keys) {
3943
4266
  if (k.kid === kid) {
3944
- cache.set(kid, { jwk: k, expiresAt: Date.now() + ttlSec * 1e3 });
4267
+ cache2.set(kid, { jwk: k, expiresAt: Date.now() + ttlSec * 1e3 });
3945
4268
  return k;
3946
4269
  }
3947
4270
  }
@@ -3956,7 +4279,7 @@ function createMastercardRegistry(options = {}) {
3956
4279
  // src/transport/registry/web-bot-auth.ts
3957
4280
  var DIRECTORY_PATH = "/.well-known/http-message-signatures-directory";
3958
4281
  function createWebBotAuthRegistry(options = {}) {
3959
- const cache = /* @__PURE__ */ new Map();
4282
+ const cache2 = /* @__PURE__ */ new Map();
3960
4283
  const ttlSec = options.cacheTtlSec ?? 3600;
3961
4284
  const fetchFn = options.fetch ?? globalThis.fetch;
3962
4285
  return {
@@ -3965,7 +4288,7 @@ function createWebBotAuthRegistry(options = {}) {
3965
4288
  if (!kid) return null;
3966
4289
  const directoryUrl = resolveDirectoryUrl(options.directoryUrl, context?.origin);
3967
4290
  if (!directoryUrl) return null;
3968
- const cached = cache.get(directoryUrl);
4291
+ const cached = cache2.get(directoryUrl);
3969
4292
  const now = Date.now();
3970
4293
  if (cached && cached.expiresAt > now) {
3971
4294
  return findKeyByKid(cached.keys, kid);
@@ -3975,7 +4298,7 @@ function createWebBotAuthRegistry(options = {}) {
3975
4298
  if (!res.ok) return null;
3976
4299
  const body = await res.json();
3977
4300
  const keys = body.keys ?? [];
3978
- cache.set(directoryUrl, { keys, expiresAt: now + ttlSec * 1e3 });
4301
+ cache2.set(directoryUrl, { keys, expiresAt: now + ttlSec * 1e3 });
3979
4302
  return findKeyByKid(keys, kid);
3980
4303
  } catch {
3981
4304
  return null;
@@ -4114,19 +4437,22 @@ function extractFromMcpBody(astrasyncMeta, args, key) {
4114
4437
  }
4115
4438
  return { value: void 0, source: void 0 };
4116
4439
  }
4117
- function mcpToPdlss(parsed, headerPurpose, headerAction) {
4118
- const resource = parsed.toolName ? `mcp:tool/${parsed.toolName}` : `mcp:method/${parsed.method}`;
4440
+ function mcpToPdlss(parsed, requestPath, headerPurpose, headerAction, toolGate) {
4441
+ const resource = toolGate?.resource ?? requestPath;
4119
4442
  let purpose;
4120
4443
  let purposeSource;
4121
- if (headerPurpose) {
4444
+ if (toolGate?.purpose !== void 0) {
4445
+ purpose = toolGate.purpose;
4446
+ purposeSource = "tool_gate";
4447
+ } else if (headerPurpose) {
4122
4448
  purpose = headerPurpose;
4123
4449
  purposeSource = "header";
4124
4450
  } else if (parsed.purposeFromBody && parsed.purposeSourceFromBody) {
4125
4451
  purpose = parsed.purposeFromBody;
4126
4452
  purposeSource = parsed.purposeSourceFromBody;
4127
4453
  } else {
4128
- purpose = "mcp_invoke";
4129
- purposeSource = "default_mcp_invoke";
4454
+ purpose = void 0;
4455
+ purposeSource = void 0;
4130
4456
  }
4131
4457
  let action;
4132
4458
  let actionSource;
@@ -4137,7 +4463,7 @@ function mcpToPdlss(parsed, headerPurpose, headerAction) {
4137
4463
  action = parsed.actionFromBody;
4138
4464
  actionSource = parsed.actionSourceFromBody;
4139
4465
  } else {
4140
- action = parsed.toolName ? `${parsed.method}:${parsed.toolName}` : parsed.method;
4466
+ action = parsed.toolName ? parsed.method === "tools/call" ? parsed.toolName : `${parsed.method}:${parsed.toolName}` : parsed.method;
4141
4467
  actionSource = "transport_layer";
4142
4468
  }
4143
4469
  return { purpose, action, resource, purposeSource, actionSource };
@@ -4151,11 +4477,28 @@ function mcpRiskTier(parsed) {
4151
4477
  }
4152
4478
 
4153
4479
  // src/adapters/mcp.ts
4480
+ function normalizeToolGate(gate) {
4481
+ return typeof gate === "string" ? { minAccessLevel: gate } : gate;
4482
+ }
4154
4483
  function readSingleHeader(value) {
4155
4484
  if (typeof value === "string") return value;
4156
4485
  if (Array.isArray(value)) return value[0];
4157
4486
  return void 0;
4158
4487
  }
4488
+ function dedupeFailures2(result) {
4489
+ if (result.failures && result.failures.length > 1) {
4490
+ const seen = /* @__PURE__ */ new Set();
4491
+ result.failures = result.failures.filter((f) => {
4492
+ const key = `${f.dimension}|${f.message}|${f.guidance ?? ""}`;
4493
+ if (seen.has(key)) return false;
4494
+ seen.add(key);
4495
+ return true;
4496
+ });
4497
+ }
4498
+ if (result.denialReasons && result.denialReasons.length > 1) {
4499
+ result.denialReasons = [...new Set(result.denialReasons)];
4500
+ }
4501
+ }
4159
4502
  function defaultMcpDenied(result, req, res) {
4160
4503
  const id = req.body?.id ?? null;
4161
4504
  const status = !result.identityVerified ? 401 : 403;
@@ -4180,11 +4523,17 @@ function defaultMcpDenied(result, req, res) {
4180
4523
  });
4181
4524
  }
4182
4525
  function resolveMinAccessLevel(parsed, opts) {
4183
- if (parsed.toolName && opts.toolGates && opts.toolGates[parsed.toolName] !== void 0) {
4184
- return { level: opts.toolGates[parsed.toolName], source: "toolGate" };
4526
+ if (!parsed.toolName) {
4527
+ if (opts.methodGates && opts.methodGates[parsed.method] !== void 0) {
4528
+ return { level: opts.methodGates[parsed.method], source: "methodGate" };
4529
+ }
4530
+ return { level: "none", source: "discovery_default" };
4185
4531
  }
4186
- if (opts.methodGates && opts.methodGates[parsed.method] !== void 0) {
4187
- return { level: opts.methodGates[parsed.method], source: "methodGate" };
4532
+ if (opts.toolGates && opts.toolGates[parsed.toolName] !== void 0) {
4533
+ return {
4534
+ level: normalizeToolGate(opts.toolGates[parsed.toolName]).minAccessLevel,
4535
+ source: "toolGate"
4536
+ };
4188
4537
  }
4189
4538
  return { level: mcpRiskTier(parsed), source: "tier" };
4190
4539
  }
@@ -4195,12 +4544,17 @@ function createMcpMiddleware(options) {
4195
4544
  onAgentIdMismatch = "reject",
4196
4545
  skip = false,
4197
4546
  onDenied = defaultMcpDenied,
4198
- trustVerifiedHop = true,
4547
+ trustVerifiedHop = false,
4199
4548
  verifiedHopMaxAgeMs,
4200
4549
  recordDecisions,
4201
4550
  enableRuntimeChallenge = true,
4551
+ failOnError = "open",
4202
4552
  ...config
4203
4553
  } = options;
4554
+ if (config.apiBaseUrl) {
4555
+ prefetchWellKnown(config.apiBaseUrl).catch(() => {
4556
+ });
4557
+ }
4204
4558
  return async (req, res, next) => {
4205
4559
  try {
4206
4560
  if (skip) return next();
@@ -4213,6 +4567,7 @@ function createMcpMiddleware(options) {
4213
4567
  return next();
4214
4568
  }
4215
4569
  req.mcpRequest = parsed;
4570
+ const wellKnownUrls = config.apiBaseUrl ? await getWellKnownUrls(config.apiBaseUrl).catch(() => void 0) : void 0;
4216
4571
  const headerRaw = req.headers["x-astra-id"] ?? req.headers["x-astra-agentid"];
4217
4572
  const headerAstraId = typeof headerRaw === "string" ? headerRaw : Array.isArray(headerRaw) ? headerRaw[0] : void 0;
4218
4573
  const bodyAstraId = parsed.agentIdFromBody;
@@ -4266,9 +4621,17 @@ function createMcpMiddleware(options) {
4266
4621
  }
4267
4622
  return next();
4268
4623
  }
4624
+ const rawGate = parsed.toolName && toolGates?.[parsed.toolName];
4625
+ const gate = rawGate ? normalizeToolGate(rawGate) : void 0;
4269
4626
  const headerPurpose = readSingleHeader(req.headers["x-astra-purpose"]);
4270
4627
  const headerAction = readSingleHeader(req.headers["x-astra-action"]);
4271
- const pdlss = mcpToPdlss(parsed, headerPurpose, headerAction);
4628
+ const pdlss = mcpToPdlss(
4629
+ parsed,
4630
+ req.path,
4631
+ headerPurpose,
4632
+ headerAction,
4633
+ gate ? { purpose: gate.purpose, resource: gate.resource } : void 0
4634
+ );
4272
4635
  if (config.debug) {
4273
4636
  console.debug("[mcp-middleware] pdlss resolved", {
4274
4637
  purpose_source: pdlss.purposeSource,
@@ -4306,6 +4669,7 @@ function createMcpMiddleware(options) {
4306
4669
  recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
4307
4670
  });
4308
4671
  }
4672
+ dedupeFailures2(result);
4309
4673
  onDenied(result, req, res);
4310
4674
  return;
4311
4675
  }
@@ -4328,6 +4692,13 @@ function createMcpMiddleware(options) {
4328
4692
  };
4329
4693
  result.failures = [...result.failures ?? [], insufficientFailure];
4330
4694
  result.denialReasons = [...result.denialReasons ?? [], insufficientFailure.message];
4695
+ if (!result.guidance && wellKnownUrls) {
4696
+ result.guidance = {
4697
+ message: insufficientFailure.message,
4698
+ registrationUrl: wellKnownUrls.registrationUrl,
4699
+ documentationUrl: wellKnownUrls.documentationUrl
4700
+ };
4701
+ }
4331
4702
  if (shouldRecordDecisions) {
4332
4703
  const overrideKind = gateSource === "toolGate" ? "toolGate" : gateSource === "methodGate" ? "methodGate" : "other";
4333
4704
  const override = {
@@ -4351,6 +4722,7 @@ function createMcpMiddleware(options) {
4351
4722
  });
4352
4723
  }
4353
4724
  }
4725
+ dedupeFailures2(result);
4354
4726
  onDenied(result, req, res);
4355
4727
  return;
4356
4728
  }
@@ -4374,7 +4746,38 @@ function createMcpMiddleware(options) {
4374
4746
  }
4375
4747
  next();
4376
4748
  } catch (error) {
4749
+ const errorClass = error instanceof Error ? error.constructor.name : typeof error;
4750
+ const correlationId = req.headers["x-request-id"] || req.headers["x-correlation-id"] || `gen-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
4377
4751
  console.error("[VerificationGateway/MCP] Middleware error:", error);
4752
+ console.warn(
4753
+ `[SHADOW] would-have-denied: errorClass=${errorClass} route=${req.method}:${req.path} merchantId=${config.counterpartyId ?? "unknown"} correlationId=${correlationId}`
4754
+ );
4755
+ if (failOnError === "closed") {
4756
+ const result = {
4757
+ identityVerified: false,
4758
+ policyAllowed: false,
4759
+ accessLevel: "none",
4760
+ denialReasons: [`MCP middleware internal error: ${errorClass}`],
4761
+ failures: [
4762
+ {
4763
+ dimension: "middleware.internal_error",
4764
+ message: `Middleware threw ${errorClass} \u2014 failing closed`
4765
+ }
4766
+ ],
4767
+ verifiedAt: /* @__PURE__ */ new Date(),
4768
+ correlationId
4769
+ };
4770
+ const catchUrls = config.apiBaseUrl ? await getWellKnownUrls(config.apiBaseUrl).catch(() => void 0) : void 0;
4771
+ if (catchUrls) {
4772
+ result.guidance = {
4773
+ message: `Middleware threw ${errorClass} \u2014 failing closed`,
4774
+ registrationUrl: catchUrls.registrationUrl,
4775
+ documentationUrl: catchUrls.documentationUrl
4776
+ };
4777
+ }
4778
+ dedupeFailures2(result);
4779
+ return onDenied(result, req, res);
4780
+ }
4378
4781
  next();
4379
4782
  }
4380
4783
  };
@@ -5082,11 +5485,14 @@ var VERSION = "2.0.0";
5082
5485
  extractCredentials,
5083
5486
  extractMcpCredentials,
5084
5487
  getAccessLevelForScore,
5488
+ getCachedWellKnownUrls,
5085
5489
  getCapabilities,
5086
5490
  getTrustLevel,
5491
+ getWellKnownUrls,
5087
5492
  hasCredentials,
5088
5493
  hasMinimumAccess,
5089
5494
  nextjs,
5495
+ prefetchWellKnown,
5090
5496
  quickVerify,
5091
5497
  recordDecision,
5092
5498
  sdk,