@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
@@ -18,7 +18,66 @@ function hasMinimumAccess(actual, required) {
18
18
  }
19
19
 
20
20
  // src/version.ts
21
- var SDK_VERSION = "2.4.12";
21
+ var SDK_VERSION = "2.4.13";
22
+
23
+ // src/well-known.ts
24
+ var CACHE_TTL_MS = 60 * 60 * 1e3;
25
+ var cache = /* @__PURE__ */ new Map();
26
+ var inflight = /* @__PURE__ */ new Map();
27
+ function wellKnownUrl(apiBaseUrl) {
28
+ const base = apiBaseUrl.replace(/\/api\/?$/, "");
29
+ return `${base}/.well-known/agentic-commerce`;
30
+ }
31
+ async function fetchWellKnown(apiBaseUrl) {
32
+ const url = wellKnownUrl(apiBaseUrl);
33
+ const response = await fetch(url, {
34
+ method: "GET",
35
+ headers: { Accept: "application/json" },
36
+ signal: AbortSignal.timeout(5e3)
37
+ });
38
+ if (!response.ok) {
39
+ throw new Error(
40
+ `AstraSync platform must expose /.well-known/agentic-commerce; got ${response.status} from ${url}. SDK cannot initialise without it.`
41
+ );
42
+ }
43
+ const data = await response.json();
44
+ if (!data.registrationUrl || !data.documentationUrl || !data.verifyAccessUrl) {
45
+ throw new Error(
46
+ `/.well-known/agentic-commerce response missing required fields (registrationUrl, documentationUrl, verifyAccessUrl).`
47
+ );
48
+ }
49
+ return data;
50
+ }
51
+ function prefetchWellKnown(apiBaseUrl) {
52
+ const existing = inflight.get(apiBaseUrl);
53
+ if (existing) return existing;
54
+ const promise = fetchWellKnown(apiBaseUrl).then((data) => {
55
+ cache.set(apiBaseUrl, { data, fetchedAt: Date.now() });
56
+ inflight.delete(apiBaseUrl);
57
+ return data;
58
+ }).catch((err) => {
59
+ inflight.delete(apiBaseUrl);
60
+ throw err;
61
+ });
62
+ inflight.set(apiBaseUrl, promise);
63
+ return promise;
64
+ }
65
+ async function getWellKnownUrls(apiBaseUrl) {
66
+ const entry = cache.get(apiBaseUrl);
67
+ if (entry) {
68
+ if (Date.now() - entry.fetchedAt > CACHE_TTL_MS) {
69
+ prefetchWellKnown(apiBaseUrl).catch(() => {
70
+ });
71
+ }
72
+ return entry.data;
73
+ }
74
+ const pending = inflight.get(apiBaseUrl);
75
+ if (pending) return pending;
76
+ return prefetchWellKnown(apiBaseUrl);
77
+ }
78
+ function getCachedWellKnownUrls(apiBaseUrl) {
79
+ return cache.get(apiBaseUrl)?.data;
80
+ }
22
81
 
23
82
  // src/verify.ts
24
83
  var DEFAULT_CONFIG = {
@@ -37,22 +96,27 @@ var DEFAULT_CONFIG = {
37
96
  };
38
97
  var initCheckPerformed = false;
39
98
  var deprecationWarningShown = false;
40
- async function performInitCheck(apiBaseUrl, debug) {
99
+ async function performInitCheck(apiBaseUrl, debug, strictInit) {
41
100
  initCheckPerformed = true;
42
101
  try {
43
102
  const probeUrl = `${apiBaseUrl}/agents/verify-access`;
44
103
  const response = await fetch(probeUrl, { method: "HEAD" });
45
104
  const contentType = response.headers.get("content-type") ?? "";
46
105
  if (contentType.startsWith("text/html")) {
47
- console.warn(
48
- `[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.`
49
- );
106
+ 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).`;
107
+ if (strictInit) {
108
+ throw new Error(`${message} (strictInit=true)`);
109
+ }
110
+ console.warn(`${message} Set disableInitChecks: true on GatewayConfig to silence.`);
50
111
  } else if (debug) {
51
112
  console.log(
52
113
  `[VerificationGateway] init check passed for ${apiBaseUrl} (content-type: ${contentType})`
53
114
  );
54
115
  }
55
116
  } catch (err) {
117
+ if (strictInit) {
118
+ throw err;
119
+ }
56
120
  if (debug) {
57
121
  console.log(`[VerificationGateway] init check failed (non-blocking): ${String(err)}`);
58
122
  }
@@ -76,7 +140,23 @@ function getCacheKey(request) {
76
140
  request.counterpartyType || "",
77
141
  request.isSubAgentRequest ? "1" : "0",
78
142
  request.parentAgentId || "",
79
- request.subAgentDepth ?? ""
143
+ request.subAgentDepth ?? "",
144
+ // Audit F-A1-07: previously-missing dimensions that DO affect the
145
+ // backend verdict. Without these, two requests with different
146
+ // durations (e.g. 60s vs 86400s) collided on the same cache key and
147
+ // the shorter-duration allow served the longer-duration request.
148
+ request.durationRequired ?? "",
149
+ request.invocationProtocol || "",
150
+ request.enableRuntimeChallenge ? "1" : "0",
151
+ // callerMetadata fields contribute to risk model; include the ones
152
+ // backend reads. sourceIp/userAgent/forwardedFor change per-request
153
+ // so their inclusion effectively forces a re-check for any varying
154
+ // client (the right behavior — IP-driven anomaly scoring shouldn't
155
+ // be cached across IPs).
156
+ request.callerMetadata?.sourceIp || "",
157
+ request.callerMetadata?.userAgent || "",
158
+ request.callerMetadata?.forwardedFor || "",
159
+ request.callerMetadata?.agentCardUrl || ""
80
160
  ].join("|");
81
161
  }
82
162
  function getCachedResult(request) {
@@ -102,9 +182,13 @@ function cacheResult(request, result, configuredTtl) {
102
182
  }
103
183
  function extractCredentials(headers, query) {
104
184
  const credentials = {};
185
+ const ASTRA_ID_PATTERN = /^ASTRAE?-[A-Za-z0-9_-]{1,64}$/;
105
186
  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"];
106
187
  if (astraIdHeader) {
107
- credentials.astraId = Array.isArray(astraIdHeader) ? astraIdHeader[0] : astraIdHeader;
188
+ const raw = Array.isArray(astraIdHeader) ? astraIdHeader[0] : typeof astraIdHeader === "string" ? astraIdHeader : void 0;
189
+ if (typeof raw === "string" && ASTRA_ID_PATTERN.test(raw)) {
190
+ credentials.astraId = raw;
191
+ }
108
192
  }
109
193
  const apiKeyHeader = headers["x-api-key"] || headers["X-Api-Key"] || headers["X-API-KEY"];
110
194
  if (apiKeyHeader) {
@@ -113,9 +197,11 @@ function extractCredentials(headers, query) {
113
197
  const authHeader = headers["authorization"] || headers["Authorization"];
114
198
  if (authHeader) {
115
199
  const authValue = Array.isArray(authHeader) ? authHeader[0] : authHeader;
116
- credentials.authorizationHeader = authValue;
117
- if (authValue.startsWith("Bearer ")) {
118
- credentials.jwt = authValue.slice(7);
200
+ if (typeof authValue === "string") {
201
+ credentials.authorizationHeader = authValue;
202
+ if (authValue.startsWith("Bearer ")) {
203
+ credentials.jwt = authValue.slice(7);
204
+ }
119
205
  }
120
206
  }
121
207
  if (query) {
@@ -128,21 +214,22 @@ function extractCredentials(headers, query) {
128
214
  }
129
215
  return credentials;
130
216
  }
131
- function createGuidanceResponse(config, reason, options = {}) {
217
+ function createGuidanceResponse(_config, reason, options = {}) {
132
218
  const source = options.source ?? "no_credentials";
133
219
  const isApiError = source === "api_error";
220
+ const urls = options.urls;
134
221
  const guidance = isApiError ? {
135
222
  message: "Verification is temporarily unavailable. Retry with exponential backoff; if the issue persists, contact support with the correlationId.",
136
- registrationUrl: `${config.apiBaseUrl.replace("/api", "")}/register`,
137
- documentationUrl: `${config.apiBaseUrl.replace("/api", "")}/docs/agent-access`,
223
+ registrationUrl: urls?.registrationUrl ?? "",
224
+ documentationUrl: urls?.documentationUrl ?? "",
138
225
  steps: [
139
226
  "Retry the request with exponential backoff",
140
227
  "If failures persist, share the correlationId with support"
141
228
  ]
142
229
  } : {
143
230
  message: "This service verifies AI agents before granting access. Please register your agent with AstraSync.",
144
- registrationUrl: `${config.apiBaseUrl.replace("/api", "")}/register`,
145
- documentationUrl: `${config.apiBaseUrl.replace("/api", "")}/docs/agent-access`,
231
+ registrationUrl: urls?.registrationUrl ?? "",
232
+ documentationUrl: urls?.documentationUrl ?? "",
146
233
  steps: [
147
234
  "Register for an AstraSync account",
148
235
  "Create and register your agent",
@@ -184,7 +271,7 @@ async function callVerifyAccessAPI(config, request) {
184
271
  const { credentials, ...requestData } = request;
185
272
  const body = {
186
273
  ...credentials.astraId && { agentId: credentials.astraId },
187
- purpose: requestData.purpose || "general"
274
+ ...requestData.purpose && { purpose: requestData.purpose }
188
275
  };
189
276
  if (requestData.action) body.action = requestData.action;
190
277
  if (requestData.resourceType) body.resourceType = requestData.resourceType;
@@ -218,12 +305,8 @@ async function callVerifyAccessAPI(config, request) {
218
305
  "Content-Type": "application/json",
219
306
  ...config.customHeaders
220
307
  };
221
- if (credentials.authorizationHeader) {
222
- headers["Authorization"] = credentials.authorizationHeader;
223
- } else if (config.apiKey) {
224
- headers["Authorization"] = `Bearer ${config.apiKey}`;
225
- }
226
308
  if (config.apiKey) {
309
+ headers["Authorization"] = `Bearer ${config.apiKey}`;
227
310
  headers["X-API-Key"] = config.apiKey;
228
311
  }
229
312
  try {
@@ -268,8 +351,13 @@ async function callVerifyAccessAPI(config, request) {
268
351
  }
269
352
  async function verify(config, request) {
270
353
  const mergedConfig = { ...DEFAULT_CONFIG, ...config };
354
+ const urls = mergedConfig.apiBaseUrl ? getCachedWellKnownUrls(mergedConfig.apiBaseUrl) : void 0;
271
355
  if (!initCheckPerformed && !mergedConfig.disableInitChecks && mergedConfig.apiBaseUrl) {
272
- void performInitCheck(mergedConfig.apiBaseUrl, mergedConfig.debug);
356
+ if (mergedConfig.strictInit) {
357
+ await performInitCheck(mergedConfig.apiBaseUrl, mergedConfig.debug, true);
358
+ } else {
359
+ void performInitCheck(mergedConfig.apiBaseUrl, mergedConfig.debug, false);
360
+ }
273
361
  }
274
362
  if (!deprecationWarningShown && (config.minTrustScore !== void 0 || config.minTrustScoreForFull !== void 0)) {
275
363
  deprecationWarningShown = true;
@@ -300,7 +388,8 @@ async function verify(config, request) {
300
388
  if (!apiResponse.success) {
301
389
  return createGuidanceResponse(mergedConfig, apiResponse.error, {
302
390
  source: "api_error",
303
- correlationId: apiResponse.correlationId
391
+ correlationId: apiResponse.correlationId,
392
+ urls
304
393
  });
305
394
  }
306
395
  if (!apiResponse.access?.allowed) {
@@ -323,8 +412,8 @@ async function verify(config, request) {
323
412
  requiresApproval: apiResponse.access?.requiresApproval,
324
413
  guidance: {
325
414
  message: apiResponse.access?.reason || "Access denied by PDLSS policy",
326
- registrationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/register`,
327
- documentationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/docs/pdlss`
415
+ registrationUrl: urls?.registrationUrl ?? "",
416
+ documentationUrl: urls?.documentationUrl ?? ""
328
417
  },
329
418
  verifiedAt: /* @__PURE__ */ new Date(),
330
419
  // Extract sessionId so decisions can be recorded for denials too
@@ -393,13 +482,15 @@ async function verify(config, request) {
393
482
  result.denialReasons = result.recommendationReasons || [
394
483
  "Access denied by AstraSync recommendation"
395
484
  ];
396
- if (result.runtimeChallenge) {
397
- result.guidance = {
398
- message: `Verification failed: ${result.runtimeChallenge.reason || "runtime challenge failed"}`,
399
- registrationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/register`,
400
- documentationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/docs/runtime-challenge`
401
- };
402
- }
485
+ result.guidance = result.runtimeChallenge ? {
486
+ message: `Verification failed: ${result.runtimeChallenge.reason || "runtime challenge failed"}`,
487
+ registrationUrl: urls?.registrationUrl ?? "",
488
+ documentationUrl: urls?.documentationUrl ?? ""
489
+ } : {
490
+ message: result.recommendationReasons?.[0] || "Access denied by AstraSync recommendation",
491
+ registrationUrl: urls?.registrationUrl ?? "",
492
+ documentationUrl: urls?.documentationUrl ?? ""
493
+ };
403
494
  } else if (result.recommendation === "step_up_required") {
404
495
  result.requiresStepUp = true;
405
496
  if (ACCESS_LEVEL_HIERARCHY[result.accessLevel] > ACCESS_LEVEL_HIERARCHY["read-only"]) {
@@ -588,18 +679,43 @@ function defaultExtractPurpose(req) {
588
679
  return "general";
589
680
  }
590
681
  }
591
- function matchRoute(pattern, path) {
682
+ function matchRoute(pattern, path, opts) {
592
683
  const regexPattern = pattern.replace(/\*/g, ".*").replace(/\//g, "\\/");
593
- const regex = new RegExp(`^${regexPattern}$`);
594
- return regex.test(path);
684
+ const caseSensitiveRegex = new RegExp(`^${regexPattern}$`);
685
+ const caseSensitiveResult = caseSensitiveRegex.test(path);
686
+ if (!opts?.caseInsensitive && !opts?.logShadowDivergence) {
687
+ return caseSensitiveResult;
688
+ }
689
+ const caseInsensitiveRegex = new RegExp(`^${regexPattern}$`, "i");
690
+ const caseInsensitiveResult = caseInsensitiveRegex.test(path);
691
+ if (opts?.logShadowDivergence && caseSensitiveResult !== caseInsensitiveResult) {
692
+ console.warn(
693
+ `[SHADOW] matchRoute case-insensitive would change result: pattern=${pattern} path=${path} caseSensitive=${caseSensitiveResult} caseInsensitive=${caseInsensitiveResult} correlationId=${opts.correlationId ?? "unknown"}`
694
+ );
695
+ }
696
+ return opts?.caseInsensitive ? caseInsensitiveResult : caseSensitiveResult;
595
697
  }
596
- function findRouteConfig(routes, path, method) {
698
+ function findRouteConfig(routes, path, method, opts) {
597
699
  return routes.find((route) => {
598
700
  const methodMatches = route.method === "*" || route.method.toUpperCase() === method.toUpperCase();
599
- const pathMatches = matchRoute(route.pattern, path);
701
+ const pathMatches = matchRoute(route.pattern, path, opts);
600
702
  return methodMatches && pathMatches;
601
703
  });
602
704
  }
705
+ function dedupeFailures(result) {
706
+ if (result.failures && result.failures.length > 1) {
707
+ const seen = /* @__PURE__ */ new Set();
708
+ result.failures = result.failures.filter((f) => {
709
+ const key = `${f.dimension}|${f.message}|${f.guidance ?? ""}`;
710
+ if (seen.has(key)) return false;
711
+ seen.add(key);
712
+ return true;
713
+ });
714
+ }
715
+ if (result.denialReasons && result.denialReasons.length > 1) {
716
+ result.denialReasons = [...new Set(result.denialReasons)];
717
+ }
718
+ }
603
719
  function defaultOnDenied(result, _req, res) {
604
720
  const statusCode = !result.identityVerified ? 401 : 403;
605
721
  res.setHeader("X-Astra-Gateway-Mode", "enforced");
@@ -626,8 +742,14 @@ function createMiddleware(options) {
626
742
  recordDecisions,
627
743
  enableRuntimeChallenge = true,
628
744
  routesRefreshMs = DEFAULT_ROUTES_REFRESH_MS,
745
+ failOnError = "open",
746
+ caseInsensitiveRouteMatch = false,
629
747
  ...config
630
748
  } = options;
749
+ if (config.apiBaseUrl) {
750
+ prefetchWellKnown(config.apiBaseUrl).catch(() => {
751
+ });
752
+ }
631
753
  let cachedRoutes = [];
632
754
  let lastFetchAt = 0;
633
755
  let refreshing = null;
@@ -648,7 +770,7 @@ function createMiddleware(options) {
648
770
  cachedRoutes = fetched;
649
771
  lastFetchAt = Date.now();
650
772
  if (cachedRoutes.length === 0 && !warnedEmptyRoutes) {
651
- const dashboard = config.dashboardUrl ?? "https://app.astrasync.ai";
773
+ const dashboard = config.dashboardUrl ?? "https://astrasync.ai/dashboard";
652
774
  console.warn(
653
775
  `[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`
654
776
  );
@@ -674,7 +796,12 @@ function createMiddleware(options) {
674
796
  refreshing = null;
675
797
  });
676
798
  }
677
- const routeConfig = findRouteConfig(cachedRoutes, req.path, req.method);
799
+ const correlationId = req.headers["x-request-id"] || req.headers["x-correlation-id"];
800
+ const routeConfig = findRouteConfig(cachedRoutes, req.path, req.method, {
801
+ caseInsensitive: caseInsensitiveRouteMatch,
802
+ logShadowDivergence: true,
803
+ correlationId
804
+ });
678
805
  if (!routeConfig) {
679
806
  if (config.setPassThroughHeader) {
680
807
  res.setHeader("X-Astra-Gateway-Mode", "unenforced");
@@ -685,6 +812,7 @@ function createMiddleware(options) {
685
812
  }
686
813
  return next();
687
814
  }
815
+ const wellKnownUrls = config.apiBaseUrl ? await getWellKnownUrls(config.apiBaseUrl).catch(() => void 0) : void 0;
688
816
  const credentials = customExtractCredentials ? customExtractCredentials(req) : defaultExtractCredentials(req);
689
817
  const shouldEnforce = routeConfig.minAccessLevel !== "none";
690
818
  if (routeConfig.minAccessLevel === "none" && (!config.evaluateAlwaysIfCredentialed || !credentials.astraId)) {
@@ -706,8 +834,8 @@ function createMiddleware(options) {
706
834
  denialReasons: preCheckFailures.map((f) => f.message),
707
835
  guidance: {
708
836
  message: "Request exceeds counterparty-defined PDLSS limits.",
709
- registrationUrl: `${config.apiBaseUrl?.replace("/api", "")}/register`,
710
- documentationUrl: `${config.apiBaseUrl?.replace("/api", "")}/docs/pdlss`
837
+ registrationUrl: wellKnownUrls?.registrationUrl ?? "",
838
+ documentationUrl: wellKnownUrls?.documentationUrl ?? ""
711
839
  },
712
840
  verifiedAt: /* @__PURE__ */ new Date()
713
841
  };
@@ -721,13 +849,19 @@ function createMiddleware(options) {
721
849
  requestMethod: req.method
722
850
  }).catch(() => {
723
851
  });
852
+ dedupeFailures(result2);
724
853
  onDenied(result2, req, res);
725
854
  return;
726
855
  }
727
856
  const shouldRecordDecisions = recordDecisions !== false;
728
857
  const forwardedFor = req.headers["x-forwarded-for"];
729
858
  const forwardedForStr = Array.isArray(forwardedFor) ? forwardedFor.join(", ") : forwardedFor;
730
- const originalClientIp = forwardedForStr ? forwardedForStr.split(",")[0].trim() : req.ip;
859
+ const originalClientIp = req.ip ?? (forwardedForStr ? forwardedForStr.split(",")[0].trim() : void 0);
860
+ if (!req.ip && forwardedForStr) {
861
+ console.warn(
862
+ "[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."
863
+ );
864
+ }
731
865
  const agentCardUrl = typeof req.headers["x-astrasync-agent-card"] === "string" ? req.headers["x-astrasync-agent-card"] : void 0;
732
866
  const result = await verify(config, {
733
867
  credentials,
@@ -758,6 +892,7 @@ function createMiddleware(options) {
758
892
  recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
759
893
  });
760
894
  }
895
+ dedupeFailures(result);
761
896
  onDenied(result, req, res);
762
897
  return;
763
898
  }
@@ -780,10 +915,18 @@ function createMiddleware(options) {
780
915
  };
781
916
  result.failures = [...result.failures ?? [], insufficientFailure];
782
917
  result.denialReasons = [...result.denialReasons ?? [], insufficientFailure.message];
918
+ if (!result.guidance && wellKnownUrls) {
919
+ result.guidance = {
920
+ message: insufficientFailure.message,
921
+ registrationUrl: wellKnownUrls.registrationUrl,
922
+ documentationUrl: wellKnownUrls.documentationUrl
923
+ };
924
+ }
783
925
  if (shouldRecordDecisions && sessionId) {
784
926
  recordDecision(config, sessionId, "denied", insufficientFailure.message).catch(() => {
785
927
  });
786
928
  }
929
+ dedupeFailures(result);
787
930
  onDenied(result, req, res);
788
931
  return;
789
932
  }
@@ -796,10 +939,18 @@ function createMiddleware(options) {
796
939
  };
797
940
  result.failures = [...result.failures ?? [], trustFailure];
798
941
  result.denialReasons = [trustFailure.message];
942
+ if (!result.guidance && wellKnownUrls) {
943
+ result.guidance = {
944
+ message: trustFailure.message,
945
+ registrationUrl: wellKnownUrls.registrationUrl,
946
+ documentationUrl: wellKnownUrls.documentationUrl
947
+ };
948
+ }
799
949
  if (shouldRecordDecisions && sessionId) {
800
950
  recordDecision(config, sessionId, "denied", trustFailure.message).catch(() => {
801
951
  });
802
952
  }
953
+ dedupeFailures(result);
803
954
  onDenied(result, req, res);
804
955
  return;
805
956
  }
@@ -814,7 +965,38 @@ function createMiddleware(options) {
814
965
  }
815
966
  next();
816
967
  } catch (error) {
968
+ const errorClass = error instanceof Error ? error.constructor.name : typeof error;
969
+ const correlationId = req.headers["x-request-id"] || req.headers["x-correlation-id"] || `gen-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
817
970
  console.error("[VerificationGateway] Middleware error:", error);
971
+ console.warn(
972
+ `[SHADOW] would-have-denied: errorClass=${errorClass} route=${req.method}:${req.path} merchantId=${config.counterpartyId ?? "unknown"} correlationId=${correlationId}`
973
+ );
974
+ if (failOnError === "closed") {
975
+ const result = {
976
+ identityVerified: false,
977
+ policyAllowed: false,
978
+ accessLevel: "none",
979
+ denialReasons: [`Verification middleware internal error: ${errorClass}`],
980
+ failures: [
981
+ {
982
+ dimension: "middleware.internal_error",
983
+ message: `Middleware threw ${errorClass} \u2014 failing closed`
984
+ }
985
+ ],
986
+ verifiedAt: /* @__PURE__ */ new Date(),
987
+ correlationId
988
+ };
989
+ const catchUrls = config.apiBaseUrl ? await getWellKnownUrls(config.apiBaseUrl).catch(() => void 0) : void 0;
990
+ if (catchUrls) {
991
+ result.guidance = {
992
+ message: `Middleware threw ${errorClass} \u2014 failing closed`,
993
+ registrationUrl: catchUrls.registrationUrl,
994
+ documentationUrl: catchUrls.documentationUrl
995
+ };
996
+ }
997
+ dedupeFailures(result);
998
+ return onDenied(result, req, res);
999
+ }
818
1000
  next();
819
1001
  }
820
1002
  };