@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
@@ -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.12.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",
@@ -643,11 +657,44 @@ function createGuidanceResponse(_config, reason, options = {}) {
643
657
  // coercing the absent recommendation to a policy 'deny'. Misconfig is
644
658
  // deterministic and intentionally NOT marked retryable.
645
659
  ...isApiError && { retryable: true },
660
+ ...options.httpStatus !== void 0 && { httpStatus: options.httpStatus },
661
+ ...options.retryAfterMs !== void 0 && { retryAfterMs: options.retryAfterMs },
646
662
  correlationId: options.correlationId,
647
663
  attemptId: options.attemptId,
648
664
  verifiedAt: /* @__PURE__ */ new Date()
649
665
  };
650
666
  }
667
+ async function readJsonSafe(response) {
668
+ try {
669
+ const parsed = await response.json();
670
+ return parsed && typeof parsed === "object" ? parsed : {};
671
+ } catch {
672
+ return {};
673
+ }
674
+ }
675
+ function rejectedIdentityGuidance(failures) {
676
+ const f = failures?.find((x) => REJECTED_IDENTITY_DIMENSIONS.has(x.dimension));
677
+ if (!f) return void 0;
678
+ if (f.dimension === "agent.lookup") {
679
+ return `The presented agent identifier is not registered with AstraSync. ${UNKNOWN_AGENT_GUIDANCE}`;
680
+ }
681
+ return f.guidance ? `${f.message} ${f.guidance}` : f.message;
682
+ }
683
+ var MAX_RETRY_AFTER_MS = 15 * 60 * 1e3;
684
+ function parseRetryAfterMs(response) {
685
+ const raw = response.headers?.get?.("retry-after");
686
+ if (!raw) return void 0;
687
+ const trimmed = raw.trim();
688
+ let ms;
689
+ if (/^\d+(\.\d+)?$/.test(trimmed)) {
690
+ ms = Number(trimmed) * 1e3;
691
+ } else {
692
+ const at = Date.parse(trimmed);
693
+ if (Number.isNaN(at)) return void 0;
694
+ ms = at - Date.now();
695
+ }
696
+ return Math.min(MAX_RETRY_AFTER_MS, Math.max(0, Math.round(ms)));
697
+ }
651
698
  async function callVerifyAccessAPI(config, request) {
652
699
  const { credentials, ...requestData } = request;
653
700
  const body = {
@@ -704,7 +751,8 @@ async function callVerifyAccessAPI(config, request) {
704
751
  body: JSON.stringify(body),
705
752
  signal: AbortSignal.timeout(config.verifyTimeoutMs ?? 1e4)
706
753
  });
707
- const data = await response.json();
754
+ const data = await readJsonSafe(response);
755
+ const retryAfterMs = parseRetryAfterMs(response);
708
756
  if (response.status === 410) {
709
757
  return {
710
758
  success: true,
@@ -742,6 +790,23 @@ async function callVerifyAccessAPI(config, request) {
742
790
  }
743
791
  };
744
792
  }
793
+ if (response.status === 404 && credentials.astraId && (data?.error === "AGENT_NOT_FOUND" || typeof data?.message === "string" && /agent/i.test(data.message))) {
794
+ return {
795
+ success: true,
796
+ attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0,
797
+ access: {
798
+ allowed: false,
799
+ reason: "unknown_agent",
800
+ failures: [
801
+ {
802
+ dimension: "identity.unknown_agent",
803
+ message: "The presented agent identifier is not registered with AstraSync.",
804
+ guidance: UNKNOWN_AGENT_GUIDANCE
805
+ }
806
+ ]
807
+ }
808
+ };
809
+ }
745
810
  if (!response.ok) {
746
811
  if (response.status === 401) {
747
812
  return {
@@ -756,9 +821,11 @@ async function callVerifyAccessAPI(config, request) {
756
821
  }
757
822
  return {
758
823
  success: false,
759
- error: data.message || data.error || `API returned ${response.status}`,
824
+ error: typeof data?.message === "string" && data.message || typeof data?.error === "string" && data.error || `API returned ${response.status}`,
760
825
  correlationId: typeof data?.correlationId === "string" ? data.correlationId : void 0,
761
- attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0
826
+ attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0,
827
+ httpStatus: response.status,
828
+ ...retryAfterMs !== void 0 && { retryAfterMs }
762
829
  };
763
830
  }
764
831
  return data;
@@ -830,12 +897,14 @@ async function verify(config, request, options) {
830
897
  correlationId: apiResponse.correlationId,
831
898
  attemptId: apiResponse.attemptId,
832
899
  remediation: apiResponse.remediation,
900
+ httpStatus: apiResponse.httpStatus,
901
+ retryAfterMs: apiResponse.retryAfterMs,
833
902
  urls
834
903
  });
835
904
  }
836
905
  if (!apiResponse.access?.allowed) {
837
906
  const aggregatedFailures = apiResponse.access?.failures;
838
- const idVerifiedFromBackend = apiResponse.verificationContext?.idVerified === true;
907
+ const idVerifiedFromBackend = apiResponse.verificationContext?.idVerified === true && !aggregatedFailures?.some((f) => REJECTED_IDENTITY_DIMENSIONS.has(f.dimension));
839
908
  const result2 = {
840
909
  identityVerified: idVerifiedFromBackend,
841
910
  policyAllowed: false,
@@ -847,7 +916,10 @@ async function verify(config, request, options) {
847
916
  requiresStepUp: apiResponse.access?.requiresStepUp,
848
917
  requiresApproval: apiResponse.access?.requiresApproval,
849
918
  guidance: {
850
- message: apiResponse.access?.reason || "Access denied by PDLSS policy",
919
+ // 5.12.0: a presented-but-rejected id (malformed / unknown) carries
920
+ // actionable registration guidance on its failure — surface it
921
+ // instead of the bare machine reason ('unknown_agent').
922
+ message: rejectedIdentityGuidance(aggregatedFailures) ?? (apiResponse.access?.reason || "Access denied by PDLSS policy"),
851
923
  registrationUrl: urls?.registrationUrl ?? "",
852
924
  documentationUrl: urls?.documentationUrl ?? "",
853
925
  // 5.1.0: anonymous callers land on this branch — steer MCP-hosted
@@ -947,7 +1019,7 @@ async function verify(config, request, options) {
947
1019
  registrationUrl: urls?.registrationUrl ?? "",
948
1020
  documentationUrl: urls?.documentationUrl ?? ""
949
1021
  };
950
- } else if (result.recommendation === "step_up_required") {
1022
+ } else if (result.recommendation === "step_up_required" && result.identityVerified) {
951
1023
  result.requiresStepUp = true;
952
1024
  result.denialReasons = serverDenialReasons ?? result.recommendationReasons ?? ["Step-up verification required"];
953
1025
  }
@@ -957,21 +1029,6 @@ async function verify(config, request, options) {
957
1029
  }
958
1030
  return result;
959
1031
  }
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
1032
 
976
1033
  // src/edge-core/request.ts
977
1034
  function headerValue(req, name) {
@@ -1507,42 +1564,263 @@ async function withBudget(promise, ms) {
1507
1564
  if (timer) clearTimeout(timer);
1508
1565
  }
1509
1566
  }
1510
- async function emitBeacon(options, req, classification, budgetMs, queryValueAllowlist) {
1567
+ var destKeyOf = (d) => `${d.apiBaseUrl}\0${d.apiKey ?? ""}\0${d.counterparty}\0${d.adapter ?? ""}`;
1568
+ var BeaconBuffer = class {
1569
+ constructor(options = {}) {
1570
+ this.queue = [];
1571
+ this.droppedSinceLast = 0;
1572
+ this.droppedTotal = 0;
1573
+ this.deliveredTotal = 0;
1574
+ this.backoffUntil = 0;
1575
+ this.collapse = /* @__PURE__ */ new Map();
1576
+ /** Keys the backend answered 401 for — later batches go unauthenticated. */
1577
+ this.rejectedKeys = /* @__PURE__ */ new Set();
1578
+ this.opts = {
1579
+ maxItems: options.maxItems ?? 1e3,
1580
+ maxBatch: options.maxBatch ?? 100,
1581
+ maxAttempts: options.maxAttempts ?? 3,
1582
+ collapseWindowMs: options.collapseWindowMs ?? 6e4,
1583
+ collapseMaxKeys: options.collapseMaxKeys ?? 1e3,
1584
+ requestTimeoutMs: options.requestTimeoutMs ?? 1e4,
1585
+ retryBackoffMs: options.retryBackoffMs ?? 1e3,
1586
+ now: options.now ?? Date.now
1587
+ };
1588
+ }
1589
+ /**
1590
+ * Queue one beacon. With a `collapseKey`, a repeat inside the window is
1591
+ * merged into a held aggregate (sent when the window closes) instead of
1592
+ * becoming its own item.
1593
+ */
1594
+ enqueue(dest, item, collapseKey) {
1595
+ const now = this.opts.now();
1596
+ if (collapseKey) {
1597
+ const entry = this.collapse.get(collapseKey);
1598
+ if (entry && now - entry.firstSeenAt < this.opts.collapseWindowMs) {
1599
+ for (const target of [entry.first, entry.held]) {
1600
+ if (target && !target.inFlight && this.queue.includes(target)) {
1601
+ target.item.repeatCount = (target.item.repeatCount ?? 1) + 1;
1602
+ return;
1603
+ }
1604
+ }
1605
+ const held = this.push(
1606
+ dest,
1607
+ { ...item, repeatCount: 1 },
1608
+ entry.firstSeenAt + this.opts.collapseWindowMs
1609
+ );
1610
+ entry.held = held;
1611
+ return;
1612
+ }
1613
+ this.collapse.delete(collapseKey);
1614
+ if (this.collapse.size >= this.opts.collapseMaxKeys) {
1615
+ const oldest = this.collapse.keys().next().value;
1616
+ if (oldest !== void 0) this.collapse.delete(oldest);
1617
+ }
1618
+ const fresh = { firstSeenAt: now };
1619
+ this.collapse.set(collapseKey, fresh);
1620
+ fresh.first = this.push(dest, item, now);
1621
+ return;
1622
+ }
1623
+ this.push(dest, item, now);
1624
+ }
1625
+ /**
1626
+ * Deliver every eligible queued item. Concurrent callers share ONE
1627
+ * in-flight drain (it survives the caller's budget and a Lambda freeze),
1628
+ * so nothing in flight is ever sent twice; items queued while it runs are
1629
+ * picked up by the same drain.
1630
+ */
1631
+ flush() {
1632
+ if (!this.inFlight) {
1633
+ this.inFlight = this.drain().finally(() => {
1634
+ this.inFlight = void 0;
1635
+ });
1636
+ }
1637
+ return this.inFlight;
1638
+ }
1639
+ /** Counters for tests and diagnostics. */
1640
+ stats() {
1641
+ return {
1642
+ queueDepth: this.queue.length,
1643
+ droppedSinceLast: this.droppedSinceLast,
1644
+ droppedTotal: this.droppedTotal,
1645
+ deliveredTotal: this.deliveredTotal,
1646
+ flushing: this.inFlight !== void 0
1647
+ };
1648
+ }
1649
+ push(dest, item, eligibleAt) {
1650
+ while (this.queue.length >= this.opts.maxItems) this.dropOldest();
1651
+ const entry = {
1652
+ dest,
1653
+ destKey: destKeyOf(dest),
1654
+ item,
1655
+ attempts: 0,
1656
+ eligibleAt,
1657
+ inFlight: false
1658
+ };
1659
+ this.queue.push(entry);
1660
+ return entry;
1661
+ }
1662
+ dropOldest() {
1663
+ if (this.queue.shift()) this.countDropped(1);
1664
+ }
1665
+ countDropped(n) {
1666
+ this.droppedSinceLast += n;
1667
+ this.droppedTotal += n;
1668
+ }
1669
+ async drain() {
1670
+ for (; ; ) {
1671
+ const now = this.opts.now();
1672
+ if (now < this.backoffUntil) return;
1673
+ const batch = this.takeBatch(now);
1674
+ if (batch.length === 0) return;
1675
+ const ok = await this.send(batch);
1676
+ if (!ok) return;
1677
+ }
1678
+ }
1679
+ /** Oldest eligible item + up to maxBatch-1 more for the same destination. */
1680
+ takeBatch(now) {
1681
+ const first = this.queue.find((q) => q.eligibleAt <= now);
1682
+ if (!first) return [];
1683
+ const batch = [];
1684
+ const rest = [];
1685
+ for (const q of this.queue) {
1686
+ if (batch.length < this.opts.maxBatch && q.destKey === first.destKey && q.eligibleAt <= now) {
1687
+ q.inFlight = true;
1688
+ batch.push(q);
1689
+ } else {
1690
+ rest.push(q);
1691
+ }
1692
+ }
1693
+ this.queue = rest;
1694
+ return batch;
1695
+ }
1696
+ /** POST one batch. Returns false when the drain should pause (backoff). */
1697
+ async send(batch) {
1698
+ const dest = batch[0].dest;
1699
+ const useKey = dest.apiKey && !this.rejectedKeys.has(dest.apiKey) ? dest.apiKey : void 0;
1700
+ const reportedDropped = this.droppedSinceLast;
1701
+ const meta = {
1702
+ droppedSinceLast: reportedDropped,
1703
+ queueDepth: this.queue.length,
1704
+ adapter: dest.adapter ?? "edge-core",
1705
+ version: SDK_VERSION
1706
+ };
1707
+ const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
1708
+ const response = await withBudget(
1709
+ sdkFetch(`${dest.apiBaseUrl}/verification-activity/edge-events`, {
1710
+ method: "POST",
1711
+ headers: {
1712
+ "Content-Type": "application/json",
1713
+ ...useKey && { Authorization: `Bearer ${useKey}` }
1714
+ },
1715
+ body: JSON.stringify({ events: batch.map((q) => q.item), meta }),
1716
+ ...controller && { signal: controller.signal }
1717
+ }),
1718
+ this.opts.requestTimeoutMs
1719
+ );
1720
+ if (!response) controller?.abort();
1721
+ const status = response?.status;
1722
+ for (const q of batch) q.inFlight = false;
1723
+ if (status !== void 0 && status >= 200 && status < 300) {
1724
+ this.droppedSinceLast = Math.max(0, this.droppedSinceLast - reportedDropped);
1725
+ this.deliveredTotal += batch.length;
1726
+ return true;
1727
+ }
1728
+ if (status === 401 && useKey) {
1729
+ this.rejectedKeys.add(useKey);
1730
+ this.requeue(batch, false);
1731
+ return true;
1732
+ }
1733
+ if (status === 429) {
1734
+ const retryAfter = Number(response?.headers.get("retry-after"));
1735
+ const waitMs = Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(retryAfter, 120) * 1e3 : 3e4;
1736
+ this.backoffUntil = this.opts.now() + waitMs;
1737
+ this.requeue(batch, false);
1738
+ return false;
1739
+ }
1740
+ if (status !== void 0 && status >= 400 && status < 500) {
1741
+ this.countDropped(batch.length);
1742
+ return true;
1743
+ }
1744
+ this.backoffUntil = this.opts.now() + this.opts.retryBackoffMs;
1745
+ this.requeue(batch, true);
1746
+ return false;
1747
+ }
1748
+ /** Put a failed batch back at the head of the queue (order preserved). */
1749
+ requeue(batch, countAttempt) {
1750
+ const keep = [];
1751
+ for (const q of batch) {
1752
+ if (countAttempt) q.attempts += 1;
1753
+ if (q.attempts >= this.opts.maxAttempts) this.countDropped(1);
1754
+ else keep.push(q);
1755
+ }
1756
+ this.queue = [...keep, ...this.queue];
1757
+ while (this.queue.length > this.opts.maxItems) this.dropOldest();
1758
+ }
1759
+ };
1760
+ var beaconBuffer = new BeaconBuffer();
1761
+ function getBeaconBuffer() {
1762
+ return beaconBuffer;
1763
+ }
1764
+ function resetBeaconBuffer(options) {
1765
+ beaconBuffer = new BeaconBuffer(options);
1766
+ return beaconBuffer;
1767
+ }
1768
+ async function emitBeacon(options, req, classification, budgetMs, queryValueAllowlist, apiKey) {
1511
1769
  const attemptId = extractAttemptId(req);
1512
1770
  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
1771
+ const userAgent = headerValue(req, "user-agent");
1772
+ const item = {
1773
+ ...options.counterpartyId && { counterpartyId: options.counterpartyId },
1774
+ ...options.counterpartyUrl && { counterpartyUrl: options.counterpartyUrl },
1775
+ counterpartyType: "website",
1776
+ sourceIp: req.clientIp,
1777
+ userAgent,
1778
+ requestPath: req.uri,
1779
+ requestMethod: req.method,
1780
+ // Attempt-chain handle (header or ?astra_attempt= deep link) — joins
1781
+ // this beacon into the agent's research chain via pe_attempt_idx.
1782
+ ...attemptId && { attemptId },
1783
+ // First-class referer + accept: traffic-source and surface-classifier
1784
+ // inputs (previously buried inside observedMetadata.headers).
1785
+ referer: headerValue(req, "referer"),
1786
+ accept: headerValue(req, "accept"),
1787
+ ...declaredSource && { declaredSource },
1788
+ // Evidence fields the backend persists into the event record.
1789
+ protocol: classification.protocol,
1790
+ evidence: classification.evidence,
1791
+ // The full sanitized header + connection capture for the unregistered
1792
+ // caller.
1793
+ observedMetadata: buildObservedMetadata(req, queryValueAllowlist),
1794
+ // Capture time — batched delivery skews createdAt by the flush delay.
1795
+ observedAt: (/* @__PURE__ */ new Date()).toISOString()
1796
+ };
1797
+ for (const k of Object.keys(item)) if (item[k] === void 0) delete item[k];
1798
+ const buffer = getBeaconBuffer();
1799
+ buffer.enqueue(
1800
+ {
1801
+ apiBaseUrl: options.apiBaseUrl,
1802
+ ...apiKey && { apiKey },
1803
+ counterparty: options.counterpartyId || options.counterpartyUrl,
1804
+ ...options.adapter && { adapter: options.adapter }
1805
+ },
1806
+ item,
1807
+ // Repeat collapse needs a client identity; without an IP, distinct
1808
+ // callers sharing a UA would merge, so don't collapse.
1809
+ req.clientIp ? [
1810
+ options.counterpartyId || options.counterpartyUrl,
1811
+ req.clientIp,
1812
+ userAgent ?? "",
1813
+ req.method,
1814
+ req.uri,
1815
+ attemptId ?? ""
1816
+ ].join("\0") : void 0
1540
1817
  );
1818
+ const flushing = buffer.flush();
1541
1819
  if (options.waitUntil) {
1542
- options.waitUntil(budgeted);
1820
+ options.waitUntil(flushing);
1543
1821
  return;
1544
1822
  }
1545
- await budgeted;
1823
+ await withBudget(flushing, budgetMs);
1546
1824
  }
1547
1825
  function sampled(rate, random = Math.random) {
1548
1826
  if (rate <= 0) return false;
@@ -1707,7 +1985,7 @@ async function runTier(options, req, classification, policy, apiKey, capture) {
1707
1985
  }
1708
1986
  return outcomeFromVerifyResult(classification, result);
1709
1987
  }
1710
- await emitBeacon(options, req, classification, beaconMs, queryValueAllowlist);
1988
+ await emitBeacon(options, req, classification, beaconMs, queryValueAllowlist, apiKey);
1711
1989
  const outcome = baseOutcome(classification);
1712
1990
  if (policy.mode === "enforce" && policy.depth !== "classify") {
1713
1991
  outcome.decision = "deny";
@@ -1722,7 +2000,8 @@ async function runTier(options, req, classification, policy, apiKey, capture) {
1722
2000
  req,
1723
2001
  { ...classification, protocol: classification.protocol ?? "ua-heuristic" },
1724
2002
  beaconMs,
1725
- queryValueAllowlist
2003
+ queryValueAllowlist,
2004
+ apiKey
1726
2005
  );
1727
2006
  }
1728
2007
  return baseOutcome(classification);
@@ -1776,6 +2055,7 @@ async function evaluateEdgeRequest(options, req, config, apiKey) {
1776
2055
  // Annotate the CommonJS export names for ESM import in node:
1777
2056
  0 && (module.exports = {
1778
2057
  ASAT_HEADERS,
2058
+ BeaconBuffer,
1779
2059
  _patterns,
1780
2060
  applyOutcome,
1781
2061
  buildDenyBody,
@@ -1786,10 +2066,12 @@ async function evaluateEdgeRequest(options, req, config, apiKey) {
1786
2066
  detectCommerce,
1787
2067
  emitBeacon,
1788
2068
  evaluateEdgeRequest,
2069
+ getBeaconBuffer,
1789
2070
  headerValue,
1790
2071
  outcomeFromVerifyResult,
1791
2072
  passUntouched,
1792
2073
  requestUrl,
2074
+ resetBeaconBuffer,
1793
2075
  resolvePolicy,
1794
2076
  runTier,
1795
2077
  sampled,