@astrasyncai/verification-gateway 5.11.0 → 5.13.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 (98) hide show
  1. package/README.md +4 -0
  2. package/dist/adapters/express.d.mts +1 -1
  3. package/dist/adapters/express.d.ts +1 -1
  4. package/dist/adapters/express.js +394 -39
  5. package/dist/adapters/express.js.map +1 -1
  6. package/dist/adapters/express.mjs +394 -39
  7. package/dist/adapters/express.mjs.map +1 -1
  8. package/dist/adapters/mcp.d.mts +5 -1
  9. package/dist/adapters/mcp.d.ts +5 -1
  10. package/dist/adapters/mcp.js +193 -49
  11. package/dist/adapters/mcp.js.map +1 -1
  12. package/dist/adapters/mcp.mjs +193 -49
  13. package/dist/adapters/mcp.mjs.map +1 -1
  14. package/dist/adapters/nextjs.d.mts +1 -1
  15. package/dist/adapters/nextjs.d.ts +1 -1
  16. package/dist/adapters/nextjs.js +387 -22
  17. package/dist/adapters/nextjs.js.map +1 -1
  18. package/dist/adapters/nextjs.mjs +387 -22
  19. package/dist/adapters/nextjs.mjs.map +1 -1
  20. package/dist/adapters/sdk.d.mts +9 -1
  21. package/dist/adapters/sdk.d.ts +9 -1
  22. package/dist/adapters/sdk.js +125 -16
  23. package/dist/adapters/sdk.js.map +1 -1
  24. package/dist/adapters/sdk.mjs +125 -16
  25. package/dist/adapters/sdk.mjs.map +1 -1
  26. package/dist/agent/index.js +1 -1
  27. package/dist/agent/index.js.map +1 -1
  28. package/dist/agent/index.mjs +1 -1
  29. package/dist/agent/index.mjs.map +1 -1
  30. package/dist/bin/astrasync-claude-hook.js +309 -15
  31. package/dist/bin/astrasync-codex-hook.js +309 -15
  32. package/dist/bin/astrasync-guard.js +309 -15
  33. package/dist/bin/astrasync.js +322 -17
  34. package/dist/browser/background.js +309 -15
  35. package/dist/browser/background.js.map +1 -1
  36. package/dist/browser/background.mjs +309 -15
  37. package/dist/browser/background.mjs.map +1 -1
  38. package/dist/cli/index.js +1 -1
  39. package/dist/cli/index.js.map +1 -1
  40. package/dist/cli/index.mjs +1 -1
  41. package/dist/cli/index.mjs.map +1 -1
  42. package/dist/codex/index.js +309 -15
  43. package/dist/codex/index.js.map +1 -1
  44. package/dist/codex/index.mjs +309 -15
  45. package/dist/codex/index.mjs.map +1 -1
  46. package/dist/cursor/extension.js +309 -15
  47. package/dist/cursor/extension.js.map +1 -1
  48. package/dist/cursor/extension.mjs +309 -15
  49. package/dist/cursor/extension.mjs.map +1 -1
  50. package/dist/edge-config.d.mts +1 -1
  51. package/dist/edge-config.d.ts +1 -1
  52. package/dist/edge-config.js +1 -1
  53. package/dist/edge-config.js.map +1 -1
  54. package/dist/edge-config.mjs +1 -1
  55. package/dist/edge-config.mjs.map +1 -1
  56. package/dist/edge-core/index.d.mts +128 -13
  57. package/dist/edge-core/index.d.ts +128 -13
  58. package/dist/edge-core/index.js +362 -63
  59. package/dist/edge-core/index.js.map +1 -1
  60. package/dist/edge-core/index.mjs +359 -63
  61. package/dist/edge-core/index.mjs.map +1 -1
  62. package/dist/gateway/gateway.js +309 -15
  63. package/dist/gateway/gateway.js.map +1 -1
  64. package/dist/gateway/gateway.mjs +309 -15
  65. package/dist/gateway/gateway.mjs.map +1 -1
  66. package/dist/git-trigger/git-hooks.d.mts +1 -1
  67. package/dist/git-trigger/git-hooks.d.ts +1 -1
  68. package/dist/index.d.mts +118 -15
  69. package/dist/index.d.ts +118 -15
  70. package/dist/index.js +521 -72
  71. package/dist/index.js.map +1 -1
  72. package/dist/index.mjs +515 -72
  73. package/dist/index.mjs.map +1 -1
  74. package/dist/platform-signatures.js +4 -1
  75. package/dist/platform-signatures.js.map +1 -1
  76. package/dist/platform-signatures.mjs +4 -1
  77. package/dist/platform-signatures.mjs.map +1 -1
  78. package/dist/registration/index.js +1 -1
  79. package/dist/registration/index.js.map +1 -1
  80. package/dist/registration/index.mjs +1 -1
  81. package/dist/registration/index.mjs.map +1 -1
  82. package/dist/transport/index.js +1 -1
  83. package/dist/transport/index.js.map +1 -1
  84. package/dist/transport/index.mjs +1 -1
  85. package/dist/transport/index.mjs.map +1 -1
  86. package/dist/{types-CTmUpNpS.d.mts → types-BCkwBA84.d.mts} +12 -0
  87. package/dist/{types-CTmUpNpS.d.ts → types-BCkwBA84.d.ts} +12 -0
  88. package/dist/{types-CtFXmDzm.d.ts → types-C_b4QTyO.d.ts} +18 -0
  89. package/dist/{types-DQrn_kei.d.mts → types-DlshIIR-.d.mts} +18 -0
  90. package/dist/ui/index.d.mts +1 -1
  91. package/dist/ui/index.d.ts +1 -1
  92. package/dist/verify.d.mts +35 -14
  93. package/dist/verify.d.ts +35 -14
  94. package/dist/verify.js +165 -25
  95. package/dist/verify.js.map +1 -1
  96. package/dist/verify.mjs +164 -25
  97. package/dist/verify.mjs.map +1 -1
  98. package/package.json +3 -2
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var SDK_VERSION = "5.11.0";
2
+ var SDK_VERSION = "5.13.0";
3
3
 
4
4
  // src/http.ts
5
5
  var SDK_USER_AGENT = `astrasync-sdk/${SDK_VERSION}`;
@@ -69,23 +69,58 @@ function prefetchWellKnown(apiBaseUrl) {
69
69
  inflight.set(apiBaseUrl, promise);
70
70
  return promise;
71
71
  }
72
- async function getWellKnownUrls(apiBaseUrl) {
73
- const entry = cache.get(apiBaseUrl);
74
- if (entry) {
75
- if (Date.now() - entry.fetchedAt > CACHE_TTL_MS) {
76
- prefetchWellKnown(apiBaseUrl).catch(() => {
77
- });
78
- }
79
- return entry.data;
80
- }
81
- const pending = inflight.get(apiBaseUrl);
82
- if (pending) return pending;
83
- return prefetchWellKnown(apiBaseUrl);
84
- }
85
72
  function getCachedWellKnownUrls(apiBaseUrl) {
86
73
  return cache.get(apiBaseUrl)?.data;
87
74
  }
88
75
 
76
+ // src/failure-posture.ts
77
+ var VERIFICATION_STATUS_HEADER = "X-Astra-Verification";
78
+ var DEFAULT_UNAVAILABLE_RETRY_AFTER_S = 30;
79
+ var REJECTED_IDENTITY_DIMENSIONS = /* @__PURE__ */ new Set([
80
+ "identity.invalid_agent_id",
81
+ "identity.unknown_agent",
82
+ "agent.lookup"
83
+ ]);
84
+ var UNKNOWN_AGENT_GUIDANCE = "Register your agent with AstraSync (or, inside an MCP host, add the AstraSync MCP connector for keyless, human-approved registration), then present the canonical ASTRA-* id issued after owner approval.";
85
+ var UNAVAILABLE_DIMENSIONS = /* @__PURE__ */ new Set([
86
+ "verify_access.api_error",
87
+ "middleware.internal_error"
88
+ ]);
89
+ function isVerificationUnavailable(result) {
90
+ if (result.misconfigured) return false;
91
+ return (result.failures ?? []).some((f) => UNAVAILABLE_DIMENSIONS.has(f.dimension));
92
+ }
93
+ function unavailableRetryAfterSeconds(result) {
94
+ if (typeof result.retryAfterMs === "number" && Number.isFinite(result.retryAfterMs)) {
95
+ return Math.max(1, Math.ceil(result.retryAfterMs / 1e3));
96
+ }
97
+ return DEFAULT_UNAVAILABLE_RETRY_AFTER_S;
98
+ }
99
+ function buildVerificationUnavailableBody(result) {
100
+ return {
101
+ success: false,
102
+ error: {
103
+ code: "VERIFICATION_UNAVAILABLE",
104
+ message: "Agent verification is temporarily unavailable and this path enforces verification. Your identity was not evaluated and no denial was recorded \u2014 retry after the Retry-After interval.",
105
+ retryable: true,
106
+ retryAfterSeconds: unavailableRetryAfterSeconds(result),
107
+ ...result.failures && { failures: result.failures },
108
+ ...result.correlationId && { correlationId: result.correlationId },
109
+ guidance: {
110
+ message: "Retry the request after the Retry-After interval, with exponential backoff; the condition is transient."
111
+ }
112
+ }
113
+ };
114
+ }
115
+ function isRejectedIdentityClaim(result) {
116
+ return (result.failures ?? []).some((f) => REJECTED_IDENTITY_DIMENSIONS.has(f.dimension));
117
+ }
118
+ function denyStatusFor(result, policyDeniedCode) {
119
+ if (isRejectedIdentityClaim(result)) return { status: 403, code: "AGENT_IDENTITY_REJECTED" };
120
+ if (result.identityVerified) return { status: 403, code: policyDeniedCode };
121
+ return { status: 401, code: "UNAUTHORIZED" };
122
+ }
123
+
89
124
  // src/verify.ts
90
125
  var DEFAULT_CONFIG = {
91
126
  apiBaseUrl: "https://astrasync.ai/api",
@@ -98,7 +133,7 @@ var DEFAULT_CONFIG = {
98
133
  };
99
134
  var initCheckPerformed = false;
100
135
  var deprecationWarningShown = false;
101
- async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey) {
136
+ async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey, counterparty) {
102
137
  initCheckPerformed = true;
103
138
  try {
104
139
  const probeUrl = `${apiBaseUrl}/agents/verify-access`;
@@ -116,7 +151,8 @@ async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey) {
116
151
  );
117
152
  }
118
153
  if (apiKey) {
119
- const selfCheck = await sdkFetch(`${apiBaseUrl}/agents/verify-access/self-check`, {
154
+ const q = counterparty?.id ? `?counterpartyId=${encodeURIComponent(counterparty.id)}` : counterparty?.url ? `?counterpartyUrl=${encodeURIComponent(counterparty.url)}` : "";
155
+ const selfCheck = await sdkFetch(`${apiBaseUrl}/agents/verify-access/self-check${q}`, {
120
156
  headers: { Authorization: `Bearer ${apiKey}` }
121
157
  });
122
158
  if (selfCheck.status === 401) {
@@ -128,6 +164,13 @@ async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey) {
128
164
  console.error(message);
129
165
  } else if (selfCheck.ok) {
130
166
  const body = await selfCheck.json().catch(() => ({}));
167
+ if (body.counterparty && body.counterparty.canVerify === false) {
168
+ const message = `[VerificationGateway] the configured AstraSync API key cannot verify for this endpoint (${body.counterparty.message ?? "endpoint not found or owned by another account"}). Every inbound agent will be turned away until the key or counterparty is corrected.`;
169
+ if (strictInit) {
170
+ throw new Error(`${message} (strictInit=true)`);
171
+ }
172
+ console.error(message);
173
+ }
131
174
  if (typeof body.daysToExpiry === "number" && body.daysToExpiry <= 7) {
132
175
  console.warn(
133
176
  `[VerificationGateway] the configured AstraSync API key expires in ${body.daysToExpiry} day(s) \u2014 rotate it at Settings \u2192 Developer Tools before integrations start failing.`
@@ -266,11 +309,11 @@ function createGuidanceResponse(_config, reason, options = {}) {
266
309
  const isMisconfig = source === "merchant_misconfig";
267
310
  const urls = options.urls;
268
311
  const guidance = isMisconfig ? {
269
- message: "This site's AstraSync integration is misconfigured (its API key was revoked, expired, or rotated). Your identity and the AstraSync platform are fine \u2014 contact the site operator. Retrying will not help.",
312
+ message: "This site's AstraSync integration is misconfigured (its API key was revoked, expired, rotated, or belongs to another account). Your identity and the AstraSync platform are fine \u2014 contact the site operator. Retrying will not help.",
270
313
  registrationUrl: urls?.registrationUrl ?? "",
271
314
  documentationUrl: options.remediation?.docsUrl ?? urls?.documentationUrl ?? "",
272
315
  steps: [
273
- "Report the failure to the site operator \u2014 their AstraSync API key needs rotating",
316
+ "Report the failure to the site operator \u2014 their AstraSync API key needs replacing",
274
317
  "Do not retry: the failure is deterministic until the operator updates their key"
275
318
  ]
276
319
  } : isApiError ? {
@@ -331,11 +374,44 @@ function createGuidanceResponse(_config, reason, options = {}) {
331
374
  // coercing the absent recommendation to a policy 'deny'. Misconfig is
332
375
  // deterministic and intentionally NOT marked retryable.
333
376
  ...isApiError && { retryable: true },
377
+ ...options.httpStatus !== void 0 && { httpStatus: options.httpStatus },
378
+ ...options.retryAfterMs !== void 0 && { retryAfterMs: options.retryAfterMs },
334
379
  correlationId: options.correlationId,
335
380
  attemptId: options.attemptId,
336
381
  verifiedAt: /* @__PURE__ */ new Date()
337
382
  };
338
383
  }
384
+ async function readJsonSafe(response) {
385
+ try {
386
+ const parsed = await response.json();
387
+ return parsed && typeof parsed === "object" ? parsed : {};
388
+ } catch {
389
+ return {};
390
+ }
391
+ }
392
+ function rejectedIdentityGuidance(failures) {
393
+ const f = failures?.find((x) => REJECTED_IDENTITY_DIMENSIONS.has(x.dimension));
394
+ if (!f) return void 0;
395
+ if (f.dimension === "agent.lookup") {
396
+ return `The presented agent identifier is not registered with AstraSync. ${UNKNOWN_AGENT_GUIDANCE}`;
397
+ }
398
+ return f.guidance ? `${f.message} ${f.guidance}` : f.message;
399
+ }
400
+ var MAX_RETRY_AFTER_MS = 15 * 60 * 1e3;
401
+ function parseRetryAfterMs(response) {
402
+ const raw = response.headers?.get?.("retry-after");
403
+ if (!raw) return void 0;
404
+ const trimmed = raw.trim();
405
+ let ms;
406
+ if (/^\d+(\.\d+)?$/.test(trimmed)) {
407
+ ms = Number(trimmed) * 1e3;
408
+ } else {
409
+ const at = Date.parse(trimmed);
410
+ if (Number.isNaN(at)) return void 0;
411
+ ms = at - Date.now();
412
+ }
413
+ return Math.min(MAX_RETRY_AFTER_MS, Math.max(0, Math.round(ms)));
414
+ }
339
415
  async function callVerifyAccessAPI(config, request) {
340
416
  const { credentials, ...requestData } = request;
341
417
  const body = {
@@ -392,7 +468,8 @@ async function callVerifyAccessAPI(config, request) {
392
468
  body: JSON.stringify(body),
393
469
  signal: AbortSignal.timeout(config.verifyTimeoutMs ?? 1e4)
394
470
  });
395
- const data = await response.json();
471
+ const data = await readJsonSafe(response);
472
+ const retryAfterMs = parseRetryAfterMs(response);
396
473
  if (response.status === 410) {
397
474
  return {
398
475
  success: true,
@@ -430,13 +507,31 @@ async function callVerifyAccessAPI(config, request) {
430
507
  }
431
508
  };
432
509
  }
510
+ if (response.status === 404 && credentials.astraId && (data?.error === "AGENT_NOT_FOUND" || typeof data?.message === "string" && /agent/i.test(data.message))) {
511
+ return {
512
+ success: true,
513
+ attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0,
514
+ access: {
515
+ allowed: false,
516
+ reason: "unknown_agent",
517
+ failures: [
518
+ {
519
+ dimension: "identity.unknown_agent",
520
+ message: "The presented agent identifier is not registered with AstraSync.",
521
+ guidance: UNKNOWN_AGENT_GUIDANCE
522
+ }
523
+ ]
524
+ }
525
+ };
526
+ }
433
527
  if (!response.ok) {
434
- if (response.status === 401) {
528
+ const keyMismatch = response.status === 403 && data?.error === "COUNTERPARTY_KEY_MISMATCH";
529
+ if (response.status === 401 || keyMismatch) {
435
530
  return {
436
531
  success: false,
437
532
  error: data.message || data.error || "Integration API key rejected",
438
533
  misconfig: true,
439
- misconfigCode: typeof data?.code === "string" ? data.code : void 0,
534
+ misconfigCode: keyMismatch ? "counterparty_key_mismatch" : typeof data?.code === "string" ? data.code : void 0,
440
535
  remediation: data?.remediation && typeof data.remediation === "object" ? data.remediation : void 0,
441
536
  correlationId: typeof data?.correlationId === "string" ? data.correlationId : void 0,
442
537
  attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0
@@ -444,9 +539,11 @@ async function callVerifyAccessAPI(config, request) {
444
539
  }
445
540
  return {
446
541
  success: false,
447
- error: data.message || data.error || `API returned ${response.status}`,
542
+ error: typeof data?.message === "string" && data.message || typeof data?.error === "string" && data.error || `API returned ${response.status}`,
448
543
  correlationId: typeof data?.correlationId === "string" ? data.correlationId : void 0,
449
- attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0
544
+ attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0,
545
+ httpStatus: response.status,
546
+ ...retryAfterMs !== void 0 && { retryAfterMs }
450
547
  };
451
548
  }
452
549
  return data;
@@ -467,14 +564,22 @@ async function verify(config, request, options) {
467
564
  mergedConfig.apiBaseUrl,
468
565
  mergedConfig.debug,
469
566
  true,
470
- mergedConfig.apiKey
567
+ mergedConfig.apiKey,
568
+ {
569
+ id: mergedConfig.counterpartyId,
570
+ url: mergedConfig.counterpartyUrl ?? request.counterpartyUrl
571
+ }
471
572
  );
472
573
  } else {
473
574
  void performInitCheck(
474
575
  mergedConfig.apiBaseUrl,
475
576
  mergedConfig.debug,
476
577
  false,
477
- mergedConfig.apiKey
578
+ mergedConfig.apiKey,
579
+ {
580
+ id: mergedConfig.counterpartyId,
581
+ url: mergedConfig.counterpartyUrl ?? request.counterpartyUrl
582
+ }
478
583
  );
479
584
  }
480
585
  }
@@ -518,12 +623,14 @@ async function verify(config, request, options) {
518
623
  correlationId: apiResponse.correlationId,
519
624
  attemptId: apiResponse.attemptId,
520
625
  remediation: apiResponse.remediation,
626
+ httpStatus: apiResponse.httpStatus,
627
+ retryAfterMs: apiResponse.retryAfterMs,
521
628
  urls
522
629
  });
523
630
  }
524
631
  if (!apiResponse.access?.allowed) {
525
632
  const aggregatedFailures = apiResponse.access?.failures;
526
- const idVerifiedFromBackend = apiResponse.verificationContext?.idVerified === true;
633
+ const idVerifiedFromBackend = apiResponse.verificationContext?.idVerified === true && !aggregatedFailures?.some((f) => REJECTED_IDENTITY_DIMENSIONS.has(f.dimension));
527
634
  const result2 = {
528
635
  identityVerified: idVerifiedFromBackend,
529
636
  policyAllowed: false,
@@ -535,7 +642,10 @@ async function verify(config, request, options) {
535
642
  requiresStepUp: apiResponse.access?.requiresStepUp,
536
643
  requiresApproval: apiResponse.access?.requiresApproval,
537
644
  guidance: {
538
- message: apiResponse.access?.reason || "Access denied by PDLSS policy",
645
+ // 5.12.0: a presented-but-rejected id (malformed / unknown) carries
646
+ // actionable registration guidance on its failure — surface it
647
+ // instead of the bare machine reason ('unknown_agent').
648
+ message: rejectedIdentityGuidance(aggregatedFailures) ?? (apiResponse.access?.reason || "Access denied by PDLSS policy"),
539
649
  registrationUrl: urls?.registrationUrl ?? "",
540
650
  documentationUrl: urls?.documentationUrl ?? "",
541
651
  // 5.1.0: anonymous callers land on this branch — steer MCP-hosted
@@ -635,7 +745,7 @@ async function verify(config, request, options) {
635
745
  registrationUrl: urls?.registrationUrl ?? "",
636
746
  documentationUrl: urls?.documentationUrl ?? ""
637
747
  };
638
- } else if (result.recommendation === "step_up_required") {
748
+ } else if (result.recommendation === "step_up_required" && result.identityVerified) {
639
749
  result.requiresStepUp = true;
640
750
  result.denialReasons = serverDenialReasons ?? result.recommendationReasons ?? ["Step-up verification required"];
641
751
  }
@@ -1109,8 +1219,37 @@ function dedupeFailures(result) {
1109
1219
  result.denialReasons = [...new Set(result.denialReasons)];
1110
1220
  }
1111
1221
  }
1222
+ function isReadOnlyMcpMethod(parsed) {
1223
+ return parsed.method !== "tools/call";
1224
+ }
1112
1225
  function defaultMcpDenied(result, req, res) {
1113
1226
  const id = req.body?.id ?? null;
1227
+ if (isVerificationUnavailable(result)) {
1228
+ const retryAfterSeconds = unavailableRetryAfterSeconds(result);
1229
+ const body = buildVerificationUnavailableBody(result).error;
1230
+ res.setHeader("X-Astra-Gateway-Mode", "enforced");
1231
+ res.setHeader(VERIFICATION_STATUS_HEADER, "unavailable");
1232
+ res.setHeader("Retry-After", String(retryAfterSeconds));
1233
+ res.status(503).json({
1234
+ jsonrpc: "2.0",
1235
+ id,
1236
+ error: {
1237
+ // -32003 → verification temporarily unavailable (retryable)
1238
+ code: -32003,
1239
+ message: body.message,
1240
+ data: {
1241
+ code: body.code,
1242
+ recommendation: "error",
1243
+ retryable: true,
1244
+ retryAfterSeconds,
1245
+ guidance: body.guidance,
1246
+ failures: result.failures,
1247
+ correlationId: result.correlationId
1248
+ }
1249
+ }
1250
+ });
1251
+ return;
1252
+ }
1114
1253
  if (result.misconfigured) {
1115
1254
  res.setHeader("X-Astra-Gateway-Mode", "enforced");
1116
1255
  res.status(503).json({
@@ -1129,7 +1268,7 @@ function defaultMcpDenied(result, req, res) {
1129
1268
  });
1130
1269
  return;
1131
1270
  }
1132
- const status = !result.identityVerified ? 401 : 403;
1271
+ const { status, code: denyCode } = denyStatusFor(result, "INSUFFICIENT_ACCESS");
1133
1272
  res.setHeader("X-Astra-Gateway-Mode", "enforced");
1134
1273
  res.status(status).json({
1135
1274
  jsonrpc: "2.0",
@@ -1139,9 +1278,11 @@ function defaultMcpDenied(result, req, res) {
1139
1278
  // -32000 → unauthorized (no identity resolved)
1140
1279
  // -32001 → insufficient access (identity OK, policy denied)
1141
1280
  // -32002 → merchant verification misconfigured (handled above)
1281
+ // -32003 → verification temporarily unavailable (handled above)
1142
1282
  code: !result.identityVerified ? -32e3 : -32001,
1143
1283
  message: result.denialReasons?.[0] ?? "Access denied",
1144
1284
  data: {
1285
+ code: denyCode,
1145
1286
  guidance: result.guidance,
1146
1287
  // Round-10: aggregated per-dimension detail + correlation handle.
1147
1288
  failures: result.failures,
@@ -1269,6 +1410,9 @@ function createMcpMiddleware(options) {
1269
1410
  resolved_action: pdlss.action
1270
1411
  });
1271
1412
  }
1413
+ if (!pdlss.purpose && !shouldEnforce) {
1414
+ return next();
1415
+ }
1272
1416
  if (!pdlss.purpose) {
1273
1417
  const id = req.body?.id ?? null;
1274
1418
  res.status(400).json({
@@ -1288,6 +1432,9 @@ function createMcpMiddleware(options) {
1288
1432
  }
1289
1433
  const counterpartyUrl = config.counterpartyUrl || `${req.protocol}://${req.get("host")}${req.path}`;
1290
1434
  const shouldRecordDecisions = recordDecisions !== false;
1435
+ const forwardedFor = req.headers["x-forwarded-for"];
1436
+ const forwardedForStr = Array.isArray(forwardedFor) ? forwardedFor.join(", ") : forwardedFor;
1437
+ const sourceIp = req.ip ?? (forwardedForStr ? forwardedForStr.split(",")[0].trim() : void 0);
1291
1438
  const verificationRequest = {
1292
1439
  credentials,
1293
1440
  purpose: pdlss.purpose,
@@ -1302,9 +1449,10 @@ function createMcpMiddleware(options) {
1302
1449
  counterpartyType: config.counterpartyType || "mcp_server",
1303
1450
  enableRuntimeChallenge,
1304
1451
  callerMetadata: {
1305
- sourceIp: req.ip,
1452
+ sourceIp,
1306
1453
  userAgent: req.headers["user-agent"],
1307
1454
  host: req.headers.host,
1455
+ ...forwardedForStr && { forwardedFor: forwardedForStr },
1308
1456
  // Capture EVERY inbound header (parity with the express adapter —
1309
1457
  // MCP-only merchants have no edge to do it upstream) plus the
1310
1458
  // CDN-derived connection block. Local string work, secrets
@@ -1327,6 +1475,21 @@ function createMcpMiddleware(options) {
1327
1475
  result.stepUpOutcome = outcome;
1328
1476
  }
1329
1477
  }
1478
+ if (!result.policyAllowed && isVerificationUnavailable(result) && (!shouldEnforce || isReadOnlyMcpMethod(parsed) && !credentials.astraId && !credentials.jwt)) {
1479
+ res.setHeader(VERIFICATION_STATUS_HEADER, "unavailable");
1480
+ return next();
1481
+ }
1482
+ if (!shouldEnforce) {
1483
+ if (config.setPassThroughHeader) {
1484
+ res.setHeader("X-Astra-Gateway-Mode", "enforced");
1485
+ res.setHeader("X-Astra-Gateway-Reason", "evaluated-not-enforced");
1486
+ }
1487
+ if (shouldRecordDecisions && sessionId) {
1488
+ recordDecision(config, sessionId, "granted").catch(() => {
1489
+ });
1490
+ }
1491
+ return next();
1492
+ }
1330
1493
  if (!result.policyAllowed) {
1331
1494
  if (shouldRecordDecisions && sessionId) {
1332
1495
  recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
@@ -1346,17 +1509,6 @@ function createMcpMiddleware(options) {
1346
1509
  onDenied(result, req, res);
1347
1510
  return;
1348
1511
  }
1349
- if (!shouldEnforce) {
1350
- if (config.setPassThroughHeader) {
1351
- res.setHeader("X-Astra-Gateway-Mode", "enforced");
1352
- res.setHeader("X-Astra-Gateway-Reason", "evaluated-not-enforced");
1353
- }
1354
- if (shouldRecordDecisions && sessionId) {
1355
- recordDecision(config, sessionId, "granted").catch(() => {
1356
- });
1357
- }
1358
- return next();
1359
- }
1360
1512
  if (effectiveAstraId) {
1361
1513
  res.setHeader(
1362
1514
  MCP_VERIFIED_HOP_HEADER,
@@ -1397,14 +1549,6 @@ function createMcpMiddleware(options) {
1397
1549
  verifiedAt: /* @__PURE__ */ new Date(),
1398
1550
  correlationId
1399
1551
  };
1400
- const catchUrls = config.apiBaseUrl ? await getWellKnownUrls(config.apiBaseUrl).catch(() => void 0) : void 0;
1401
- if (catchUrls) {
1402
- result.guidance = {
1403
- message: `Middleware threw ${errorClass} \u2014 failing closed`,
1404
- registrationUrl: catchUrls.registrationUrl,
1405
- documentationUrl: catchUrls.documentationUrl
1406
- };
1407
- }
1408
1552
  dedupeFailures(result);
1409
1553
  return onDenied(result, req, res);
1410
1554
  }