@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
  import { Request, Response, RequestHandler } from 'express';
2
- import { d as VerificationResult, G as GatewayConfig } from '../types-CTmUpNpS.mjs';
2
+ import { d as VerificationResult, G as GatewayConfig } from '../types-BCkwBA84.mjs';
3
3
 
4
4
  /**
5
5
  * X-Astra-Verified-Hop — cross-hop verify-access dedupe marker.
@@ -384,6 +384,10 @@ interface McpMiddlewareOptions extends GatewayConfig {
384
384
  * record what WOULD be denied with `failOnError: 'closed'` so merchants
385
385
  * can grep correlationId for impact analysis during the observation
386
386
  * window. Default flips to `'closed'` in a follow-up release.
387
+ *
388
+ * 5.12.0: `'closed'` answers the retryable verification-unavailable
389
+ * JSON-RPC error (HTTP 503 + Retry-After, code -32003) — never an
390
+ * identity-shaped 401.
387
391
  */
388
392
  failOnError?: 'open' | 'closed';
389
393
  }
@@ -1,5 +1,5 @@
1
1
  import { Request, Response, RequestHandler } from 'express';
2
- import { d as VerificationResult, G as GatewayConfig } from '../types-CTmUpNpS.js';
2
+ import { d as VerificationResult, G as GatewayConfig } from '../types-BCkwBA84.js';
3
3
 
4
4
  /**
5
5
  * X-Astra-Verified-Hop — cross-hop verify-access dedupe marker.
@@ -384,6 +384,10 @@ interface McpMiddlewareOptions extends GatewayConfig {
384
384
  * record what WOULD be denied with `failOnError: 'closed'` so merchants
385
385
  * can grep correlationId for impact analysis during the observation
386
386
  * window. Default flips to `'closed'` in a follow-up release.
387
+ *
388
+ * 5.12.0: `'closed'` answers the retryable verification-unavailable
389
+ * JSON-RPC error (HTTP 503 + Retry-After, code -32003) — never an
390
+ * identity-shaped 401.
387
391
  */
388
392
  failOnError?: 'open' | 'closed';
389
393
  }
@@ -32,7 +32,7 @@ __export(mcp_exports, {
32
32
  module.exports = __toCommonJS(mcp_exports);
33
33
 
34
34
  // src/version.ts
35
- var SDK_VERSION = "5.11.0";
35
+ var SDK_VERSION = "5.13.0";
36
36
 
37
37
  // src/http.ts
38
38
  var SDK_USER_AGENT = `astrasync-sdk/${SDK_VERSION}`;
@@ -102,23 +102,58 @@ function prefetchWellKnown(apiBaseUrl) {
102
102
  inflight.set(apiBaseUrl, promise);
103
103
  return promise;
104
104
  }
105
- async function getWellKnownUrls(apiBaseUrl) {
106
- const entry = cache.get(apiBaseUrl);
107
- if (entry) {
108
- if (Date.now() - entry.fetchedAt > CACHE_TTL_MS) {
109
- prefetchWellKnown(apiBaseUrl).catch(() => {
110
- });
111
- }
112
- return entry.data;
113
- }
114
- const pending = inflight.get(apiBaseUrl);
115
- if (pending) return pending;
116
- return prefetchWellKnown(apiBaseUrl);
117
- }
118
105
  function getCachedWellKnownUrls(apiBaseUrl) {
119
106
  return cache.get(apiBaseUrl)?.data;
120
107
  }
121
108
 
109
+ // src/failure-posture.ts
110
+ var VERIFICATION_STATUS_HEADER = "X-Astra-Verification";
111
+ var DEFAULT_UNAVAILABLE_RETRY_AFTER_S = 30;
112
+ var REJECTED_IDENTITY_DIMENSIONS = /* @__PURE__ */ new Set([
113
+ "identity.invalid_agent_id",
114
+ "identity.unknown_agent",
115
+ "agent.lookup"
116
+ ]);
117
+ 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.";
118
+ var UNAVAILABLE_DIMENSIONS = /* @__PURE__ */ new Set([
119
+ "verify_access.api_error",
120
+ "middleware.internal_error"
121
+ ]);
122
+ function isVerificationUnavailable(result) {
123
+ if (result.misconfigured) return false;
124
+ return (result.failures ?? []).some((f) => UNAVAILABLE_DIMENSIONS.has(f.dimension));
125
+ }
126
+ function unavailableRetryAfterSeconds(result) {
127
+ if (typeof result.retryAfterMs === "number" && Number.isFinite(result.retryAfterMs)) {
128
+ return Math.max(1, Math.ceil(result.retryAfterMs / 1e3));
129
+ }
130
+ return DEFAULT_UNAVAILABLE_RETRY_AFTER_S;
131
+ }
132
+ function buildVerificationUnavailableBody(result) {
133
+ return {
134
+ success: false,
135
+ error: {
136
+ code: "VERIFICATION_UNAVAILABLE",
137
+ 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.",
138
+ retryable: true,
139
+ retryAfterSeconds: unavailableRetryAfterSeconds(result),
140
+ ...result.failures && { failures: result.failures },
141
+ ...result.correlationId && { correlationId: result.correlationId },
142
+ guidance: {
143
+ message: "Retry the request after the Retry-After interval, with exponential backoff; the condition is transient."
144
+ }
145
+ }
146
+ };
147
+ }
148
+ function isRejectedIdentityClaim(result) {
149
+ return (result.failures ?? []).some((f) => REJECTED_IDENTITY_DIMENSIONS.has(f.dimension));
150
+ }
151
+ function denyStatusFor(result, policyDeniedCode) {
152
+ if (isRejectedIdentityClaim(result)) return { status: 403, code: "AGENT_IDENTITY_REJECTED" };
153
+ if (result.identityVerified) return { status: 403, code: policyDeniedCode };
154
+ return { status: 401, code: "UNAUTHORIZED" };
155
+ }
156
+
122
157
  // src/verify.ts
123
158
  var DEFAULT_CONFIG = {
124
159
  apiBaseUrl: "https://astrasync.ai/api",
@@ -131,7 +166,7 @@ var DEFAULT_CONFIG = {
131
166
  };
132
167
  var initCheckPerformed = false;
133
168
  var deprecationWarningShown = false;
134
- async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey) {
169
+ async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey, counterparty) {
135
170
  initCheckPerformed = true;
136
171
  try {
137
172
  const probeUrl = `${apiBaseUrl}/agents/verify-access`;
@@ -149,7 +184,8 @@ async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey) {
149
184
  );
150
185
  }
151
186
  if (apiKey) {
152
- const selfCheck = await sdkFetch(`${apiBaseUrl}/agents/verify-access/self-check`, {
187
+ const q = counterparty?.id ? `?counterpartyId=${encodeURIComponent(counterparty.id)}` : counterparty?.url ? `?counterpartyUrl=${encodeURIComponent(counterparty.url)}` : "";
188
+ const selfCheck = await sdkFetch(`${apiBaseUrl}/agents/verify-access/self-check${q}`, {
153
189
  headers: { Authorization: `Bearer ${apiKey}` }
154
190
  });
155
191
  if (selfCheck.status === 401) {
@@ -161,6 +197,13 @@ async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey) {
161
197
  console.error(message);
162
198
  } else if (selfCheck.ok) {
163
199
  const body = await selfCheck.json().catch(() => ({}));
200
+ if (body.counterparty && body.counterparty.canVerify === false) {
201
+ 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.`;
202
+ if (strictInit) {
203
+ throw new Error(`${message} (strictInit=true)`);
204
+ }
205
+ console.error(message);
206
+ }
164
207
  if (typeof body.daysToExpiry === "number" && body.daysToExpiry <= 7) {
165
208
  console.warn(
166
209
  `[VerificationGateway] the configured AstraSync API key expires in ${body.daysToExpiry} day(s) \u2014 rotate it at Settings \u2192 Developer Tools before integrations start failing.`
@@ -299,11 +342,11 @@ function createGuidanceResponse(_config, reason, options = {}) {
299
342
  const isMisconfig = source === "merchant_misconfig";
300
343
  const urls = options.urls;
301
344
  const guidance = isMisconfig ? {
302
- 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.",
345
+ 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.",
303
346
  registrationUrl: urls?.registrationUrl ?? "",
304
347
  documentationUrl: options.remediation?.docsUrl ?? urls?.documentationUrl ?? "",
305
348
  steps: [
306
- "Report the failure to the site operator \u2014 their AstraSync API key needs rotating",
349
+ "Report the failure to the site operator \u2014 their AstraSync API key needs replacing",
307
350
  "Do not retry: the failure is deterministic until the operator updates their key"
308
351
  ]
309
352
  } : isApiError ? {
@@ -364,11 +407,44 @@ function createGuidanceResponse(_config, reason, options = {}) {
364
407
  // coercing the absent recommendation to a policy 'deny'. Misconfig is
365
408
  // deterministic and intentionally NOT marked retryable.
366
409
  ...isApiError && { retryable: true },
410
+ ...options.httpStatus !== void 0 && { httpStatus: options.httpStatus },
411
+ ...options.retryAfterMs !== void 0 && { retryAfterMs: options.retryAfterMs },
367
412
  correlationId: options.correlationId,
368
413
  attemptId: options.attemptId,
369
414
  verifiedAt: /* @__PURE__ */ new Date()
370
415
  };
371
416
  }
417
+ async function readJsonSafe(response) {
418
+ try {
419
+ const parsed = await response.json();
420
+ return parsed && typeof parsed === "object" ? parsed : {};
421
+ } catch {
422
+ return {};
423
+ }
424
+ }
425
+ function rejectedIdentityGuidance(failures) {
426
+ const f = failures?.find((x) => REJECTED_IDENTITY_DIMENSIONS.has(x.dimension));
427
+ if (!f) return void 0;
428
+ if (f.dimension === "agent.lookup") {
429
+ return `The presented agent identifier is not registered with AstraSync. ${UNKNOWN_AGENT_GUIDANCE}`;
430
+ }
431
+ return f.guidance ? `${f.message} ${f.guidance}` : f.message;
432
+ }
433
+ var MAX_RETRY_AFTER_MS = 15 * 60 * 1e3;
434
+ function parseRetryAfterMs(response) {
435
+ const raw = response.headers?.get?.("retry-after");
436
+ if (!raw) return void 0;
437
+ const trimmed = raw.trim();
438
+ let ms;
439
+ if (/^\d+(\.\d+)?$/.test(trimmed)) {
440
+ ms = Number(trimmed) * 1e3;
441
+ } else {
442
+ const at = Date.parse(trimmed);
443
+ if (Number.isNaN(at)) return void 0;
444
+ ms = at - Date.now();
445
+ }
446
+ return Math.min(MAX_RETRY_AFTER_MS, Math.max(0, Math.round(ms)));
447
+ }
372
448
  async function callVerifyAccessAPI(config, request) {
373
449
  const { credentials, ...requestData } = request;
374
450
  const body = {
@@ -425,7 +501,8 @@ async function callVerifyAccessAPI(config, request) {
425
501
  body: JSON.stringify(body),
426
502
  signal: AbortSignal.timeout(config.verifyTimeoutMs ?? 1e4)
427
503
  });
428
- const data = await response.json();
504
+ const data = await readJsonSafe(response);
505
+ const retryAfterMs = parseRetryAfterMs(response);
429
506
  if (response.status === 410) {
430
507
  return {
431
508
  success: true,
@@ -463,13 +540,31 @@ async function callVerifyAccessAPI(config, request) {
463
540
  }
464
541
  };
465
542
  }
543
+ if (response.status === 404 && credentials.astraId && (data?.error === "AGENT_NOT_FOUND" || typeof data?.message === "string" && /agent/i.test(data.message))) {
544
+ return {
545
+ success: true,
546
+ attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0,
547
+ access: {
548
+ allowed: false,
549
+ reason: "unknown_agent",
550
+ failures: [
551
+ {
552
+ dimension: "identity.unknown_agent",
553
+ message: "The presented agent identifier is not registered with AstraSync.",
554
+ guidance: UNKNOWN_AGENT_GUIDANCE
555
+ }
556
+ ]
557
+ }
558
+ };
559
+ }
466
560
  if (!response.ok) {
467
- if (response.status === 401) {
561
+ const keyMismatch = response.status === 403 && data?.error === "COUNTERPARTY_KEY_MISMATCH";
562
+ if (response.status === 401 || keyMismatch) {
468
563
  return {
469
564
  success: false,
470
565
  error: data.message || data.error || "Integration API key rejected",
471
566
  misconfig: true,
472
- misconfigCode: typeof data?.code === "string" ? data.code : void 0,
567
+ misconfigCode: keyMismatch ? "counterparty_key_mismatch" : typeof data?.code === "string" ? data.code : void 0,
473
568
  remediation: data?.remediation && typeof data.remediation === "object" ? data.remediation : void 0,
474
569
  correlationId: typeof data?.correlationId === "string" ? data.correlationId : void 0,
475
570
  attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0
@@ -477,9 +572,11 @@ async function callVerifyAccessAPI(config, request) {
477
572
  }
478
573
  return {
479
574
  success: false,
480
- error: data.message || data.error || `API returned ${response.status}`,
575
+ error: typeof data?.message === "string" && data.message || typeof data?.error === "string" && data.error || `API returned ${response.status}`,
481
576
  correlationId: typeof data?.correlationId === "string" ? data.correlationId : void 0,
482
- attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0
577
+ attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0,
578
+ httpStatus: response.status,
579
+ ...retryAfterMs !== void 0 && { retryAfterMs }
483
580
  };
484
581
  }
485
582
  return data;
@@ -500,14 +597,22 @@ async function verify(config, request, options) {
500
597
  mergedConfig.apiBaseUrl,
501
598
  mergedConfig.debug,
502
599
  true,
503
- mergedConfig.apiKey
600
+ mergedConfig.apiKey,
601
+ {
602
+ id: mergedConfig.counterpartyId,
603
+ url: mergedConfig.counterpartyUrl ?? request.counterpartyUrl
604
+ }
504
605
  );
505
606
  } else {
506
607
  void performInitCheck(
507
608
  mergedConfig.apiBaseUrl,
508
609
  mergedConfig.debug,
509
610
  false,
510
- mergedConfig.apiKey
611
+ mergedConfig.apiKey,
612
+ {
613
+ id: mergedConfig.counterpartyId,
614
+ url: mergedConfig.counterpartyUrl ?? request.counterpartyUrl
615
+ }
511
616
  );
512
617
  }
513
618
  }
@@ -551,12 +656,14 @@ async function verify(config, request, options) {
551
656
  correlationId: apiResponse.correlationId,
552
657
  attemptId: apiResponse.attemptId,
553
658
  remediation: apiResponse.remediation,
659
+ httpStatus: apiResponse.httpStatus,
660
+ retryAfterMs: apiResponse.retryAfterMs,
554
661
  urls
555
662
  });
556
663
  }
557
664
  if (!apiResponse.access?.allowed) {
558
665
  const aggregatedFailures = apiResponse.access?.failures;
559
- const idVerifiedFromBackend = apiResponse.verificationContext?.idVerified === true;
666
+ const idVerifiedFromBackend = apiResponse.verificationContext?.idVerified === true && !aggregatedFailures?.some((f) => REJECTED_IDENTITY_DIMENSIONS.has(f.dimension));
560
667
  const result2 = {
561
668
  identityVerified: idVerifiedFromBackend,
562
669
  policyAllowed: false,
@@ -568,7 +675,10 @@ async function verify(config, request, options) {
568
675
  requiresStepUp: apiResponse.access?.requiresStepUp,
569
676
  requiresApproval: apiResponse.access?.requiresApproval,
570
677
  guidance: {
571
- message: apiResponse.access?.reason || "Access denied by PDLSS policy",
678
+ // 5.12.0: a presented-but-rejected id (malformed / unknown) carries
679
+ // actionable registration guidance on its failure — surface it
680
+ // instead of the bare machine reason ('unknown_agent').
681
+ message: rejectedIdentityGuidance(aggregatedFailures) ?? (apiResponse.access?.reason || "Access denied by PDLSS policy"),
572
682
  registrationUrl: urls?.registrationUrl ?? "",
573
683
  documentationUrl: urls?.documentationUrl ?? "",
574
684
  // 5.1.0: anonymous callers land on this branch — steer MCP-hosted
@@ -668,7 +778,7 @@ async function verify(config, request, options) {
668
778
  registrationUrl: urls?.registrationUrl ?? "",
669
779
  documentationUrl: urls?.documentationUrl ?? ""
670
780
  };
671
- } else if (result.recommendation === "step_up_required") {
781
+ } else if (result.recommendation === "step_up_required" && result.identityVerified) {
672
782
  result.requiresStepUp = true;
673
783
  result.denialReasons = serverDenialReasons ?? result.recommendationReasons ?? ["Step-up verification required"];
674
784
  }
@@ -1142,8 +1252,37 @@ function dedupeFailures(result) {
1142
1252
  result.denialReasons = [...new Set(result.denialReasons)];
1143
1253
  }
1144
1254
  }
1255
+ function isReadOnlyMcpMethod(parsed) {
1256
+ return parsed.method !== "tools/call";
1257
+ }
1145
1258
  function defaultMcpDenied(result, req, res) {
1146
1259
  const id = req.body?.id ?? null;
1260
+ if (isVerificationUnavailable(result)) {
1261
+ const retryAfterSeconds = unavailableRetryAfterSeconds(result);
1262
+ const body = buildVerificationUnavailableBody(result).error;
1263
+ res.setHeader("X-Astra-Gateway-Mode", "enforced");
1264
+ res.setHeader(VERIFICATION_STATUS_HEADER, "unavailable");
1265
+ res.setHeader("Retry-After", String(retryAfterSeconds));
1266
+ res.status(503).json({
1267
+ jsonrpc: "2.0",
1268
+ id,
1269
+ error: {
1270
+ // -32003 → verification temporarily unavailable (retryable)
1271
+ code: -32003,
1272
+ message: body.message,
1273
+ data: {
1274
+ code: body.code,
1275
+ recommendation: "error",
1276
+ retryable: true,
1277
+ retryAfterSeconds,
1278
+ guidance: body.guidance,
1279
+ failures: result.failures,
1280
+ correlationId: result.correlationId
1281
+ }
1282
+ }
1283
+ });
1284
+ return;
1285
+ }
1147
1286
  if (result.misconfigured) {
1148
1287
  res.setHeader("X-Astra-Gateway-Mode", "enforced");
1149
1288
  res.status(503).json({
@@ -1162,7 +1301,7 @@ function defaultMcpDenied(result, req, res) {
1162
1301
  });
1163
1302
  return;
1164
1303
  }
1165
- const status = !result.identityVerified ? 401 : 403;
1304
+ const { status, code: denyCode } = denyStatusFor(result, "INSUFFICIENT_ACCESS");
1166
1305
  res.setHeader("X-Astra-Gateway-Mode", "enforced");
1167
1306
  res.status(status).json({
1168
1307
  jsonrpc: "2.0",
@@ -1172,9 +1311,11 @@ function defaultMcpDenied(result, req, res) {
1172
1311
  // -32000 → unauthorized (no identity resolved)
1173
1312
  // -32001 → insufficient access (identity OK, policy denied)
1174
1313
  // -32002 → merchant verification misconfigured (handled above)
1314
+ // -32003 → verification temporarily unavailable (handled above)
1175
1315
  code: !result.identityVerified ? -32e3 : -32001,
1176
1316
  message: result.denialReasons?.[0] ?? "Access denied",
1177
1317
  data: {
1318
+ code: denyCode,
1178
1319
  guidance: result.guidance,
1179
1320
  // Round-10: aggregated per-dimension detail + correlation handle.
1180
1321
  failures: result.failures,
@@ -1302,6 +1443,9 @@ function createMcpMiddleware(options) {
1302
1443
  resolved_action: pdlss.action
1303
1444
  });
1304
1445
  }
1446
+ if (!pdlss.purpose && !shouldEnforce) {
1447
+ return next();
1448
+ }
1305
1449
  if (!pdlss.purpose) {
1306
1450
  const id = req.body?.id ?? null;
1307
1451
  res.status(400).json({
@@ -1321,6 +1465,9 @@ function createMcpMiddleware(options) {
1321
1465
  }
1322
1466
  const counterpartyUrl = config.counterpartyUrl || `${req.protocol}://${req.get("host")}${req.path}`;
1323
1467
  const shouldRecordDecisions = recordDecisions !== false;
1468
+ const forwardedFor = req.headers["x-forwarded-for"];
1469
+ const forwardedForStr = Array.isArray(forwardedFor) ? forwardedFor.join(", ") : forwardedFor;
1470
+ const sourceIp = req.ip ?? (forwardedForStr ? forwardedForStr.split(",")[0].trim() : void 0);
1324
1471
  const verificationRequest = {
1325
1472
  credentials,
1326
1473
  purpose: pdlss.purpose,
@@ -1335,9 +1482,10 @@ function createMcpMiddleware(options) {
1335
1482
  counterpartyType: config.counterpartyType || "mcp_server",
1336
1483
  enableRuntimeChallenge,
1337
1484
  callerMetadata: {
1338
- sourceIp: req.ip,
1485
+ sourceIp,
1339
1486
  userAgent: req.headers["user-agent"],
1340
1487
  host: req.headers.host,
1488
+ ...forwardedForStr && { forwardedFor: forwardedForStr },
1341
1489
  // Capture EVERY inbound header (parity with the express adapter —
1342
1490
  // MCP-only merchants have no edge to do it upstream) plus the
1343
1491
  // CDN-derived connection block. Local string work, secrets
@@ -1360,6 +1508,21 @@ function createMcpMiddleware(options) {
1360
1508
  result.stepUpOutcome = outcome;
1361
1509
  }
1362
1510
  }
1511
+ if (!result.policyAllowed && isVerificationUnavailable(result) && (!shouldEnforce || isReadOnlyMcpMethod(parsed) && !credentials.astraId && !credentials.jwt)) {
1512
+ res.setHeader(VERIFICATION_STATUS_HEADER, "unavailable");
1513
+ return next();
1514
+ }
1515
+ if (!shouldEnforce) {
1516
+ if (config.setPassThroughHeader) {
1517
+ res.setHeader("X-Astra-Gateway-Mode", "enforced");
1518
+ res.setHeader("X-Astra-Gateway-Reason", "evaluated-not-enforced");
1519
+ }
1520
+ if (shouldRecordDecisions && sessionId) {
1521
+ recordDecision(config, sessionId, "granted").catch(() => {
1522
+ });
1523
+ }
1524
+ return next();
1525
+ }
1363
1526
  if (!result.policyAllowed) {
1364
1527
  if (shouldRecordDecisions && sessionId) {
1365
1528
  recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
@@ -1379,17 +1542,6 @@ function createMcpMiddleware(options) {
1379
1542
  onDenied(result, req, res);
1380
1543
  return;
1381
1544
  }
1382
- if (!shouldEnforce) {
1383
- if (config.setPassThroughHeader) {
1384
- res.setHeader("X-Astra-Gateway-Mode", "enforced");
1385
- res.setHeader("X-Astra-Gateway-Reason", "evaluated-not-enforced");
1386
- }
1387
- if (shouldRecordDecisions && sessionId) {
1388
- recordDecision(config, sessionId, "granted").catch(() => {
1389
- });
1390
- }
1391
- return next();
1392
- }
1393
1545
  if (effectiveAstraId) {
1394
1546
  res.setHeader(
1395
1547
  MCP_VERIFIED_HOP_HEADER,
@@ -1430,14 +1582,6 @@ function createMcpMiddleware(options) {
1430
1582
  verifiedAt: /* @__PURE__ */ new Date(),
1431
1583
  correlationId
1432
1584
  };
1433
- const catchUrls = config.apiBaseUrl ? await getWellKnownUrls(config.apiBaseUrl).catch(() => void 0) : void 0;
1434
- if (catchUrls) {
1435
- result.guidance = {
1436
- message: `Middleware threw ${errorClass} \u2014 failing closed`,
1437
- registrationUrl: catchUrls.registrationUrl,
1438
- documentationUrl: catchUrls.documentationUrl
1439
- };
1440
- }
1441
1585
  dedupeFailures(result);
1442
1586
  return onDenied(result, req, res);
1443
1587
  }