@astrasyncai/verification-gateway 5.11.0 → 5.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +4 -0
  2. package/dist/adapters/express.d.mts +1 -1
  3. package/dist/adapters/express.d.ts +1 -1
  4. package/dist/adapters/express.js +394 -39
  5. package/dist/adapters/express.js.map +1 -1
  6. package/dist/adapters/express.mjs +394 -39
  7. package/dist/adapters/express.mjs.map +1 -1
  8. package/dist/adapters/mcp.d.mts +5 -1
  9. package/dist/adapters/mcp.d.ts +5 -1
  10. package/dist/adapters/mcp.js +193 -49
  11. package/dist/adapters/mcp.js.map +1 -1
  12. package/dist/adapters/mcp.mjs +193 -49
  13. package/dist/adapters/mcp.mjs.map +1 -1
  14. package/dist/adapters/nextjs.d.mts +1 -1
  15. package/dist/adapters/nextjs.d.ts +1 -1
  16. package/dist/adapters/nextjs.js +387 -22
  17. package/dist/adapters/nextjs.js.map +1 -1
  18. package/dist/adapters/nextjs.mjs +387 -22
  19. package/dist/adapters/nextjs.mjs.map +1 -1
  20. package/dist/adapters/sdk.d.mts +9 -1
  21. package/dist/adapters/sdk.d.ts +9 -1
  22. package/dist/adapters/sdk.js +125 -16
  23. package/dist/adapters/sdk.js.map +1 -1
  24. package/dist/adapters/sdk.mjs +125 -16
  25. package/dist/adapters/sdk.mjs.map +1 -1
  26. package/dist/agent/index.js +1 -1
  27. package/dist/agent/index.js.map +1 -1
  28. package/dist/agent/index.mjs +1 -1
  29. package/dist/agent/index.mjs.map +1 -1
  30. package/dist/bin/astrasync-claude-hook.js +309 -15
  31. package/dist/bin/astrasync-codex-hook.js +309 -15
  32. package/dist/bin/astrasync-guard.js +309 -15
  33. package/dist/bin/astrasync.js +322 -17
  34. package/dist/browser/background.js +309 -15
  35. package/dist/browser/background.js.map +1 -1
  36. package/dist/browser/background.mjs +309 -15
  37. package/dist/browser/background.mjs.map +1 -1
  38. package/dist/cli/index.js +1 -1
  39. package/dist/cli/index.js.map +1 -1
  40. package/dist/cli/index.mjs +1 -1
  41. package/dist/cli/index.mjs.map +1 -1
  42. package/dist/codex/index.js +309 -15
  43. package/dist/codex/index.js.map +1 -1
  44. package/dist/codex/index.mjs +309 -15
  45. package/dist/codex/index.mjs.map +1 -1
  46. package/dist/cursor/extension.js +309 -15
  47. package/dist/cursor/extension.js.map +1 -1
  48. package/dist/cursor/extension.mjs +309 -15
  49. package/dist/cursor/extension.mjs.map +1 -1
  50. package/dist/edge-config.d.mts +1 -1
  51. package/dist/edge-config.d.ts +1 -1
  52. package/dist/edge-config.js +1 -1
  53. package/dist/edge-config.js.map +1 -1
  54. package/dist/edge-config.mjs +1 -1
  55. package/dist/edge-config.mjs.map +1 -1
  56. package/dist/edge-core/index.d.mts +128 -13
  57. package/dist/edge-core/index.d.ts +128 -13
  58. package/dist/edge-core/index.js +362 -63
  59. package/dist/edge-core/index.js.map +1 -1
  60. package/dist/edge-core/index.mjs +359 -63
  61. package/dist/edge-core/index.mjs.map +1 -1
  62. package/dist/gateway/gateway.js +309 -15
  63. package/dist/gateway/gateway.js.map +1 -1
  64. package/dist/gateway/gateway.mjs +309 -15
  65. package/dist/gateway/gateway.mjs.map +1 -1
  66. package/dist/git-trigger/git-hooks.d.mts +1 -1
  67. package/dist/git-trigger/git-hooks.d.ts +1 -1
  68. package/dist/index.d.mts +118 -15
  69. package/dist/index.d.ts +118 -15
  70. package/dist/index.js +521 -72
  71. package/dist/index.js.map +1 -1
  72. package/dist/index.mjs +515 -72
  73. package/dist/index.mjs.map +1 -1
  74. package/dist/platform-signatures.js +4 -1
  75. package/dist/platform-signatures.js.map +1 -1
  76. package/dist/platform-signatures.mjs +4 -1
  77. package/dist/platform-signatures.mjs.map +1 -1
  78. package/dist/registration/index.js +1 -1
  79. package/dist/registration/index.js.map +1 -1
  80. package/dist/registration/index.mjs +1 -1
  81. package/dist/registration/index.mjs.map +1 -1
  82. package/dist/transport/index.js +1 -1
  83. package/dist/transport/index.js.map +1 -1
  84. package/dist/transport/index.mjs +1 -1
  85. package/dist/transport/index.mjs.map +1 -1
  86. package/dist/{types-CTmUpNpS.d.mts → types-BCkwBA84.d.mts} +12 -0
  87. package/dist/{types-CTmUpNpS.d.ts → types-BCkwBA84.d.ts} +12 -0
  88. package/dist/{types-CtFXmDzm.d.ts → types-C_b4QTyO.d.ts} +18 -0
  89. package/dist/{types-DQrn_kei.d.mts → types-DlshIIR-.d.mts} +18 -0
  90. package/dist/ui/index.d.mts +1 -1
  91. package/dist/ui/index.d.ts +1 -1
  92. package/dist/verify.d.mts +35 -14
  93. package/dist/verify.d.ts +35 -14
  94. package/dist/verify.js +165 -25
  95. package/dist/verify.js.map +1 -1
  96. package/dist/verify.mjs +164 -25
  97. package/dist/verify.mjs.map +1 -1
  98. package/package.json +3 -2
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var SDK_VERSION = "5.11.0";
2
+ var SDK_VERSION = "5.13.0";
3
3
 
4
4
  // src/http.ts
5
5
  var SDK_USER_AGENT = `astrasync-sdk/${SDK_VERSION}`;
@@ -212,7 +212,10 @@ var PLATFORM_AGENT_SIGNATURES = [
212
212
  {
213
213
  vendor: "chatgpt",
214
214
  displayName: "OpenAI ChatGPT",
215
- uaPatterns: [/chatgpt/i, /openai/i, /gpt-?\d/i],
215
+ // GPTBot (training crawler) and OAI-SearchBot (ChatGPT search) are
216
+ // OpenAI's declared crawler UAs — `gpt-?\d` never matched them, so they
217
+ // classified as unattributed crawlers (round-27.4 W5).
218
+ uaPatterns: [/chatgpt/i, /openai/i, /gpt-?\d/i, /\bGPTBot\b/i, /\bOAI-SearchBot\b/i],
216
219
  agentCardPatterns: [/openai\.com/i, /chatgpt\.com/i],
217
220
  apiKeyFormatPatterns: [/^sk-proj/i, /^sk-svcacct/i],
218
221
  headerPatterns: [/^openai-organization/i, /^openai-project/i, /^openai-version/i]
@@ -354,6 +357,14 @@ function getCachedWellKnownUrls(apiBaseUrl) {
354
357
  return cache.get(apiBaseUrl)?.data;
355
358
  }
356
359
 
360
+ // src/failure-posture.ts
361
+ var REJECTED_IDENTITY_DIMENSIONS = /* @__PURE__ */ new Set([
362
+ "identity.invalid_agent_id",
363
+ "identity.unknown_agent",
364
+ "agent.lookup"
365
+ ]);
366
+ 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.";
367
+
357
368
  // src/verify.ts
358
369
  var DEFAULT_CONFIG = {
359
370
  apiBaseUrl: "https://astrasync.ai/api",
@@ -366,7 +377,7 @@ var DEFAULT_CONFIG = {
366
377
  };
367
378
  var initCheckPerformed = false;
368
379
  var deprecationWarningShown = false;
369
- async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey) {
380
+ async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey, counterparty) {
370
381
  initCheckPerformed = true;
371
382
  try {
372
383
  const probeUrl = `${apiBaseUrl}/agents/verify-access`;
@@ -384,7 +395,8 @@ async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey) {
384
395
  );
385
396
  }
386
397
  if (apiKey) {
387
- const selfCheck = await sdkFetch(`${apiBaseUrl}/agents/verify-access/self-check`, {
398
+ const q = counterparty?.id ? `?counterpartyId=${encodeURIComponent(counterparty.id)}` : counterparty?.url ? `?counterpartyUrl=${encodeURIComponent(counterparty.url)}` : "";
399
+ const selfCheck = await sdkFetch(`${apiBaseUrl}/agents/verify-access/self-check${q}`, {
388
400
  headers: { Authorization: `Bearer ${apiKey}` }
389
401
  });
390
402
  if (selfCheck.status === 401) {
@@ -396,6 +408,13 @@ async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey) {
396
408
  console.error(message);
397
409
  } else if (selfCheck.ok) {
398
410
  const body = await selfCheck.json().catch(() => ({}));
411
+ if (body.counterparty && body.counterparty.canVerify === false) {
412
+ 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.`;
413
+ if (strictInit) {
414
+ throw new Error(`${message} (strictInit=true)`);
415
+ }
416
+ console.error(message);
417
+ }
399
418
  if (typeof body.daysToExpiry === "number" && body.daysToExpiry <= 7) {
400
419
  console.warn(
401
420
  `[VerificationGateway] the configured AstraSync API key expires in ${body.daysToExpiry} day(s) \u2014 rotate it at Settings \u2192 Developer Tools before integrations start failing.`
@@ -534,11 +553,11 @@ function createGuidanceResponse(_config, reason, options = {}) {
534
553
  const isMisconfig = source === "merchant_misconfig";
535
554
  const urls = options.urls;
536
555
  const guidance = isMisconfig ? {
537
- 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.",
556
+ 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.",
538
557
  registrationUrl: urls?.registrationUrl ?? "",
539
558
  documentationUrl: options.remediation?.docsUrl ?? urls?.documentationUrl ?? "",
540
559
  steps: [
541
- "Report the failure to the site operator \u2014 their AstraSync API key needs rotating",
560
+ "Report the failure to the site operator \u2014 their AstraSync API key needs replacing",
542
561
  "Do not retry: the failure is deterministic until the operator updates their key"
543
562
  ]
544
563
  } : isApiError ? {
@@ -599,11 +618,44 @@ function createGuidanceResponse(_config, reason, options = {}) {
599
618
  // coercing the absent recommendation to a policy 'deny'. Misconfig is
600
619
  // deterministic and intentionally NOT marked retryable.
601
620
  ...isApiError && { retryable: true },
621
+ ...options.httpStatus !== void 0 && { httpStatus: options.httpStatus },
622
+ ...options.retryAfterMs !== void 0 && { retryAfterMs: options.retryAfterMs },
602
623
  correlationId: options.correlationId,
603
624
  attemptId: options.attemptId,
604
625
  verifiedAt: /* @__PURE__ */ new Date()
605
626
  };
606
627
  }
628
+ async function readJsonSafe(response) {
629
+ try {
630
+ const parsed = await response.json();
631
+ return parsed && typeof parsed === "object" ? parsed : {};
632
+ } catch {
633
+ return {};
634
+ }
635
+ }
636
+ function rejectedIdentityGuidance(failures) {
637
+ const f = failures?.find((x) => REJECTED_IDENTITY_DIMENSIONS.has(x.dimension));
638
+ if (!f) return void 0;
639
+ if (f.dimension === "agent.lookup") {
640
+ return `The presented agent identifier is not registered with AstraSync. ${UNKNOWN_AGENT_GUIDANCE}`;
641
+ }
642
+ return f.guidance ? `${f.message} ${f.guidance}` : f.message;
643
+ }
644
+ var MAX_RETRY_AFTER_MS = 15 * 60 * 1e3;
645
+ function parseRetryAfterMs(response) {
646
+ const raw = response.headers?.get?.("retry-after");
647
+ if (!raw) return void 0;
648
+ const trimmed = raw.trim();
649
+ let ms;
650
+ if (/^\d+(\.\d+)?$/.test(trimmed)) {
651
+ ms = Number(trimmed) * 1e3;
652
+ } else {
653
+ const at = Date.parse(trimmed);
654
+ if (Number.isNaN(at)) return void 0;
655
+ ms = at - Date.now();
656
+ }
657
+ return Math.min(MAX_RETRY_AFTER_MS, Math.max(0, Math.round(ms)));
658
+ }
607
659
  async function callVerifyAccessAPI(config, request) {
608
660
  const { credentials, ...requestData } = request;
609
661
  const body = {
@@ -660,7 +712,8 @@ async function callVerifyAccessAPI(config, request) {
660
712
  body: JSON.stringify(body),
661
713
  signal: AbortSignal.timeout(config.verifyTimeoutMs ?? 1e4)
662
714
  });
663
- const data = await response.json();
715
+ const data = await readJsonSafe(response);
716
+ const retryAfterMs = parseRetryAfterMs(response);
664
717
  if (response.status === 410) {
665
718
  return {
666
719
  success: true,
@@ -698,13 +751,31 @@ async function callVerifyAccessAPI(config, request) {
698
751
  }
699
752
  };
700
753
  }
754
+ if (response.status === 404 && credentials.astraId && (data?.error === "AGENT_NOT_FOUND" || typeof data?.message === "string" && /agent/i.test(data.message))) {
755
+ return {
756
+ success: true,
757
+ attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0,
758
+ access: {
759
+ allowed: false,
760
+ reason: "unknown_agent",
761
+ failures: [
762
+ {
763
+ dimension: "identity.unknown_agent",
764
+ message: "The presented agent identifier is not registered with AstraSync.",
765
+ guidance: UNKNOWN_AGENT_GUIDANCE
766
+ }
767
+ ]
768
+ }
769
+ };
770
+ }
701
771
  if (!response.ok) {
702
- if (response.status === 401) {
772
+ const keyMismatch = response.status === 403 && data?.error === "COUNTERPARTY_KEY_MISMATCH";
773
+ if (response.status === 401 || keyMismatch) {
703
774
  return {
704
775
  success: false,
705
776
  error: data.message || data.error || "Integration API key rejected",
706
777
  misconfig: true,
707
- misconfigCode: typeof data?.code === "string" ? data.code : void 0,
778
+ misconfigCode: keyMismatch ? "counterparty_key_mismatch" : typeof data?.code === "string" ? data.code : void 0,
708
779
  remediation: data?.remediation && typeof data.remediation === "object" ? data.remediation : void 0,
709
780
  correlationId: typeof data?.correlationId === "string" ? data.correlationId : void 0,
710
781
  attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0
@@ -712,9 +783,11 @@ async function callVerifyAccessAPI(config, request) {
712
783
  }
713
784
  return {
714
785
  success: false,
715
- error: data.message || data.error || `API returned ${response.status}`,
786
+ error: typeof data?.message === "string" && data.message || typeof data?.error === "string" && data.error || `API returned ${response.status}`,
716
787
  correlationId: typeof data?.correlationId === "string" ? data.correlationId : void 0,
717
- attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0
788
+ attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0,
789
+ httpStatus: response.status,
790
+ ...retryAfterMs !== void 0 && { retryAfterMs }
718
791
  };
719
792
  }
720
793
  return data;
@@ -735,14 +808,22 @@ async function verify(config, request, options) {
735
808
  mergedConfig.apiBaseUrl,
736
809
  mergedConfig.debug,
737
810
  true,
738
- mergedConfig.apiKey
811
+ mergedConfig.apiKey,
812
+ {
813
+ id: mergedConfig.counterpartyId,
814
+ url: mergedConfig.counterpartyUrl ?? request.counterpartyUrl
815
+ }
739
816
  );
740
817
  } else {
741
818
  void performInitCheck(
742
819
  mergedConfig.apiBaseUrl,
743
820
  mergedConfig.debug,
744
821
  false,
745
- mergedConfig.apiKey
822
+ mergedConfig.apiKey,
823
+ {
824
+ id: mergedConfig.counterpartyId,
825
+ url: mergedConfig.counterpartyUrl ?? request.counterpartyUrl
826
+ }
746
827
  );
747
828
  }
748
829
  }
@@ -786,12 +867,14 @@ async function verify(config, request, options) {
786
867
  correlationId: apiResponse.correlationId,
787
868
  attemptId: apiResponse.attemptId,
788
869
  remediation: apiResponse.remediation,
870
+ httpStatus: apiResponse.httpStatus,
871
+ retryAfterMs: apiResponse.retryAfterMs,
789
872
  urls
790
873
  });
791
874
  }
792
875
  if (!apiResponse.access?.allowed) {
793
876
  const aggregatedFailures = apiResponse.access?.failures;
794
- const idVerifiedFromBackend = apiResponse.verificationContext?.idVerified === true;
877
+ const idVerifiedFromBackend = apiResponse.verificationContext?.idVerified === true && !aggregatedFailures?.some((f) => REJECTED_IDENTITY_DIMENSIONS.has(f.dimension));
795
878
  const result2 = {
796
879
  identityVerified: idVerifiedFromBackend,
797
880
  policyAllowed: false,
@@ -803,7 +886,10 @@ async function verify(config, request, options) {
803
886
  requiresStepUp: apiResponse.access?.requiresStepUp,
804
887
  requiresApproval: apiResponse.access?.requiresApproval,
805
888
  guidance: {
806
- message: apiResponse.access?.reason || "Access denied by PDLSS policy",
889
+ // 5.12.0: a presented-but-rejected id (malformed / unknown) carries
890
+ // actionable registration guidance on its failure — surface it
891
+ // instead of the bare machine reason ('unknown_agent').
892
+ message: rejectedIdentityGuidance(aggregatedFailures) ?? (apiResponse.access?.reason || "Access denied by PDLSS policy"),
807
893
  registrationUrl: urls?.registrationUrl ?? "",
808
894
  documentationUrl: urls?.documentationUrl ?? "",
809
895
  // 5.1.0: anonymous callers land on this branch — steer MCP-hosted
@@ -903,7 +989,7 @@ async function verify(config, request, options) {
903
989
  registrationUrl: urls?.registrationUrl ?? "",
904
990
  documentationUrl: urls?.documentationUrl ?? ""
905
991
  };
906
- } else if (result.recommendation === "step_up_required") {
992
+ } else if (result.recommendation === "step_up_required" && result.identityVerified) {
907
993
  result.requiresStepUp = true;
908
994
  result.denialReasons = serverDenialReasons ?? result.recommendationReasons ?? ["Step-up verification required"];
909
995
  }
@@ -913,21 +999,6 @@ async function verify(config, request, options) {
913
999
  }
914
1000
  return result;
915
1001
  }
916
- async function reportUnregisteredAttempt(config, data) {
917
- const apiBaseUrl = config.apiBaseUrl || DEFAULT_CONFIG.apiBaseUrl;
918
- const { counterpartyUrl, ...rest } = data;
919
- const body = {
920
- ...rest,
921
- ...counterpartyUrl && { counterpartyUrl },
922
- ...config.counterpartyId && { counterpartyId: config.counterpartyId }
923
- };
924
- await sdkFetch(`${apiBaseUrl}/verification-activity/unregistered-attempt`, {
925
- method: "POST",
926
- headers: { "Content-Type": "application/json" },
927
- body: JSON.stringify(body)
928
- }).catch(() => {
929
- });
930
- }
931
1002
 
932
1003
  // src/edge-core/request.ts
933
1004
  function headerValue(req, name) {
@@ -1463,42 +1534,263 @@ async function withBudget(promise, ms) {
1463
1534
  if (timer) clearTimeout(timer);
1464
1535
  }
1465
1536
  }
1466
- async function emitBeacon(options, req, classification, budgetMs, queryValueAllowlist) {
1537
+ var destKeyOf = (d) => `${d.apiBaseUrl}\0${d.apiKey ?? ""}\0${d.counterparty}\0${d.adapter ?? ""}`;
1538
+ var BeaconBuffer = class {
1539
+ constructor(options = {}) {
1540
+ this.queue = [];
1541
+ this.droppedSinceLast = 0;
1542
+ this.droppedTotal = 0;
1543
+ this.deliveredTotal = 0;
1544
+ this.backoffUntil = 0;
1545
+ this.collapse = /* @__PURE__ */ new Map();
1546
+ /** Keys the backend answered 401 for — later batches go unauthenticated. */
1547
+ this.rejectedKeys = /* @__PURE__ */ new Set();
1548
+ this.opts = {
1549
+ maxItems: options.maxItems ?? 1e3,
1550
+ maxBatch: options.maxBatch ?? 100,
1551
+ maxAttempts: options.maxAttempts ?? 3,
1552
+ collapseWindowMs: options.collapseWindowMs ?? 6e4,
1553
+ collapseMaxKeys: options.collapseMaxKeys ?? 1e3,
1554
+ requestTimeoutMs: options.requestTimeoutMs ?? 1e4,
1555
+ retryBackoffMs: options.retryBackoffMs ?? 1e3,
1556
+ now: options.now ?? Date.now
1557
+ };
1558
+ }
1559
+ /**
1560
+ * Queue one beacon. With a `collapseKey`, a repeat inside the window is
1561
+ * merged into a held aggregate (sent when the window closes) instead of
1562
+ * becoming its own item.
1563
+ */
1564
+ enqueue(dest, item, collapseKey) {
1565
+ const now = this.opts.now();
1566
+ if (collapseKey) {
1567
+ const entry = this.collapse.get(collapseKey);
1568
+ if (entry && now - entry.firstSeenAt < this.opts.collapseWindowMs) {
1569
+ for (const target of [entry.first, entry.held]) {
1570
+ if (target && !target.inFlight && this.queue.includes(target)) {
1571
+ target.item.repeatCount = (target.item.repeatCount ?? 1) + 1;
1572
+ return;
1573
+ }
1574
+ }
1575
+ const held = this.push(
1576
+ dest,
1577
+ { ...item, repeatCount: 1 },
1578
+ entry.firstSeenAt + this.opts.collapseWindowMs
1579
+ );
1580
+ entry.held = held;
1581
+ return;
1582
+ }
1583
+ this.collapse.delete(collapseKey);
1584
+ if (this.collapse.size >= this.opts.collapseMaxKeys) {
1585
+ const oldest = this.collapse.keys().next().value;
1586
+ if (oldest !== void 0) this.collapse.delete(oldest);
1587
+ }
1588
+ const fresh = { firstSeenAt: now };
1589
+ this.collapse.set(collapseKey, fresh);
1590
+ fresh.first = this.push(dest, item, now);
1591
+ return;
1592
+ }
1593
+ this.push(dest, item, now);
1594
+ }
1595
+ /**
1596
+ * Deliver every eligible queued item. Concurrent callers share ONE
1597
+ * in-flight drain (it survives the caller's budget and a Lambda freeze),
1598
+ * so nothing in flight is ever sent twice; items queued while it runs are
1599
+ * picked up by the same drain.
1600
+ */
1601
+ flush() {
1602
+ if (!this.inFlight) {
1603
+ this.inFlight = this.drain().finally(() => {
1604
+ this.inFlight = void 0;
1605
+ });
1606
+ }
1607
+ return this.inFlight;
1608
+ }
1609
+ /** Counters for tests and diagnostics. */
1610
+ stats() {
1611
+ return {
1612
+ queueDepth: this.queue.length,
1613
+ droppedSinceLast: this.droppedSinceLast,
1614
+ droppedTotal: this.droppedTotal,
1615
+ deliveredTotal: this.deliveredTotal,
1616
+ flushing: this.inFlight !== void 0
1617
+ };
1618
+ }
1619
+ push(dest, item, eligibleAt) {
1620
+ while (this.queue.length >= this.opts.maxItems) this.dropOldest();
1621
+ const entry = {
1622
+ dest,
1623
+ destKey: destKeyOf(dest),
1624
+ item,
1625
+ attempts: 0,
1626
+ eligibleAt,
1627
+ inFlight: false
1628
+ };
1629
+ this.queue.push(entry);
1630
+ return entry;
1631
+ }
1632
+ dropOldest() {
1633
+ if (this.queue.shift()) this.countDropped(1);
1634
+ }
1635
+ countDropped(n) {
1636
+ this.droppedSinceLast += n;
1637
+ this.droppedTotal += n;
1638
+ }
1639
+ async drain() {
1640
+ for (; ; ) {
1641
+ const now = this.opts.now();
1642
+ if (now < this.backoffUntil) return;
1643
+ const batch = this.takeBatch(now);
1644
+ if (batch.length === 0) return;
1645
+ const ok = await this.send(batch);
1646
+ if (!ok) return;
1647
+ }
1648
+ }
1649
+ /** Oldest eligible item + up to maxBatch-1 more for the same destination. */
1650
+ takeBatch(now) {
1651
+ const first = this.queue.find((q) => q.eligibleAt <= now);
1652
+ if (!first) return [];
1653
+ const batch = [];
1654
+ const rest = [];
1655
+ for (const q of this.queue) {
1656
+ if (batch.length < this.opts.maxBatch && q.destKey === first.destKey && q.eligibleAt <= now) {
1657
+ q.inFlight = true;
1658
+ batch.push(q);
1659
+ } else {
1660
+ rest.push(q);
1661
+ }
1662
+ }
1663
+ this.queue = rest;
1664
+ return batch;
1665
+ }
1666
+ /** POST one batch. Returns false when the drain should pause (backoff). */
1667
+ async send(batch) {
1668
+ const dest = batch[0].dest;
1669
+ const useKey = dest.apiKey && !this.rejectedKeys.has(dest.apiKey) ? dest.apiKey : void 0;
1670
+ const reportedDropped = this.droppedSinceLast;
1671
+ const meta = {
1672
+ droppedSinceLast: reportedDropped,
1673
+ queueDepth: this.queue.length,
1674
+ adapter: dest.adapter ?? "edge-core",
1675
+ version: SDK_VERSION
1676
+ };
1677
+ const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
1678
+ const response = await withBudget(
1679
+ sdkFetch(`${dest.apiBaseUrl}/verification-activity/edge-events`, {
1680
+ method: "POST",
1681
+ headers: {
1682
+ "Content-Type": "application/json",
1683
+ ...useKey && { Authorization: `Bearer ${useKey}` }
1684
+ },
1685
+ body: JSON.stringify({ events: batch.map((q) => q.item), meta }),
1686
+ ...controller && { signal: controller.signal }
1687
+ }),
1688
+ this.opts.requestTimeoutMs
1689
+ );
1690
+ if (!response) controller?.abort();
1691
+ const status = response?.status;
1692
+ for (const q of batch) q.inFlight = false;
1693
+ if (status !== void 0 && status >= 200 && status < 300) {
1694
+ this.droppedSinceLast = Math.max(0, this.droppedSinceLast - reportedDropped);
1695
+ this.deliveredTotal += batch.length;
1696
+ return true;
1697
+ }
1698
+ if (status === 401 && useKey) {
1699
+ this.rejectedKeys.add(useKey);
1700
+ this.requeue(batch, false);
1701
+ return true;
1702
+ }
1703
+ if (status === 429) {
1704
+ const retryAfter = Number(response?.headers.get("retry-after"));
1705
+ const waitMs = Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(retryAfter, 120) * 1e3 : 3e4;
1706
+ this.backoffUntil = this.opts.now() + waitMs;
1707
+ this.requeue(batch, false);
1708
+ return false;
1709
+ }
1710
+ if (status !== void 0 && status >= 400 && status < 500) {
1711
+ this.countDropped(batch.length);
1712
+ return true;
1713
+ }
1714
+ this.backoffUntil = this.opts.now() + this.opts.retryBackoffMs;
1715
+ this.requeue(batch, true);
1716
+ return false;
1717
+ }
1718
+ /** Put a failed batch back at the head of the queue (order preserved). */
1719
+ requeue(batch, countAttempt) {
1720
+ const keep = [];
1721
+ for (const q of batch) {
1722
+ if (countAttempt) q.attempts += 1;
1723
+ if (q.attempts >= this.opts.maxAttempts) this.countDropped(1);
1724
+ else keep.push(q);
1725
+ }
1726
+ this.queue = [...keep, ...this.queue];
1727
+ while (this.queue.length > this.opts.maxItems) this.dropOldest();
1728
+ }
1729
+ };
1730
+ var beaconBuffer = new BeaconBuffer();
1731
+ function getBeaconBuffer() {
1732
+ return beaconBuffer;
1733
+ }
1734
+ function resetBeaconBuffer(options) {
1735
+ beaconBuffer = new BeaconBuffer(options);
1736
+ return beaconBuffer;
1737
+ }
1738
+ async function emitBeacon(options, req, classification, budgetMs, queryValueAllowlist, apiKey) {
1467
1739
  const attemptId = extractAttemptId(req);
1468
1740
  const declaredSource = normalizeDeclaredSource(headerValue(req, "x-astra-source"));
1469
- const budgeted = withBudget(
1470
- reportUnregisteredAttempt(
1471
- { apiBaseUrl: options.apiBaseUrl, counterpartyId: options.counterpartyId },
1472
- {
1473
- counterpartyUrl: options.counterpartyUrl,
1474
- counterpartyType: "website",
1475
- sourceIp: req.clientIp,
1476
- userAgent: headerValue(req, "user-agent"),
1477
- requestPath: req.uri,
1478
- requestMethod: req.method,
1479
- // Attempt-chain handle (header or ?astra_attempt= deep link) — joins
1480
- // this beacon into the agent's research chain via pe_attempt_idx.
1481
- ...attemptId && { attemptId },
1482
- // First-class referer + accept: traffic-source and surface-classifier
1483
- // inputs (previously buried inside observedMetadata.headers).
1484
- referer: headerValue(req, "referer"),
1485
- accept: headerValue(req, "accept"),
1486
- ...declaredSource && { declaredSource },
1487
- // Evidence fields the backend persists into the event record.
1488
- protocol: classification.protocol,
1489
- evidence: classification.evidence,
1490
- // The full sanitized header + connection capture for the
1491
- // unregistered caller (fire-and-forget, so free).
1492
- observedMetadata: buildObservedMetadata(req, queryValueAllowlist)
1493
- }
1494
- ),
1495
- budgetMs
1741
+ const userAgent = headerValue(req, "user-agent");
1742
+ const item = {
1743
+ ...options.counterpartyId && { counterpartyId: options.counterpartyId },
1744
+ ...options.counterpartyUrl && { counterpartyUrl: options.counterpartyUrl },
1745
+ counterpartyType: "website",
1746
+ sourceIp: req.clientIp,
1747
+ userAgent,
1748
+ requestPath: req.uri,
1749
+ requestMethod: req.method,
1750
+ // Attempt-chain handle (header or ?astra_attempt= deep link) — joins
1751
+ // this beacon into the agent's research chain via pe_attempt_idx.
1752
+ ...attemptId && { attemptId },
1753
+ // First-class referer + accept: traffic-source and surface-classifier
1754
+ // inputs (previously buried inside observedMetadata.headers).
1755
+ referer: headerValue(req, "referer"),
1756
+ accept: headerValue(req, "accept"),
1757
+ ...declaredSource && { declaredSource },
1758
+ // Evidence fields the backend persists into the event record.
1759
+ protocol: classification.protocol,
1760
+ evidence: classification.evidence,
1761
+ // The full sanitized header + connection capture for the unregistered
1762
+ // caller.
1763
+ observedMetadata: buildObservedMetadata(req, queryValueAllowlist),
1764
+ // Capture time — batched delivery skews createdAt by the flush delay.
1765
+ observedAt: (/* @__PURE__ */ new Date()).toISOString()
1766
+ };
1767
+ for (const k of Object.keys(item)) if (item[k] === void 0) delete item[k];
1768
+ const buffer = getBeaconBuffer();
1769
+ buffer.enqueue(
1770
+ {
1771
+ apiBaseUrl: options.apiBaseUrl,
1772
+ ...apiKey && { apiKey },
1773
+ counterparty: options.counterpartyId || options.counterpartyUrl,
1774
+ ...options.adapter && { adapter: options.adapter }
1775
+ },
1776
+ item,
1777
+ // Repeat collapse needs a client identity; without an IP, distinct
1778
+ // callers sharing a UA would merge, so don't collapse.
1779
+ req.clientIp ? [
1780
+ options.counterpartyId || options.counterpartyUrl,
1781
+ req.clientIp,
1782
+ userAgent ?? "",
1783
+ req.method,
1784
+ req.uri,
1785
+ attemptId ?? ""
1786
+ ].join("\0") : void 0
1496
1787
  );
1788
+ const flushing = buffer.flush();
1497
1789
  if (options.waitUntil) {
1498
- options.waitUntil(budgeted);
1790
+ options.waitUntil(flushing);
1499
1791
  return;
1500
1792
  }
1501
- await budgeted;
1793
+ await withBudget(flushing, budgetMs);
1502
1794
  }
1503
1795
  function sampled(rate, random = Math.random) {
1504
1796
  if (rate <= 0) return false;
@@ -1663,7 +1955,7 @@ async function runTier(options, req, classification, policy, apiKey, capture) {
1663
1955
  }
1664
1956
  return outcomeFromVerifyResult(classification, result);
1665
1957
  }
1666
- await emitBeacon(options, req, classification, beaconMs, queryValueAllowlist);
1958
+ await emitBeacon(options, req, classification, beaconMs, queryValueAllowlist, apiKey);
1667
1959
  const outcome = baseOutcome(classification);
1668
1960
  if (policy.mode === "enforce" && policy.depth !== "classify") {
1669
1961
  outcome.decision = "deny";
@@ -1678,7 +1970,8 @@ async function runTier(options, req, classification, policy, apiKey, capture) {
1678
1970
  req,
1679
1971
  { ...classification, protocol: classification.protocol ?? "ua-heuristic" },
1680
1972
  beaconMs,
1681
- queryValueAllowlist
1973
+ queryValueAllowlist,
1974
+ apiKey
1682
1975
  );
1683
1976
  }
1684
1977
  return baseOutcome(classification);
@@ -1731,6 +2024,7 @@ async function evaluateEdgeRequest(options, req, config, apiKey) {
1731
2024
  }
1732
2025
  export {
1733
2026
  ASAT_HEADERS,
2027
+ BeaconBuffer,
1734
2028
  _patterns,
1735
2029
  applyOutcome,
1736
2030
  buildDenyBody,
@@ -1741,10 +2035,12 @@ export {
1741
2035
  detectCommerce,
1742
2036
  emitBeacon,
1743
2037
  evaluateEdgeRequest,
2038
+ getBeaconBuffer,
1744
2039
  headerValue,
1745
2040
  outcomeFromVerifyResult,
1746
2041
  passUntouched,
1747
2042
  requestUrl,
2043
+ resetBeaconBuffer,
1748
2044
  resolvePolicy,
1749
2045
  runTier,
1750
2046
  sampled,