@astrasyncai/verification-gateway 5.10.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.
- package/README.md +4 -0
- package/dist/adapters/express.d.mts +1 -1
- package/dist/adapters/express.d.ts +1 -1
- package/dist/adapters/express.js +359 -30
- package/dist/adapters/express.js.map +1 -1
- package/dist/adapters/express.mjs +359 -30
- package/dist/adapters/express.mjs.map +1 -1
- package/dist/adapters/mcp.d.mts +5 -1
- package/dist/adapters/mcp.d.ts +5 -1
- package/dist/adapters/mcp.js +168 -41
- package/dist/adapters/mcp.js.map +1 -1
- package/dist/adapters/mcp.mjs +168 -41
- package/dist/adapters/mcp.mjs.map +1 -1
- package/dist/adapters/nextjs.d.mts +1 -1
- package/dist/adapters/nextjs.d.ts +1 -1
- package/dist/adapters/nextjs.js +352 -13
- package/dist/adapters/nextjs.js.map +1 -1
- package/dist/adapters/nextjs.mjs +352 -13
- package/dist/adapters/nextjs.mjs.map +1 -1
- package/dist/adapters/sdk.d.mts +9 -1
- package/dist/adapters/sdk.d.ts +9 -1
- package/dist/adapters/sdk.js +100 -8
- package/dist/adapters/sdk.js.map +1 -1
- package/dist/adapters/sdk.mjs +100 -8
- package/dist/adapters/sdk.mjs.map +1 -1
- package/dist/agent/index.js +1 -1
- package/dist/agent/index.js.map +1 -1
- package/dist/agent/index.mjs +1 -1
- package/dist/agent/index.mjs.map +1 -1
- package/dist/bin/astrasync-claude-hook.js +805 -16
- package/dist/bin/astrasync-codex-hook.js +805 -16
- package/dist/bin/astrasync-guard.js +894 -71
- package/dist/bin/astrasync.js +951 -74
- package/dist/browser/background.js +284 -7
- package/dist/browser/background.js.map +1 -1
- package/dist/browser/background.mjs +284 -7
- package/dist/browser/background.mjs.map +1 -1
- package/dist/cli/index.d.mts +8 -8
- package/dist/cli/index.d.ts +8 -8
- package/dist/cli/index.js +22 -64
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +22 -64
- package/dist/cli/index.mjs.map +1 -1
- package/dist/codex/index.d.mts +5 -0
- package/dist/codex/index.d.ts +5 -0
- package/dist/codex/index.js +821 -24
- package/dist/codex/index.js.map +1 -1
- package/dist/codex/index.mjs +823 -26
- package/dist/codex/index.mjs.map +1 -1
- package/dist/cursor/extension.d.mts +9 -2
- package/dist/cursor/extension.d.ts +9 -2
- package/dist/cursor/extension.js +787 -11
- package/dist/cursor/extension.js.map +1 -1
- package/dist/cursor/extension.mjs +789 -11
- package/dist/cursor/extension.mjs.map +1 -1
- package/dist/edge-config.d.mts +1 -1
- package/dist/edge-config.d.ts +1 -1
- package/dist/edge-config.js +1 -1
- package/dist/edge-config.js.map +1 -1
- package/dist/edge-config.mjs +1 -1
- package/dist/edge-config.mjs.map +1 -1
- package/dist/edge-core/index.d.mts +128 -13
- package/dist/edge-core/index.d.ts +128 -13
- package/dist/edge-core/index.js +337 -55
- package/dist/edge-core/index.js.map +1 -1
- package/dist/edge-core/index.mjs +334 -55
- package/dist/edge-core/index.mjs.map +1 -1
- package/dist/enrolment-store-8DyaIOLe.d.mts +26 -0
- package/dist/enrolment-store-8DyaIOLe.d.ts +26 -0
- package/dist/gateway/gateway.js +284 -7
- package/dist/gateway/gateway.js.map +1 -1
- package/dist/gateway/gateway.mjs +284 -7
- package/dist/gateway/gateway.mjs.map +1 -1
- package/dist/git-trigger/git-hooks.d.mts +1 -1
- package/dist/git-trigger/git-hooks.d.ts +1 -1
- package/dist/index.d.mts +215 -5
- package/dist/index.d.ts +215 -5
- package/dist/index.js +3756 -80
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3748 -80
- package/dist/index.mjs.map +1 -1
- package/dist/platform-signatures.js +4 -1
- package/dist/platform-signatures.js.map +1 -1
- package/dist/platform-signatures.mjs +4 -1
- package/dist/platform-signatures.mjs.map +1 -1
- package/dist/registration/index.js +1 -1
- package/dist/registration/index.js.map +1 -1
- package/dist/registration/index.mjs +1 -1
- package/dist/registration/index.mjs.map +1 -1
- package/dist/transport/index.js +1 -1
- package/dist/transport/index.js.map +1 -1
- package/dist/transport/index.mjs +1 -1
- package/dist/transport/index.mjs.map +1 -1
- package/dist/{types-CTmUpNpS.d.mts → types-BCkwBA84.d.mts} +12 -0
- package/dist/{types-CTmUpNpS.d.ts → types-BCkwBA84.d.ts} +12 -0
- package/dist/{types-CtFXmDzm.d.ts → types-C_b4QTyO.d.ts} +18 -0
- package/dist/{types-DQrn_kei.d.mts → types-DlshIIR-.d.mts} +18 -0
- package/dist/ui/index.d.mts +1 -1
- package/dist/ui/index.d.ts +1 -1
- package/dist/verify.d.mts +35 -4
- package/dist/verify.d.ts +35 -4
- package/dist/verify.js +130 -16
- package/dist/verify.js.map +1 -1
- package/dist/verify.mjs +129 -16
- package/dist/verify.mjs.map +1 -1
- package/package.json +1 -1
package/dist/edge-core/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/version.ts
|
|
2
|
-
var SDK_VERSION = "5.
|
|
2
|
+
var SDK_VERSION = "5.12.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
|
-
|
|
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",
|
|
@@ -599,11 +610,44 @@ function createGuidanceResponse(_config, reason, options = {}) {
|
|
|
599
610
|
// coercing the absent recommendation to a policy 'deny'. Misconfig is
|
|
600
611
|
// deterministic and intentionally NOT marked retryable.
|
|
601
612
|
...isApiError && { retryable: true },
|
|
613
|
+
...options.httpStatus !== void 0 && { httpStatus: options.httpStatus },
|
|
614
|
+
...options.retryAfterMs !== void 0 && { retryAfterMs: options.retryAfterMs },
|
|
602
615
|
correlationId: options.correlationId,
|
|
603
616
|
attemptId: options.attemptId,
|
|
604
617
|
verifiedAt: /* @__PURE__ */ new Date()
|
|
605
618
|
};
|
|
606
619
|
}
|
|
620
|
+
async function readJsonSafe(response) {
|
|
621
|
+
try {
|
|
622
|
+
const parsed = await response.json();
|
|
623
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
624
|
+
} catch {
|
|
625
|
+
return {};
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
function rejectedIdentityGuidance(failures) {
|
|
629
|
+
const f = failures?.find((x) => REJECTED_IDENTITY_DIMENSIONS.has(x.dimension));
|
|
630
|
+
if (!f) return void 0;
|
|
631
|
+
if (f.dimension === "agent.lookup") {
|
|
632
|
+
return `The presented agent identifier is not registered with AstraSync. ${UNKNOWN_AGENT_GUIDANCE}`;
|
|
633
|
+
}
|
|
634
|
+
return f.guidance ? `${f.message} ${f.guidance}` : f.message;
|
|
635
|
+
}
|
|
636
|
+
var MAX_RETRY_AFTER_MS = 15 * 60 * 1e3;
|
|
637
|
+
function parseRetryAfterMs(response) {
|
|
638
|
+
const raw = response.headers?.get?.("retry-after");
|
|
639
|
+
if (!raw) return void 0;
|
|
640
|
+
const trimmed = raw.trim();
|
|
641
|
+
let ms;
|
|
642
|
+
if (/^\d+(\.\d+)?$/.test(trimmed)) {
|
|
643
|
+
ms = Number(trimmed) * 1e3;
|
|
644
|
+
} else {
|
|
645
|
+
const at = Date.parse(trimmed);
|
|
646
|
+
if (Number.isNaN(at)) return void 0;
|
|
647
|
+
ms = at - Date.now();
|
|
648
|
+
}
|
|
649
|
+
return Math.min(MAX_RETRY_AFTER_MS, Math.max(0, Math.round(ms)));
|
|
650
|
+
}
|
|
607
651
|
async function callVerifyAccessAPI(config, request) {
|
|
608
652
|
const { credentials, ...requestData } = request;
|
|
609
653
|
const body = {
|
|
@@ -660,7 +704,8 @@ async function callVerifyAccessAPI(config, request) {
|
|
|
660
704
|
body: JSON.stringify(body),
|
|
661
705
|
signal: AbortSignal.timeout(config.verifyTimeoutMs ?? 1e4)
|
|
662
706
|
});
|
|
663
|
-
const data = await response
|
|
707
|
+
const data = await readJsonSafe(response);
|
|
708
|
+
const retryAfterMs = parseRetryAfterMs(response);
|
|
664
709
|
if (response.status === 410) {
|
|
665
710
|
return {
|
|
666
711
|
success: true,
|
|
@@ -698,6 +743,23 @@ async function callVerifyAccessAPI(config, request) {
|
|
|
698
743
|
}
|
|
699
744
|
};
|
|
700
745
|
}
|
|
746
|
+
if (response.status === 404 && credentials.astraId && (data?.error === "AGENT_NOT_FOUND" || typeof data?.message === "string" && /agent/i.test(data.message))) {
|
|
747
|
+
return {
|
|
748
|
+
success: true,
|
|
749
|
+
attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0,
|
|
750
|
+
access: {
|
|
751
|
+
allowed: false,
|
|
752
|
+
reason: "unknown_agent",
|
|
753
|
+
failures: [
|
|
754
|
+
{
|
|
755
|
+
dimension: "identity.unknown_agent",
|
|
756
|
+
message: "The presented agent identifier is not registered with AstraSync.",
|
|
757
|
+
guidance: UNKNOWN_AGENT_GUIDANCE
|
|
758
|
+
}
|
|
759
|
+
]
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
}
|
|
701
763
|
if (!response.ok) {
|
|
702
764
|
if (response.status === 401) {
|
|
703
765
|
return {
|
|
@@ -712,9 +774,11 @@ async function callVerifyAccessAPI(config, request) {
|
|
|
712
774
|
}
|
|
713
775
|
return {
|
|
714
776
|
success: false,
|
|
715
|
-
error: data.message || data.error || `API returned ${response.status}`,
|
|
777
|
+
error: typeof data?.message === "string" && data.message || typeof data?.error === "string" && data.error || `API returned ${response.status}`,
|
|
716
778
|
correlationId: typeof data?.correlationId === "string" ? data.correlationId : void 0,
|
|
717
|
-
attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0
|
|
779
|
+
attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0,
|
|
780
|
+
httpStatus: response.status,
|
|
781
|
+
...retryAfterMs !== void 0 && { retryAfterMs }
|
|
718
782
|
};
|
|
719
783
|
}
|
|
720
784
|
return data;
|
|
@@ -786,12 +850,14 @@ async function verify(config, request, options) {
|
|
|
786
850
|
correlationId: apiResponse.correlationId,
|
|
787
851
|
attemptId: apiResponse.attemptId,
|
|
788
852
|
remediation: apiResponse.remediation,
|
|
853
|
+
httpStatus: apiResponse.httpStatus,
|
|
854
|
+
retryAfterMs: apiResponse.retryAfterMs,
|
|
789
855
|
urls
|
|
790
856
|
});
|
|
791
857
|
}
|
|
792
858
|
if (!apiResponse.access?.allowed) {
|
|
793
859
|
const aggregatedFailures = apiResponse.access?.failures;
|
|
794
|
-
const idVerifiedFromBackend = apiResponse.verificationContext?.idVerified === true;
|
|
860
|
+
const idVerifiedFromBackend = apiResponse.verificationContext?.idVerified === true && !aggregatedFailures?.some((f) => REJECTED_IDENTITY_DIMENSIONS.has(f.dimension));
|
|
795
861
|
const result2 = {
|
|
796
862
|
identityVerified: idVerifiedFromBackend,
|
|
797
863
|
policyAllowed: false,
|
|
@@ -803,7 +869,10 @@ async function verify(config, request, options) {
|
|
|
803
869
|
requiresStepUp: apiResponse.access?.requiresStepUp,
|
|
804
870
|
requiresApproval: apiResponse.access?.requiresApproval,
|
|
805
871
|
guidance: {
|
|
806
|
-
|
|
872
|
+
// 5.12.0: a presented-but-rejected id (malformed / unknown) carries
|
|
873
|
+
// actionable registration guidance on its failure — surface it
|
|
874
|
+
// instead of the bare machine reason ('unknown_agent').
|
|
875
|
+
message: rejectedIdentityGuidance(aggregatedFailures) ?? (apiResponse.access?.reason || "Access denied by PDLSS policy"),
|
|
807
876
|
registrationUrl: urls?.registrationUrl ?? "",
|
|
808
877
|
documentationUrl: urls?.documentationUrl ?? "",
|
|
809
878
|
// 5.1.0: anonymous callers land on this branch — steer MCP-hosted
|
|
@@ -903,7 +972,7 @@ async function verify(config, request, options) {
|
|
|
903
972
|
registrationUrl: urls?.registrationUrl ?? "",
|
|
904
973
|
documentationUrl: urls?.documentationUrl ?? ""
|
|
905
974
|
};
|
|
906
|
-
} else if (result.recommendation === "step_up_required") {
|
|
975
|
+
} else if (result.recommendation === "step_up_required" && result.identityVerified) {
|
|
907
976
|
result.requiresStepUp = true;
|
|
908
977
|
result.denialReasons = serverDenialReasons ?? result.recommendationReasons ?? ["Step-up verification required"];
|
|
909
978
|
}
|
|
@@ -913,21 +982,6 @@ async function verify(config, request, options) {
|
|
|
913
982
|
}
|
|
914
983
|
return result;
|
|
915
984
|
}
|
|
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
985
|
|
|
932
986
|
// src/edge-core/request.ts
|
|
933
987
|
function headerValue(req, name) {
|
|
@@ -1463,42 +1517,263 @@ async function withBudget(promise, ms) {
|
|
|
1463
1517
|
if (timer) clearTimeout(timer);
|
|
1464
1518
|
}
|
|
1465
1519
|
}
|
|
1466
|
-
|
|
1520
|
+
var destKeyOf = (d) => `${d.apiBaseUrl}\0${d.apiKey ?? ""}\0${d.counterparty}\0${d.adapter ?? ""}`;
|
|
1521
|
+
var BeaconBuffer = class {
|
|
1522
|
+
constructor(options = {}) {
|
|
1523
|
+
this.queue = [];
|
|
1524
|
+
this.droppedSinceLast = 0;
|
|
1525
|
+
this.droppedTotal = 0;
|
|
1526
|
+
this.deliveredTotal = 0;
|
|
1527
|
+
this.backoffUntil = 0;
|
|
1528
|
+
this.collapse = /* @__PURE__ */ new Map();
|
|
1529
|
+
/** Keys the backend answered 401 for — later batches go unauthenticated. */
|
|
1530
|
+
this.rejectedKeys = /* @__PURE__ */ new Set();
|
|
1531
|
+
this.opts = {
|
|
1532
|
+
maxItems: options.maxItems ?? 1e3,
|
|
1533
|
+
maxBatch: options.maxBatch ?? 100,
|
|
1534
|
+
maxAttempts: options.maxAttempts ?? 3,
|
|
1535
|
+
collapseWindowMs: options.collapseWindowMs ?? 6e4,
|
|
1536
|
+
collapseMaxKeys: options.collapseMaxKeys ?? 1e3,
|
|
1537
|
+
requestTimeoutMs: options.requestTimeoutMs ?? 1e4,
|
|
1538
|
+
retryBackoffMs: options.retryBackoffMs ?? 1e3,
|
|
1539
|
+
now: options.now ?? Date.now
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
/**
|
|
1543
|
+
* Queue one beacon. With a `collapseKey`, a repeat inside the window is
|
|
1544
|
+
* merged into a held aggregate (sent when the window closes) instead of
|
|
1545
|
+
* becoming its own item.
|
|
1546
|
+
*/
|
|
1547
|
+
enqueue(dest, item, collapseKey) {
|
|
1548
|
+
const now = this.opts.now();
|
|
1549
|
+
if (collapseKey) {
|
|
1550
|
+
const entry = this.collapse.get(collapseKey);
|
|
1551
|
+
if (entry && now - entry.firstSeenAt < this.opts.collapseWindowMs) {
|
|
1552
|
+
for (const target of [entry.first, entry.held]) {
|
|
1553
|
+
if (target && !target.inFlight && this.queue.includes(target)) {
|
|
1554
|
+
target.item.repeatCount = (target.item.repeatCount ?? 1) + 1;
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
const held = this.push(
|
|
1559
|
+
dest,
|
|
1560
|
+
{ ...item, repeatCount: 1 },
|
|
1561
|
+
entry.firstSeenAt + this.opts.collapseWindowMs
|
|
1562
|
+
);
|
|
1563
|
+
entry.held = held;
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
this.collapse.delete(collapseKey);
|
|
1567
|
+
if (this.collapse.size >= this.opts.collapseMaxKeys) {
|
|
1568
|
+
const oldest = this.collapse.keys().next().value;
|
|
1569
|
+
if (oldest !== void 0) this.collapse.delete(oldest);
|
|
1570
|
+
}
|
|
1571
|
+
const fresh = { firstSeenAt: now };
|
|
1572
|
+
this.collapse.set(collapseKey, fresh);
|
|
1573
|
+
fresh.first = this.push(dest, item, now);
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
this.push(dest, item, now);
|
|
1577
|
+
}
|
|
1578
|
+
/**
|
|
1579
|
+
* Deliver every eligible queued item. Concurrent callers share ONE
|
|
1580
|
+
* in-flight drain (it survives the caller's budget and a Lambda freeze),
|
|
1581
|
+
* so nothing in flight is ever sent twice; items queued while it runs are
|
|
1582
|
+
* picked up by the same drain.
|
|
1583
|
+
*/
|
|
1584
|
+
flush() {
|
|
1585
|
+
if (!this.inFlight) {
|
|
1586
|
+
this.inFlight = this.drain().finally(() => {
|
|
1587
|
+
this.inFlight = void 0;
|
|
1588
|
+
});
|
|
1589
|
+
}
|
|
1590
|
+
return this.inFlight;
|
|
1591
|
+
}
|
|
1592
|
+
/** Counters for tests and diagnostics. */
|
|
1593
|
+
stats() {
|
|
1594
|
+
return {
|
|
1595
|
+
queueDepth: this.queue.length,
|
|
1596
|
+
droppedSinceLast: this.droppedSinceLast,
|
|
1597
|
+
droppedTotal: this.droppedTotal,
|
|
1598
|
+
deliveredTotal: this.deliveredTotal,
|
|
1599
|
+
flushing: this.inFlight !== void 0
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
push(dest, item, eligibleAt) {
|
|
1603
|
+
while (this.queue.length >= this.opts.maxItems) this.dropOldest();
|
|
1604
|
+
const entry = {
|
|
1605
|
+
dest,
|
|
1606
|
+
destKey: destKeyOf(dest),
|
|
1607
|
+
item,
|
|
1608
|
+
attempts: 0,
|
|
1609
|
+
eligibleAt,
|
|
1610
|
+
inFlight: false
|
|
1611
|
+
};
|
|
1612
|
+
this.queue.push(entry);
|
|
1613
|
+
return entry;
|
|
1614
|
+
}
|
|
1615
|
+
dropOldest() {
|
|
1616
|
+
if (this.queue.shift()) this.countDropped(1);
|
|
1617
|
+
}
|
|
1618
|
+
countDropped(n) {
|
|
1619
|
+
this.droppedSinceLast += n;
|
|
1620
|
+
this.droppedTotal += n;
|
|
1621
|
+
}
|
|
1622
|
+
async drain() {
|
|
1623
|
+
for (; ; ) {
|
|
1624
|
+
const now = this.opts.now();
|
|
1625
|
+
if (now < this.backoffUntil) return;
|
|
1626
|
+
const batch = this.takeBatch(now);
|
|
1627
|
+
if (batch.length === 0) return;
|
|
1628
|
+
const ok = await this.send(batch);
|
|
1629
|
+
if (!ok) return;
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
/** Oldest eligible item + up to maxBatch-1 more for the same destination. */
|
|
1633
|
+
takeBatch(now) {
|
|
1634
|
+
const first = this.queue.find((q) => q.eligibleAt <= now);
|
|
1635
|
+
if (!first) return [];
|
|
1636
|
+
const batch = [];
|
|
1637
|
+
const rest = [];
|
|
1638
|
+
for (const q of this.queue) {
|
|
1639
|
+
if (batch.length < this.opts.maxBatch && q.destKey === first.destKey && q.eligibleAt <= now) {
|
|
1640
|
+
q.inFlight = true;
|
|
1641
|
+
batch.push(q);
|
|
1642
|
+
} else {
|
|
1643
|
+
rest.push(q);
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
this.queue = rest;
|
|
1647
|
+
return batch;
|
|
1648
|
+
}
|
|
1649
|
+
/** POST one batch. Returns false when the drain should pause (backoff). */
|
|
1650
|
+
async send(batch) {
|
|
1651
|
+
const dest = batch[0].dest;
|
|
1652
|
+
const useKey = dest.apiKey && !this.rejectedKeys.has(dest.apiKey) ? dest.apiKey : void 0;
|
|
1653
|
+
const reportedDropped = this.droppedSinceLast;
|
|
1654
|
+
const meta = {
|
|
1655
|
+
droppedSinceLast: reportedDropped,
|
|
1656
|
+
queueDepth: this.queue.length,
|
|
1657
|
+
adapter: dest.adapter ?? "edge-core",
|
|
1658
|
+
version: SDK_VERSION
|
|
1659
|
+
};
|
|
1660
|
+
const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
|
|
1661
|
+
const response = await withBudget(
|
|
1662
|
+
sdkFetch(`${dest.apiBaseUrl}/verification-activity/edge-events`, {
|
|
1663
|
+
method: "POST",
|
|
1664
|
+
headers: {
|
|
1665
|
+
"Content-Type": "application/json",
|
|
1666
|
+
...useKey && { Authorization: `Bearer ${useKey}` }
|
|
1667
|
+
},
|
|
1668
|
+
body: JSON.stringify({ events: batch.map((q) => q.item), meta }),
|
|
1669
|
+
...controller && { signal: controller.signal }
|
|
1670
|
+
}),
|
|
1671
|
+
this.opts.requestTimeoutMs
|
|
1672
|
+
);
|
|
1673
|
+
if (!response) controller?.abort();
|
|
1674
|
+
const status = response?.status;
|
|
1675
|
+
for (const q of batch) q.inFlight = false;
|
|
1676
|
+
if (status !== void 0 && status >= 200 && status < 300) {
|
|
1677
|
+
this.droppedSinceLast = Math.max(0, this.droppedSinceLast - reportedDropped);
|
|
1678
|
+
this.deliveredTotal += batch.length;
|
|
1679
|
+
return true;
|
|
1680
|
+
}
|
|
1681
|
+
if (status === 401 && useKey) {
|
|
1682
|
+
this.rejectedKeys.add(useKey);
|
|
1683
|
+
this.requeue(batch, false);
|
|
1684
|
+
return true;
|
|
1685
|
+
}
|
|
1686
|
+
if (status === 429) {
|
|
1687
|
+
const retryAfter = Number(response?.headers.get("retry-after"));
|
|
1688
|
+
const waitMs = Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(retryAfter, 120) * 1e3 : 3e4;
|
|
1689
|
+
this.backoffUntil = this.opts.now() + waitMs;
|
|
1690
|
+
this.requeue(batch, false);
|
|
1691
|
+
return false;
|
|
1692
|
+
}
|
|
1693
|
+
if (status !== void 0 && status >= 400 && status < 500) {
|
|
1694
|
+
this.countDropped(batch.length);
|
|
1695
|
+
return true;
|
|
1696
|
+
}
|
|
1697
|
+
this.backoffUntil = this.opts.now() + this.opts.retryBackoffMs;
|
|
1698
|
+
this.requeue(batch, true);
|
|
1699
|
+
return false;
|
|
1700
|
+
}
|
|
1701
|
+
/** Put a failed batch back at the head of the queue (order preserved). */
|
|
1702
|
+
requeue(batch, countAttempt) {
|
|
1703
|
+
const keep = [];
|
|
1704
|
+
for (const q of batch) {
|
|
1705
|
+
if (countAttempt) q.attempts += 1;
|
|
1706
|
+
if (q.attempts >= this.opts.maxAttempts) this.countDropped(1);
|
|
1707
|
+
else keep.push(q);
|
|
1708
|
+
}
|
|
1709
|
+
this.queue = [...keep, ...this.queue];
|
|
1710
|
+
while (this.queue.length > this.opts.maxItems) this.dropOldest();
|
|
1711
|
+
}
|
|
1712
|
+
};
|
|
1713
|
+
var beaconBuffer = new BeaconBuffer();
|
|
1714
|
+
function getBeaconBuffer() {
|
|
1715
|
+
return beaconBuffer;
|
|
1716
|
+
}
|
|
1717
|
+
function resetBeaconBuffer(options) {
|
|
1718
|
+
beaconBuffer = new BeaconBuffer(options);
|
|
1719
|
+
return beaconBuffer;
|
|
1720
|
+
}
|
|
1721
|
+
async function emitBeacon(options, req, classification, budgetMs, queryValueAllowlist, apiKey) {
|
|
1467
1722
|
const attemptId = extractAttemptId(req);
|
|
1468
1723
|
const declaredSource = normalizeDeclaredSource(headerValue(req, "x-astra-source"));
|
|
1469
|
-
const
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1724
|
+
const userAgent = headerValue(req, "user-agent");
|
|
1725
|
+
const item = {
|
|
1726
|
+
...options.counterpartyId && { counterpartyId: options.counterpartyId },
|
|
1727
|
+
...options.counterpartyUrl && { counterpartyUrl: options.counterpartyUrl },
|
|
1728
|
+
counterpartyType: "website",
|
|
1729
|
+
sourceIp: req.clientIp,
|
|
1730
|
+
userAgent,
|
|
1731
|
+
requestPath: req.uri,
|
|
1732
|
+
requestMethod: req.method,
|
|
1733
|
+
// Attempt-chain handle (header or ?astra_attempt= deep link) — joins
|
|
1734
|
+
// this beacon into the agent's research chain via pe_attempt_idx.
|
|
1735
|
+
...attemptId && { attemptId },
|
|
1736
|
+
// First-class referer + accept: traffic-source and surface-classifier
|
|
1737
|
+
// inputs (previously buried inside observedMetadata.headers).
|
|
1738
|
+
referer: headerValue(req, "referer"),
|
|
1739
|
+
accept: headerValue(req, "accept"),
|
|
1740
|
+
...declaredSource && { declaredSource },
|
|
1741
|
+
// Evidence fields the backend persists into the event record.
|
|
1742
|
+
protocol: classification.protocol,
|
|
1743
|
+
evidence: classification.evidence,
|
|
1744
|
+
// The full sanitized header + connection capture for the unregistered
|
|
1745
|
+
// caller.
|
|
1746
|
+
observedMetadata: buildObservedMetadata(req, queryValueAllowlist),
|
|
1747
|
+
// Capture time — batched delivery skews createdAt by the flush delay.
|
|
1748
|
+
observedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1749
|
+
};
|
|
1750
|
+
for (const k of Object.keys(item)) if (item[k] === void 0) delete item[k];
|
|
1751
|
+
const buffer = getBeaconBuffer();
|
|
1752
|
+
buffer.enqueue(
|
|
1753
|
+
{
|
|
1754
|
+
apiBaseUrl: options.apiBaseUrl,
|
|
1755
|
+
...apiKey && { apiKey },
|
|
1756
|
+
counterparty: options.counterpartyId || options.counterpartyUrl,
|
|
1757
|
+
...options.adapter && { adapter: options.adapter }
|
|
1758
|
+
},
|
|
1759
|
+
item,
|
|
1760
|
+
// Repeat collapse needs a client identity; without an IP, distinct
|
|
1761
|
+
// callers sharing a UA would merge, so don't collapse.
|
|
1762
|
+
req.clientIp ? [
|
|
1763
|
+
options.counterpartyId || options.counterpartyUrl,
|
|
1764
|
+
req.clientIp,
|
|
1765
|
+
userAgent ?? "",
|
|
1766
|
+
req.method,
|
|
1767
|
+
req.uri,
|
|
1768
|
+
attemptId ?? ""
|
|
1769
|
+
].join("\0") : void 0
|
|
1496
1770
|
);
|
|
1771
|
+
const flushing = buffer.flush();
|
|
1497
1772
|
if (options.waitUntil) {
|
|
1498
|
-
options.waitUntil(
|
|
1773
|
+
options.waitUntil(flushing);
|
|
1499
1774
|
return;
|
|
1500
1775
|
}
|
|
1501
|
-
await
|
|
1776
|
+
await withBudget(flushing, budgetMs);
|
|
1502
1777
|
}
|
|
1503
1778
|
function sampled(rate, random = Math.random) {
|
|
1504
1779
|
if (rate <= 0) return false;
|
|
@@ -1663,7 +1938,7 @@ async function runTier(options, req, classification, policy, apiKey, capture) {
|
|
|
1663
1938
|
}
|
|
1664
1939
|
return outcomeFromVerifyResult(classification, result);
|
|
1665
1940
|
}
|
|
1666
|
-
await emitBeacon(options, req, classification, beaconMs, queryValueAllowlist);
|
|
1941
|
+
await emitBeacon(options, req, classification, beaconMs, queryValueAllowlist, apiKey);
|
|
1667
1942
|
const outcome = baseOutcome(classification);
|
|
1668
1943
|
if (policy.mode === "enforce" && policy.depth !== "classify") {
|
|
1669
1944
|
outcome.decision = "deny";
|
|
@@ -1678,7 +1953,8 @@ async function runTier(options, req, classification, policy, apiKey, capture) {
|
|
|
1678
1953
|
req,
|
|
1679
1954
|
{ ...classification, protocol: classification.protocol ?? "ua-heuristic" },
|
|
1680
1955
|
beaconMs,
|
|
1681
|
-
queryValueAllowlist
|
|
1956
|
+
queryValueAllowlist,
|
|
1957
|
+
apiKey
|
|
1682
1958
|
);
|
|
1683
1959
|
}
|
|
1684
1960
|
return baseOutcome(classification);
|
|
@@ -1731,6 +2007,7 @@ async function evaluateEdgeRequest(options, req, config, apiKey) {
|
|
|
1731
2007
|
}
|
|
1732
2008
|
export {
|
|
1733
2009
|
ASAT_HEADERS,
|
|
2010
|
+
BeaconBuffer,
|
|
1734
2011
|
_patterns,
|
|
1735
2012
|
applyOutcome,
|
|
1736
2013
|
buildDenyBody,
|
|
@@ -1741,10 +2018,12 @@ export {
|
|
|
1741
2018
|
detectCommerce,
|
|
1742
2019
|
emitBeacon,
|
|
1743
2020
|
evaluateEdgeRequest,
|
|
2021
|
+
getBeaconBuffer,
|
|
1744
2022
|
headerValue,
|
|
1745
2023
|
outcomeFromVerifyResult,
|
|
1746
2024
|
passUntouched,
|
|
1747
2025
|
requestUrl,
|
|
2026
|
+
resetBeaconBuffer,
|
|
1748
2027
|
resolvePolicy,
|
|
1749
2028
|
runTier,
|
|
1750
2029
|
sampled,
|