@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
package/dist/index.js CHANGED
@@ -51,6 +51,7 @@ __export(src_exports, {
51
51
  RegistrationTimeoutError: () => RegistrationTimeoutError,
52
52
  SDK_USER_AGENT: () => SDK_USER_AGENT,
53
53
  TRUST_LEVEL_RANGES: () => TRUST_LEVEL_RANGES,
54
+ VERIFICATION_STATUS_HEADER: () => VERIFICATION_STATUS_HEADER,
54
55
  VERSION: () => SDK_VERSION,
55
56
  _resetEdgeConfigCache: () => _resetEdgeConfigCache,
56
57
  agent: () => agent_exports,
@@ -58,9 +59,11 @@ __export(src_exports, {
58
59
  awaitStepUpApproval: () => awaitStepUpApproval,
59
60
  buildGuidance: () => buildGuidance,
60
61
  buildSdkObservedMetadata: () => buildSdkObservedMetadata,
62
+ buildVerificationUnavailableBody: () => buildVerificationUnavailableBody,
61
63
  clearCache: () => clearCache,
62
64
  createMcpMiddleware: () => createMcpMiddleware,
63
65
  degradeToObserve: () => degradeToObserve,
66
+ denyStatusFor: () => denyStatusFor,
64
67
  deriveConnectionFromHeaders: () => deriveConnectionFromHeaders,
65
68
  detectPlatformFingerprint: () => detectPlatformFingerprint,
66
69
  detectRuntime: () => detectRuntime,
@@ -77,6 +80,8 @@ __export(src_exports, {
77
80
  getWellKnownUrls: () => getWellKnownUrls,
78
81
  hasCredentials: () => hasCredentials,
79
82
  isEdgeConfig: () => isEdgeConfig,
83
+ isRejectedIdentityClaim: () => isRejectedIdentityClaim,
84
+ isVerificationUnavailable: () => isVerificationUnavailable,
80
85
  isVerifiedHopValidFor: () => isVerifiedHopValidFor,
81
86
  matchEdgePathRule: () => matchEdgePathRule,
82
87
  matchPlatformSignature: () => matchPlatformSignature,
@@ -96,6 +101,7 @@ __export(src_exports, {
96
101
  serializeVerifiedHop: () => serializeVerifiedHop,
97
102
  setMcpMeta: () => setMcpMeta,
98
103
  transport: () => transport_exports,
104
+ unavailableRetryAfterSeconds: () => unavailableRetryAfterSeconds,
99
105
  validateLocalPolicy: () => validatePolicy,
100
106
  verify: () => verify
101
107
  });
@@ -116,7 +122,7 @@ function getTrustLevel(score) {
116
122
  }
117
123
 
118
124
  // src/version.ts
119
- var SDK_VERSION = "5.11.0";
125
+ var SDK_VERSION = "5.13.0";
120
126
 
121
127
  // src/http.ts
122
128
  var SDK_USER_AGENT = `astrasync-sdk/${SDK_VERSION}`;
@@ -195,6 +201,59 @@ function getCachedWellKnownUrls(apiBaseUrl) {
195
201
  return cache.get(apiBaseUrl)?.data;
196
202
  }
197
203
 
204
+ // src/failure-posture.ts
205
+ var VERIFICATION_STATUS_HEADER = "X-Astra-Verification";
206
+ var DEFAULT_UNAVAILABLE_RETRY_AFTER_S = 30;
207
+ var REJECTED_IDENTITY_DIMENSIONS = /* @__PURE__ */ new Set([
208
+ "identity.invalid_agent_id",
209
+ "identity.unknown_agent",
210
+ "agent.lookup"
211
+ ]);
212
+ 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.";
213
+ var UNAVAILABLE_DIMENSIONS = /* @__PURE__ */ new Set([
214
+ "verify_access.api_error",
215
+ "middleware.internal_error"
216
+ ]);
217
+ function isVerificationUnavailable(result) {
218
+ if (result.misconfigured) return false;
219
+ return (result.failures ?? []).some((f) => UNAVAILABLE_DIMENSIONS.has(f.dimension));
220
+ }
221
+ function unavailableRetryAfterSeconds(result) {
222
+ if (typeof result.retryAfterMs === "number" && Number.isFinite(result.retryAfterMs)) {
223
+ return Math.max(1, Math.ceil(result.retryAfterMs / 1e3));
224
+ }
225
+ return DEFAULT_UNAVAILABLE_RETRY_AFTER_S;
226
+ }
227
+ function isAnonymousRead(method, credentials) {
228
+ const m = (method ?? "").toUpperCase();
229
+ if (m !== "GET" && m !== "HEAD") return false;
230
+ return !credentials.astraId && !credentials.jwt;
231
+ }
232
+ function buildVerificationUnavailableBody(result) {
233
+ return {
234
+ success: false,
235
+ error: {
236
+ code: "VERIFICATION_UNAVAILABLE",
237
+ 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.",
238
+ retryable: true,
239
+ retryAfterSeconds: unavailableRetryAfterSeconds(result),
240
+ ...result.failures && { failures: result.failures },
241
+ ...result.correlationId && { correlationId: result.correlationId },
242
+ guidance: {
243
+ message: "Retry the request after the Retry-After interval, with exponential backoff; the condition is transient."
244
+ }
245
+ }
246
+ };
247
+ }
248
+ function isRejectedIdentityClaim(result) {
249
+ return (result.failures ?? []).some((f) => REJECTED_IDENTITY_DIMENSIONS.has(f.dimension));
250
+ }
251
+ function denyStatusFor(result, policyDeniedCode) {
252
+ if (isRejectedIdentityClaim(result)) return { status: 403, code: "AGENT_IDENTITY_REJECTED" };
253
+ if (result.identityVerified) return { status: 403, code: policyDeniedCode };
254
+ return { status: 401, code: "UNAUTHORIZED" };
255
+ }
256
+
198
257
  // src/verify.ts
199
258
  var DEFAULT_CONFIG = {
200
259
  apiBaseUrl: "https://astrasync.ai/api",
@@ -207,7 +266,7 @@ var DEFAULT_CONFIG = {
207
266
  };
208
267
  var initCheckPerformed = false;
209
268
  var deprecationWarningShown = false;
210
- async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey) {
269
+ async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey, counterparty) {
211
270
  initCheckPerformed = true;
212
271
  try {
213
272
  const probeUrl = `${apiBaseUrl}/agents/verify-access`;
@@ -225,7 +284,8 @@ async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey) {
225
284
  );
226
285
  }
227
286
  if (apiKey) {
228
- const selfCheck = await sdkFetch(`${apiBaseUrl}/agents/verify-access/self-check`, {
287
+ const q = counterparty?.id ? `?counterpartyId=${encodeURIComponent(counterparty.id)}` : counterparty?.url ? `?counterpartyUrl=${encodeURIComponent(counterparty.url)}` : "";
288
+ const selfCheck = await sdkFetch(`${apiBaseUrl}/agents/verify-access/self-check${q}`, {
229
289
  headers: { Authorization: `Bearer ${apiKey}` }
230
290
  });
231
291
  if (selfCheck.status === 401) {
@@ -237,6 +297,13 @@ async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey) {
237
297
  console.error(message);
238
298
  } else if (selfCheck.ok) {
239
299
  const body = await selfCheck.json().catch(() => ({}));
300
+ if (body.counterparty && body.counterparty.canVerify === false) {
301
+ 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.`;
302
+ if (strictInit) {
303
+ throw new Error(`${message} (strictInit=true)`);
304
+ }
305
+ console.error(message);
306
+ }
240
307
  if (typeof body.daysToExpiry === "number" && body.daysToExpiry <= 7) {
241
308
  console.warn(
242
309
  `[VerificationGateway] the configured AstraSync API key expires in ${body.daysToExpiry} day(s) \u2014 rotate it at Settings \u2192 Developer Tools before integrations start failing.`
@@ -381,11 +448,11 @@ function createGuidanceResponse(_config, reason, options = {}) {
381
448
  const isMisconfig = source === "merchant_misconfig";
382
449
  const urls = options.urls;
383
450
  const guidance = isMisconfig ? {
384
- 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.",
451
+ 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.",
385
452
  registrationUrl: urls?.registrationUrl ?? "",
386
453
  documentationUrl: options.remediation?.docsUrl ?? urls?.documentationUrl ?? "",
387
454
  steps: [
388
- "Report the failure to the site operator \u2014 their AstraSync API key needs rotating",
455
+ "Report the failure to the site operator \u2014 their AstraSync API key needs replacing",
389
456
  "Do not retry: the failure is deterministic until the operator updates their key"
390
457
  ]
391
458
  } : isApiError ? {
@@ -446,11 +513,44 @@ function createGuidanceResponse(_config, reason, options = {}) {
446
513
  // coercing the absent recommendation to a policy 'deny'. Misconfig is
447
514
  // deterministic and intentionally NOT marked retryable.
448
515
  ...isApiError && { retryable: true },
516
+ ...options.httpStatus !== void 0 && { httpStatus: options.httpStatus },
517
+ ...options.retryAfterMs !== void 0 && { retryAfterMs: options.retryAfterMs },
449
518
  correlationId: options.correlationId,
450
519
  attemptId: options.attemptId,
451
520
  verifiedAt: /* @__PURE__ */ new Date()
452
521
  };
453
522
  }
523
+ async function readJsonSafe(response) {
524
+ try {
525
+ const parsed = await response.json();
526
+ return parsed && typeof parsed === "object" ? parsed : {};
527
+ } catch {
528
+ return {};
529
+ }
530
+ }
531
+ function rejectedIdentityGuidance(failures) {
532
+ const f = failures?.find((x) => REJECTED_IDENTITY_DIMENSIONS.has(x.dimension));
533
+ if (!f) return void 0;
534
+ if (f.dimension === "agent.lookup") {
535
+ return `The presented agent identifier is not registered with AstraSync. ${UNKNOWN_AGENT_GUIDANCE}`;
536
+ }
537
+ return f.guidance ? `${f.message} ${f.guidance}` : f.message;
538
+ }
539
+ var MAX_RETRY_AFTER_MS = 15 * 60 * 1e3;
540
+ function parseRetryAfterMs(response) {
541
+ const raw = response.headers?.get?.("retry-after");
542
+ if (!raw) return void 0;
543
+ const trimmed = raw.trim();
544
+ let ms;
545
+ if (/^\d+(\.\d+)?$/.test(trimmed)) {
546
+ ms = Number(trimmed) * 1e3;
547
+ } else {
548
+ const at = Date.parse(trimmed);
549
+ if (Number.isNaN(at)) return void 0;
550
+ ms = at - Date.now();
551
+ }
552
+ return Math.min(MAX_RETRY_AFTER_MS, Math.max(0, Math.round(ms)));
553
+ }
454
554
  async function callVerifyAccessAPI(config, request) {
455
555
  const { credentials, ...requestData } = request;
456
556
  const body = {
@@ -507,7 +607,8 @@ async function callVerifyAccessAPI(config, request) {
507
607
  body: JSON.stringify(body),
508
608
  signal: AbortSignal.timeout(config.verifyTimeoutMs ?? 1e4)
509
609
  });
510
- const data = await response.json();
610
+ const data = await readJsonSafe(response);
611
+ const retryAfterMs = parseRetryAfterMs(response);
511
612
  if (response.status === 410) {
512
613
  return {
513
614
  success: true,
@@ -545,13 +646,31 @@ async function callVerifyAccessAPI(config, request) {
545
646
  }
546
647
  };
547
648
  }
649
+ if (response.status === 404 && credentials.astraId && (data?.error === "AGENT_NOT_FOUND" || typeof data?.message === "string" && /agent/i.test(data.message))) {
650
+ return {
651
+ success: true,
652
+ attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0,
653
+ access: {
654
+ allowed: false,
655
+ reason: "unknown_agent",
656
+ failures: [
657
+ {
658
+ dimension: "identity.unknown_agent",
659
+ message: "The presented agent identifier is not registered with AstraSync.",
660
+ guidance: UNKNOWN_AGENT_GUIDANCE
661
+ }
662
+ ]
663
+ }
664
+ };
665
+ }
548
666
  if (!response.ok) {
549
- if (response.status === 401) {
667
+ const keyMismatch = response.status === 403 && data?.error === "COUNTERPARTY_KEY_MISMATCH";
668
+ if (response.status === 401 || keyMismatch) {
550
669
  return {
551
670
  success: false,
552
671
  error: data.message || data.error || "Integration API key rejected",
553
672
  misconfig: true,
554
- misconfigCode: typeof data?.code === "string" ? data.code : void 0,
673
+ misconfigCode: keyMismatch ? "counterparty_key_mismatch" : typeof data?.code === "string" ? data.code : void 0,
555
674
  remediation: data?.remediation && typeof data.remediation === "object" ? data.remediation : void 0,
556
675
  correlationId: typeof data?.correlationId === "string" ? data.correlationId : void 0,
557
676
  attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0
@@ -559,9 +678,11 @@ async function callVerifyAccessAPI(config, request) {
559
678
  }
560
679
  return {
561
680
  success: false,
562
- error: data.message || data.error || `API returned ${response.status}`,
681
+ error: typeof data?.message === "string" && data.message || typeof data?.error === "string" && data.error || `API returned ${response.status}`,
563
682
  correlationId: typeof data?.correlationId === "string" ? data.correlationId : void 0,
564
- attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0
683
+ attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0,
684
+ httpStatus: response.status,
685
+ ...retryAfterMs !== void 0 && { retryAfterMs }
565
686
  };
566
687
  }
567
688
  return data;
@@ -582,14 +703,22 @@ async function verify(config, request, options) {
582
703
  mergedConfig.apiBaseUrl,
583
704
  mergedConfig.debug,
584
705
  true,
585
- mergedConfig.apiKey
706
+ mergedConfig.apiKey,
707
+ {
708
+ id: mergedConfig.counterpartyId,
709
+ url: mergedConfig.counterpartyUrl ?? request.counterpartyUrl
710
+ }
586
711
  );
587
712
  } else {
588
713
  void performInitCheck(
589
714
  mergedConfig.apiBaseUrl,
590
715
  mergedConfig.debug,
591
716
  false,
592
- mergedConfig.apiKey
717
+ mergedConfig.apiKey,
718
+ {
719
+ id: mergedConfig.counterpartyId,
720
+ url: mergedConfig.counterpartyUrl ?? request.counterpartyUrl
721
+ }
593
722
  );
594
723
  }
595
724
  }
@@ -633,12 +762,14 @@ async function verify(config, request, options) {
633
762
  correlationId: apiResponse.correlationId,
634
763
  attemptId: apiResponse.attemptId,
635
764
  remediation: apiResponse.remediation,
765
+ httpStatus: apiResponse.httpStatus,
766
+ retryAfterMs: apiResponse.retryAfterMs,
636
767
  urls
637
768
  });
638
769
  }
639
770
  if (!apiResponse.access?.allowed) {
640
771
  const aggregatedFailures = apiResponse.access?.failures;
641
- const idVerifiedFromBackend = apiResponse.verificationContext?.idVerified === true;
772
+ const idVerifiedFromBackend = apiResponse.verificationContext?.idVerified === true && !aggregatedFailures?.some((f) => REJECTED_IDENTITY_DIMENSIONS.has(f.dimension));
642
773
  const result2 = {
643
774
  identityVerified: idVerifiedFromBackend,
644
775
  policyAllowed: false,
@@ -650,7 +781,10 @@ async function verify(config, request, options) {
650
781
  requiresStepUp: apiResponse.access?.requiresStepUp,
651
782
  requiresApproval: apiResponse.access?.requiresApproval,
652
783
  guidance: {
653
- message: apiResponse.access?.reason || "Access denied by PDLSS policy",
784
+ // 5.12.0: a presented-but-rejected id (malformed / unknown) carries
785
+ // actionable registration guidance on its failure — surface it
786
+ // instead of the bare machine reason ('unknown_agent').
787
+ message: rejectedIdentityGuidance(aggregatedFailures) ?? (apiResponse.access?.reason || "Access denied by PDLSS policy"),
654
788
  registrationUrl: urls?.registrationUrl ?? "",
655
789
  documentationUrl: urls?.documentationUrl ?? "",
656
790
  // 5.1.0: anonymous callers land on this branch — steer MCP-hosted
@@ -750,7 +884,7 @@ async function verify(config, request, options) {
750
884
  registrationUrl: urls?.registrationUrl ?? "",
751
885
  documentationUrl: urls?.documentationUrl ?? ""
752
886
  };
753
- } else if (result.recommendation === "step_up_required") {
887
+ } else if (result.recommendation === "step_up_required" && result.identityVerified) {
754
888
  result.requiresStepUp = true;
755
889
  result.denialReasons = serverDenialReasons ?? result.recommendationReasons ?? ["Step-up verification required"];
756
890
  }
@@ -773,7 +907,7 @@ async function recordDecision(config, sessionId, decision, reason) {
773
907
  }).catch(() => {
774
908
  });
775
909
  }
776
- async function reportAttempt(config, report) {
910
+ async function reportAttempt(config, report, options = {}) {
777
911
  const apiBaseUrl = config.apiBaseUrl || DEFAULT_CONFIG.apiBaseUrl;
778
912
  const headers = { "Content-Type": "application/json" };
779
913
  if (config.apiKey) {
@@ -784,13 +918,38 @@ async function reportAttempt(config, report) {
784
918
  ...report,
785
919
  ...!report.counterpartyId && config.counterpartyId && { counterpartyId: config.counterpartyId }
786
920
  };
787
- await sdkFetch(`${apiBaseUrl}/agents/verify-access/attempt-report`, {
788
- method: "POST",
789
- headers,
790
- body: JSON.stringify(body)
791
- }).catch(() => {
792
- });
921
+ const url = `${apiBaseUrl}/agents/verify-access/attempt-report`;
922
+ const init = { method: "POST", headers, body: JSON.stringify(body) };
923
+ const fail = (failure) => {
924
+ try {
925
+ options.onFailure?.(failure);
926
+ } catch {
927
+ }
928
+ };
929
+ let response;
930
+ try {
931
+ response = await sdkFetch(url, init);
932
+ } catch (err) {
933
+ fail({ error: err instanceof Error ? err.message : String(err), retried: false });
934
+ return;
935
+ }
936
+ if (response.ok) return;
937
+ if (response.status !== 429 && response.status < 500) {
938
+ fail({ status: response.status, retried: false });
939
+ return;
940
+ }
941
+ const delayMs = Math.min(ATTEMPT_REPORT_MAX_RETRY_DELAY_MS, parseRetryAfterMs(response) ?? 500);
942
+ const timer = setTimeout(() => {
943
+ sdkFetch(url, init).then((retry) => {
944
+ if (!retry.ok) fail({ status: retry.status, retried: true });
945
+ }).catch((err) => {
946
+ fail({ error: err instanceof Error ? err.message : String(err), retried: true });
947
+ });
948
+ }, delayMs);
949
+ timer.unref?.();
793
950
  }
951
+ var ATTEMPT_REPORT_MAX_RETRY_DELAY_MS = 2e3;
952
+ var refusedPolicyReads = /* @__PURE__ */ new Set();
794
953
  async function fetchRoutes(config, counterpartyId) {
795
954
  if (!counterpartyId) return null;
796
955
  const headers = { "Content-Type": "application/json" };
@@ -803,7 +962,15 @@ async function fetchRoutes(config, counterpartyId) {
803
962
  `${config.apiBaseUrl}/endpoints/${encodeURIComponent(counterpartyId)}/routes`,
804
963
  { method: "GET", headers }
805
964
  );
806
- if (!response.ok) return null;
965
+ if (!response.ok) {
966
+ if ((response.status === 401 || response.status === 403 || response.status === 404) && !refusedPolicyReads.has(counterpartyId)) {
967
+ refusedPolicyReads.add(counterpartyId);
968
+ console.error(
969
+ `[VerificationGateway] the configured API key cannot read the route policy for ${counterpartyId} (HTTP ${response.status}). The key belongs to an account that does not own this endpoint, or the counterpartyId is wrong. The gateway is in PASS-THROUGH mode: no agent is being verified.`
970
+ );
971
+ }
972
+ return null;
973
+ }
807
974
  const body = await response.json();
808
975
  return body.data?.routes ?? [];
809
976
  } catch {
@@ -820,7 +987,10 @@ async function reportUnregisteredAttempt(config, data) {
820
987
  };
821
988
  await sdkFetch(`${apiBaseUrl}/verification-activity/unregistered-attempt`, {
822
989
  method: "POST",
823
- headers: { "Content-Type": "application/json" },
990
+ headers: {
991
+ "Content-Type": "application/json",
992
+ ...config.apiKey && { Authorization: `Bearer ${config.apiKey}` }
993
+ },
824
994
  body: JSON.stringify(body)
825
995
  }).catch(() => {
826
996
  });
@@ -842,7 +1012,11 @@ async function quickVerify(config, credentials) {
842
1012
  return {
843
1013
  identityVerified: result.identityVerified,
844
1014
  policyAllowed: result.policyAllowed,
845
- reason: result.denialReasons?.[0]
1015
+ reason: result.denialReasons?.[0],
1016
+ ...isVerificationUnavailable(result) && {
1017
+ unavailable: true,
1018
+ retryAfterMs: unavailableRetryAfterSeconds(result) * 1e3
1019
+ }
846
1020
  };
847
1021
  }
848
1022
 
@@ -1590,7 +1764,10 @@ var PLATFORM_AGENT_SIGNATURES = [
1590
1764
  {
1591
1765
  vendor: "chatgpt",
1592
1766
  displayName: "OpenAI ChatGPT",
1593
- uaPatterns: [/chatgpt/i, /openai/i, /gpt-?\d/i],
1767
+ // GPTBot (training crawler) and OAI-SearchBot (ChatGPT search) are
1768
+ // OpenAI's declared crawler UAs — `gpt-?\d` never matched them, so they
1769
+ // classified as unattributed crawlers (round-27.4 W5).
1770
+ uaPatterns: [/chatgpt/i, /openai/i, /gpt-?\d/i, /\bGPTBot\b/i, /\bOAI-SearchBot\b/i],
1594
1771
  agentCardPatterns: [/openai\.com/i, /chatgpt\.com/i],
1595
1772
  apiKeyFormatPatterns: [/^sk-proj/i, /^sk-svcacct/i],
1596
1773
  headerPatterns: [/^openai-organization/i, /^openai-project/i, /^openai-version/i]
@@ -1725,6 +1902,215 @@ var BROWSER_UA_PATTERN = /mozilla\/\d/i;
1725
1902
  var _patterns = { STATIC_EXTENSIONS, BOT_UA_PATTERN, BROWSER_UA_PATTERN, SELF_UA_PATTERN };
1726
1903
 
1727
1904
  // src/edge-core/events.ts
1905
+ async function withBudget(promise, ms) {
1906
+ let timer;
1907
+ try {
1908
+ return await Promise.race([
1909
+ promise,
1910
+ new Promise((resolve) => {
1911
+ timer = setTimeout(() => resolve(void 0), ms);
1912
+ })
1913
+ ]);
1914
+ } catch {
1915
+ return void 0;
1916
+ } finally {
1917
+ if (timer) clearTimeout(timer);
1918
+ }
1919
+ }
1920
+ var destKeyOf = (d) => `${d.apiBaseUrl}\0${d.apiKey ?? ""}\0${d.counterparty}\0${d.adapter ?? ""}`;
1921
+ var BeaconBuffer = class {
1922
+ constructor(options = {}) {
1923
+ this.queue = [];
1924
+ this.droppedSinceLast = 0;
1925
+ this.droppedTotal = 0;
1926
+ this.deliveredTotal = 0;
1927
+ this.backoffUntil = 0;
1928
+ this.collapse = /* @__PURE__ */ new Map();
1929
+ /** Keys the backend answered 401 for — later batches go unauthenticated. */
1930
+ this.rejectedKeys = /* @__PURE__ */ new Set();
1931
+ this.opts = {
1932
+ maxItems: options.maxItems ?? 1e3,
1933
+ maxBatch: options.maxBatch ?? 100,
1934
+ maxAttempts: options.maxAttempts ?? 3,
1935
+ collapseWindowMs: options.collapseWindowMs ?? 6e4,
1936
+ collapseMaxKeys: options.collapseMaxKeys ?? 1e3,
1937
+ requestTimeoutMs: options.requestTimeoutMs ?? 1e4,
1938
+ retryBackoffMs: options.retryBackoffMs ?? 1e3,
1939
+ now: options.now ?? Date.now
1940
+ };
1941
+ }
1942
+ /**
1943
+ * Queue one beacon. With a `collapseKey`, a repeat inside the window is
1944
+ * merged into a held aggregate (sent when the window closes) instead of
1945
+ * becoming its own item.
1946
+ */
1947
+ enqueue(dest, item, collapseKey) {
1948
+ const now = this.opts.now();
1949
+ if (collapseKey) {
1950
+ const entry = this.collapse.get(collapseKey);
1951
+ if (entry && now - entry.firstSeenAt < this.opts.collapseWindowMs) {
1952
+ for (const target of [entry.first, entry.held]) {
1953
+ if (target && !target.inFlight && this.queue.includes(target)) {
1954
+ target.item.repeatCount = (target.item.repeatCount ?? 1) + 1;
1955
+ return;
1956
+ }
1957
+ }
1958
+ const held = this.push(
1959
+ dest,
1960
+ { ...item, repeatCount: 1 },
1961
+ entry.firstSeenAt + this.opts.collapseWindowMs
1962
+ );
1963
+ entry.held = held;
1964
+ return;
1965
+ }
1966
+ this.collapse.delete(collapseKey);
1967
+ if (this.collapse.size >= this.opts.collapseMaxKeys) {
1968
+ const oldest = this.collapse.keys().next().value;
1969
+ if (oldest !== void 0) this.collapse.delete(oldest);
1970
+ }
1971
+ const fresh = { firstSeenAt: now };
1972
+ this.collapse.set(collapseKey, fresh);
1973
+ fresh.first = this.push(dest, item, now);
1974
+ return;
1975
+ }
1976
+ this.push(dest, item, now);
1977
+ }
1978
+ /**
1979
+ * Deliver every eligible queued item. Concurrent callers share ONE
1980
+ * in-flight drain (it survives the caller's budget and a Lambda freeze),
1981
+ * so nothing in flight is ever sent twice; items queued while it runs are
1982
+ * picked up by the same drain.
1983
+ */
1984
+ flush() {
1985
+ if (!this.inFlight) {
1986
+ this.inFlight = this.drain().finally(() => {
1987
+ this.inFlight = void 0;
1988
+ });
1989
+ }
1990
+ return this.inFlight;
1991
+ }
1992
+ /** Counters for tests and diagnostics. */
1993
+ stats() {
1994
+ return {
1995
+ queueDepth: this.queue.length,
1996
+ droppedSinceLast: this.droppedSinceLast,
1997
+ droppedTotal: this.droppedTotal,
1998
+ deliveredTotal: this.deliveredTotal,
1999
+ flushing: this.inFlight !== void 0
2000
+ };
2001
+ }
2002
+ push(dest, item, eligibleAt) {
2003
+ while (this.queue.length >= this.opts.maxItems) this.dropOldest();
2004
+ const entry = {
2005
+ dest,
2006
+ destKey: destKeyOf(dest),
2007
+ item,
2008
+ attempts: 0,
2009
+ eligibleAt,
2010
+ inFlight: false
2011
+ };
2012
+ this.queue.push(entry);
2013
+ return entry;
2014
+ }
2015
+ dropOldest() {
2016
+ if (this.queue.shift()) this.countDropped(1);
2017
+ }
2018
+ countDropped(n) {
2019
+ this.droppedSinceLast += n;
2020
+ this.droppedTotal += n;
2021
+ }
2022
+ async drain() {
2023
+ for (; ; ) {
2024
+ const now = this.opts.now();
2025
+ if (now < this.backoffUntil) return;
2026
+ const batch = this.takeBatch(now);
2027
+ if (batch.length === 0) return;
2028
+ const ok = await this.send(batch);
2029
+ if (!ok) return;
2030
+ }
2031
+ }
2032
+ /** Oldest eligible item + up to maxBatch-1 more for the same destination. */
2033
+ takeBatch(now) {
2034
+ const first = this.queue.find((q) => q.eligibleAt <= now);
2035
+ if (!first) return [];
2036
+ const batch = [];
2037
+ const rest = [];
2038
+ for (const q of this.queue) {
2039
+ if (batch.length < this.opts.maxBatch && q.destKey === first.destKey && q.eligibleAt <= now) {
2040
+ q.inFlight = true;
2041
+ batch.push(q);
2042
+ } else {
2043
+ rest.push(q);
2044
+ }
2045
+ }
2046
+ this.queue = rest;
2047
+ return batch;
2048
+ }
2049
+ /** POST one batch. Returns false when the drain should pause (backoff). */
2050
+ async send(batch) {
2051
+ const dest = batch[0].dest;
2052
+ const useKey = dest.apiKey && !this.rejectedKeys.has(dest.apiKey) ? dest.apiKey : void 0;
2053
+ const reportedDropped = this.droppedSinceLast;
2054
+ const meta = {
2055
+ droppedSinceLast: reportedDropped,
2056
+ queueDepth: this.queue.length,
2057
+ adapter: dest.adapter ?? "edge-core",
2058
+ version: SDK_VERSION
2059
+ };
2060
+ const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
2061
+ const response = await withBudget(
2062
+ sdkFetch(`${dest.apiBaseUrl}/verification-activity/edge-events`, {
2063
+ method: "POST",
2064
+ headers: {
2065
+ "Content-Type": "application/json",
2066
+ ...useKey && { Authorization: `Bearer ${useKey}` }
2067
+ },
2068
+ body: JSON.stringify({ events: batch.map((q) => q.item), meta }),
2069
+ ...controller && { signal: controller.signal }
2070
+ }),
2071
+ this.opts.requestTimeoutMs
2072
+ );
2073
+ if (!response) controller?.abort();
2074
+ const status = response?.status;
2075
+ for (const q of batch) q.inFlight = false;
2076
+ if (status !== void 0 && status >= 200 && status < 300) {
2077
+ this.droppedSinceLast = Math.max(0, this.droppedSinceLast - reportedDropped);
2078
+ this.deliveredTotal += batch.length;
2079
+ return true;
2080
+ }
2081
+ if (status === 401 && useKey) {
2082
+ this.rejectedKeys.add(useKey);
2083
+ this.requeue(batch, false);
2084
+ return true;
2085
+ }
2086
+ if (status === 429) {
2087
+ const retryAfter = Number(response?.headers.get("retry-after"));
2088
+ const waitMs = Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(retryAfter, 120) * 1e3 : 3e4;
2089
+ this.backoffUntil = this.opts.now() + waitMs;
2090
+ this.requeue(batch, false);
2091
+ return false;
2092
+ }
2093
+ if (status !== void 0 && status >= 400 && status < 500) {
2094
+ this.countDropped(batch.length);
2095
+ return true;
2096
+ }
2097
+ this.backoffUntil = this.opts.now() + this.opts.retryBackoffMs;
2098
+ this.requeue(batch, true);
2099
+ return false;
2100
+ }
2101
+ /** Put a failed batch back at the head of the queue (order preserved). */
2102
+ requeue(batch, countAttempt) {
2103
+ const keep = [];
2104
+ for (const q of batch) {
2105
+ if (countAttempt) q.attempts += 1;
2106
+ if (q.attempts >= this.opts.maxAttempts) this.countDropped(1);
2107
+ else keep.push(q);
2108
+ }
2109
+ this.queue = [...keep, ...this.queue];
2110
+ while (this.queue.length > this.opts.maxItems) this.dropOldest();
2111
+ }
2112
+ };
2113
+ var beaconBuffer = new BeaconBuffer();
1728
2114
  function sampled(rate, random = Math.random) {
1729
2115
  if (rate <= 0) return false;
1730
2116
  if (rate >= 1) return true;
@@ -1901,12 +2287,19 @@ function defaultOnDenied(result, _req, res) {
1901
2287
  });
1902
2288
  return;
1903
2289
  }
1904
- const statusCode = !result.identityVerified ? 401 : 403;
2290
+ if (isVerificationUnavailable(result)) {
2291
+ res.setHeader("X-Astra-Gateway-Mode", "enforced");
2292
+ res.setHeader(VERIFICATION_STATUS_HEADER, "unavailable");
2293
+ res.setHeader("Retry-After", String(unavailableRetryAfterSeconds(result)));
2294
+ res.status(503).json(buildVerificationUnavailableBody(result));
2295
+ return;
2296
+ }
2297
+ const { status: statusCode, code } = denyStatusFor(result, "INSUFFICIENT_ACCESS");
1905
2298
  res.setHeader("X-Astra-Gateway-Mode", "enforced");
1906
2299
  res.status(statusCode).json({
1907
2300
  success: false,
1908
2301
  error: {
1909
- code: !result.identityVerified ? "UNAUTHORIZED" : "INSUFFICIENT_ACCESS",
2302
+ code,
1910
2303
  message: result.denialReasons?.[0] || "Access denied",
1911
2304
  guidance: result.guidance,
1912
2305
  // Round-10: aggregated per-dimension detail + correlation handle.
@@ -2154,6 +2547,21 @@ function createMiddleware(options) {
2154
2547
  result.stepUpOutcome = outcome;
2155
2548
  }
2156
2549
  }
2550
+ if (!result.policyAllowed && isVerificationUnavailable(result) && (!shouldEnforce || isAnonymousRead(req.method, credentials))) {
2551
+ res.setHeader(VERIFICATION_STATUS_HEADER, "unavailable");
2552
+ return next();
2553
+ }
2554
+ if (!shouldEnforce) {
2555
+ if (config.setPassThroughHeader) {
2556
+ res.setHeader("X-Astra-Gateway-Mode", "enforced");
2557
+ res.setHeader("X-Astra-Gateway-Reason", "evaluated-not-enforced");
2558
+ }
2559
+ if (shouldRecordDecisions && sessionId) {
2560
+ recordDecision(config, sessionId, "granted").catch(() => {
2561
+ });
2562
+ }
2563
+ return next();
2564
+ }
2157
2565
  if (!result.policyAllowed) {
2158
2566
  if (shouldRecordDecisions && sessionId) {
2159
2567
  recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
@@ -2173,17 +2581,6 @@ function createMiddleware(options) {
2173
2581
  onDenied(result, req, res);
2174
2582
  return;
2175
2583
  }
2176
- if (!shouldEnforce) {
2177
- if (config.setPassThroughHeader) {
2178
- res.setHeader("X-Astra-Gateway-Mode", "enforced");
2179
- res.setHeader("X-Astra-Gateway-Reason", "evaluated-not-enforced");
2180
- }
2181
- if (shouldRecordDecisions && sessionId) {
2182
- recordDecision(config, sessionId, "granted").catch(() => {
2183
- });
2184
- }
2185
- return next();
2186
- }
2187
2584
  if (routeConfig.minTrustScore && result.agent) {
2188
2585
  if (result.agent.trustScore < routeConfig.minTrustScore) {
2189
2586
  const trustFailure = {
@@ -2239,14 +2636,6 @@ function createMiddleware(options) {
2239
2636
  verifiedAt: /* @__PURE__ */ new Date(),
2240
2637
  correlationId
2241
2638
  };
2242
- const catchUrls = config.apiBaseUrl ? await getWellKnownUrls(config.apiBaseUrl).catch(() => void 0) : void 0;
2243
- if (catchUrls) {
2244
- result.guidance = {
2245
- message: `Middleware threw ${errorClass} \u2014 failing closed`,
2246
- registrationUrl: catchUrls.registrationUrl,
2247
- documentationUrl: catchUrls.documentationUrl
2248
- };
2249
- }
2250
2639
  dedupeFailures(result);
2251
2640
  return onDenied(result, req, res);
2252
2641
  }
@@ -2666,6 +3055,20 @@ function createMiddleware2(options) {
2666
3055
  }
2667
3056
  const approvalRequired = result.policyAllowed && requiresHumanApproval(result);
2668
3057
  if (approvalRequired) annotateApprovalRequired(result);
3058
+ if (!result.policyAllowed && isVerificationUnavailable(result)) {
3059
+ if (isAnonymousRead(request.method, credentials)) {
3060
+ const passThrough = NextResponse.next();
3061
+ passThrough.headers.set(VERIFICATION_STATUS_HEADER, "unavailable");
3062
+ return passThrough;
3063
+ }
3064
+ return NextResponse.json(buildVerificationUnavailableBody(result), {
3065
+ status: 503,
3066
+ headers: {
3067
+ "Retry-After": String(unavailableRetryAfterSeconds(result)),
3068
+ [VERIFICATION_STATUS_HEADER]: "unavailable"
3069
+ }
3070
+ });
3071
+ }
2669
3072
  if (!result.policyAllowed || approvalRequired) {
2670
3073
  if (result.misconfigured && pathname.startsWith("/api/")) {
2671
3074
  return NextResponse.json(
@@ -2683,13 +3086,12 @@ function createMiddleware2(options) {
2683
3086
  );
2684
3087
  }
2685
3088
  if (pathname.startsWith("/api/")) {
3089
+ const denyStatus = denyStatusFor(result, "POLICY_DENIED");
2686
3090
  return NextResponse.json(
2687
3091
  {
2688
3092
  success: false,
2689
3093
  error: {
2690
- // Round-18 G4: 401 → identity missing (re-auth); 403 → identity
2691
- // OK, policy denied (update PDLSS / step up).
2692
- code: !result.identityVerified ? "UNAUTHORIZED" : "POLICY_DENIED",
3094
+ code: denyStatus.code,
2693
3095
  message: result.denialReasons?.[0] || "Access denied",
2694
3096
  guidance: result.guidance,
2695
3097
  failures: result.failures,
@@ -2699,7 +3101,7 @@ function createMiddleware2(options) {
2699
3101
  }
2700
3102
  },
2701
3103
  {
2702
- status: !result.identityVerified ? 401 : 403,
3104
+ status: denyStatus.status,
2703
3105
  ...stepUpHoldOutcome && {
2704
3106
  headers: { "X-Astra-Step-Up": stepUpHoldOutcome }
2705
3107
  }
@@ -2826,6 +3228,12 @@ var VerificationGatewayClient = class {
2826
3228
  * fulfil against it only once the order is settled / pending_merchant. An
2827
3229
  * explicit `buyerEmail` (here or on the bridge handoff body) wins over the
2828
3230
  * account default.
3231
+ *
3232
+ * 5.12.0 approval binding: pass the bridge handoff body's `attemptId` (and
3233
+ * the same `checkoutSessionId`) so a step-up approval minted for this cart
3234
+ * is matched to — and only to — this confirm. A held cart's approval is
3235
+ * bound to its checkout session and exact value; a confirm for any other
3236
+ * cart is held for its own approval.
2829
3237
  */
2830
3238
  async confirmCheckout(options) {
2831
3239
  const credentials = {
@@ -2845,6 +3253,7 @@ var VerificationGatewayClient = class {
2845
3253
  commercePhase: "confirm",
2846
3254
  checkoutSessionId: options.checkoutSessionId,
2847
3255
  checkoutItems: options.checkoutItems,
3256
+ ...options.attemptId && { attemptId: options.attemptId },
2848
3257
  buyerEmail: options.buyerEmail,
2849
3258
  counterpartyUrl: options.counterpartyUrl,
2850
3259
  counterpartyType: options.counterpartyType
@@ -5645,8 +6054,37 @@ function dedupeFailures2(result) {
5645
6054
  result.denialReasons = [...new Set(result.denialReasons)];
5646
6055
  }
5647
6056
  }
6057
+ function isReadOnlyMcpMethod(parsed) {
6058
+ return parsed.method !== "tools/call";
6059
+ }
5648
6060
  function defaultMcpDenied(result, req, res) {
5649
6061
  const id = req.body?.id ?? null;
6062
+ if (isVerificationUnavailable(result)) {
6063
+ const retryAfterSeconds = unavailableRetryAfterSeconds(result);
6064
+ const body = buildVerificationUnavailableBody(result).error;
6065
+ res.setHeader("X-Astra-Gateway-Mode", "enforced");
6066
+ res.setHeader(VERIFICATION_STATUS_HEADER, "unavailable");
6067
+ res.setHeader("Retry-After", String(retryAfterSeconds));
6068
+ res.status(503).json({
6069
+ jsonrpc: "2.0",
6070
+ id,
6071
+ error: {
6072
+ // -32003 → verification temporarily unavailable (retryable)
6073
+ code: -32003,
6074
+ message: body.message,
6075
+ data: {
6076
+ code: body.code,
6077
+ recommendation: "error",
6078
+ retryable: true,
6079
+ retryAfterSeconds,
6080
+ guidance: body.guidance,
6081
+ failures: result.failures,
6082
+ correlationId: result.correlationId
6083
+ }
6084
+ }
6085
+ });
6086
+ return;
6087
+ }
5650
6088
  if (result.misconfigured) {
5651
6089
  res.setHeader("X-Astra-Gateway-Mode", "enforced");
5652
6090
  res.status(503).json({
@@ -5665,7 +6103,7 @@ function defaultMcpDenied(result, req, res) {
5665
6103
  });
5666
6104
  return;
5667
6105
  }
5668
- const status = !result.identityVerified ? 401 : 403;
6106
+ const { status, code: denyCode } = denyStatusFor(result, "INSUFFICIENT_ACCESS");
5669
6107
  res.setHeader("X-Astra-Gateway-Mode", "enforced");
5670
6108
  res.status(status).json({
5671
6109
  jsonrpc: "2.0",
@@ -5675,9 +6113,11 @@ function defaultMcpDenied(result, req, res) {
5675
6113
  // -32000 → unauthorized (no identity resolved)
5676
6114
  // -32001 → insufficient access (identity OK, policy denied)
5677
6115
  // -32002 → merchant verification misconfigured (handled above)
6116
+ // -32003 → verification temporarily unavailable (handled above)
5678
6117
  code: !result.identityVerified ? -32e3 : -32001,
5679
6118
  message: result.denialReasons?.[0] ?? "Access denied",
5680
6119
  data: {
6120
+ code: denyCode,
5681
6121
  guidance: result.guidance,
5682
6122
  // Round-10: aggregated per-dimension detail + correlation handle.
5683
6123
  failures: result.failures,
@@ -5805,6 +6245,9 @@ function createMcpMiddleware(options) {
5805
6245
  resolved_action: pdlss.action
5806
6246
  });
5807
6247
  }
6248
+ if (!pdlss.purpose && !shouldEnforce) {
6249
+ return next();
6250
+ }
5808
6251
  if (!pdlss.purpose) {
5809
6252
  const id = req.body?.id ?? null;
5810
6253
  res.status(400).json({
@@ -5824,6 +6267,9 @@ function createMcpMiddleware(options) {
5824
6267
  }
5825
6268
  const counterpartyUrl = config.counterpartyUrl || `${req.protocol}://${req.get("host")}${req.path}`;
5826
6269
  const shouldRecordDecisions = recordDecisions !== false;
6270
+ const forwardedFor = req.headers["x-forwarded-for"];
6271
+ const forwardedForStr = Array.isArray(forwardedFor) ? forwardedFor.join(", ") : forwardedFor;
6272
+ const sourceIp = req.ip ?? (forwardedForStr ? forwardedForStr.split(",")[0].trim() : void 0);
5827
6273
  const verificationRequest = {
5828
6274
  credentials,
5829
6275
  purpose: pdlss.purpose,
@@ -5838,9 +6284,10 @@ function createMcpMiddleware(options) {
5838
6284
  counterpartyType: config.counterpartyType || "mcp_server",
5839
6285
  enableRuntimeChallenge,
5840
6286
  callerMetadata: {
5841
- sourceIp: req.ip,
6287
+ sourceIp,
5842
6288
  userAgent: req.headers["user-agent"],
5843
6289
  host: req.headers.host,
6290
+ ...forwardedForStr && { forwardedFor: forwardedForStr },
5844
6291
  // Capture EVERY inbound header (parity with the express adapter —
5845
6292
  // MCP-only merchants have no edge to do it upstream) plus the
5846
6293
  // CDN-derived connection block. Local string work, secrets
@@ -5863,6 +6310,21 @@ function createMcpMiddleware(options) {
5863
6310
  result.stepUpOutcome = outcome;
5864
6311
  }
5865
6312
  }
6313
+ if (!result.policyAllowed && isVerificationUnavailable(result) && (!shouldEnforce || isReadOnlyMcpMethod(parsed) && !credentials.astraId && !credentials.jwt)) {
6314
+ res.setHeader(VERIFICATION_STATUS_HEADER, "unavailable");
6315
+ return next();
6316
+ }
6317
+ if (!shouldEnforce) {
6318
+ if (config.setPassThroughHeader) {
6319
+ res.setHeader("X-Astra-Gateway-Mode", "enforced");
6320
+ res.setHeader("X-Astra-Gateway-Reason", "evaluated-not-enforced");
6321
+ }
6322
+ if (shouldRecordDecisions && sessionId) {
6323
+ recordDecision(config, sessionId, "granted").catch(() => {
6324
+ });
6325
+ }
6326
+ return next();
6327
+ }
5866
6328
  if (!result.policyAllowed) {
5867
6329
  if (shouldRecordDecisions && sessionId) {
5868
6330
  recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
@@ -5882,17 +6344,6 @@ function createMcpMiddleware(options) {
5882
6344
  onDenied(result, req, res);
5883
6345
  return;
5884
6346
  }
5885
- if (!shouldEnforce) {
5886
- if (config.setPassThroughHeader) {
5887
- res.setHeader("X-Astra-Gateway-Mode", "enforced");
5888
- res.setHeader("X-Astra-Gateway-Reason", "evaluated-not-enforced");
5889
- }
5890
- if (shouldRecordDecisions && sessionId) {
5891
- recordDecision(config, sessionId, "granted").catch(() => {
5892
- });
5893
- }
5894
- return next();
5895
- }
5896
6347
  if (effectiveAstraId) {
5897
6348
  res.setHeader(
5898
6349
  MCP_VERIFIED_HOP_HEADER,
@@ -5933,14 +6384,6 @@ function createMcpMiddleware(options) {
5933
6384
  verifiedAt: /* @__PURE__ */ new Date(),
5934
6385
  correlationId
5935
6386
  };
5936
- const catchUrls = config.apiBaseUrl ? await getWellKnownUrls(config.apiBaseUrl).catch(() => void 0) : void 0;
5937
- if (catchUrls) {
5938
- result.guidance = {
5939
- message: `Middleware threw ${errorClass} \u2014 failing closed`,
5940
- registrationUrl: catchUrls.registrationUrl,
5941
- documentationUrl: catchUrls.documentationUrl
5942
- };
5943
- }
5944
6387
  dedupeFailures2(result);
5945
6388
  return onDenied(result, req, res);
5946
6389
  }
@@ -9948,6 +10391,7 @@ function parseYaml(yamlContent) {
9948
10391
  RegistrationTimeoutError,
9949
10392
  SDK_USER_AGENT,
9950
10393
  TRUST_LEVEL_RANGES,
10394
+ VERIFICATION_STATUS_HEADER,
9951
10395
  VERSION,
9952
10396
  _resetEdgeConfigCache,
9953
10397
  agent,
@@ -9955,9 +10399,11 @@ function parseYaml(yamlContent) {
9955
10399
  awaitStepUpApproval,
9956
10400
  buildGuidance,
9957
10401
  buildSdkObservedMetadata,
10402
+ buildVerificationUnavailableBody,
9958
10403
  clearCache,
9959
10404
  createMcpMiddleware,
9960
10405
  degradeToObserve,
10406
+ denyStatusFor,
9961
10407
  deriveConnectionFromHeaders,
9962
10408
  detectPlatformFingerprint,
9963
10409
  detectRuntime,
@@ -9974,6 +10420,8 @@ function parseYaml(yamlContent) {
9974
10420
  getWellKnownUrls,
9975
10421
  hasCredentials,
9976
10422
  isEdgeConfig,
10423
+ isRejectedIdentityClaim,
10424
+ isVerificationUnavailable,
9977
10425
  isVerifiedHopValidFor,
9978
10426
  matchEdgePathRule,
9979
10427
  matchPlatformSignature,
@@ -9993,6 +10441,7 @@ function parseYaml(yamlContent) {
9993
10441
  serializeVerifiedHop,
9994
10442
  setMcpMeta,
9995
10443
  transport,
10444
+ unavailableRetryAfterSeconds,
9996
10445
  validateLocalPolicy,
9997
10446
  verify
9998
10447
  });