@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
@@ -1,6 +1,6 @@
1
1
  import { AstraSyncGateway } from '../gateway/gateway.mjs';
2
- import { A as AgentAction, I as InterceptResult, P as PDLSSContext, V as VerificationDecision } from '../types-DNK2BgIf.mjs';
3
- import '../types-L15pYd2c.mjs';
2
+ import { A as AgentAction, I as InterceptResult, P as PDLSSContext, V as VerificationDecision } from '../types-CgDCUfo8.mjs';
3
+ import '../types-B3USs-Kx.mjs';
4
4
 
5
5
  /**
6
6
  * PlatformAdapter Interface
@@ -1,6 +1,6 @@
1
1
  import { AstraSyncGateway } from '../gateway/gateway.js';
2
- import { A as AgentAction, I as InterceptResult, P as PDLSSContext, V as VerificationDecision } from '../types-DoWIuzfj.js';
3
- import '../types-L15pYd2c.js';
2
+ import { A as AgentAction, I as InterceptResult, P as PDLSSContext, V as VerificationDecision } from '../types-R5N4ET6x.js';
3
+ import '../types-B3USs-Kx.js';
4
4
 
5
5
  /**
6
6
  * PlatformAdapter Interface
@@ -1,3 +1,3 @@
1
1
  import 'express';
2
- import '../types-L15pYd2c.mjs';
3
- export { c as createMiddleware, a as extractAstraSyncCredentials } from '../express-4WStX3PV.mjs';
2
+ import '../types-B3USs-Kx.mjs';
3
+ export { c as createMiddleware, a as extractAstraSyncCredentials } from '../express-ienhAXps.mjs';
@@ -1,3 +1,3 @@
1
1
  import 'express';
2
- import '../types-L15pYd2c.js';
3
- export { c as createMiddleware, a as extractAstraSyncCredentials } from '../express-C1ePFB7n.js';
2
+ import '../types-B3USs-Kx.js';
3
+ export { c as createMiddleware, a as extractAstraSyncCredentials } from '../express-CrfwoNAR.js';
@@ -45,7 +45,66 @@ function hasMinimumAccess(actual, required) {
45
45
  }
46
46
 
47
47
  // src/version.ts
48
- var SDK_VERSION = "2.4.12";
48
+ var SDK_VERSION = "2.4.13";
49
+
50
+ // src/well-known.ts
51
+ var CACHE_TTL_MS = 60 * 60 * 1e3;
52
+ var cache = /* @__PURE__ */ new Map();
53
+ var inflight = /* @__PURE__ */ new Map();
54
+ function wellKnownUrl(apiBaseUrl) {
55
+ const base = apiBaseUrl.replace(/\/api\/?$/, "");
56
+ return `${base}/.well-known/agentic-commerce`;
57
+ }
58
+ async function fetchWellKnown(apiBaseUrl) {
59
+ const url = wellKnownUrl(apiBaseUrl);
60
+ const response = await fetch(url, {
61
+ method: "GET",
62
+ headers: { Accept: "application/json" },
63
+ signal: AbortSignal.timeout(5e3)
64
+ });
65
+ if (!response.ok) {
66
+ throw new Error(
67
+ `AstraSync platform must expose /.well-known/agentic-commerce; got ${response.status} from ${url}. SDK cannot initialise without it.`
68
+ );
69
+ }
70
+ const data = await response.json();
71
+ if (!data.registrationUrl || !data.documentationUrl || !data.verifyAccessUrl) {
72
+ throw new Error(
73
+ `/.well-known/agentic-commerce response missing required fields (registrationUrl, documentationUrl, verifyAccessUrl).`
74
+ );
75
+ }
76
+ return data;
77
+ }
78
+ function prefetchWellKnown(apiBaseUrl) {
79
+ const existing = inflight.get(apiBaseUrl);
80
+ if (existing) return existing;
81
+ const promise = fetchWellKnown(apiBaseUrl).then((data) => {
82
+ cache.set(apiBaseUrl, { data, fetchedAt: Date.now() });
83
+ inflight.delete(apiBaseUrl);
84
+ return data;
85
+ }).catch((err) => {
86
+ inflight.delete(apiBaseUrl);
87
+ throw err;
88
+ });
89
+ inflight.set(apiBaseUrl, promise);
90
+ return promise;
91
+ }
92
+ async function getWellKnownUrls(apiBaseUrl) {
93
+ const entry = cache.get(apiBaseUrl);
94
+ if (entry) {
95
+ if (Date.now() - entry.fetchedAt > CACHE_TTL_MS) {
96
+ prefetchWellKnown(apiBaseUrl).catch(() => {
97
+ });
98
+ }
99
+ return entry.data;
100
+ }
101
+ const pending = inflight.get(apiBaseUrl);
102
+ if (pending) return pending;
103
+ return prefetchWellKnown(apiBaseUrl);
104
+ }
105
+ function getCachedWellKnownUrls(apiBaseUrl) {
106
+ return cache.get(apiBaseUrl)?.data;
107
+ }
49
108
 
50
109
  // src/verify.ts
51
110
  var DEFAULT_CONFIG = {
@@ -64,22 +123,27 @@ var DEFAULT_CONFIG = {
64
123
  };
65
124
  var initCheckPerformed = false;
66
125
  var deprecationWarningShown = false;
67
- async function performInitCheck(apiBaseUrl, debug) {
126
+ async function performInitCheck(apiBaseUrl, debug, strictInit) {
68
127
  initCheckPerformed = true;
69
128
  try {
70
129
  const probeUrl = `${apiBaseUrl}/agents/verify-access`;
71
130
  const response = await fetch(probeUrl, { method: "HEAD" });
72
131
  const contentType = response.headers.get("content-type") ?? "";
73
132
  if (contentType.startsWith("text/html")) {
74
- console.warn(
75
- `[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.`
76
- );
133
+ 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).`;
134
+ if (strictInit) {
135
+ throw new Error(`${message} (strictInit=true)`);
136
+ }
137
+ console.warn(`${message} Set disableInitChecks: true on GatewayConfig to silence.`);
77
138
  } else if (debug) {
78
139
  console.log(
79
140
  `[VerificationGateway] init check passed for ${apiBaseUrl} (content-type: ${contentType})`
80
141
  );
81
142
  }
82
143
  } catch (err) {
144
+ if (strictInit) {
145
+ throw err;
146
+ }
83
147
  if (debug) {
84
148
  console.log(`[VerificationGateway] init check failed (non-blocking): ${String(err)}`);
85
149
  }
@@ -103,7 +167,23 @@ function getCacheKey(request) {
103
167
  request.counterpartyType || "",
104
168
  request.isSubAgentRequest ? "1" : "0",
105
169
  request.parentAgentId || "",
106
- request.subAgentDepth ?? ""
170
+ request.subAgentDepth ?? "",
171
+ // Audit F-A1-07: previously-missing dimensions that DO affect the
172
+ // backend verdict. Without these, two requests with different
173
+ // durations (e.g. 60s vs 86400s) collided on the same cache key and
174
+ // the shorter-duration allow served the longer-duration request.
175
+ request.durationRequired ?? "",
176
+ request.invocationProtocol || "",
177
+ request.enableRuntimeChallenge ? "1" : "0",
178
+ // callerMetadata fields contribute to risk model; include the ones
179
+ // backend reads. sourceIp/userAgent/forwardedFor change per-request
180
+ // so their inclusion effectively forces a re-check for any varying
181
+ // client (the right behavior — IP-driven anomaly scoring shouldn't
182
+ // be cached across IPs).
183
+ request.callerMetadata?.sourceIp || "",
184
+ request.callerMetadata?.userAgent || "",
185
+ request.callerMetadata?.forwardedFor || "",
186
+ request.callerMetadata?.agentCardUrl || ""
107
187
  ].join("|");
108
188
  }
109
189
  function getCachedResult(request) {
@@ -129,9 +209,13 @@ function cacheResult(request, result, configuredTtl) {
129
209
  }
130
210
  function extractCredentials(headers, query) {
131
211
  const credentials = {};
212
+ const ASTRA_ID_PATTERN = /^ASTRAE?-[A-Za-z0-9_-]{1,64}$/;
132
213
  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"];
133
214
  if (astraIdHeader) {
134
- credentials.astraId = Array.isArray(astraIdHeader) ? astraIdHeader[0] : astraIdHeader;
215
+ const raw = Array.isArray(astraIdHeader) ? astraIdHeader[0] : typeof astraIdHeader === "string" ? astraIdHeader : void 0;
216
+ if (typeof raw === "string" && ASTRA_ID_PATTERN.test(raw)) {
217
+ credentials.astraId = raw;
218
+ }
135
219
  }
136
220
  const apiKeyHeader = headers["x-api-key"] || headers["X-Api-Key"] || headers["X-API-KEY"];
137
221
  if (apiKeyHeader) {
@@ -140,9 +224,11 @@ function extractCredentials(headers, query) {
140
224
  const authHeader = headers["authorization"] || headers["Authorization"];
141
225
  if (authHeader) {
142
226
  const authValue = Array.isArray(authHeader) ? authHeader[0] : authHeader;
143
- credentials.authorizationHeader = authValue;
144
- if (authValue.startsWith("Bearer ")) {
145
- credentials.jwt = authValue.slice(7);
227
+ if (typeof authValue === "string") {
228
+ credentials.authorizationHeader = authValue;
229
+ if (authValue.startsWith("Bearer ")) {
230
+ credentials.jwt = authValue.slice(7);
231
+ }
146
232
  }
147
233
  }
148
234
  if (query) {
@@ -155,21 +241,22 @@ function extractCredentials(headers, query) {
155
241
  }
156
242
  return credentials;
157
243
  }
158
- function createGuidanceResponse(config, reason, options = {}) {
244
+ function createGuidanceResponse(_config, reason, options = {}) {
159
245
  const source = options.source ?? "no_credentials";
160
246
  const isApiError = source === "api_error";
247
+ const urls = options.urls;
161
248
  const guidance = isApiError ? {
162
249
  message: "Verification is temporarily unavailable. Retry with exponential backoff; if the issue persists, contact support with the correlationId.",
163
- registrationUrl: `${config.apiBaseUrl.replace("/api", "")}/register`,
164
- documentationUrl: `${config.apiBaseUrl.replace("/api", "")}/docs/agent-access`,
250
+ registrationUrl: urls?.registrationUrl ?? "",
251
+ documentationUrl: urls?.documentationUrl ?? "",
165
252
  steps: [
166
253
  "Retry the request with exponential backoff",
167
254
  "If failures persist, share the correlationId with support"
168
255
  ]
169
256
  } : {
170
257
  message: "This service verifies AI agents before granting access. Please register your agent with AstraSync.",
171
- registrationUrl: `${config.apiBaseUrl.replace("/api", "")}/register`,
172
- documentationUrl: `${config.apiBaseUrl.replace("/api", "")}/docs/agent-access`,
258
+ registrationUrl: urls?.registrationUrl ?? "",
259
+ documentationUrl: urls?.documentationUrl ?? "",
173
260
  steps: [
174
261
  "Register for an AstraSync account",
175
262
  "Create and register your agent",
@@ -211,7 +298,7 @@ async function callVerifyAccessAPI(config, request) {
211
298
  const { credentials, ...requestData } = request;
212
299
  const body = {
213
300
  ...credentials.astraId && { agentId: credentials.astraId },
214
- purpose: requestData.purpose || "general"
301
+ ...requestData.purpose && { purpose: requestData.purpose }
215
302
  };
216
303
  if (requestData.action) body.action = requestData.action;
217
304
  if (requestData.resourceType) body.resourceType = requestData.resourceType;
@@ -245,12 +332,8 @@ async function callVerifyAccessAPI(config, request) {
245
332
  "Content-Type": "application/json",
246
333
  ...config.customHeaders
247
334
  };
248
- if (credentials.authorizationHeader) {
249
- headers["Authorization"] = credentials.authorizationHeader;
250
- } else if (config.apiKey) {
251
- headers["Authorization"] = `Bearer ${config.apiKey}`;
252
- }
253
335
  if (config.apiKey) {
336
+ headers["Authorization"] = `Bearer ${config.apiKey}`;
254
337
  headers["X-API-Key"] = config.apiKey;
255
338
  }
256
339
  try {
@@ -295,8 +378,13 @@ async function callVerifyAccessAPI(config, request) {
295
378
  }
296
379
  async function verify(config, request) {
297
380
  const mergedConfig = { ...DEFAULT_CONFIG, ...config };
381
+ const urls = mergedConfig.apiBaseUrl ? getCachedWellKnownUrls(mergedConfig.apiBaseUrl) : void 0;
298
382
  if (!initCheckPerformed && !mergedConfig.disableInitChecks && mergedConfig.apiBaseUrl) {
299
- void performInitCheck(mergedConfig.apiBaseUrl, mergedConfig.debug);
383
+ if (mergedConfig.strictInit) {
384
+ await performInitCheck(mergedConfig.apiBaseUrl, mergedConfig.debug, true);
385
+ } else {
386
+ void performInitCheck(mergedConfig.apiBaseUrl, mergedConfig.debug, false);
387
+ }
300
388
  }
301
389
  if (!deprecationWarningShown && (config.minTrustScore !== void 0 || config.minTrustScoreForFull !== void 0)) {
302
390
  deprecationWarningShown = true;
@@ -327,7 +415,8 @@ async function verify(config, request) {
327
415
  if (!apiResponse.success) {
328
416
  return createGuidanceResponse(mergedConfig, apiResponse.error, {
329
417
  source: "api_error",
330
- correlationId: apiResponse.correlationId
418
+ correlationId: apiResponse.correlationId,
419
+ urls
331
420
  });
332
421
  }
333
422
  if (!apiResponse.access?.allowed) {
@@ -350,8 +439,8 @@ async function verify(config, request) {
350
439
  requiresApproval: apiResponse.access?.requiresApproval,
351
440
  guidance: {
352
441
  message: apiResponse.access?.reason || "Access denied by PDLSS policy",
353
- registrationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/register`,
354
- documentationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/docs/pdlss`
442
+ registrationUrl: urls?.registrationUrl ?? "",
443
+ documentationUrl: urls?.documentationUrl ?? ""
355
444
  },
356
445
  verifiedAt: /* @__PURE__ */ new Date(),
357
446
  // Extract sessionId so decisions can be recorded for denials too
@@ -420,13 +509,15 @@ async function verify(config, request) {
420
509
  result.denialReasons = result.recommendationReasons || [
421
510
  "Access denied by AstraSync recommendation"
422
511
  ];
423
- if (result.runtimeChallenge) {
424
- result.guidance = {
425
- message: `Verification failed: ${result.runtimeChallenge.reason || "runtime challenge failed"}`,
426
- registrationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/register`,
427
- documentationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/docs/runtime-challenge`
428
- };
429
- }
512
+ result.guidance = result.runtimeChallenge ? {
513
+ message: `Verification failed: ${result.runtimeChallenge.reason || "runtime challenge failed"}`,
514
+ registrationUrl: urls?.registrationUrl ?? "",
515
+ documentationUrl: urls?.documentationUrl ?? ""
516
+ } : {
517
+ message: result.recommendationReasons?.[0] || "Access denied by AstraSync recommendation",
518
+ registrationUrl: urls?.registrationUrl ?? "",
519
+ documentationUrl: urls?.documentationUrl ?? ""
520
+ };
430
521
  } else if (result.recommendation === "step_up_required") {
431
522
  result.requiresStepUp = true;
432
523
  if (ACCESS_LEVEL_HIERARCHY[result.accessLevel] > ACCESS_LEVEL_HIERARCHY["read-only"]) {
@@ -615,18 +706,43 @@ function defaultExtractPurpose(req) {
615
706
  return "general";
616
707
  }
617
708
  }
618
- function matchRoute(pattern, path) {
709
+ function matchRoute(pattern, path, opts) {
619
710
  const regexPattern = pattern.replace(/\*/g, ".*").replace(/\//g, "\\/");
620
- const regex = new RegExp(`^${regexPattern}$`);
621
- return regex.test(path);
711
+ const caseSensitiveRegex = new RegExp(`^${regexPattern}$`);
712
+ const caseSensitiveResult = caseSensitiveRegex.test(path);
713
+ if (!opts?.caseInsensitive && !opts?.logShadowDivergence) {
714
+ return caseSensitiveResult;
715
+ }
716
+ const caseInsensitiveRegex = new RegExp(`^${regexPattern}$`, "i");
717
+ const caseInsensitiveResult = caseInsensitiveRegex.test(path);
718
+ if (opts?.logShadowDivergence && caseSensitiveResult !== caseInsensitiveResult) {
719
+ console.warn(
720
+ `[SHADOW] matchRoute case-insensitive would change result: pattern=${pattern} path=${path} caseSensitive=${caseSensitiveResult} caseInsensitive=${caseInsensitiveResult} correlationId=${opts.correlationId ?? "unknown"}`
721
+ );
722
+ }
723
+ return opts?.caseInsensitive ? caseInsensitiveResult : caseSensitiveResult;
622
724
  }
623
- function findRouteConfig(routes, path, method) {
725
+ function findRouteConfig(routes, path, method, opts) {
624
726
  return routes.find((route) => {
625
727
  const methodMatches = route.method === "*" || route.method.toUpperCase() === method.toUpperCase();
626
- const pathMatches = matchRoute(route.pattern, path);
728
+ const pathMatches = matchRoute(route.pattern, path, opts);
627
729
  return methodMatches && pathMatches;
628
730
  });
629
731
  }
732
+ function dedupeFailures(result) {
733
+ if (result.failures && result.failures.length > 1) {
734
+ const seen = /* @__PURE__ */ new Set();
735
+ result.failures = result.failures.filter((f) => {
736
+ const key = `${f.dimension}|${f.message}|${f.guidance ?? ""}`;
737
+ if (seen.has(key)) return false;
738
+ seen.add(key);
739
+ return true;
740
+ });
741
+ }
742
+ if (result.denialReasons && result.denialReasons.length > 1) {
743
+ result.denialReasons = [...new Set(result.denialReasons)];
744
+ }
745
+ }
630
746
  function defaultOnDenied(result, _req, res) {
631
747
  const statusCode = !result.identityVerified ? 401 : 403;
632
748
  res.setHeader("X-Astra-Gateway-Mode", "enforced");
@@ -653,8 +769,14 @@ function createMiddleware(options) {
653
769
  recordDecisions,
654
770
  enableRuntimeChallenge = true,
655
771
  routesRefreshMs = DEFAULT_ROUTES_REFRESH_MS,
772
+ failOnError = "open",
773
+ caseInsensitiveRouteMatch = false,
656
774
  ...config
657
775
  } = options;
776
+ if (config.apiBaseUrl) {
777
+ prefetchWellKnown(config.apiBaseUrl).catch(() => {
778
+ });
779
+ }
658
780
  let cachedRoutes = [];
659
781
  let lastFetchAt = 0;
660
782
  let refreshing = null;
@@ -675,7 +797,7 @@ function createMiddleware(options) {
675
797
  cachedRoutes = fetched;
676
798
  lastFetchAt = Date.now();
677
799
  if (cachedRoutes.length === 0 && !warnedEmptyRoutes) {
678
- const dashboard = config.dashboardUrl ?? "https://app.astrasync.ai";
800
+ const dashboard = config.dashboardUrl ?? "https://astrasync.ai/dashboard";
679
801
  console.warn(
680
802
  `[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`
681
803
  );
@@ -701,7 +823,12 @@ function createMiddleware(options) {
701
823
  refreshing = null;
702
824
  });
703
825
  }
704
- const routeConfig = findRouteConfig(cachedRoutes, req.path, req.method);
826
+ const correlationId = req.headers["x-request-id"] || req.headers["x-correlation-id"];
827
+ const routeConfig = findRouteConfig(cachedRoutes, req.path, req.method, {
828
+ caseInsensitive: caseInsensitiveRouteMatch,
829
+ logShadowDivergence: true,
830
+ correlationId
831
+ });
705
832
  if (!routeConfig) {
706
833
  if (config.setPassThroughHeader) {
707
834
  res.setHeader("X-Astra-Gateway-Mode", "unenforced");
@@ -712,6 +839,7 @@ function createMiddleware(options) {
712
839
  }
713
840
  return next();
714
841
  }
842
+ const wellKnownUrls = config.apiBaseUrl ? await getWellKnownUrls(config.apiBaseUrl).catch(() => void 0) : void 0;
715
843
  const credentials = customExtractCredentials ? customExtractCredentials(req) : defaultExtractCredentials(req);
716
844
  const shouldEnforce = routeConfig.minAccessLevel !== "none";
717
845
  if (routeConfig.minAccessLevel === "none" && (!config.evaluateAlwaysIfCredentialed || !credentials.astraId)) {
@@ -733,8 +861,8 @@ function createMiddleware(options) {
733
861
  denialReasons: preCheckFailures.map((f) => f.message),
734
862
  guidance: {
735
863
  message: "Request exceeds counterparty-defined PDLSS limits.",
736
- registrationUrl: `${config.apiBaseUrl?.replace("/api", "")}/register`,
737
- documentationUrl: `${config.apiBaseUrl?.replace("/api", "")}/docs/pdlss`
864
+ registrationUrl: wellKnownUrls?.registrationUrl ?? "",
865
+ documentationUrl: wellKnownUrls?.documentationUrl ?? ""
738
866
  },
739
867
  verifiedAt: /* @__PURE__ */ new Date()
740
868
  };
@@ -748,13 +876,19 @@ function createMiddleware(options) {
748
876
  requestMethod: req.method
749
877
  }).catch(() => {
750
878
  });
879
+ dedupeFailures(result2);
751
880
  onDenied(result2, req, res);
752
881
  return;
753
882
  }
754
883
  const shouldRecordDecisions = recordDecisions !== false;
755
884
  const forwardedFor = req.headers["x-forwarded-for"];
756
885
  const forwardedForStr = Array.isArray(forwardedFor) ? forwardedFor.join(", ") : forwardedFor;
757
- const originalClientIp = forwardedForStr ? forwardedForStr.split(",")[0].trim() : req.ip;
886
+ const originalClientIp = req.ip ?? (forwardedForStr ? forwardedForStr.split(",")[0].trim() : void 0);
887
+ if (!req.ip && forwardedForStr) {
888
+ console.warn(
889
+ "[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."
890
+ );
891
+ }
758
892
  const agentCardUrl = typeof req.headers["x-astrasync-agent-card"] === "string" ? req.headers["x-astrasync-agent-card"] : void 0;
759
893
  const result = await verify(config, {
760
894
  credentials,
@@ -785,6 +919,7 @@ function createMiddleware(options) {
785
919
  recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
786
920
  });
787
921
  }
922
+ dedupeFailures(result);
788
923
  onDenied(result, req, res);
789
924
  return;
790
925
  }
@@ -807,10 +942,18 @@ function createMiddleware(options) {
807
942
  };
808
943
  result.failures = [...result.failures ?? [], insufficientFailure];
809
944
  result.denialReasons = [...result.denialReasons ?? [], insufficientFailure.message];
945
+ if (!result.guidance && wellKnownUrls) {
946
+ result.guidance = {
947
+ message: insufficientFailure.message,
948
+ registrationUrl: wellKnownUrls.registrationUrl,
949
+ documentationUrl: wellKnownUrls.documentationUrl
950
+ };
951
+ }
810
952
  if (shouldRecordDecisions && sessionId) {
811
953
  recordDecision(config, sessionId, "denied", insufficientFailure.message).catch(() => {
812
954
  });
813
955
  }
956
+ dedupeFailures(result);
814
957
  onDenied(result, req, res);
815
958
  return;
816
959
  }
@@ -823,10 +966,18 @@ function createMiddleware(options) {
823
966
  };
824
967
  result.failures = [...result.failures ?? [], trustFailure];
825
968
  result.denialReasons = [trustFailure.message];
969
+ if (!result.guidance && wellKnownUrls) {
970
+ result.guidance = {
971
+ message: trustFailure.message,
972
+ registrationUrl: wellKnownUrls.registrationUrl,
973
+ documentationUrl: wellKnownUrls.documentationUrl
974
+ };
975
+ }
826
976
  if (shouldRecordDecisions && sessionId) {
827
977
  recordDecision(config, sessionId, "denied", trustFailure.message).catch(() => {
828
978
  });
829
979
  }
980
+ dedupeFailures(result);
830
981
  onDenied(result, req, res);
831
982
  return;
832
983
  }
@@ -841,7 +992,38 @@ function createMiddleware(options) {
841
992
  }
842
993
  next();
843
994
  } catch (error) {
995
+ const errorClass = error instanceof Error ? error.constructor.name : typeof error;
996
+ const correlationId = req.headers["x-request-id"] || req.headers["x-correlation-id"] || `gen-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
844
997
  console.error("[VerificationGateway] Middleware error:", error);
998
+ console.warn(
999
+ `[SHADOW] would-have-denied: errorClass=${errorClass} route=${req.method}:${req.path} merchantId=${config.counterpartyId ?? "unknown"} correlationId=${correlationId}`
1000
+ );
1001
+ if (failOnError === "closed") {
1002
+ const result = {
1003
+ identityVerified: false,
1004
+ policyAllowed: false,
1005
+ accessLevel: "none",
1006
+ denialReasons: [`Verification middleware internal error: ${errorClass}`],
1007
+ failures: [
1008
+ {
1009
+ dimension: "middleware.internal_error",
1010
+ message: `Middleware threw ${errorClass} \u2014 failing closed`
1011
+ }
1012
+ ],
1013
+ verifiedAt: /* @__PURE__ */ new Date(),
1014
+ correlationId
1015
+ };
1016
+ const catchUrls = config.apiBaseUrl ? await getWellKnownUrls(config.apiBaseUrl).catch(() => void 0) : void 0;
1017
+ if (catchUrls) {
1018
+ result.guidance = {
1019
+ message: `Middleware threw ${errorClass} \u2014 failing closed`,
1020
+ registrationUrl: catchUrls.registrationUrl,
1021
+ documentationUrl: catchUrls.documentationUrl
1022
+ };
1023
+ }
1024
+ dedupeFailures(result);
1025
+ return onDenied(result, req, res);
1026
+ }
845
1027
  next();
846
1028
  }
847
1029
  };