@astrasyncai/verification-gateway 5.11.0 → 5.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +284 -7
- package/dist/bin/astrasync-codex-hook.js +284 -7
- package/dist/bin/astrasync-guard.js +284 -7
- package/dist/bin/astrasync.js +297 -9
- 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.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 +284 -7
- package/dist/codex/index.js.map +1 -1
- package/dist/codex/index.mjs +284 -7
- package/dist/codex/index.mjs.map +1 -1
- package/dist/cursor/extension.js +284 -7
- package/dist/cursor/extension.js.map +1 -1
- package/dist/cursor/extension.mjs +284 -7
- 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/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 +118 -5
- package/dist/index.d.ts +118 -5
- package/dist/index.js +486 -63
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +480 -63
- 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/README.md
CHANGED
|
@@ -272,6 +272,10 @@ import express from 'express';
|
|
|
272
272
|
import { createMcpMiddleware } from '@astrasyncai/verification-gateway/mcp';
|
|
273
273
|
|
|
274
274
|
const mcp = express();
|
|
275
|
+
// Behind a load balancer / CDN, set trust proxy to your real hop count so
|
|
276
|
+
// req.ip (the client IP recorded on verify-access) is the caller, not the
|
|
277
|
+
// proxy. The middleware never trusts a raw leftmost X-Forwarded-For over it.
|
|
278
|
+
mcp.set('trust proxy', 1);
|
|
275
279
|
mcp.use(express.json());
|
|
276
280
|
mcp.use(
|
|
277
281
|
createMcpMiddleware({
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RequestHandler, Request } from 'express';
|
|
2
|
-
import { d as VerificationResult, E as ExpressMiddlewareOptions, a as AstraSyncCredentials } from '../types-
|
|
2
|
+
import { d as VerificationResult, E as ExpressMiddlewareOptions, a as AstraSyncCredentials } from '../types-BCkwBA84.mjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* AstraSync Universal Verification Gateway - Express Middleware
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RequestHandler, Request } from 'express';
|
|
2
|
-
import { d as VerificationResult, E as ExpressMiddlewareOptions, a as AstraSyncCredentials } from '../types-
|
|
2
|
+
import { d as VerificationResult, E as ExpressMiddlewareOptions, a as AstraSyncCredentials } from '../types-BCkwBA84.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* AstraSync Universal Verification Gateway - Express Middleware
|
package/dist/adapters/express.js
CHANGED
|
@@ -26,7 +26,7 @@ __export(express_exports, {
|
|
|
26
26
|
module.exports = __toCommonJS(express_exports);
|
|
27
27
|
|
|
28
28
|
// src/version.ts
|
|
29
|
-
var SDK_VERSION = "5.
|
|
29
|
+
var SDK_VERSION = "5.12.0";
|
|
30
30
|
|
|
31
31
|
// src/http.ts
|
|
32
32
|
var SDK_USER_AGENT = `astrasync-sdk/${SDK_VERSION}`;
|
|
@@ -171,6 +171,59 @@ function getCachedWellKnownUrls(apiBaseUrl) {
|
|
|
171
171
|
return cache.get(apiBaseUrl)?.data;
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
// src/failure-posture.ts
|
|
175
|
+
var VERIFICATION_STATUS_HEADER = "X-Astra-Verification";
|
|
176
|
+
var DEFAULT_UNAVAILABLE_RETRY_AFTER_S = 30;
|
|
177
|
+
var REJECTED_IDENTITY_DIMENSIONS = /* @__PURE__ */ new Set([
|
|
178
|
+
"identity.invalid_agent_id",
|
|
179
|
+
"identity.unknown_agent",
|
|
180
|
+
"agent.lookup"
|
|
181
|
+
]);
|
|
182
|
+
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.";
|
|
183
|
+
var UNAVAILABLE_DIMENSIONS = /* @__PURE__ */ new Set([
|
|
184
|
+
"verify_access.api_error",
|
|
185
|
+
"middleware.internal_error"
|
|
186
|
+
]);
|
|
187
|
+
function isVerificationUnavailable(result) {
|
|
188
|
+
if (result.misconfigured) return false;
|
|
189
|
+
return (result.failures ?? []).some((f) => UNAVAILABLE_DIMENSIONS.has(f.dimension));
|
|
190
|
+
}
|
|
191
|
+
function unavailableRetryAfterSeconds(result) {
|
|
192
|
+
if (typeof result.retryAfterMs === "number" && Number.isFinite(result.retryAfterMs)) {
|
|
193
|
+
return Math.max(1, Math.ceil(result.retryAfterMs / 1e3));
|
|
194
|
+
}
|
|
195
|
+
return DEFAULT_UNAVAILABLE_RETRY_AFTER_S;
|
|
196
|
+
}
|
|
197
|
+
function isAnonymousRead(method, credentials) {
|
|
198
|
+
const m = (method ?? "").toUpperCase();
|
|
199
|
+
if (m !== "GET" && m !== "HEAD") return false;
|
|
200
|
+
return !credentials.astraId && !credentials.jwt;
|
|
201
|
+
}
|
|
202
|
+
function buildVerificationUnavailableBody(result) {
|
|
203
|
+
return {
|
|
204
|
+
success: false,
|
|
205
|
+
error: {
|
|
206
|
+
code: "VERIFICATION_UNAVAILABLE",
|
|
207
|
+
message: "Agent verification is temporarily unavailable and this path enforces verification. Your identity was not evaluated and no denial was recorded \u2014 retry after the Retry-After interval.",
|
|
208
|
+
retryable: true,
|
|
209
|
+
retryAfterSeconds: unavailableRetryAfterSeconds(result),
|
|
210
|
+
...result.failures && { failures: result.failures },
|
|
211
|
+
...result.correlationId && { correlationId: result.correlationId },
|
|
212
|
+
guidance: {
|
|
213
|
+
message: "Retry the request after the Retry-After interval, with exponential backoff; the condition is transient."
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function isRejectedIdentityClaim(result) {
|
|
219
|
+
return (result.failures ?? []).some((f) => REJECTED_IDENTITY_DIMENSIONS.has(f.dimension));
|
|
220
|
+
}
|
|
221
|
+
function denyStatusFor(result, policyDeniedCode) {
|
|
222
|
+
if (isRejectedIdentityClaim(result)) return { status: 403, code: "AGENT_IDENTITY_REJECTED" };
|
|
223
|
+
if (result.identityVerified) return { status: 403, code: policyDeniedCode };
|
|
224
|
+
return { status: 401, code: "UNAUTHORIZED" };
|
|
225
|
+
}
|
|
226
|
+
|
|
174
227
|
// src/verify.ts
|
|
175
228
|
var DEFAULT_CONFIG = {
|
|
176
229
|
apiBaseUrl: "https://astrasync.ai/api",
|
|
@@ -416,11 +469,44 @@ function createGuidanceResponse(_config, reason, options = {}) {
|
|
|
416
469
|
// coercing the absent recommendation to a policy 'deny'. Misconfig is
|
|
417
470
|
// deterministic and intentionally NOT marked retryable.
|
|
418
471
|
...isApiError && { retryable: true },
|
|
472
|
+
...options.httpStatus !== void 0 && { httpStatus: options.httpStatus },
|
|
473
|
+
...options.retryAfterMs !== void 0 && { retryAfterMs: options.retryAfterMs },
|
|
419
474
|
correlationId: options.correlationId,
|
|
420
475
|
attemptId: options.attemptId,
|
|
421
476
|
verifiedAt: /* @__PURE__ */ new Date()
|
|
422
477
|
};
|
|
423
478
|
}
|
|
479
|
+
async function readJsonSafe(response) {
|
|
480
|
+
try {
|
|
481
|
+
const parsed = await response.json();
|
|
482
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
483
|
+
} catch {
|
|
484
|
+
return {};
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
function rejectedIdentityGuidance(failures) {
|
|
488
|
+
const f = failures?.find((x) => REJECTED_IDENTITY_DIMENSIONS.has(x.dimension));
|
|
489
|
+
if (!f) return void 0;
|
|
490
|
+
if (f.dimension === "agent.lookup") {
|
|
491
|
+
return `The presented agent identifier is not registered with AstraSync. ${UNKNOWN_AGENT_GUIDANCE}`;
|
|
492
|
+
}
|
|
493
|
+
return f.guidance ? `${f.message} ${f.guidance}` : f.message;
|
|
494
|
+
}
|
|
495
|
+
var MAX_RETRY_AFTER_MS = 15 * 60 * 1e3;
|
|
496
|
+
function parseRetryAfterMs(response) {
|
|
497
|
+
const raw = response.headers?.get?.("retry-after");
|
|
498
|
+
if (!raw) return void 0;
|
|
499
|
+
const trimmed = raw.trim();
|
|
500
|
+
let ms;
|
|
501
|
+
if (/^\d+(\.\d+)?$/.test(trimmed)) {
|
|
502
|
+
ms = Number(trimmed) * 1e3;
|
|
503
|
+
} else {
|
|
504
|
+
const at = Date.parse(trimmed);
|
|
505
|
+
if (Number.isNaN(at)) return void 0;
|
|
506
|
+
ms = at - Date.now();
|
|
507
|
+
}
|
|
508
|
+
return Math.min(MAX_RETRY_AFTER_MS, Math.max(0, Math.round(ms)));
|
|
509
|
+
}
|
|
424
510
|
async function callVerifyAccessAPI(config, request) {
|
|
425
511
|
const { credentials, ...requestData } = request;
|
|
426
512
|
const body = {
|
|
@@ -477,7 +563,8 @@ async function callVerifyAccessAPI(config, request) {
|
|
|
477
563
|
body: JSON.stringify(body),
|
|
478
564
|
signal: AbortSignal.timeout(config.verifyTimeoutMs ?? 1e4)
|
|
479
565
|
});
|
|
480
|
-
const data = await response
|
|
566
|
+
const data = await readJsonSafe(response);
|
|
567
|
+
const retryAfterMs = parseRetryAfterMs(response);
|
|
481
568
|
if (response.status === 410) {
|
|
482
569
|
return {
|
|
483
570
|
success: true,
|
|
@@ -515,6 +602,23 @@ async function callVerifyAccessAPI(config, request) {
|
|
|
515
602
|
}
|
|
516
603
|
};
|
|
517
604
|
}
|
|
605
|
+
if (response.status === 404 && credentials.astraId && (data?.error === "AGENT_NOT_FOUND" || typeof data?.message === "string" && /agent/i.test(data.message))) {
|
|
606
|
+
return {
|
|
607
|
+
success: true,
|
|
608
|
+
attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0,
|
|
609
|
+
access: {
|
|
610
|
+
allowed: false,
|
|
611
|
+
reason: "unknown_agent",
|
|
612
|
+
failures: [
|
|
613
|
+
{
|
|
614
|
+
dimension: "identity.unknown_agent",
|
|
615
|
+
message: "The presented agent identifier is not registered with AstraSync.",
|
|
616
|
+
guidance: UNKNOWN_AGENT_GUIDANCE
|
|
617
|
+
}
|
|
618
|
+
]
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
}
|
|
518
622
|
if (!response.ok) {
|
|
519
623
|
if (response.status === 401) {
|
|
520
624
|
return {
|
|
@@ -529,9 +633,11 @@ async function callVerifyAccessAPI(config, request) {
|
|
|
529
633
|
}
|
|
530
634
|
return {
|
|
531
635
|
success: false,
|
|
532
|
-
error: data.message || data.error || `API returned ${response.status}`,
|
|
636
|
+
error: typeof data?.message === "string" && data.message || typeof data?.error === "string" && data.error || `API returned ${response.status}`,
|
|
533
637
|
correlationId: typeof data?.correlationId === "string" ? data.correlationId : void 0,
|
|
534
|
-
attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0
|
|
638
|
+
attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0,
|
|
639
|
+
httpStatus: response.status,
|
|
640
|
+
...retryAfterMs !== void 0 && { retryAfterMs }
|
|
535
641
|
};
|
|
536
642
|
}
|
|
537
643
|
return data;
|
|
@@ -603,12 +709,14 @@ async function verify(config, request, options) {
|
|
|
603
709
|
correlationId: apiResponse.correlationId,
|
|
604
710
|
attemptId: apiResponse.attemptId,
|
|
605
711
|
remediation: apiResponse.remediation,
|
|
712
|
+
httpStatus: apiResponse.httpStatus,
|
|
713
|
+
retryAfterMs: apiResponse.retryAfterMs,
|
|
606
714
|
urls
|
|
607
715
|
});
|
|
608
716
|
}
|
|
609
717
|
if (!apiResponse.access?.allowed) {
|
|
610
718
|
const aggregatedFailures = apiResponse.access?.failures;
|
|
611
|
-
const idVerifiedFromBackend = apiResponse.verificationContext?.idVerified === true;
|
|
719
|
+
const idVerifiedFromBackend = apiResponse.verificationContext?.idVerified === true && !aggregatedFailures?.some((f) => REJECTED_IDENTITY_DIMENSIONS.has(f.dimension));
|
|
612
720
|
const result2 = {
|
|
613
721
|
identityVerified: idVerifiedFromBackend,
|
|
614
722
|
policyAllowed: false,
|
|
@@ -620,7 +728,10 @@ async function verify(config, request, options) {
|
|
|
620
728
|
requiresStepUp: apiResponse.access?.requiresStepUp,
|
|
621
729
|
requiresApproval: apiResponse.access?.requiresApproval,
|
|
622
730
|
guidance: {
|
|
623
|
-
|
|
731
|
+
// 5.12.0: a presented-but-rejected id (malformed / unknown) carries
|
|
732
|
+
// actionable registration guidance on its failure — surface it
|
|
733
|
+
// instead of the bare machine reason ('unknown_agent').
|
|
734
|
+
message: rejectedIdentityGuidance(aggregatedFailures) ?? (apiResponse.access?.reason || "Access denied by PDLSS policy"),
|
|
624
735
|
registrationUrl: urls?.registrationUrl ?? "",
|
|
625
736
|
documentationUrl: urls?.documentationUrl ?? "",
|
|
626
737
|
// 5.1.0: anonymous callers land on this branch — steer MCP-hosted
|
|
@@ -720,7 +831,7 @@ async function verify(config, request, options) {
|
|
|
720
831
|
registrationUrl: urls?.registrationUrl ?? "",
|
|
721
832
|
documentationUrl: urls?.documentationUrl ?? ""
|
|
722
833
|
};
|
|
723
|
-
} else if (result.recommendation === "step_up_required") {
|
|
834
|
+
} else if (result.recommendation === "step_up_required" && result.identityVerified) {
|
|
724
835
|
result.requiresStepUp = true;
|
|
725
836
|
result.denialReasons = serverDenialReasons ?? result.recommendationReasons ?? ["Step-up verification required"];
|
|
726
837
|
}
|
|
@@ -772,7 +883,10 @@ async function reportUnregisteredAttempt(config, data) {
|
|
|
772
883
|
};
|
|
773
884
|
await sdkFetch(`${apiBaseUrl}/verification-activity/unregistered-attempt`, {
|
|
774
885
|
method: "POST",
|
|
775
|
-
headers: {
|
|
886
|
+
headers: {
|
|
887
|
+
"Content-Type": "application/json",
|
|
888
|
+
...config.apiKey && { Authorization: `Bearer ${config.apiKey}` }
|
|
889
|
+
},
|
|
776
890
|
body: JSON.stringify(body)
|
|
777
891
|
}).catch(() => {
|
|
778
892
|
});
|
|
@@ -1362,7 +1476,10 @@ var PLATFORM_AGENT_SIGNATURES = [
|
|
|
1362
1476
|
{
|
|
1363
1477
|
vendor: "chatgpt",
|
|
1364
1478
|
displayName: "OpenAI ChatGPT",
|
|
1365
|
-
|
|
1479
|
+
// GPTBot (training crawler) and OAI-SearchBot (ChatGPT search) are
|
|
1480
|
+
// OpenAI's declared crawler UAs — `gpt-?\d` never matched them, so they
|
|
1481
|
+
// classified as unattributed crawlers (round-27.4 W5).
|
|
1482
|
+
uaPatterns: [/chatgpt/i, /openai/i, /gpt-?\d/i, /\bGPTBot\b/i, /\bOAI-SearchBot\b/i],
|
|
1366
1483
|
agentCardPatterns: [/openai\.com/i, /chatgpt\.com/i],
|
|
1367
1484
|
apiKeyFormatPatterns: [/^sk-proj/i, /^sk-svcacct/i],
|
|
1368
1485
|
headerPatterns: [/^openai-organization/i, /^openai-project/i, /^openai-version/i]
|
|
@@ -1497,6 +1614,215 @@ var BROWSER_UA_PATTERN = /mozilla\/\d/i;
|
|
|
1497
1614
|
var _patterns = { STATIC_EXTENSIONS, BOT_UA_PATTERN, BROWSER_UA_PATTERN, SELF_UA_PATTERN };
|
|
1498
1615
|
|
|
1499
1616
|
// src/edge-core/events.ts
|
|
1617
|
+
async function withBudget(promise, ms) {
|
|
1618
|
+
let timer;
|
|
1619
|
+
try {
|
|
1620
|
+
return await Promise.race([
|
|
1621
|
+
promise,
|
|
1622
|
+
new Promise((resolve) => {
|
|
1623
|
+
timer = setTimeout(() => resolve(void 0), ms);
|
|
1624
|
+
})
|
|
1625
|
+
]);
|
|
1626
|
+
} catch {
|
|
1627
|
+
return void 0;
|
|
1628
|
+
} finally {
|
|
1629
|
+
if (timer) clearTimeout(timer);
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
var destKeyOf = (d) => `${d.apiBaseUrl}\0${d.apiKey ?? ""}\0${d.counterparty}\0${d.adapter ?? ""}`;
|
|
1633
|
+
var BeaconBuffer = class {
|
|
1634
|
+
constructor(options = {}) {
|
|
1635
|
+
this.queue = [];
|
|
1636
|
+
this.droppedSinceLast = 0;
|
|
1637
|
+
this.droppedTotal = 0;
|
|
1638
|
+
this.deliveredTotal = 0;
|
|
1639
|
+
this.backoffUntil = 0;
|
|
1640
|
+
this.collapse = /* @__PURE__ */ new Map();
|
|
1641
|
+
/** Keys the backend answered 401 for — later batches go unauthenticated. */
|
|
1642
|
+
this.rejectedKeys = /* @__PURE__ */ new Set();
|
|
1643
|
+
this.opts = {
|
|
1644
|
+
maxItems: options.maxItems ?? 1e3,
|
|
1645
|
+
maxBatch: options.maxBatch ?? 100,
|
|
1646
|
+
maxAttempts: options.maxAttempts ?? 3,
|
|
1647
|
+
collapseWindowMs: options.collapseWindowMs ?? 6e4,
|
|
1648
|
+
collapseMaxKeys: options.collapseMaxKeys ?? 1e3,
|
|
1649
|
+
requestTimeoutMs: options.requestTimeoutMs ?? 1e4,
|
|
1650
|
+
retryBackoffMs: options.retryBackoffMs ?? 1e3,
|
|
1651
|
+
now: options.now ?? Date.now
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
/**
|
|
1655
|
+
* Queue one beacon. With a `collapseKey`, a repeat inside the window is
|
|
1656
|
+
* merged into a held aggregate (sent when the window closes) instead of
|
|
1657
|
+
* becoming its own item.
|
|
1658
|
+
*/
|
|
1659
|
+
enqueue(dest, item, collapseKey) {
|
|
1660
|
+
const now = this.opts.now();
|
|
1661
|
+
if (collapseKey) {
|
|
1662
|
+
const entry = this.collapse.get(collapseKey);
|
|
1663
|
+
if (entry && now - entry.firstSeenAt < this.opts.collapseWindowMs) {
|
|
1664
|
+
for (const target of [entry.first, entry.held]) {
|
|
1665
|
+
if (target && !target.inFlight && this.queue.includes(target)) {
|
|
1666
|
+
target.item.repeatCount = (target.item.repeatCount ?? 1) + 1;
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
const held = this.push(
|
|
1671
|
+
dest,
|
|
1672
|
+
{ ...item, repeatCount: 1 },
|
|
1673
|
+
entry.firstSeenAt + this.opts.collapseWindowMs
|
|
1674
|
+
);
|
|
1675
|
+
entry.held = held;
|
|
1676
|
+
return;
|
|
1677
|
+
}
|
|
1678
|
+
this.collapse.delete(collapseKey);
|
|
1679
|
+
if (this.collapse.size >= this.opts.collapseMaxKeys) {
|
|
1680
|
+
const oldest = this.collapse.keys().next().value;
|
|
1681
|
+
if (oldest !== void 0) this.collapse.delete(oldest);
|
|
1682
|
+
}
|
|
1683
|
+
const fresh = { firstSeenAt: now };
|
|
1684
|
+
this.collapse.set(collapseKey, fresh);
|
|
1685
|
+
fresh.first = this.push(dest, item, now);
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
this.push(dest, item, now);
|
|
1689
|
+
}
|
|
1690
|
+
/**
|
|
1691
|
+
* Deliver every eligible queued item. Concurrent callers share ONE
|
|
1692
|
+
* in-flight drain (it survives the caller's budget and a Lambda freeze),
|
|
1693
|
+
* so nothing in flight is ever sent twice; items queued while it runs are
|
|
1694
|
+
* picked up by the same drain.
|
|
1695
|
+
*/
|
|
1696
|
+
flush() {
|
|
1697
|
+
if (!this.inFlight) {
|
|
1698
|
+
this.inFlight = this.drain().finally(() => {
|
|
1699
|
+
this.inFlight = void 0;
|
|
1700
|
+
});
|
|
1701
|
+
}
|
|
1702
|
+
return this.inFlight;
|
|
1703
|
+
}
|
|
1704
|
+
/** Counters for tests and diagnostics. */
|
|
1705
|
+
stats() {
|
|
1706
|
+
return {
|
|
1707
|
+
queueDepth: this.queue.length,
|
|
1708
|
+
droppedSinceLast: this.droppedSinceLast,
|
|
1709
|
+
droppedTotal: this.droppedTotal,
|
|
1710
|
+
deliveredTotal: this.deliveredTotal,
|
|
1711
|
+
flushing: this.inFlight !== void 0
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
push(dest, item, eligibleAt) {
|
|
1715
|
+
while (this.queue.length >= this.opts.maxItems) this.dropOldest();
|
|
1716
|
+
const entry = {
|
|
1717
|
+
dest,
|
|
1718
|
+
destKey: destKeyOf(dest),
|
|
1719
|
+
item,
|
|
1720
|
+
attempts: 0,
|
|
1721
|
+
eligibleAt,
|
|
1722
|
+
inFlight: false
|
|
1723
|
+
};
|
|
1724
|
+
this.queue.push(entry);
|
|
1725
|
+
return entry;
|
|
1726
|
+
}
|
|
1727
|
+
dropOldest() {
|
|
1728
|
+
if (this.queue.shift()) this.countDropped(1);
|
|
1729
|
+
}
|
|
1730
|
+
countDropped(n) {
|
|
1731
|
+
this.droppedSinceLast += n;
|
|
1732
|
+
this.droppedTotal += n;
|
|
1733
|
+
}
|
|
1734
|
+
async drain() {
|
|
1735
|
+
for (; ; ) {
|
|
1736
|
+
const now = this.opts.now();
|
|
1737
|
+
if (now < this.backoffUntil) return;
|
|
1738
|
+
const batch = this.takeBatch(now);
|
|
1739
|
+
if (batch.length === 0) return;
|
|
1740
|
+
const ok = await this.send(batch);
|
|
1741
|
+
if (!ok) return;
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
/** Oldest eligible item + up to maxBatch-1 more for the same destination. */
|
|
1745
|
+
takeBatch(now) {
|
|
1746
|
+
const first = this.queue.find((q) => q.eligibleAt <= now);
|
|
1747
|
+
if (!first) return [];
|
|
1748
|
+
const batch = [];
|
|
1749
|
+
const rest = [];
|
|
1750
|
+
for (const q of this.queue) {
|
|
1751
|
+
if (batch.length < this.opts.maxBatch && q.destKey === first.destKey && q.eligibleAt <= now) {
|
|
1752
|
+
q.inFlight = true;
|
|
1753
|
+
batch.push(q);
|
|
1754
|
+
} else {
|
|
1755
|
+
rest.push(q);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
this.queue = rest;
|
|
1759
|
+
return batch;
|
|
1760
|
+
}
|
|
1761
|
+
/** POST one batch. Returns false when the drain should pause (backoff). */
|
|
1762
|
+
async send(batch) {
|
|
1763
|
+
const dest = batch[0].dest;
|
|
1764
|
+
const useKey = dest.apiKey && !this.rejectedKeys.has(dest.apiKey) ? dest.apiKey : void 0;
|
|
1765
|
+
const reportedDropped = this.droppedSinceLast;
|
|
1766
|
+
const meta = {
|
|
1767
|
+
droppedSinceLast: reportedDropped,
|
|
1768
|
+
queueDepth: this.queue.length,
|
|
1769
|
+
adapter: dest.adapter ?? "edge-core",
|
|
1770
|
+
version: SDK_VERSION
|
|
1771
|
+
};
|
|
1772
|
+
const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
|
|
1773
|
+
const response = await withBudget(
|
|
1774
|
+
sdkFetch(`${dest.apiBaseUrl}/verification-activity/edge-events`, {
|
|
1775
|
+
method: "POST",
|
|
1776
|
+
headers: {
|
|
1777
|
+
"Content-Type": "application/json",
|
|
1778
|
+
...useKey && { Authorization: `Bearer ${useKey}` }
|
|
1779
|
+
},
|
|
1780
|
+
body: JSON.stringify({ events: batch.map((q) => q.item), meta }),
|
|
1781
|
+
...controller && { signal: controller.signal }
|
|
1782
|
+
}),
|
|
1783
|
+
this.opts.requestTimeoutMs
|
|
1784
|
+
);
|
|
1785
|
+
if (!response) controller?.abort();
|
|
1786
|
+
const status = response?.status;
|
|
1787
|
+
for (const q of batch) q.inFlight = false;
|
|
1788
|
+
if (status !== void 0 && status >= 200 && status < 300) {
|
|
1789
|
+
this.droppedSinceLast = Math.max(0, this.droppedSinceLast - reportedDropped);
|
|
1790
|
+
this.deliveredTotal += batch.length;
|
|
1791
|
+
return true;
|
|
1792
|
+
}
|
|
1793
|
+
if (status === 401 && useKey) {
|
|
1794
|
+
this.rejectedKeys.add(useKey);
|
|
1795
|
+
this.requeue(batch, false);
|
|
1796
|
+
return true;
|
|
1797
|
+
}
|
|
1798
|
+
if (status === 429) {
|
|
1799
|
+
const retryAfter = Number(response?.headers.get("retry-after"));
|
|
1800
|
+
const waitMs = Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(retryAfter, 120) * 1e3 : 3e4;
|
|
1801
|
+
this.backoffUntil = this.opts.now() + waitMs;
|
|
1802
|
+
this.requeue(batch, false);
|
|
1803
|
+
return false;
|
|
1804
|
+
}
|
|
1805
|
+
if (status !== void 0 && status >= 400 && status < 500) {
|
|
1806
|
+
this.countDropped(batch.length);
|
|
1807
|
+
return true;
|
|
1808
|
+
}
|
|
1809
|
+
this.backoffUntil = this.opts.now() + this.opts.retryBackoffMs;
|
|
1810
|
+
this.requeue(batch, true);
|
|
1811
|
+
return false;
|
|
1812
|
+
}
|
|
1813
|
+
/** Put a failed batch back at the head of the queue (order preserved). */
|
|
1814
|
+
requeue(batch, countAttempt) {
|
|
1815
|
+
const keep = [];
|
|
1816
|
+
for (const q of batch) {
|
|
1817
|
+
if (countAttempt) q.attempts += 1;
|
|
1818
|
+
if (q.attempts >= this.opts.maxAttempts) this.countDropped(1);
|
|
1819
|
+
else keep.push(q);
|
|
1820
|
+
}
|
|
1821
|
+
this.queue = [...keep, ...this.queue];
|
|
1822
|
+
while (this.queue.length > this.opts.maxItems) this.dropOldest();
|
|
1823
|
+
}
|
|
1824
|
+
};
|
|
1825
|
+
var beaconBuffer = new BeaconBuffer();
|
|
1500
1826
|
function sampled(rate, random = Math.random) {
|
|
1501
1827
|
if (rate <= 0) return false;
|
|
1502
1828
|
if (rate >= 1) return true;
|
|
@@ -1673,12 +1999,19 @@ function defaultOnDenied(result, _req, res) {
|
|
|
1673
1999
|
});
|
|
1674
2000
|
return;
|
|
1675
2001
|
}
|
|
1676
|
-
|
|
2002
|
+
if (isVerificationUnavailable(result)) {
|
|
2003
|
+
res.setHeader("X-Astra-Gateway-Mode", "enforced");
|
|
2004
|
+
res.setHeader(VERIFICATION_STATUS_HEADER, "unavailable");
|
|
2005
|
+
res.setHeader("Retry-After", String(unavailableRetryAfterSeconds(result)));
|
|
2006
|
+
res.status(503).json(buildVerificationUnavailableBody(result));
|
|
2007
|
+
return;
|
|
2008
|
+
}
|
|
2009
|
+
const { status: statusCode, code } = denyStatusFor(result, "INSUFFICIENT_ACCESS");
|
|
1677
2010
|
res.setHeader("X-Astra-Gateway-Mode", "enforced");
|
|
1678
2011
|
res.status(statusCode).json({
|
|
1679
2012
|
success: false,
|
|
1680
2013
|
error: {
|
|
1681
|
-
code
|
|
2014
|
+
code,
|
|
1682
2015
|
message: result.denialReasons?.[0] || "Access denied",
|
|
1683
2016
|
guidance: result.guidance,
|
|
1684
2017
|
// Round-10: aggregated per-dimension detail + correlation handle.
|
|
@@ -1926,6 +2259,21 @@ function createMiddleware(options) {
|
|
|
1926
2259
|
result.stepUpOutcome = outcome;
|
|
1927
2260
|
}
|
|
1928
2261
|
}
|
|
2262
|
+
if (!result.policyAllowed && isVerificationUnavailable(result) && (!shouldEnforce || isAnonymousRead(req.method, credentials))) {
|
|
2263
|
+
res.setHeader(VERIFICATION_STATUS_HEADER, "unavailable");
|
|
2264
|
+
return next();
|
|
2265
|
+
}
|
|
2266
|
+
if (!shouldEnforce) {
|
|
2267
|
+
if (config.setPassThroughHeader) {
|
|
2268
|
+
res.setHeader("X-Astra-Gateway-Mode", "enforced");
|
|
2269
|
+
res.setHeader("X-Astra-Gateway-Reason", "evaluated-not-enforced");
|
|
2270
|
+
}
|
|
2271
|
+
if (shouldRecordDecisions && sessionId) {
|
|
2272
|
+
recordDecision(config, sessionId, "granted").catch(() => {
|
|
2273
|
+
});
|
|
2274
|
+
}
|
|
2275
|
+
return next();
|
|
2276
|
+
}
|
|
1929
2277
|
if (!result.policyAllowed) {
|
|
1930
2278
|
if (shouldRecordDecisions && sessionId) {
|
|
1931
2279
|
recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
|
|
@@ -1945,17 +2293,6 @@ function createMiddleware(options) {
|
|
|
1945
2293
|
onDenied(result, req, res);
|
|
1946
2294
|
return;
|
|
1947
2295
|
}
|
|
1948
|
-
if (!shouldEnforce) {
|
|
1949
|
-
if (config.setPassThroughHeader) {
|
|
1950
|
-
res.setHeader("X-Astra-Gateway-Mode", "enforced");
|
|
1951
|
-
res.setHeader("X-Astra-Gateway-Reason", "evaluated-not-enforced");
|
|
1952
|
-
}
|
|
1953
|
-
if (shouldRecordDecisions && sessionId) {
|
|
1954
|
-
recordDecision(config, sessionId, "granted").catch(() => {
|
|
1955
|
-
});
|
|
1956
|
-
}
|
|
1957
|
-
return next();
|
|
1958
|
-
}
|
|
1959
2296
|
if (routeConfig.minTrustScore && result.agent) {
|
|
1960
2297
|
if (result.agent.trustScore < routeConfig.minTrustScore) {
|
|
1961
2298
|
const trustFailure = {
|
|
@@ -2011,14 +2348,6 @@ function createMiddleware(options) {
|
|
|
2011
2348
|
verifiedAt: /* @__PURE__ */ new Date(),
|
|
2012
2349
|
correlationId
|
|
2013
2350
|
};
|
|
2014
|
-
const catchUrls = config.apiBaseUrl ? await getWellKnownUrls(config.apiBaseUrl).catch(() => void 0) : void 0;
|
|
2015
|
-
if (catchUrls) {
|
|
2016
|
-
result.guidance = {
|
|
2017
|
-
message: `Middleware threw ${errorClass} \u2014 failing closed`,
|
|
2018
|
-
registrationUrl: catchUrls.registrationUrl,
|
|
2019
|
-
documentationUrl: catchUrls.documentationUrl
|
|
2020
|
-
};
|
|
2021
|
-
}
|
|
2022
2351
|
dedupeFailures(result);
|
|
2023
2352
|
return onDenied(result, req, res);
|
|
2024
2353
|
}
|