@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
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var edge_core_exports = {};
22
22
  __export(edge_core_exports, {
23
23
  ASAT_HEADERS: () => ASAT_HEADERS,
24
+ BeaconBuffer: () => BeaconBuffer,
24
25
  _patterns: () => _patterns,
25
26
  applyOutcome: () => applyOutcome,
26
27
  buildDenyBody: () => buildDenyBody,
@@ -31,10 +32,12 @@ __export(edge_core_exports, {
31
32
  detectCommerce: () => detectCommerce,
32
33
  emitBeacon: () => emitBeacon,
33
34
  evaluateEdgeRequest: () => evaluateEdgeRequest,
35
+ getBeaconBuffer: () => getBeaconBuffer,
34
36
  headerValue: () => headerValue,
35
37
  outcomeFromVerifyResult: () => outcomeFromVerifyResult,
36
38
  passUntouched: () => passUntouched,
37
39
  requestUrl: () => requestUrl,
40
+ resetBeaconBuffer: () => resetBeaconBuffer,
38
41
  resolvePolicy: () => resolvePolicy,
39
42
  runTier: () => runTier,
40
43
  sampled: () => sampled,
@@ -43,7 +46,7 @@ __export(edge_core_exports, {
43
46
  module.exports = __toCommonJS(edge_core_exports);
44
47
 
45
48
  // src/version.ts
46
- var SDK_VERSION = "5.11.0";
49
+ var SDK_VERSION = "5.13.0";
47
50
 
48
51
  // src/http.ts
49
52
  var SDK_USER_AGENT = `astrasync-sdk/${SDK_VERSION}`;
@@ -256,7 +259,10 @@ var PLATFORM_AGENT_SIGNATURES = [
256
259
  {
257
260
  vendor: "chatgpt",
258
261
  displayName: "OpenAI ChatGPT",
259
- uaPatterns: [/chatgpt/i, /openai/i, /gpt-?\d/i],
262
+ // GPTBot (training crawler) and OAI-SearchBot (ChatGPT search) are
263
+ // OpenAI's declared crawler UAs — `gpt-?\d` never matched them, so they
264
+ // classified as unattributed crawlers (round-27.4 W5).
265
+ uaPatterns: [/chatgpt/i, /openai/i, /gpt-?\d/i, /\bGPTBot\b/i, /\bOAI-SearchBot\b/i],
260
266
  agentCardPatterns: [/openai\.com/i, /chatgpt\.com/i],
261
267
  apiKeyFormatPatterns: [/^sk-proj/i, /^sk-svcacct/i],
262
268
  headerPatterns: [/^openai-organization/i, /^openai-project/i, /^openai-version/i]
@@ -398,6 +404,14 @@ function getCachedWellKnownUrls(apiBaseUrl) {
398
404
  return cache.get(apiBaseUrl)?.data;
399
405
  }
400
406
 
407
+ // src/failure-posture.ts
408
+ var REJECTED_IDENTITY_DIMENSIONS = /* @__PURE__ */ new Set([
409
+ "identity.invalid_agent_id",
410
+ "identity.unknown_agent",
411
+ "agent.lookup"
412
+ ]);
413
+ 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.";
414
+
401
415
  // src/verify.ts
402
416
  var DEFAULT_CONFIG = {
403
417
  apiBaseUrl: "https://astrasync.ai/api",
@@ -410,7 +424,7 @@ var DEFAULT_CONFIG = {
410
424
  };
411
425
  var initCheckPerformed = false;
412
426
  var deprecationWarningShown = false;
413
- async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey) {
427
+ async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey, counterparty) {
414
428
  initCheckPerformed = true;
415
429
  try {
416
430
  const probeUrl = `${apiBaseUrl}/agents/verify-access`;
@@ -428,7 +442,8 @@ async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey) {
428
442
  );
429
443
  }
430
444
  if (apiKey) {
431
- const selfCheck = await sdkFetch(`${apiBaseUrl}/agents/verify-access/self-check`, {
445
+ const q = counterparty?.id ? `?counterpartyId=${encodeURIComponent(counterparty.id)}` : counterparty?.url ? `?counterpartyUrl=${encodeURIComponent(counterparty.url)}` : "";
446
+ const selfCheck = await sdkFetch(`${apiBaseUrl}/agents/verify-access/self-check${q}`, {
432
447
  headers: { Authorization: `Bearer ${apiKey}` }
433
448
  });
434
449
  if (selfCheck.status === 401) {
@@ -440,6 +455,13 @@ async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey) {
440
455
  console.error(message);
441
456
  } else if (selfCheck.ok) {
442
457
  const body = await selfCheck.json().catch(() => ({}));
458
+ if (body.counterparty && body.counterparty.canVerify === false) {
459
+ 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.`;
460
+ if (strictInit) {
461
+ throw new Error(`${message} (strictInit=true)`);
462
+ }
463
+ console.error(message);
464
+ }
443
465
  if (typeof body.daysToExpiry === "number" && body.daysToExpiry <= 7) {
444
466
  console.warn(
445
467
  `[VerificationGateway] the configured AstraSync API key expires in ${body.daysToExpiry} day(s) \u2014 rotate it at Settings \u2192 Developer Tools before integrations start failing.`
@@ -578,11 +600,11 @@ function createGuidanceResponse(_config, reason, options = {}) {
578
600
  const isMisconfig = source === "merchant_misconfig";
579
601
  const urls = options.urls;
580
602
  const guidance = isMisconfig ? {
581
- 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.",
603
+ 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.",
582
604
  registrationUrl: urls?.registrationUrl ?? "",
583
605
  documentationUrl: options.remediation?.docsUrl ?? urls?.documentationUrl ?? "",
584
606
  steps: [
585
- "Report the failure to the site operator \u2014 their AstraSync API key needs rotating",
607
+ "Report the failure to the site operator \u2014 their AstraSync API key needs replacing",
586
608
  "Do not retry: the failure is deterministic until the operator updates their key"
587
609
  ]
588
610
  } : isApiError ? {
@@ -643,11 +665,44 @@ function createGuidanceResponse(_config, reason, options = {}) {
643
665
  // coercing the absent recommendation to a policy 'deny'. Misconfig is
644
666
  // deterministic and intentionally NOT marked retryable.
645
667
  ...isApiError && { retryable: true },
668
+ ...options.httpStatus !== void 0 && { httpStatus: options.httpStatus },
669
+ ...options.retryAfterMs !== void 0 && { retryAfterMs: options.retryAfterMs },
646
670
  correlationId: options.correlationId,
647
671
  attemptId: options.attemptId,
648
672
  verifiedAt: /* @__PURE__ */ new Date()
649
673
  };
650
674
  }
675
+ async function readJsonSafe(response) {
676
+ try {
677
+ const parsed = await response.json();
678
+ return parsed && typeof parsed === "object" ? parsed : {};
679
+ } catch {
680
+ return {};
681
+ }
682
+ }
683
+ function rejectedIdentityGuidance(failures) {
684
+ const f = failures?.find((x) => REJECTED_IDENTITY_DIMENSIONS.has(x.dimension));
685
+ if (!f) return void 0;
686
+ if (f.dimension === "agent.lookup") {
687
+ return `The presented agent identifier is not registered with AstraSync. ${UNKNOWN_AGENT_GUIDANCE}`;
688
+ }
689
+ return f.guidance ? `${f.message} ${f.guidance}` : f.message;
690
+ }
691
+ var MAX_RETRY_AFTER_MS = 15 * 60 * 1e3;
692
+ function parseRetryAfterMs(response) {
693
+ const raw = response.headers?.get?.("retry-after");
694
+ if (!raw) return void 0;
695
+ const trimmed = raw.trim();
696
+ let ms;
697
+ if (/^\d+(\.\d+)?$/.test(trimmed)) {
698
+ ms = Number(trimmed) * 1e3;
699
+ } else {
700
+ const at = Date.parse(trimmed);
701
+ if (Number.isNaN(at)) return void 0;
702
+ ms = at - Date.now();
703
+ }
704
+ return Math.min(MAX_RETRY_AFTER_MS, Math.max(0, Math.round(ms)));
705
+ }
651
706
  async function callVerifyAccessAPI(config, request) {
652
707
  const { credentials, ...requestData } = request;
653
708
  const body = {
@@ -704,7 +759,8 @@ async function callVerifyAccessAPI(config, request) {
704
759
  body: JSON.stringify(body),
705
760
  signal: AbortSignal.timeout(config.verifyTimeoutMs ?? 1e4)
706
761
  });
707
- const data = await response.json();
762
+ const data = await readJsonSafe(response);
763
+ const retryAfterMs = parseRetryAfterMs(response);
708
764
  if (response.status === 410) {
709
765
  return {
710
766
  success: true,
@@ -742,13 +798,31 @@ async function callVerifyAccessAPI(config, request) {
742
798
  }
743
799
  };
744
800
  }
801
+ if (response.status === 404 && credentials.astraId && (data?.error === "AGENT_NOT_FOUND" || typeof data?.message === "string" && /agent/i.test(data.message))) {
802
+ return {
803
+ success: true,
804
+ attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0,
805
+ access: {
806
+ allowed: false,
807
+ reason: "unknown_agent",
808
+ failures: [
809
+ {
810
+ dimension: "identity.unknown_agent",
811
+ message: "The presented agent identifier is not registered with AstraSync.",
812
+ guidance: UNKNOWN_AGENT_GUIDANCE
813
+ }
814
+ ]
815
+ }
816
+ };
817
+ }
745
818
  if (!response.ok) {
746
- if (response.status === 401) {
819
+ const keyMismatch = response.status === 403 && data?.error === "COUNTERPARTY_KEY_MISMATCH";
820
+ if (response.status === 401 || keyMismatch) {
747
821
  return {
748
822
  success: false,
749
823
  error: data.message || data.error || "Integration API key rejected",
750
824
  misconfig: true,
751
- misconfigCode: typeof data?.code === "string" ? data.code : void 0,
825
+ misconfigCode: keyMismatch ? "counterparty_key_mismatch" : typeof data?.code === "string" ? data.code : void 0,
752
826
  remediation: data?.remediation && typeof data.remediation === "object" ? data.remediation : void 0,
753
827
  correlationId: typeof data?.correlationId === "string" ? data.correlationId : void 0,
754
828
  attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0
@@ -756,9 +830,11 @@ async function callVerifyAccessAPI(config, request) {
756
830
  }
757
831
  return {
758
832
  success: false,
759
- error: data.message || data.error || `API returned ${response.status}`,
833
+ error: typeof data?.message === "string" && data.message || typeof data?.error === "string" && data.error || `API returned ${response.status}`,
760
834
  correlationId: typeof data?.correlationId === "string" ? data.correlationId : void 0,
761
- attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0
835
+ attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0,
836
+ httpStatus: response.status,
837
+ ...retryAfterMs !== void 0 && { retryAfterMs }
762
838
  };
763
839
  }
764
840
  return data;
@@ -779,14 +855,22 @@ async function verify(config, request, options) {
779
855
  mergedConfig.apiBaseUrl,
780
856
  mergedConfig.debug,
781
857
  true,
782
- mergedConfig.apiKey
858
+ mergedConfig.apiKey,
859
+ {
860
+ id: mergedConfig.counterpartyId,
861
+ url: mergedConfig.counterpartyUrl ?? request.counterpartyUrl
862
+ }
783
863
  );
784
864
  } else {
785
865
  void performInitCheck(
786
866
  mergedConfig.apiBaseUrl,
787
867
  mergedConfig.debug,
788
868
  false,
789
- mergedConfig.apiKey
869
+ mergedConfig.apiKey,
870
+ {
871
+ id: mergedConfig.counterpartyId,
872
+ url: mergedConfig.counterpartyUrl ?? request.counterpartyUrl
873
+ }
790
874
  );
791
875
  }
792
876
  }
@@ -830,12 +914,14 @@ async function verify(config, request, options) {
830
914
  correlationId: apiResponse.correlationId,
831
915
  attemptId: apiResponse.attemptId,
832
916
  remediation: apiResponse.remediation,
917
+ httpStatus: apiResponse.httpStatus,
918
+ retryAfterMs: apiResponse.retryAfterMs,
833
919
  urls
834
920
  });
835
921
  }
836
922
  if (!apiResponse.access?.allowed) {
837
923
  const aggregatedFailures = apiResponse.access?.failures;
838
- const idVerifiedFromBackend = apiResponse.verificationContext?.idVerified === true;
924
+ const idVerifiedFromBackend = apiResponse.verificationContext?.idVerified === true && !aggregatedFailures?.some((f) => REJECTED_IDENTITY_DIMENSIONS.has(f.dimension));
839
925
  const result2 = {
840
926
  identityVerified: idVerifiedFromBackend,
841
927
  policyAllowed: false,
@@ -847,7 +933,10 @@ async function verify(config, request, options) {
847
933
  requiresStepUp: apiResponse.access?.requiresStepUp,
848
934
  requiresApproval: apiResponse.access?.requiresApproval,
849
935
  guidance: {
850
- message: apiResponse.access?.reason || "Access denied by PDLSS policy",
936
+ // 5.12.0: a presented-but-rejected id (malformed / unknown) carries
937
+ // actionable registration guidance on its failure — surface it
938
+ // instead of the bare machine reason ('unknown_agent').
939
+ message: rejectedIdentityGuidance(aggregatedFailures) ?? (apiResponse.access?.reason || "Access denied by PDLSS policy"),
851
940
  registrationUrl: urls?.registrationUrl ?? "",
852
941
  documentationUrl: urls?.documentationUrl ?? "",
853
942
  // 5.1.0: anonymous callers land on this branch — steer MCP-hosted
@@ -947,7 +1036,7 @@ async function verify(config, request, options) {
947
1036
  registrationUrl: urls?.registrationUrl ?? "",
948
1037
  documentationUrl: urls?.documentationUrl ?? ""
949
1038
  };
950
- } else if (result.recommendation === "step_up_required") {
1039
+ } else if (result.recommendation === "step_up_required" && result.identityVerified) {
951
1040
  result.requiresStepUp = true;
952
1041
  result.denialReasons = serverDenialReasons ?? result.recommendationReasons ?? ["Step-up verification required"];
953
1042
  }
@@ -957,21 +1046,6 @@ async function verify(config, request, options) {
957
1046
  }
958
1047
  return result;
959
1048
  }
960
- async function reportUnregisteredAttempt(config, data) {
961
- const apiBaseUrl = config.apiBaseUrl || DEFAULT_CONFIG.apiBaseUrl;
962
- const { counterpartyUrl, ...rest } = data;
963
- const body = {
964
- ...rest,
965
- ...counterpartyUrl && { counterpartyUrl },
966
- ...config.counterpartyId && { counterpartyId: config.counterpartyId }
967
- };
968
- await sdkFetch(`${apiBaseUrl}/verification-activity/unregistered-attempt`, {
969
- method: "POST",
970
- headers: { "Content-Type": "application/json" },
971
- body: JSON.stringify(body)
972
- }).catch(() => {
973
- });
974
- }
975
1049
 
976
1050
  // src/edge-core/request.ts
977
1051
  function headerValue(req, name) {
@@ -1507,42 +1581,263 @@ async function withBudget(promise, ms) {
1507
1581
  if (timer) clearTimeout(timer);
1508
1582
  }
1509
1583
  }
1510
- async function emitBeacon(options, req, classification, budgetMs, queryValueAllowlist) {
1584
+ var destKeyOf = (d) => `${d.apiBaseUrl}\0${d.apiKey ?? ""}\0${d.counterparty}\0${d.adapter ?? ""}`;
1585
+ var BeaconBuffer = class {
1586
+ constructor(options = {}) {
1587
+ this.queue = [];
1588
+ this.droppedSinceLast = 0;
1589
+ this.droppedTotal = 0;
1590
+ this.deliveredTotal = 0;
1591
+ this.backoffUntil = 0;
1592
+ this.collapse = /* @__PURE__ */ new Map();
1593
+ /** Keys the backend answered 401 for — later batches go unauthenticated. */
1594
+ this.rejectedKeys = /* @__PURE__ */ new Set();
1595
+ this.opts = {
1596
+ maxItems: options.maxItems ?? 1e3,
1597
+ maxBatch: options.maxBatch ?? 100,
1598
+ maxAttempts: options.maxAttempts ?? 3,
1599
+ collapseWindowMs: options.collapseWindowMs ?? 6e4,
1600
+ collapseMaxKeys: options.collapseMaxKeys ?? 1e3,
1601
+ requestTimeoutMs: options.requestTimeoutMs ?? 1e4,
1602
+ retryBackoffMs: options.retryBackoffMs ?? 1e3,
1603
+ now: options.now ?? Date.now
1604
+ };
1605
+ }
1606
+ /**
1607
+ * Queue one beacon. With a `collapseKey`, a repeat inside the window is
1608
+ * merged into a held aggregate (sent when the window closes) instead of
1609
+ * becoming its own item.
1610
+ */
1611
+ enqueue(dest, item, collapseKey) {
1612
+ const now = this.opts.now();
1613
+ if (collapseKey) {
1614
+ const entry = this.collapse.get(collapseKey);
1615
+ if (entry && now - entry.firstSeenAt < this.opts.collapseWindowMs) {
1616
+ for (const target of [entry.first, entry.held]) {
1617
+ if (target && !target.inFlight && this.queue.includes(target)) {
1618
+ target.item.repeatCount = (target.item.repeatCount ?? 1) + 1;
1619
+ return;
1620
+ }
1621
+ }
1622
+ const held = this.push(
1623
+ dest,
1624
+ { ...item, repeatCount: 1 },
1625
+ entry.firstSeenAt + this.opts.collapseWindowMs
1626
+ );
1627
+ entry.held = held;
1628
+ return;
1629
+ }
1630
+ this.collapse.delete(collapseKey);
1631
+ if (this.collapse.size >= this.opts.collapseMaxKeys) {
1632
+ const oldest = this.collapse.keys().next().value;
1633
+ if (oldest !== void 0) this.collapse.delete(oldest);
1634
+ }
1635
+ const fresh = { firstSeenAt: now };
1636
+ this.collapse.set(collapseKey, fresh);
1637
+ fresh.first = this.push(dest, item, now);
1638
+ return;
1639
+ }
1640
+ this.push(dest, item, now);
1641
+ }
1642
+ /**
1643
+ * Deliver every eligible queued item. Concurrent callers share ONE
1644
+ * in-flight drain (it survives the caller's budget and a Lambda freeze),
1645
+ * so nothing in flight is ever sent twice; items queued while it runs are
1646
+ * picked up by the same drain.
1647
+ */
1648
+ flush() {
1649
+ if (!this.inFlight) {
1650
+ this.inFlight = this.drain().finally(() => {
1651
+ this.inFlight = void 0;
1652
+ });
1653
+ }
1654
+ return this.inFlight;
1655
+ }
1656
+ /** Counters for tests and diagnostics. */
1657
+ stats() {
1658
+ return {
1659
+ queueDepth: this.queue.length,
1660
+ droppedSinceLast: this.droppedSinceLast,
1661
+ droppedTotal: this.droppedTotal,
1662
+ deliveredTotal: this.deliveredTotal,
1663
+ flushing: this.inFlight !== void 0
1664
+ };
1665
+ }
1666
+ push(dest, item, eligibleAt) {
1667
+ while (this.queue.length >= this.opts.maxItems) this.dropOldest();
1668
+ const entry = {
1669
+ dest,
1670
+ destKey: destKeyOf(dest),
1671
+ item,
1672
+ attempts: 0,
1673
+ eligibleAt,
1674
+ inFlight: false
1675
+ };
1676
+ this.queue.push(entry);
1677
+ return entry;
1678
+ }
1679
+ dropOldest() {
1680
+ if (this.queue.shift()) this.countDropped(1);
1681
+ }
1682
+ countDropped(n) {
1683
+ this.droppedSinceLast += n;
1684
+ this.droppedTotal += n;
1685
+ }
1686
+ async drain() {
1687
+ for (; ; ) {
1688
+ const now = this.opts.now();
1689
+ if (now < this.backoffUntil) return;
1690
+ const batch = this.takeBatch(now);
1691
+ if (batch.length === 0) return;
1692
+ const ok = await this.send(batch);
1693
+ if (!ok) return;
1694
+ }
1695
+ }
1696
+ /** Oldest eligible item + up to maxBatch-1 more for the same destination. */
1697
+ takeBatch(now) {
1698
+ const first = this.queue.find((q) => q.eligibleAt <= now);
1699
+ if (!first) return [];
1700
+ const batch = [];
1701
+ const rest = [];
1702
+ for (const q of this.queue) {
1703
+ if (batch.length < this.opts.maxBatch && q.destKey === first.destKey && q.eligibleAt <= now) {
1704
+ q.inFlight = true;
1705
+ batch.push(q);
1706
+ } else {
1707
+ rest.push(q);
1708
+ }
1709
+ }
1710
+ this.queue = rest;
1711
+ return batch;
1712
+ }
1713
+ /** POST one batch. Returns false when the drain should pause (backoff). */
1714
+ async send(batch) {
1715
+ const dest = batch[0].dest;
1716
+ const useKey = dest.apiKey && !this.rejectedKeys.has(dest.apiKey) ? dest.apiKey : void 0;
1717
+ const reportedDropped = this.droppedSinceLast;
1718
+ const meta = {
1719
+ droppedSinceLast: reportedDropped,
1720
+ queueDepth: this.queue.length,
1721
+ adapter: dest.adapter ?? "edge-core",
1722
+ version: SDK_VERSION
1723
+ };
1724
+ const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
1725
+ const response = await withBudget(
1726
+ sdkFetch(`${dest.apiBaseUrl}/verification-activity/edge-events`, {
1727
+ method: "POST",
1728
+ headers: {
1729
+ "Content-Type": "application/json",
1730
+ ...useKey && { Authorization: `Bearer ${useKey}` }
1731
+ },
1732
+ body: JSON.stringify({ events: batch.map((q) => q.item), meta }),
1733
+ ...controller && { signal: controller.signal }
1734
+ }),
1735
+ this.opts.requestTimeoutMs
1736
+ );
1737
+ if (!response) controller?.abort();
1738
+ const status = response?.status;
1739
+ for (const q of batch) q.inFlight = false;
1740
+ if (status !== void 0 && status >= 200 && status < 300) {
1741
+ this.droppedSinceLast = Math.max(0, this.droppedSinceLast - reportedDropped);
1742
+ this.deliveredTotal += batch.length;
1743
+ return true;
1744
+ }
1745
+ if (status === 401 && useKey) {
1746
+ this.rejectedKeys.add(useKey);
1747
+ this.requeue(batch, false);
1748
+ return true;
1749
+ }
1750
+ if (status === 429) {
1751
+ const retryAfter = Number(response?.headers.get("retry-after"));
1752
+ const waitMs = Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(retryAfter, 120) * 1e3 : 3e4;
1753
+ this.backoffUntil = this.opts.now() + waitMs;
1754
+ this.requeue(batch, false);
1755
+ return false;
1756
+ }
1757
+ if (status !== void 0 && status >= 400 && status < 500) {
1758
+ this.countDropped(batch.length);
1759
+ return true;
1760
+ }
1761
+ this.backoffUntil = this.opts.now() + this.opts.retryBackoffMs;
1762
+ this.requeue(batch, true);
1763
+ return false;
1764
+ }
1765
+ /** Put a failed batch back at the head of the queue (order preserved). */
1766
+ requeue(batch, countAttempt) {
1767
+ const keep = [];
1768
+ for (const q of batch) {
1769
+ if (countAttempt) q.attempts += 1;
1770
+ if (q.attempts >= this.opts.maxAttempts) this.countDropped(1);
1771
+ else keep.push(q);
1772
+ }
1773
+ this.queue = [...keep, ...this.queue];
1774
+ while (this.queue.length > this.opts.maxItems) this.dropOldest();
1775
+ }
1776
+ };
1777
+ var beaconBuffer = new BeaconBuffer();
1778
+ function getBeaconBuffer() {
1779
+ return beaconBuffer;
1780
+ }
1781
+ function resetBeaconBuffer(options) {
1782
+ beaconBuffer = new BeaconBuffer(options);
1783
+ return beaconBuffer;
1784
+ }
1785
+ async function emitBeacon(options, req, classification, budgetMs, queryValueAllowlist, apiKey) {
1511
1786
  const attemptId = extractAttemptId(req);
1512
1787
  const declaredSource = normalizeDeclaredSource(headerValue(req, "x-astra-source"));
1513
- const budgeted = withBudget(
1514
- reportUnregisteredAttempt(
1515
- { apiBaseUrl: options.apiBaseUrl, counterpartyId: options.counterpartyId },
1516
- {
1517
- counterpartyUrl: options.counterpartyUrl,
1518
- counterpartyType: "website",
1519
- sourceIp: req.clientIp,
1520
- userAgent: headerValue(req, "user-agent"),
1521
- requestPath: req.uri,
1522
- requestMethod: req.method,
1523
- // Attempt-chain handle (header or ?astra_attempt= deep link) — joins
1524
- // this beacon into the agent's research chain via pe_attempt_idx.
1525
- ...attemptId && { attemptId },
1526
- // First-class referer + accept: traffic-source and surface-classifier
1527
- // inputs (previously buried inside observedMetadata.headers).
1528
- referer: headerValue(req, "referer"),
1529
- accept: headerValue(req, "accept"),
1530
- ...declaredSource && { declaredSource },
1531
- // Evidence fields the backend persists into the event record.
1532
- protocol: classification.protocol,
1533
- evidence: classification.evidence,
1534
- // The full sanitized header + connection capture for the
1535
- // unregistered caller (fire-and-forget, so free).
1536
- observedMetadata: buildObservedMetadata(req, queryValueAllowlist)
1537
- }
1538
- ),
1539
- budgetMs
1788
+ const userAgent = headerValue(req, "user-agent");
1789
+ const item = {
1790
+ ...options.counterpartyId && { counterpartyId: options.counterpartyId },
1791
+ ...options.counterpartyUrl && { counterpartyUrl: options.counterpartyUrl },
1792
+ counterpartyType: "website",
1793
+ sourceIp: req.clientIp,
1794
+ userAgent,
1795
+ requestPath: req.uri,
1796
+ requestMethod: req.method,
1797
+ // Attempt-chain handle (header or ?astra_attempt= deep link) — joins
1798
+ // this beacon into the agent's research chain via pe_attempt_idx.
1799
+ ...attemptId && { attemptId },
1800
+ // First-class referer + accept: traffic-source and surface-classifier
1801
+ // inputs (previously buried inside observedMetadata.headers).
1802
+ referer: headerValue(req, "referer"),
1803
+ accept: headerValue(req, "accept"),
1804
+ ...declaredSource && { declaredSource },
1805
+ // Evidence fields the backend persists into the event record.
1806
+ protocol: classification.protocol,
1807
+ evidence: classification.evidence,
1808
+ // The full sanitized header + connection capture for the unregistered
1809
+ // caller.
1810
+ observedMetadata: buildObservedMetadata(req, queryValueAllowlist),
1811
+ // Capture time — batched delivery skews createdAt by the flush delay.
1812
+ observedAt: (/* @__PURE__ */ new Date()).toISOString()
1813
+ };
1814
+ for (const k of Object.keys(item)) if (item[k] === void 0) delete item[k];
1815
+ const buffer = getBeaconBuffer();
1816
+ buffer.enqueue(
1817
+ {
1818
+ apiBaseUrl: options.apiBaseUrl,
1819
+ ...apiKey && { apiKey },
1820
+ counterparty: options.counterpartyId || options.counterpartyUrl,
1821
+ ...options.adapter && { adapter: options.adapter }
1822
+ },
1823
+ item,
1824
+ // Repeat collapse needs a client identity; without an IP, distinct
1825
+ // callers sharing a UA would merge, so don't collapse.
1826
+ req.clientIp ? [
1827
+ options.counterpartyId || options.counterpartyUrl,
1828
+ req.clientIp,
1829
+ userAgent ?? "",
1830
+ req.method,
1831
+ req.uri,
1832
+ attemptId ?? ""
1833
+ ].join("\0") : void 0
1540
1834
  );
1835
+ const flushing = buffer.flush();
1541
1836
  if (options.waitUntil) {
1542
- options.waitUntil(budgeted);
1837
+ options.waitUntil(flushing);
1543
1838
  return;
1544
1839
  }
1545
- await budgeted;
1840
+ await withBudget(flushing, budgetMs);
1546
1841
  }
1547
1842
  function sampled(rate, random = Math.random) {
1548
1843
  if (rate <= 0) return false;
@@ -1707,7 +2002,7 @@ async function runTier(options, req, classification, policy, apiKey, capture) {
1707
2002
  }
1708
2003
  return outcomeFromVerifyResult(classification, result);
1709
2004
  }
1710
- await emitBeacon(options, req, classification, beaconMs, queryValueAllowlist);
2005
+ await emitBeacon(options, req, classification, beaconMs, queryValueAllowlist, apiKey);
1711
2006
  const outcome = baseOutcome(classification);
1712
2007
  if (policy.mode === "enforce" && policy.depth !== "classify") {
1713
2008
  outcome.decision = "deny";
@@ -1722,7 +2017,8 @@ async function runTier(options, req, classification, policy, apiKey, capture) {
1722
2017
  req,
1723
2018
  { ...classification, protocol: classification.protocol ?? "ua-heuristic" },
1724
2019
  beaconMs,
1725
- queryValueAllowlist
2020
+ queryValueAllowlist,
2021
+ apiKey
1726
2022
  );
1727
2023
  }
1728
2024
  return baseOutcome(classification);
@@ -1776,6 +2072,7 @@ async function evaluateEdgeRequest(options, req, config, apiKey) {
1776
2072
  // Annotate the CommonJS export names for ESM import in node:
1777
2073
  0 && (module.exports = {
1778
2074
  ASAT_HEADERS,
2075
+ BeaconBuffer,
1779
2076
  _patterns,
1780
2077
  applyOutcome,
1781
2078
  buildDenyBody,
@@ -1786,10 +2083,12 @@ async function evaluateEdgeRequest(options, req, config, apiKey) {
1786
2083
  detectCommerce,
1787
2084
  emitBeacon,
1788
2085
  evaluateEdgeRequest,
2086
+ getBeaconBuffer,
1789
2087
  headerValue,
1790
2088
  outcomeFromVerifyResult,
1791
2089
  passUntouched,
1792
2090
  requestUrl,
2091
+ resetBeaconBuffer,
1793
2092
  resolvePolicy,
1794
2093
  runTier,
1795
2094
  sampled,