@astrasyncai/verification-gateway 5.11.0 → 5.12.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 +359 -30
  5. package/dist/adapters/express.js.map +1 -1
  6. package/dist/adapters/express.mjs +359 -30
  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 +168 -41
  11. package/dist/adapters/mcp.js.map +1 -1
  12. package/dist/adapters/mcp.mjs +168 -41
  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 +352 -13
  17. package/dist/adapters/nextjs.js.map +1 -1
  18. package/dist/adapters/nextjs.mjs +352 -13
  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 +100 -8
  23. package/dist/adapters/sdk.js.map +1 -1
  24. package/dist/adapters/sdk.mjs +100 -8
  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 +284 -7
  31. package/dist/bin/astrasync-codex-hook.js +284 -7
  32. package/dist/bin/astrasync-guard.js +284 -7
  33. package/dist/bin/astrasync.js +297 -9
  34. package/dist/browser/background.js +284 -7
  35. package/dist/browser/background.js.map +1 -1
  36. package/dist/browser/background.mjs +284 -7
  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 +284 -7
  43. package/dist/codex/index.js.map +1 -1
  44. package/dist/codex/index.mjs +284 -7
  45. package/dist/codex/index.mjs.map +1 -1
  46. package/dist/cursor/extension.js +284 -7
  47. package/dist/cursor/extension.js.map +1 -1
  48. package/dist/cursor/extension.mjs +284 -7
  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 +337 -55
  59. package/dist/edge-core/index.js.map +1 -1
  60. package/dist/edge-core/index.mjs +334 -55
  61. package/dist/edge-core/index.mjs.map +1 -1
  62. package/dist/gateway/gateway.js +284 -7
  63. package/dist/gateway/gateway.js.map +1 -1
  64. package/dist/gateway/gateway.mjs +284 -7
  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 -5
  69. package/dist/index.d.ts +118 -5
  70. package/dist/index.js +486 -63
  71. package/dist/index.js.map +1 -1
  72. package/dist/index.mjs +480 -63
  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 -4
  93. package/dist/verify.d.ts +35 -4
  94. package/dist/verify.js +130 -16
  95. package/dist/verify.js.map +1 -1
  96. package/dist/verify.mjs +129 -16
  97. package/dist/verify.mjs.map +1 -1
  98. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -19,7 +19,7 @@ function getTrustLevel(score) {
19
19
  }
20
20
 
21
21
  // src/version.ts
22
- var SDK_VERSION = "5.11.0";
22
+ var SDK_VERSION = "5.12.0";
23
23
 
24
24
  // src/http.ts
25
25
  var SDK_USER_AGENT = `astrasync-sdk/${SDK_VERSION}`;
@@ -98,6 +98,59 @@ function getCachedWellKnownUrls(apiBaseUrl) {
98
98
  return cache.get(apiBaseUrl)?.data;
99
99
  }
100
100
 
101
+ // src/failure-posture.ts
102
+ var VERIFICATION_STATUS_HEADER = "X-Astra-Verification";
103
+ var DEFAULT_UNAVAILABLE_RETRY_AFTER_S = 30;
104
+ var REJECTED_IDENTITY_DIMENSIONS = /* @__PURE__ */ new Set([
105
+ "identity.invalid_agent_id",
106
+ "identity.unknown_agent",
107
+ "agent.lookup"
108
+ ]);
109
+ 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.";
110
+ var UNAVAILABLE_DIMENSIONS = /* @__PURE__ */ new Set([
111
+ "verify_access.api_error",
112
+ "middleware.internal_error"
113
+ ]);
114
+ function isVerificationUnavailable(result) {
115
+ if (result.misconfigured) return false;
116
+ return (result.failures ?? []).some((f) => UNAVAILABLE_DIMENSIONS.has(f.dimension));
117
+ }
118
+ function unavailableRetryAfterSeconds(result) {
119
+ if (typeof result.retryAfterMs === "number" && Number.isFinite(result.retryAfterMs)) {
120
+ return Math.max(1, Math.ceil(result.retryAfterMs / 1e3));
121
+ }
122
+ return DEFAULT_UNAVAILABLE_RETRY_AFTER_S;
123
+ }
124
+ function isAnonymousRead(method, credentials) {
125
+ const m = (method ?? "").toUpperCase();
126
+ if (m !== "GET" && m !== "HEAD") return false;
127
+ return !credentials.astraId && !credentials.jwt;
128
+ }
129
+ function buildVerificationUnavailableBody(result) {
130
+ return {
131
+ success: false,
132
+ error: {
133
+ code: "VERIFICATION_UNAVAILABLE",
134
+ 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.",
135
+ retryable: true,
136
+ retryAfterSeconds: unavailableRetryAfterSeconds(result),
137
+ ...result.failures && { failures: result.failures },
138
+ ...result.correlationId && { correlationId: result.correlationId },
139
+ guidance: {
140
+ message: "Retry the request after the Retry-After interval, with exponential backoff; the condition is transient."
141
+ }
142
+ }
143
+ };
144
+ }
145
+ function isRejectedIdentityClaim(result) {
146
+ return (result.failures ?? []).some((f) => REJECTED_IDENTITY_DIMENSIONS.has(f.dimension));
147
+ }
148
+ function denyStatusFor(result, policyDeniedCode) {
149
+ if (isRejectedIdentityClaim(result)) return { status: 403, code: "AGENT_IDENTITY_REJECTED" };
150
+ if (result.identityVerified) return { status: 403, code: policyDeniedCode };
151
+ return { status: 401, code: "UNAUTHORIZED" };
152
+ }
153
+
101
154
  // src/verify.ts
102
155
  var DEFAULT_CONFIG = {
103
156
  apiBaseUrl: "https://astrasync.ai/api",
@@ -349,11 +402,44 @@ function createGuidanceResponse(_config, reason, options = {}) {
349
402
  // coercing the absent recommendation to a policy 'deny'. Misconfig is
350
403
  // deterministic and intentionally NOT marked retryable.
351
404
  ...isApiError && { retryable: true },
405
+ ...options.httpStatus !== void 0 && { httpStatus: options.httpStatus },
406
+ ...options.retryAfterMs !== void 0 && { retryAfterMs: options.retryAfterMs },
352
407
  correlationId: options.correlationId,
353
408
  attemptId: options.attemptId,
354
409
  verifiedAt: /* @__PURE__ */ new Date()
355
410
  };
356
411
  }
412
+ async function readJsonSafe(response) {
413
+ try {
414
+ const parsed = await response.json();
415
+ return parsed && typeof parsed === "object" ? parsed : {};
416
+ } catch {
417
+ return {};
418
+ }
419
+ }
420
+ function rejectedIdentityGuidance(failures) {
421
+ const f = failures?.find((x) => REJECTED_IDENTITY_DIMENSIONS.has(x.dimension));
422
+ if (!f) return void 0;
423
+ if (f.dimension === "agent.lookup") {
424
+ return `The presented agent identifier is not registered with AstraSync. ${UNKNOWN_AGENT_GUIDANCE}`;
425
+ }
426
+ return f.guidance ? `${f.message} ${f.guidance}` : f.message;
427
+ }
428
+ var MAX_RETRY_AFTER_MS = 15 * 60 * 1e3;
429
+ function parseRetryAfterMs(response) {
430
+ const raw = response.headers?.get?.("retry-after");
431
+ if (!raw) return void 0;
432
+ const trimmed = raw.trim();
433
+ let ms;
434
+ if (/^\d+(\.\d+)?$/.test(trimmed)) {
435
+ ms = Number(trimmed) * 1e3;
436
+ } else {
437
+ const at = Date.parse(trimmed);
438
+ if (Number.isNaN(at)) return void 0;
439
+ ms = at - Date.now();
440
+ }
441
+ return Math.min(MAX_RETRY_AFTER_MS, Math.max(0, Math.round(ms)));
442
+ }
357
443
  async function callVerifyAccessAPI(config, request) {
358
444
  const { credentials, ...requestData } = request;
359
445
  const body = {
@@ -410,7 +496,8 @@ async function callVerifyAccessAPI(config, request) {
410
496
  body: JSON.stringify(body),
411
497
  signal: AbortSignal.timeout(config.verifyTimeoutMs ?? 1e4)
412
498
  });
413
- const data = await response.json();
499
+ const data = await readJsonSafe(response);
500
+ const retryAfterMs = parseRetryAfterMs(response);
414
501
  if (response.status === 410) {
415
502
  return {
416
503
  success: true,
@@ -448,6 +535,23 @@ async function callVerifyAccessAPI(config, request) {
448
535
  }
449
536
  };
450
537
  }
538
+ if (response.status === 404 && credentials.astraId && (data?.error === "AGENT_NOT_FOUND" || typeof data?.message === "string" && /agent/i.test(data.message))) {
539
+ return {
540
+ success: true,
541
+ attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0,
542
+ access: {
543
+ allowed: false,
544
+ reason: "unknown_agent",
545
+ failures: [
546
+ {
547
+ dimension: "identity.unknown_agent",
548
+ message: "The presented agent identifier is not registered with AstraSync.",
549
+ guidance: UNKNOWN_AGENT_GUIDANCE
550
+ }
551
+ ]
552
+ }
553
+ };
554
+ }
451
555
  if (!response.ok) {
452
556
  if (response.status === 401) {
453
557
  return {
@@ -462,9 +566,11 @@ async function callVerifyAccessAPI(config, request) {
462
566
  }
463
567
  return {
464
568
  success: false,
465
- error: data.message || data.error || `API returned ${response.status}`,
569
+ error: typeof data?.message === "string" && data.message || typeof data?.error === "string" && data.error || `API returned ${response.status}`,
466
570
  correlationId: typeof data?.correlationId === "string" ? data.correlationId : void 0,
467
- attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0
571
+ attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0,
572
+ httpStatus: response.status,
573
+ ...retryAfterMs !== void 0 && { retryAfterMs }
468
574
  };
469
575
  }
470
576
  return data;
@@ -536,12 +642,14 @@ async function verify(config, request, options) {
536
642
  correlationId: apiResponse.correlationId,
537
643
  attemptId: apiResponse.attemptId,
538
644
  remediation: apiResponse.remediation,
645
+ httpStatus: apiResponse.httpStatus,
646
+ retryAfterMs: apiResponse.retryAfterMs,
539
647
  urls
540
648
  });
541
649
  }
542
650
  if (!apiResponse.access?.allowed) {
543
651
  const aggregatedFailures = apiResponse.access?.failures;
544
- const idVerifiedFromBackend = apiResponse.verificationContext?.idVerified === true;
652
+ const idVerifiedFromBackend = apiResponse.verificationContext?.idVerified === true && !aggregatedFailures?.some((f) => REJECTED_IDENTITY_DIMENSIONS.has(f.dimension));
545
653
  const result2 = {
546
654
  identityVerified: idVerifiedFromBackend,
547
655
  policyAllowed: false,
@@ -553,7 +661,10 @@ async function verify(config, request, options) {
553
661
  requiresStepUp: apiResponse.access?.requiresStepUp,
554
662
  requiresApproval: apiResponse.access?.requiresApproval,
555
663
  guidance: {
556
- message: apiResponse.access?.reason || "Access denied by PDLSS policy",
664
+ // 5.12.0: a presented-but-rejected id (malformed / unknown) carries
665
+ // actionable registration guidance on its failure — surface it
666
+ // instead of the bare machine reason ('unknown_agent').
667
+ message: rejectedIdentityGuidance(aggregatedFailures) ?? (apiResponse.access?.reason || "Access denied by PDLSS policy"),
557
668
  registrationUrl: urls?.registrationUrl ?? "",
558
669
  documentationUrl: urls?.documentationUrl ?? "",
559
670
  // 5.1.0: anonymous callers land on this branch — steer MCP-hosted
@@ -653,7 +764,7 @@ async function verify(config, request, options) {
653
764
  registrationUrl: urls?.registrationUrl ?? "",
654
765
  documentationUrl: urls?.documentationUrl ?? ""
655
766
  };
656
- } else if (result.recommendation === "step_up_required") {
767
+ } else if (result.recommendation === "step_up_required" && result.identityVerified) {
657
768
  result.requiresStepUp = true;
658
769
  result.denialReasons = serverDenialReasons ?? result.recommendationReasons ?? ["Step-up verification required"];
659
770
  }
@@ -676,7 +787,7 @@ async function recordDecision(config, sessionId, decision, reason) {
676
787
  }).catch(() => {
677
788
  });
678
789
  }
679
- async function reportAttempt(config, report) {
790
+ async function reportAttempt(config, report, options = {}) {
680
791
  const apiBaseUrl = config.apiBaseUrl || DEFAULT_CONFIG.apiBaseUrl;
681
792
  const headers = { "Content-Type": "application/json" };
682
793
  if (config.apiKey) {
@@ -687,13 +798,37 @@ async function reportAttempt(config, report) {
687
798
  ...report,
688
799
  ...!report.counterpartyId && config.counterpartyId && { counterpartyId: config.counterpartyId }
689
800
  };
690
- await sdkFetch(`${apiBaseUrl}/agents/verify-access/attempt-report`, {
691
- method: "POST",
692
- headers,
693
- body: JSON.stringify(body)
694
- }).catch(() => {
695
- });
801
+ const url = `${apiBaseUrl}/agents/verify-access/attempt-report`;
802
+ const init = { method: "POST", headers, body: JSON.stringify(body) };
803
+ const fail = (failure) => {
804
+ try {
805
+ options.onFailure?.(failure);
806
+ } catch {
807
+ }
808
+ };
809
+ let response;
810
+ try {
811
+ response = await sdkFetch(url, init);
812
+ } catch (err) {
813
+ fail({ error: err instanceof Error ? err.message : String(err), retried: false });
814
+ return;
815
+ }
816
+ if (response.ok) return;
817
+ if (response.status !== 429 && response.status < 500) {
818
+ fail({ status: response.status, retried: false });
819
+ return;
820
+ }
821
+ const delayMs = Math.min(ATTEMPT_REPORT_MAX_RETRY_DELAY_MS, parseRetryAfterMs(response) ?? 500);
822
+ const timer = setTimeout(() => {
823
+ sdkFetch(url, init).then((retry) => {
824
+ if (!retry.ok) fail({ status: retry.status, retried: true });
825
+ }).catch((err) => {
826
+ fail({ error: err instanceof Error ? err.message : String(err), retried: true });
827
+ });
828
+ }, delayMs);
829
+ timer.unref?.();
696
830
  }
831
+ var ATTEMPT_REPORT_MAX_RETRY_DELAY_MS = 2e3;
697
832
  async function fetchRoutes(config, counterpartyId) {
698
833
  if (!counterpartyId) return null;
699
834
  const headers = { "Content-Type": "application/json" };
@@ -723,7 +858,10 @@ async function reportUnregisteredAttempt(config, data) {
723
858
  };
724
859
  await sdkFetch(`${apiBaseUrl}/verification-activity/unregistered-attempt`, {
725
860
  method: "POST",
726
- headers: { "Content-Type": "application/json" },
861
+ headers: {
862
+ "Content-Type": "application/json",
863
+ ...config.apiKey && { Authorization: `Bearer ${config.apiKey}` }
864
+ },
727
865
  body: JSON.stringify(body)
728
866
  }).catch(() => {
729
867
  });
@@ -745,7 +883,11 @@ async function quickVerify(config, credentials) {
745
883
  return {
746
884
  identityVerified: result.identityVerified,
747
885
  policyAllowed: result.policyAllowed,
748
- reason: result.denialReasons?.[0]
886
+ reason: result.denialReasons?.[0],
887
+ ...isVerificationUnavailable(result) && {
888
+ unavailable: true,
889
+ retryAfterMs: unavailableRetryAfterSeconds(result) * 1e3
890
+ }
749
891
  };
750
892
  }
751
893
 
@@ -1493,7 +1635,10 @@ var PLATFORM_AGENT_SIGNATURES = [
1493
1635
  {
1494
1636
  vendor: "chatgpt",
1495
1637
  displayName: "OpenAI ChatGPT",
1496
- uaPatterns: [/chatgpt/i, /openai/i, /gpt-?\d/i],
1638
+ // GPTBot (training crawler) and OAI-SearchBot (ChatGPT search) are
1639
+ // OpenAI's declared crawler UAs — `gpt-?\d` never matched them, so they
1640
+ // classified as unattributed crawlers (round-27.4 W5).
1641
+ uaPatterns: [/chatgpt/i, /openai/i, /gpt-?\d/i, /\bGPTBot\b/i, /\bOAI-SearchBot\b/i],
1497
1642
  agentCardPatterns: [/openai\.com/i, /chatgpt\.com/i],
1498
1643
  apiKeyFormatPatterns: [/^sk-proj/i, /^sk-svcacct/i],
1499
1644
  headerPatterns: [/^openai-organization/i, /^openai-project/i, /^openai-version/i]
@@ -1628,6 +1773,215 @@ var BROWSER_UA_PATTERN = /mozilla\/\d/i;
1628
1773
  var _patterns = { STATIC_EXTENSIONS, BOT_UA_PATTERN, BROWSER_UA_PATTERN, SELF_UA_PATTERN };
1629
1774
 
1630
1775
  // src/edge-core/events.ts
1776
+ async function withBudget(promise, ms) {
1777
+ let timer;
1778
+ try {
1779
+ return await Promise.race([
1780
+ promise,
1781
+ new Promise((resolve) => {
1782
+ timer = setTimeout(() => resolve(void 0), ms);
1783
+ })
1784
+ ]);
1785
+ } catch {
1786
+ return void 0;
1787
+ } finally {
1788
+ if (timer) clearTimeout(timer);
1789
+ }
1790
+ }
1791
+ var destKeyOf = (d) => `${d.apiBaseUrl}\0${d.apiKey ?? ""}\0${d.counterparty}\0${d.adapter ?? ""}`;
1792
+ var BeaconBuffer = class {
1793
+ constructor(options = {}) {
1794
+ this.queue = [];
1795
+ this.droppedSinceLast = 0;
1796
+ this.droppedTotal = 0;
1797
+ this.deliveredTotal = 0;
1798
+ this.backoffUntil = 0;
1799
+ this.collapse = /* @__PURE__ */ new Map();
1800
+ /** Keys the backend answered 401 for — later batches go unauthenticated. */
1801
+ this.rejectedKeys = /* @__PURE__ */ new Set();
1802
+ this.opts = {
1803
+ maxItems: options.maxItems ?? 1e3,
1804
+ maxBatch: options.maxBatch ?? 100,
1805
+ maxAttempts: options.maxAttempts ?? 3,
1806
+ collapseWindowMs: options.collapseWindowMs ?? 6e4,
1807
+ collapseMaxKeys: options.collapseMaxKeys ?? 1e3,
1808
+ requestTimeoutMs: options.requestTimeoutMs ?? 1e4,
1809
+ retryBackoffMs: options.retryBackoffMs ?? 1e3,
1810
+ now: options.now ?? Date.now
1811
+ };
1812
+ }
1813
+ /**
1814
+ * Queue one beacon. With a `collapseKey`, a repeat inside the window is
1815
+ * merged into a held aggregate (sent when the window closes) instead of
1816
+ * becoming its own item.
1817
+ */
1818
+ enqueue(dest, item, collapseKey) {
1819
+ const now = this.opts.now();
1820
+ if (collapseKey) {
1821
+ const entry = this.collapse.get(collapseKey);
1822
+ if (entry && now - entry.firstSeenAt < this.opts.collapseWindowMs) {
1823
+ for (const target of [entry.first, entry.held]) {
1824
+ if (target && !target.inFlight && this.queue.includes(target)) {
1825
+ target.item.repeatCount = (target.item.repeatCount ?? 1) + 1;
1826
+ return;
1827
+ }
1828
+ }
1829
+ const held = this.push(
1830
+ dest,
1831
+ { ...item, repeatCount: 1 },
1832
+ entry.firstSeenAt + this.opts.collapseWindowMs
1833
+ );
1834
+ entry.held = held;
1835
+ return;
1836
+ }
1837
+ this.collapse.delete(collapseKey);
1838
+ if (this.collapse.size >= this.opts.collapseMaxKeys) {
1839
+ const oldest = this.collapse.keys().next().value;
1840
+ if (oldest !== void 0) this.collapse.delete(oldest);
1841
+ }
1842
+ const fresh = { firstSeenAt: now };
1843
+ this.collapse.set(collapseKey, fresh);
1844
+ fresh.first = this.push(dest, item, now);
1845
+ return;
1846
+ }
1847
+ this.push(dest, item, now);
1848
+ }
1849
+ /**
1850
+ * Deliver every eligible queued item. Concurrent callers share ONE
1851
+ * in-flight drain (it survives the caller's budget and a Lambda freeze),
1852
+ * so nothing in flight is ever sent twice; items queued while it runs are
1853
+ * picked up by the same drain.
1854
+ */
1855
+ flush() {
1856
+ if (!this.inFlight) {
1857
+ this.inFlight = this.drain().finally(() => {
1858
+ this.inFlight = void 0;
1859
+ });
1860
+ }
1861
+ return this.inFlight;
1862
+ }
1863
+ /** Counters for tests and diagnostics. */
1864
+ stats() {
1865
+ return {
1866
+ queueDepth: this.queue.length,
1867
+ droppedSinceLast: this.droppedSinceLast,
1868
+ droppedTotal: this.droppedTotal,
1869
+ deliveredTotal: this.deliveredTotal,
1870
+ flushing: this.inFlight !== void 0
1871
+ };
1872
+ }
1873
+ push(dest, item, eligibleAt) {
1874
+ while (this.queue.length >= this.opts.maxItems) this.dropOldest();
1875
+ const entry = {
1876
+ dest,
1877
+ destKey: destKeyOf(dest),
1878
+ item,
1879
+ attempts: 0,
1880
+ eligibleAt,
1881
+ inFlight: false
1882
+ };
1883
+ this.queue.push(entry);
1884
+ return entry;
1885
+ }
1886
+ dropOldest() {
1887
+ if (this.queue.shift()) this.countDropped(1);
1888
+ }
1889
+ countDropped(n) {
1890
+ this.droppedSinceLast += n;
1891
+ this.droppedTotal += n;
1892
+ }
1893
+ async drain() {
1894
+ for (; ; ) {
1895
+ const now = this.opts.now();
1896
+ if (now < this.backoffUntil) return;
1897
+ const batch = this.takeBatch(now);
1898
+ if (batch.length === 0) return;
1899
+ const ok = await this.send(batch);
1900
+ if (!ok) return;
1901
+ }
1902
+ }
1903
+ /** Oldest eligible item + up to maxBatch-1 more for the same destination. */
1904
+ takeBatch(now) {
1905
+ const first = this.queue.find((q) => q.eligibleAt <= now);
1906
+ if (!first) return [];
1907
+ const batch = [];
1908
+ const rest = [];
1909
+ for (const q of this.queue) {
1910
+ if (batch.length < this.opts.maxBatch && q.destKey === first.destKey && q.eligibleAt <= now) {
1911
+ q.inFlight = true;
1912
+ batch.push(q);
1913
+ } else {
1914
+ rest.push(q);
1915
+ }
1916
+ }
1917
+ this.queue = rest;
1918
+ return batch;
1919
+ }
1920
+ /** POST one batch. Returns false when the drain should pause (backoff). */
1921
+ async send(batch) {
1922
+ const dest = batch[0].dest;
1923
+ const useKey = dest.apiKey && !this.rejectedKeys.has(dest.apiKey) ? dest.apiKey : void 0;
1924
+ const reportedDropped = this.droppedSinceLast;
1925
+ const meta = {
1926
+ droppedSinceLast: reportedDropped,
1927
+ queueDepth: this.queue.length,
1928
+ adapter: dest.adapter ?? "edge-core",
1929
+ version: SDK_VERSION
1930
+ };
1931
+ const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
1932
+ const response = await withBudget(
1933
+ sdkFetch(`${dest.apiBaseUrl}/verification-activity/edge-events`, {
1934
+ method: "POST",
1935
+ headers: {
1936
+ "Content-Type": "application/json",
1937
+ ...useKey && { Authorization: `Bearer ${useKey}` }
1938
+ },
1939
+ body: JSON.stringify({ events: batch.map((q) => q.item), meta }),
1940
+ ...controller && { signal: controller.signal }
1941
+ }),
1942
+ this.opts.requestTimeoutMs
1943
+ );
1944
+ if (!response) controller?.abort();
1945
+ const status = response?.status;
1946
+ for (const q of batch) q.inFlight = false;
1947
+ if (status !== void 0 && status >= 200 && status < 300) {
1948
+ this.droppedSinceLast = Math.max(0, this.droppedSinceLast - reportedDropped);
1949
+ this.deliveredTotal += batch.length;
1950
+ return true;
1951
+ }
1952
+ if (status === 401 && useKey) {
1953
+ this.rejectedKeys.add(useKey);
1954
+ this.requeue(batch, false);
1955
+ return true;
1956
+ }
1957
+ if (status === 429) {
1958
+ const retryAfter = Number(response?.headers.get("retry-after"));
1959
+ const waitMs = Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(retryAfter, 120) * 1e3 : 3e4;
1960
+ this.backoffUntil = this.opts.now() + waitMs;
1961
+ this.requeue(batch, false);
1962
+ return false;
1963
+ }
1964
+ if (status !== void 0 && status >= 400 && status < 500) {
1965
+ this.countDropped(batch.length);
1966
+ return true;
1967
+ }
1968
+ this.backoffUntil = this.opts.now() + this.opts.retryBackoffMs;
1969
+ this.requeue(batch, true);
1970
+ return false;
1971
+ }
1972
+ /** Put a failed batch back at the head of the queue (order preserved). */
1973
+ requeue(batch, countAttempt) {
1974
+ const keep = [];
1975
+ for (const q of batch) {
1976
+ if (countAttempt) q.attempts += 1;
1977
+ if (q.attempts >= this.opts.maxAttempts) this.countDropped(1);
1978
+ else keep.push(q);
1979
+ }
1980
+ this.queue = [...keep, ...this.queue];
1981
+ while (this.queue.length > this.opts.maxItems) this.dropOldest();
1982
+ }
1983
+ };
1984
+ var beaconBuffer = new BeaconBuffer();
1631
1985
  function sampled(rate, random = Math.random) {
1632
1986
  if (rate <= 0) return false;
1633
1987
  if (rate >= 1) return true;
@@ -1804,12 +2158,19 @@ function defaultOnDenied(result, _req, res) {
1804
2158
  });
1805
2159
  return;
1806
2160
  }
1807
- const statusCode = !result.identityVerified ? 401 : 403;
2161
+ if (isVerificationUnavailable(result)) {
2162
+ res.setHeader("X-Astra-Gateway-Mode", "enforced");
2163
+ res.setHeader(VERIFICATION_STATUS_HEADER, "unavailable");
2164
+ res.setHeader("Retry-After", String(unavailableRetryAfterSeconds(result)));
2165
+ res.status(503).json(buildVerificationUnavailableBody(result));
2166
+ return;
2167
+ }
2168
+ const { status: statusCode, code } = denyStatusFor(result, "INSUFFICIENT_ACCESS");
1808
2169
  res.setHeader("X-Astra-Gateway-Mode", "enforced");
1809
2170
  res.status(statusCode).json({
1810
2171
  success: false,
1811
2172
  error: {
1812
- code: !result.identityVerified ? "UNAUTHORIZED" : "INSUFFICIENT_ACCESS",
2173
+ code,
1813
2174
  message: result.denialReasons?.[0] || "Access denied",
1814
2175
  guidance: result.guidance,
1815
2176
  // Round-10: aggregated per-dimension detail + correlation handle.
@@ -2057,6 +2418,21 @@ function createMiddleware(options) {
2057
2418
  result.stepUpOutcome = outcome;
2058
2419
  }
2059
2420
  }
2421
+ if (!result.policyAllowed && isVerificationUnavailable(result) && (!shouldEnforce || isAnonymousRead(req.method, credentials))) {
2422
+ res.setHeader(VERIFICATION_STATUS_HEADER, "unavailable");
2423
+ return next();
2424
+ }
2425
+ if (!shouldEnforce) {
2426
+ if (config.setPassThroughHeader) {
2427
+ res.setHeader("X-Astra-Gateway-Mode", "enforced");
2428
+ res.setHeader("X-Astra-Gateway-Reason", "evaluated-not-enforced");
2429
+ }
2430
+ if (shouldRecordDecisions && sessionId) {
2431
+ recordDecision(config, sessionId, "granted").catch(() => {
2432
+ });
2433
+ }
2434
+ return next();
2435
+ }
2060
2436
  if (!result.policyAllowed) {
2061
2437
  if (shouldRecordDecisions && sessionId) {
2062
2438
  recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
@@ -2076,17 +2452,6 @@ function createMiddleware(options) {
2076
2452
  onDenied(result, req, res);
2077
2453
  return;
2078
2454
  }
2079
- if (!shouldEnforce) {
2080
- if (config.setPassThroughHeader) {
2081
- res.setHeader("X-Astra-Gateway-Mode", "enforced");
2082
- res.setHeader("X-Astra-Gateway-Reason", "evaluated-not-enforced");
2083
- }
2084
- if (shouldRecordDecisions && sessionId) {
2085
- recordDecision(config, sessionId, "granted").catch(() => {
2086
- });
2087
- }
2088
- return next();
2089
- }
2090
2455
  if (routeConfig.minTrustScore && result.agent) {
2091
2456
  if (result.agent.trustScore < routeConfig.minTrustScore) {
2092
2457
  const trustFailure = {
@@ -2142,14 +2507,6 @@ function createMiddleware(options) {
2142
2507
  verifiedAt: /* @__PURE__ */ new Date(),
2143
2508
  correlationId
2144
2509
  };
2145
- const catchUrls = config.apiBaseUrl ? await getWellKnownUrls(config.apiBaseUrl).catch(() => void 0) : void 0;
2146
- if (catchUrls) {
2147
- result.guidance = {
2148
- message: `Middleware threw ${errorClass} \u2014 failing closed`,
2149
- registrationUrl: catchUrls.registrationUrl,
2150
- documentationUrl: catchUrls.documentationUrl
2151
- };
2152
- }
2153
2510
  dedupeFailures(result);
2154
2511
  return onDenied(result, req, res);
2155
2512
  }
@@ -2569,6 +2926,20 @@ function createMiddleware2(options) {
2569
2926
  }
2570
2927
  const approvalRequired = result.policyAllowed && requiresHumanApproval(result);
2571
2928
  if (approvalRequired) annotateApprovalRequired(result);
2929
+ if (!result.policyAllowed && isVerificationUnavailable(result)) {
2930
+ if (isAnonymousRead(request.method, credentials)) {
2931
+ const passThrough = NextResponse.next();
2932
+ passThrough.headers.set(VERIFICATION_STATUS_HEADER, "unavailable");
2933
+ return passThrough;
2934
+ }
2935
+ return NextResponse.json(buildVerificationUnavailableBody(result), {
2936
+ status: 503,
2937
+ headers: {
2938
+ "Retry-After": String(unavailableRetryAfterSeconds(result)),
2939
+ [VERIFICATION_STATUS_HEADER]: "unavailable"
2940
+ }
2941
+ });
2942
+ }
2572
2943
  if (!result.policyAllowed || approvalRequired) {
2573
2944
  if (result.misconfigured && pathname.startsWith("/api/")) {
2574
2945
  return NextResponse.json(
@@ -2586,13 +2957,12 @@ function createMiddleware2(options) {
2586
2957
  );
2587
2958
  }
2588
2959
  if (pathname.startsWith("/api/")) {
2960
+ const denyStatus = denyStatusFor(result, "POLICY_DENIED");
2589
2961
  return NextResponse.json(
2590
2962
  {
2591
2963
  success: false,
2592
2964
  error: {
2593
- // Round-18 G4: 401 → identity missing (re-auth); 403 → identity
2594
- // OK, policy denied (update PDLSS / step up).
2595
- code: !result.identityVerified ? "UNAUTHORIZED" : "POLICY_DENIED",
2965
+ code: denyStatus.code,
2596
2966
  message: result.denialReasons?.[0] || "Access denied",
2597
2967
  guidance: result.guidance,
2598
2968
  failures: result.failures,
@@ -2602,7 +2972,7 @@ function createMiddleware2(options) {
2602
2972
  }
2603
2973
  },
2604
2974
  {
2605
- status: !result.identityVerified ? 401 : 403,
2975
+ status: denyStatus.status,
2606
2976
  ...stepUpHoldOutcome && {
2607
2977
  headers: { "X-Astra-Step-Up": stepUpHoldOutcome }
2608
2978
  }
@@ -2729,6 +3099,12 @@ var VerificationGatewayClient = class {
2729
3099
  * fulfil against it only once the order is settled / pending_merchant. An
2730
3100
  * explicit `buyerEmail` (here or on the bridge handoff body) wins over the
2731
3101
  * account default.
3102
+ *
3103
+ * 5.12.0 approval binding: pass the bridge handoff body's `attemptId` (and
3104
+ * the same `checkoutSessionId`) so a step-up approval minted for this cart
3105
+ * is matched to — and only to — this confirm. A held cart's approval is
3106
+ * bound to its checkout session and exact value; a confirm for any other
3107
+ * cart is held for its own approval.
2732
3108
  */
2733
3109
  async confirmCheckout(options) {
2734
3110
  const credentials = {
@@ -2748,6 +3124,7 @@ var VerificationGatewayClient = class {
2748
3124
  commercePhase: "confirm",
2749
3125
  checkoutSessionId: options.checkoutSessionId,
2750
3126
  checkoutItems: options.checkoutItems,
3127
+ ...options.attemptId && { attemptId: options.attemptId },
2751
3128
  buyerEmail: options.buyerEmail,
2752
3129
  counterpartyUrl: options.counterpartyUrl,
2753
3130
  counterpartyType: options.counterpartyType
@@ -5551,8 +5928,37 @@ function dedupeFailures2(result) {
5551
5928
  result.denialReasons = [...new Set(result.denialReasons)];
5552
5929
  }
5553
5930
  }
5931
+ function isReadOnlyMcpMethod(parsed) {
5932
+ return parsed.method !== "tools/call";
5933
+ }
5554
5934
  function defaultMcpDenied(result, req, res) {
5555
5935
  const id = req.body?.id ?? null;
5936
+ if (isVerificationUnavailable(result)) {
5937
+ const retryAfterSeconds = unavailableRetryAfterSeconds(result);
5938
+ const body = buildVerificationUnavailableBody(result).error;
5939
+ res.setHeader("X-Astra-Gateway-Mode", "enforced");
5940
+ res.setHeader(VERIFICATION_STATUS_HEADER, "unavailable");
5941
+ res.setHeader("Retry-After", String(retryAfterSeconds));
5942
+ res.status(503).json({
5943
+ jsonrpc: "2.0",
5944
+ id,
5945
+ error: {
5946
+ // -32003 → verification temporarily unavailable (retryable)
5947
+ code: -32003,
5948
+ message: body.message,
5949
+ data: {
5950
+ code: body.code,
5951
+ recommendation: "error",
5952
+ retryable: true,
5953
+ retryAfterSeconds,
5954
+ guidance: body.guidance,
5955
+ failures: result.failures,
5956
+ correlationId: result.correlationId
5957
+ }
5958
+ }
5959
+ });
5960
+ return;
5961
+ }
5556
5962
  if (result.misconfigured) {
5557
5963
  res.setHeader("X-Astra-Gateway-Mode", "enforced");
5558
5964
  res.status(503).json({
@@ -5571,7 +5977,7 @@ function defaultMcpDenied(result, req, res) {
5571
5977
  });
5572
5978
  return;
5573
5979
  }
5574
- const status = !result.identityVerified ? 401 : 403;
5980
+ const { status, code: denyCode } = denyStatusFor(result, "INSUFFICIENT_ACCESS");
5575
5981
  res.setHeader("X-Astra-Gateway-Mode", "enforced");
5576
5982
  res.status(status).json({
5577
5983
  jsonrpc: "2.0",
@@ -5581,9 +5987,11 @@ function defaultMcpDenied(result, req, res) {
5581
5987
  // -32000 → unauthorized (no identity resolved)
5582
5988
  // -32001 → insufficient access (identity OK, policy denied)
5583
5989
  // -32002 → merchant verification misconfigured (handled above)
5990
+ // -32003 → verification temporarily unavailable (handled above)
5584
5991
  code: !result.identityVerified ? -32e3 : -32001,
5585
5992
  message: result.denialReasons?.[0] ?? "Access denied",
5586
5993
  data: {
5994
+ code: denyCode,
5587
5995
  guidance: result.guidance,
5588
5996
  // Round-10: aggregated per-dimension detail + correlation handle.
5589
5997
  failures: result.failures,
@@ -5711,6 +6119,9 @@ function createMcpMiddleware(options) {
5711
6119
  resolved_action: pdlss.action
5712
6120
  });
5713
6121
  }
6122
+ if (!pdlss.purpose && !shouldEnforce) {
6123
+ return next();
6124
+ }
5714
6125
  if (!pdlss.purpose) {
5715
6126
  const id = req.body?.id ?? null;
5716
6127
  res.status(400).json({
@@ -5730,6 +6141,9 @@ function createMcpMiddleware(options) {
5730
6141
  }
5731
6142
  const counterpartyUrl = config.counterpartyUrl || `${req.protocol}://${req.get("host")}${req.path}`;
5732
6143
  const shouldRecordDecisions = recordDecisions !== false;
6144
+ const forwardedFor = req.headers["x-forwarded-for"];
6145
+ const forwardedForStr = Array.isArray(forwardedFor) ? forwardedFor.join(", ") : forwardedFor;
6146
+ const sourceIp = req.ip ?? (forwardedForStr ? forwardedForStr.split(",")[0].trim() : void 0);
5733
6147
  const verificationRequest = {
5734
6148
  credentials,
5735
6149
  purpose: pdlss.purpose,
@@ -5744,9 +6158,10 @@ function createMcpMiddleware(options) {
5744
6158
  counterpartyType: config.counterpartyType || "mcp_server",
5745
6159
  enableRuntimeChallenge,
5746
6160
  callerMetadata: {
5747
- sourceIp: req.ip,
6161
+ sourceIp,
5748
6162
  userAgent: req.headers["user-agent"],
5749
6163
  host: req.headers.host,
6164
+ ...forwardedForStr && { forwardedFor: forwardedForStr },
5750
6165
  // Capture EVERY inbound header (parity with the express adapter —
5751
6166
  // MCP-only merchants have no edge to do it upstream) plus the
5752
6167
  // CDN-derived connection block. Local string work, secrets
@@ -5769,6 +6184,21 @@ function createMcpMiddleware(options) {
5769
6184
  result.stepUpOutcome = outcome;
5770
6185
  }
5771
6186
  }
6187
+ if (!result.policyAllowed && isVerificationUnavailable(result) && (!shouldEnforce || isReadOnlyMcpMethod(parsed) && !credentials.astraId && !credentials.jwt)) {
6188
+ res.setHeader(VERIFICATION_STATUS_HEADER, "unavailable");
6189
+ return next();
6190
+ }
6191
+ if (!shouldEnforce) {
6192
+ if (config.setPassThroughHeader) {
6193
+ res.setHeader("X-Astra-Gateway-Mode", "enforced");
6194
+ res.setHeader("X-Astra-Gateway-Reason", "evaluated-not-enforced");
6195
+ }
6196
+ if (shouldRecordDecisions && sessionId) {
6197
+ recordDecision(config, sessionId, "granted").catch(() => {
6198
+ });
6199
+ }
6200
+ return next();
6201
+ }
5772
6202
  if (!result.policyAllowed) {
5773
6203
  if (shouldRecordDecisions && sessionId) {
5774
6204
  recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
@@ -5788,17 +6218,6 @@ function createMcpMiddleware(options) {
5788
6218
  onDenied(result, req, res);
5789
6219
  return;
5790
6220
  }
5791
- if (!shouldEnforce) {
5792
- if (config.setPassThroughHeader) {
5793
- res.setHeader("X-Astra-Gateway-Mode", "enforced");
5794
- res.setHeader("X-Astra-Gateway-Reason", "evaluated-not-enforced");
5795
- }
5796
- if (shouldRecordDecisions && sessionId) {
5797
- recordDecision(config, sessionId, "granted").catch(() => {
5798
- });
5799
- }
5800
- return next();
5801
- }
5802
6221
  if (effectiveAstraId) {
5803
6222
  res.setHeader(
5804
6223
  MCP_VERIFIED_HOP_HEADER,
@@ -5839,14 +6258,6 @@ function createMcpMiddleware(options) {
5839
6258
  verifiedAt: /* @__PURE__ */ new Date(),
5840
6259
  correlationId
5841
6260
  };
5842
- const catchUrls = config.apiBaseUrl ? await getWellKnownUrls(config.apiBaseUrl).catch(() => void 0) : void 0;
5843
- if (catchUrls) {
5844
- result.guidance = {
5845
- message: `Middleware threw ${errorClass} \u2014 failing closed`,
5846
- registrationUrl: catchUrls.registrationUrl,
5847
- documentationUrl: catchUrls.documentationUrl
5848
- };
5849
- }
5850
6261
  dedupeFailures2(result);
5851
6262
  return onDenied(result, req, res);
5852
6263
  }
@@ -9853,6 +10264,7 @@ export {
9853
10264
  RegistrationTimeoutError,
9854
10265
  SDK_USER_AGENT,
9855
10266
  TRUST_LEVEL_RANGES,
10267
+ VERIFICATION_STATUS_HEADER,
9856
10268
  SDK_VERSION as VERSION,
9857
10269
  _resetEdgeConfigCache,
9858
10270
  agent_exports as agent,
@@ -9860,9 +10272,11 @@ export {
9860
10272
  awaitStepUpApproval,
9861
10273
  buildGuidance,
9862
10274
  buildSdkObservedMetadata,
10275
+ buildVerificationUnavailableBody,
9863
10276
  clearCache,
9864
10277
  createMcpMiddleware,
9865
10278
  degradeToObserve,
10279
+ denyStatusFor,
9866
10280
  deriveConnectionFromHeaders,
9867
10281
  detectPlatformFingerprint,
9868
10282
  detectRuntime,
@@ -9879,6 +10293,8 @@ export {
9879
10293
  getWellKnownUrls,
9880
10294
  hasCredentials,
9881
10295
  isEdgeConfig,
10296
+ isRejectedIdentityClaim,
10297
+ isVerificationUnavailable,
9882
10298
  isVerifiedHopValidFor,
9883
10299
  matchEdgePathRule,
9884
10300
  matchPlatformSignature,
@@ -9898,6 +10314,7 @@ export {
9898
10314
  serializeVerifiedHop,
9899
10315
  setMcpMeta,
9900
10316
  transport_exports as transport,
10317
+ unavailableRetryAfterSeconds,
9901
10318
  validatePolicy as validateLocalPolicy,
9902
10319
  verify
9903
10320
  };