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