@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.
- 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 +394 -39
- package/dist/adapters/express.js.map +1 -1
- package/dist/adapters/express.mjs +394 -39
- 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 +193 -49
- package/dist/adapters/mcp.js.map +1 -1
- package/dist/adapters/mcp.mjs +193 -49
- 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 +387 -22
- package/dist/adapters/nextjs.js.map +1 -1
- package/dist/adapters/nextjs.mjs +387 -22
- 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 +125 -16
- package/dist/adapters/sdk.js.map +1 -1
- package/dist/adapters/sdk.mjs +125 -16
- 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 +309 -15
- package/dist/bin/astrasync-codex-hook.js +309 -15
- package/dist/bin/astrasync-guard.js +309 -15
- package/dist/bin/astrasync.js +322 -17
- package/dist/browser/background.js +309 -15
- package/dist/browser/background.js.map +1 -1
- package/dist/browser/background.mjs +309 -15
- package/dist/browser/background.mjs.map +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +1 -1
- package/dist/cli/index.mjs.map +1 -1
- package/dist/codex/index.js +309 -15
- package/dist/codex/index.js.map +1 -1
- package/dist/codex/index.mjs +309 -15
- package/dist/codex/index.mjs.map +1 -1
- package/dist/cursor/extension.js +309 -15
- package/dist/cursor/extension.js.map +1 -1
- package/dist/cursor/extension.mjs +309 -15
- 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 +362 -63
- package/dist/edge-core/index.js.map +1 -1
- package/dist/edge-core/index.mjs +359 -63
- package/dist/edge-core/index.mjs.map +1 -1
- package/dist/gateway/gateway.js +309 -15
- package/dist/gateway/gateway.js.map +1 -1
- package/dist/gateway/gateway.mjs +309 -15
- 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 +118 -15
- package/dist/index.d.ts +118 -15
- package/dist/index.js +521 -72
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +515 -72
- 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 -14
- package/dist/verify.d.ts +35 -14
- package/dist/verify.js +165 -25
- package/dist/verify.js.map +1 -1
- package/dist/verify.mjs +164 -25
- package/dist/verify.mjs.map +1 -1
- package/package.json +3 -2
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.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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
1790
|
+
options.waitUntil(flushing);
|
|
1499
1791
|
return;
|
|
1500
1792
|
}
|
|
1501
|
-
await
|
|
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,
|