@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/version.ts
|
|
2
|
-
var SDK_VERSION = "5.
|
|
2
|
+
var SDK_VERSION = "5.12.0";
|
|
3
3
|
|
|
4
4
|
// src/http.ts
|
|
5
5
|
var SDK_USER_AGENT = `astrasync-sdk/${SDK_VERSION}`;
|
|
@@ -144,6 +144,59 @@ function getCachedWellKnownUrls(apiBaseUrl) {
|
|
|
144
144
|
return cache.get(apiBaseUrl)?.data;
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
// src/failure-posture.ts
|
|
148
|
+
var VERIFICATION_STATUS_HEADER = "X-Astra-Verification";
|
|
149
|
+
var DEFAULT_UNAVAILABLE_RETRY_AFTER_S = 30;
|
|
150
|
+
var REJECTED_IDENTITY_DIMENSIONS = /* @__PURE__ */ new Set([
|
|
151
|
+
"identity.invalid_agent_id",
|
|
152
|
+
"identity.unknown_agent",
|
|
153
|
+
"agent.lookup"
|
|
154
|
+
]);
|
|
155
|
+
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.";
|
|
156
|
+
var UNAVAILABLE_DIMENSIONS = /* @__PURE__ */ new Set([
|
|
157
|
+
"verify_access.api_error",
|
|
158
|
+
"middleware.internal_error"
|
|
159
|
+
]);
|
|
160
|
+
function isVerificationUnavailable(result) {
|
|
161
|
+
if (result.misconfigured) return false;
|
|
162
|
+
return (result.failures ?? []).some((f) => UNAVAILABLE_DIMENSIONS.has(f.dimension));
|
|
163
|
+
}
|
|
164
|
+
function unavailableRetryAfterSeconds(result) {
|
|
165
|
+
if (typeof result.retryAfterMs === "number" && Number.isFinite(result.retryAfterMs)) {
|
|
166
|
+
return Math.max(1, Math.ceil(result.retryAfterMs / 1e3));
|
|
167
|
+
}
|
|
168
|
+
return DEFAULT_UNAVAILABLE_RETRY_AFTER_S;
|
|
169
|
+
}
|
|
170
|
+
function isAnonymousRead(method, credentials) {
|
|
171
|
+
const m = (method ?? "").toUpperCase();
|
|
172
|
+
if (m !== "GET" && m !== "HEAD") return false;
|
|
173
|
+
return !credentials.astraId && !credentials.jwt;
|
|
174
|
+
}
|
|
175
|
+
function buildVerificationUnavailableBody(result) {
|
|
176
|
+
return {
|
|
177
|
+
success: false,
|
|
178
|
+
error: {
|
|
179
|
+
code: "VERIFICATION_UNAVAILABLE",
|
|
180
|
+
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.",
|
|
181
|
+
retryable: true,
|
|
182
|
+
retryAfterSeconds: unavailableRetryAfterSeconds(result),
|
|
183
|
+
...result.failures && { failures: result.failures },
|
|
184
|
+
...result.correlationId && { correlationId: result.correlationId },
|
|
185
|
+
guidance: {
|
|
186
|
+
message: "Retry the request after the Retry-After interval, with exponential backoff; the condition is transient."
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function isRejectedIdentityClaim(result) {
|
|
192
|
+
return (result.failures ?? []).some((f) => REJECTED_IDENTITY_DIMENSIONS.has(f.dimension));
|
|
193
|
+
}
|
|
194
|
+
function denyStatusFor(result, policyDeniedCode) {
|
|
195
|
+
if (isRejectedIdentityClaim(result)) return { status: 403, code: "AGENT_IDENTITY_REJECTED" };
|
|
196
|
+
if (result.identityVerified) return { status: 403, code: policyDeniedCode };
|
|
197
|
+
return { status: 401, code: "UNAUTHORIZED" };
|
|
198
|
+
}
|
|
199
|
+
|
|
147
200
|
// src/verify.ts
|
|
148
201
|
var DEFAULT_CONFIG = {
|
|
149
202
|
apiBaseUrl: "https://astrasync.ai/api",
|
|
@@ -389,11 +442,44 @@ function createGuidanceResponse(_config, reason, options = {}) {
|
|
|
389
442
|
// coercing the absent recommendation to a policy 'deny'. Misconfig is
|
|
390
443
|
// deterministic and intentionally NOT marked retryable.
|
|
391
444
|
...isApiError && { retryable: true },
|
|
445
|
+
...options.httpStatus !== void 0 && { httpStatus: options.httpStatus },
|
|
446
|
+
...options.retryAfterMs !== void 0 && { retryAfterMs: options.retryAfterMs },
|
|
392
447
|
correlationId: options.correlationId,
|
|
393
448
|
attemptId: options.attemptId,
|
|
394
449
|
verifiedAt: /* @__PURE__ */ new Date()
|
|
395
450
|
};
|
|
396
451
|
}
|
|
452
|
+
async function readJsonSafe(response) {
|
|
453
|
+
try {
|
|
454
|
+
const parsed = await response.json();
|
|
455
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
456
|
+
} catch {
|
|
457
|
+
return {};
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
function rejectedIdentityGuidance(failures) {
|
|
461
|
+
const f = failures?.find((x) => REJECTED_IDENTITY_DIMENSIONS.has(x.dimension));
|
|
462
|
+
if (!f) return void 0;
|
|
463
|
+
if (f.dimension === "agent.lookup") {
|
|
464
|
+
return `The presented agent identifier is not registered with AstraSync. ${UNKNOWN_AGENT_GUIDANCE}`;
|
|
465
|
+
}
|
|
466
|
+
return f.guidance ? `${f.message} ${f.guidance}` : f.message;
|
|
467
|
+
}
|
|
468
|
+
var MAX_RETRY_AFTER_MS = 15 * 60 * 1e3;
|
|
469
|
+
function parseRetryAfterMs(response) {
|
|
470
|
+
const raw = response.headers?.get?.("retry-after");
|
|
471
|
+
if (!raw) return void 0;
|
|
472
|
+
const trimmed = raw.trim();
|
|
473
|
+
let ms;
|
|
474
|
+
if (/^\d+(\.\d+)?$/.test(trimmed)) {
|
|
475
|
+
ms = Number(trimmed) * 1e3;
|
|
476
|
+
} else {
|
|
477
|
+
const at = Date.parse(trimmed);
|
|
478
|
+
if (Number.isNaN(at)) return void 0;
|
|
479
|
+
ms = at - Date.now();
|
|
480
|
+
}
|
|
481
|
+
return Math.min(MAX_RETRY_AFTER_MS, Math.max(0, Math.round(ms)));
|
|
482
|
+
}
|
|
397
483
|
async function callVerifyAccessAPI(config, request) {
|
|
398
484
|
const { credentials, ...requestData } = request;
|
|
399
485
|
const body = {
|
|
@@ -450,7 +536,8 @@ async function callVerifyAccessAPI(config, request) {
|
|
|
450
536
|
body: JSON.stringify(body),
|
|
451
537
|
signal: AbortSignal.timeout(config.verifyTimeoutMs ?? 1e4)
|
|
452
538
|
});
|
|
453
|
-
const data = await response
|
|
539
|
+
const data = await readJsonSafe(response);
|
|
540
|
+
const retryAfterMs = parseRetryAfterMs(response);
|
|
454
541
|
if (response.status === 410) {
|
|
455
542
|
return {
|
|
456
543
|
success: true,
|
|
@@ -488,6 +575,23 @@ async function callVerifyAccessAPI(config, request) {
|
|
|
488
575
|
}
|
|
489
576
|
};
|
|
490
577
|
}
|
|
578
|
+
if (response.status === 404 && credentials.astraId && (data?.error === "AGENT_NOT_FOUND" || typeof data?.message === "string" && /agent/i.test(data.message))) {
|
|
579
|
+
return {
|
|
580
|
+
success: true,
|
|
581
|
+
attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0,
|
|
582
|
+
access: {
|
|
583
|
+
allowed: false,
|
|
584
|
+
reason: "unknown_agent",
|
|
585
|
+
failures: [
|
|
586
|
+
{
|
|
587
|
+
dimension: "identity.unknown_agent",
|
|
588
|
+
message: "The presented agent identifier is not registered with AstraSync.",
|
|
589
|
+
guidance: UNKNOWN_AGENT_GUIDANCE
|
|
590
|
+
}
|
|
591
|
+
]
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
}
|
|
491
595
|
if (!response.ok) {
|
|
492
596
|
if (response.status === 401) {
|
|
493
597
|
return {
|
|
@@ -502,9 +606,11 @@ async function callVerifyAccessAPI(config, request) {
|
|
|
502
606
|
}
|
|
503
607
|
return {
|
|
504
608
|
success: false,
|
|
505
|
-
error: data.message || data.error || `API returned ${response.status}`,
|
|
609
|
+
error: typeof data?.message === "string" && data.message || typeof data?.error === "string" && data.error || `API returned ${response.status}`,
|
|
506
610
|
correlationId: typeof data?.correlationId === "string" ? data.correlationId : void 0,
|
|
507
|
-
attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0
|
|
611
|
+
attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0,
|
|
612
|
+
httpStatus: response.status,
|
|
613
|
+
...retryAfterMs !== void 0 && { retryAfterMs }
|
|
508
614
|
};
|
|
509
615
|
}
|
|
510
616
|
return data;
|
|
@@ -576,12 +682,14 @@ async function verify(config, request, options) {
|
|
|
576
682
|
correlationId: apiResponse.correlationId,
|
|
577
683
|
attemptId: apiResponse.attemptId,
|
|
578
684
|
remediation: apiResponse.remediation,
|
|
685
|
+
httpStatus: apiResponse.httpStatus,
|
|
686
|
+
retryAfterMs: apiResponse.retryAfterMs,
|
|
579
687
|
urls
|
|
580
688
|
});
|
|
581
689
|
}
|
|
582
690
|
if (!apiResponse.access?.allowed) {
|
|
583
691
|
const aggregatedFailures = apiResponse.access?.failures;
|
|
584
|
-
const idVerifiedFromBackend = apiResponse.verificationContext?.idVerified === true;
|
|
692
|
+
const idVerifiedFromBackend = apiResponse.verificationContext?.idVerified === true && !aggregatedFailures?.some((f) => REJECTED_IDENTITY_DIMENSIONS.has(f.dimension));
|
|
585
693
|
const result2 = {
|
|
586
694
|
identityVerified: idVerifiedFromBackend,
|
|
587
695
|
policyAllowed: false,
|
|
@@ -593,7 +701,10 @@ async function verify(config, request, options) {
|
|
|
593
701
|
requiresStepUp: apiResponse.access?.requiresStepUp,
|
|
594
702
|
requiresApproval: apiResponse.access?.requiresApproval,
|
|
595
703
|
guidance: {
|
|
596
|
-
|
|
704
|
+
// 5.12.0: a presented-but-rejected id (malformed / unknown) carries
|
|
705
|
+
// actionable registration guidance on its failure — surface it
|
|
706
|
+
// instead of the bare machine reason ('unknown_agent').
|
|
707
|
+
message: rejectedIdentityGuidance(aggregatedFailures) ?? (apiResponse.access?.reason || "Access denied by PDLSS policy"),
|
|
597
708
|
registrationUrl: urls?.registrationUrl ?? "",
|
|
598
709
|
documentationUrl: urls?.documentationUrl ?? "",
|
|
599
710
|
// 5.1.0: anonymous callers land on this branch — steer MCP-hosted
|
|
@@ -693,7 +804,7 @@ async function verify(config, request, options) {
|
|
|
693
804
|
registrationUrl: urls?.registrationUrl ?? "",
|
|
694
805
|
documentationUrl: urls?.documentationUrl ?? ""
|
|
695
806
|
};
|
|
696
|
-
} else if (result.recommendation === "step_up_required") {
|
|
807
|
+
} else if (result.recommendation === "step_up_required" && result.identityVerified) {
|
|
697
808
|
result.requiresStepUp = true;
|
|
698
809
|
result.denialReasons = serverDenialReasons ?? result.recommendationReasons ?? ["Step-up verification required"];
|
|
699
810
|
}
|
|
@@ -745,7 +856,10 @@ async function reportUnregisteredAttempt(config, data) {
|
|
|
745
856
|
};
|
|
746
857
|
await sdkFetch(`${apiBaseUrl}/verification-activity/unregistered-attempt`, {
|
|
747
858
|
method: "POST",
|
|
748
|
-
headers: {
|
|
859
|
+
headers: {
|
|
860
|
+
"Content-Type": "application/json",
|
|
861
|
+
...config.apiKey && { Authorization: `Bearer ${config.apiKey}` }
|
|
862
|
+
},
|
|
749
863
|
body: JSON.stringify(body)
|
|
750
864
|
}).catch(() => {
|
|
751
865
|
});
|
|
@@ -1335,7 +1449,10 @@ var PLATFORM_AGENT_SIGNATURES = [
|
|
|
1335
1449
|
{
|
|
1336
1450
|
vendor: "chatgpt",
|
|
1337
1451
|
displayName: "OpenAI ChatGPT",
|
|
1338
|
-
|
|
1452
|
+
// GPTBot (training crawler) and OAI-SearchBot (ChatGPT search) are
|
|
1453
|
+
// OpenAI's declared crawler UAs — `gpt-?\d` never matched them, so they
|
|
1454
|
+
// classified as unattributed crawlers (round-27.4 W5).
|
|
1455
|
+
uaPatterns: [/chatgpt/i, /openai/i, /gpt-?\d/i, /\bGPTBot\b/i, /\bOAI-SearchBot\b/i],
|
|
1339
1456
|
agentCardPatterns: [/openai\.com/i, /chatgpt\.com/i],
|
|
1340
1457
|
apiKeyFormatPatterns: [/^sk-proj/i, /^sk-svcacct/i],
|
|
1341
1458
|
headerPatterns: [/^openai-organization/i, /^openai-project/i, /^openai-version/i]
|
|
@@ -1470,6 +1587,215 @@ var BROWSER_UA_PATTERN = /mozilla\/\d/i;
|
|
|
1470
1587
|
var _patterns = { STATIC_EXTENSIONS, BOT_UA_PATTERN, BROWSER_UA_PATTERN, SELF_UA_PATTERN };
|
|
1471
1588
|
|
|
1472
1589
|
// src/edge-core/events.ts
|
|
1590
|
+
async function withBudget(promise, ms) {
|
|
1591
|
+
let timer;
|
|
1592
|
+
try {
|
|
1593
|
+
return await Promise.race([
|
|
1594
|
+
promise,
|
|
1595
|
+
new Promise((resolve) => {
|
|
1596
|
+
timer = setTimeout(() => resolve(void 0), ms);
|
|
1597
|
+
})
|
|
1598
|
+
]);
|
|
1599
|
+
} catch {
|
|
1600
|
+
return void 0;
|
|
1601
|
+
} finally {
|
|
1602
|
+
if (timer) clearTimeout(timer);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
var destKeyOf = (d) => `${d.apiBaseUrl}\0${d.apiKey ?? ""}\0${d.counterparty}\0${d.adapter ?? ""}`;
|
|
1606
|
+
var BeaconBuffer = class {
|
|
1607
|
+
constructor(options = {}) {
|
|
1608
|
+
this.queue = [];
|
|
1609
|
+
this.droppedSinceLast = 0;
|
|
1610
|
+
this.droppedTotal = 0;
|
|
1611
|
+
this.deliveredTotal = 0;
|
|
1612
|
+
this.backoffUntil = 0;
|
|
1613
|
+
this.collapse = /* @__PURE__ */ new Map();
|
|
1614
|
+
/** Keys the backend answered 401 for — later batches go unauthenticated. */
|
|
1615
|
+
this.rejectedKeys = /* @__PURE__ */ new Set();
|
|
1616
|
+
this.opts = {
|
|
1617
|
+
maxItems: options.maxItems ?? 1e3,
|
|
1618
|
+
maxBatch: options.maxBatch ?? 100,
|
|
1619
|
+
maxAttempts: options.maxAttempts ?? 3,
|
|
1620
|
+
collapseWindowMs: options.collapseWindowMs ?? 6e4,
|
|
1621
|
+
collapseMaxKeys: options.collapseMaxKeys ?? 1e3,
|
|
1622
|
+
requestTimeoutMs: options.requestTimeoutMs ?? 1e4,
|
|
1623
|
+
retryBackoffMs: options.retryBackoffMs ?? 1e3,
|
|
1624
|
+
now: options.now ?? Date.now
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1627
|
+
/**
|
|
1628
|
+
* Queue one beacon. With a `collapseKey`, a repeat inside the window is
|
|
1629
|
+
* merged into a held aggregate (sent when the window closes) instead of
|
|
1630
|
+
* becoming its own item.
|
|
1631
|
+
*/
|
|
1632
|
+
enqueue(dest, item, collapseKey) {
|
|
1633
|
+
const now = this.opts.now();
|
|
1634
|
+
if (collapseKey) {
|
|
1635
|
+
const entry = this.collapse.get(collapseKey);
|
|
1636
|
+
if (entry && now - entry.firstSeenAt < this.opts.collapseWindowMs) {
|
|
1637
|
+
for (const target of [entry.first, entry.held]) {
|
|
1638
|
+
if (target && !target.inFlight && this.queue.includes(target)) {
|
|
1639
|
+
target.item.repeatCount = (target.item.repeatCount ?? 1) + 1;
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
const held = this.push(
|
|
1644
|
+
dest,
|
|
1645
|
+
{ ...item, repeatCount: 1 },
|
|
1646
|
+
entry.firstSeenAt + this.opts.collapseWindowMs
|
|
1647
|
+
);
|
|
1648
|
+
entry.held = held;
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
this.collapse.delete(collapseKey);
|
|
1652
|
+
if (this.collapse.size >= this.opts.collapseMaxKeys) {
|
|
1653
|
+
const oldest = this.collapse.keys().next().value;
|
|
1654
|
+
if (oldest !== void 0) this.collapse.delete(oldest);
|
|
1655
|
+
}
|
|
1656
|
+
const fresh = { firstSeenAt: now };
|
|
1657
|
+
this.collapse.set(collapseKey, fresh);
|
|
1658
|
+
fresh.first = this.push(dest, item, now);
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
this.push(dest, item, now);
|
|
1662
|
+
}
|
|
1663
|
+
/**
|
|
1664
|
+
* Deliver every eligible queued item. Concurrent callers share ONE
|
|
1665
|
+
* in-flight drain (it survives the caller's budget and a Lambda freeze),
|
|
1666
|
+
* so nothing in flight is ever sent twice; items queued while it runs are
|
|
1667
|
+
* picked up by the same drain.
|
|
1668
|
+
*/
|
|
1669
|
+
flush() {
|
|
1670
|
+
if (!this.inFlight) {
|
|
1671
|
+
this.inFlight = this.drain().finally(() => {
|
|
1672
|
+
this.inFlight = void 0;
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1675
|
+
return this.inFlight;
|
|
1676
|
+
}
|
|
1677
|
+
/** Counters for tests and diagnostics. */
|
|
1678
|
+
stats() {
|
|
1679
|
+
return {
|
|
1680
|
+
queueDepth: this.queue.length,
|
|
1681
|
+
droppedSinceLast: this.droppedSinceLast,
|
|
1682
|
+
droppedTotal: this.droppedTotal,
|
|
1683
|
+
deliveredTotal: this.deliveredTotal,
|
|
1684
|
+
flushing: this.inFlight !== void 0
|
|
1685
|
+
};
|
|
1686
|
+
}
|
|
1687
|
+
push(dest, item, eligibleAt) {
|
|
1688
|
+
while (this.queue.length >= this.opts.maxItems) this.dropOldest();
|
|
1689
|
+
const entry = {
|
|
1690
|
+
dest,
|
|
1691
|
+
destKey: destKeyOf(dest),
|
|
1692
|
+
item,
|
|
1693
|
+
attempts: 0,
|
|
1694
|
+
eligibleAt,
|
|
1695
|
+
inFlight: false
|
|
1696
|
+
};
|
|
1697
|
+
this.queue.push(entry);
|
|
1698
|
+
return entry;
|
|
1699
|
+
}
|
|
1700
|
+
dropOldest() {
|
|
1701
|
+
if (this.queue.shift()) this.countDropped(1);
|
|
1702
|
+
}
|
|
1703
|
+
countDropped(n) {
|
|
1704
|
+
this.droppedSinceLast += n;
|
|
1705
|
+
this.droppedTotal += n;
|
|
1706
|
+
}
|
|
1707
|
+
async drain() {
|
|
1708
|
+
for (; ; ) {
|
|
1709
|
+
const now = this.opts.now();
|
|
1710
|
+
if (now < this.backoffUntil) return;
|
|
1711
|
+
const batch = this.takeBatch(now);
|
|
1712
|
+
if (batch.length === 0) return;
|
|
1713
|
+
const ok = await this.send(batch);
|
|
1714
|
+
if (!ok) return;
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
/** Oldest eligible item + up to maxBatch-1 more for the same destination. */
|
|
1718
|
+
takeBatch(now) {
|
|
1719
|
+
const first = this.queue.find((q) => q.eligibleAt <= now);
|
|
1720
|
+
if (!first) return [];
|
|
1721
|
+
const batch = [];
|
|
1722
|
+
const rest = [];
|
|
1723
|
+
for (const q of this.queue) {
|
|
1724
|
+
if (batch.length < this.opts.maxBatch && q.destKey === first.destKey && q.eligibleAt <= now) {
|
|
1725
|
+
q.inFlight = true;
|
|
1726
|
+
batch.push(q);
|
|
1727
|
+
} else {
|
|
1728
|
+
rest.push(q);
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
this.queue = rest;
|
|
1732
|
+
return batch;
|
|
1733
|
+
}
|
|
1734
|
+
/** POST one batch. Returns false when the drain should pause (backoff). */
|
|
1735
|
+
async send(batch) {
|
|
1736
|
+
const dest = batch[0].dest;
|
|
1737
|
+
const useKey = dest.apiKey && !this.rejectedKeys.has(dest.apiKey) ? dest.apiKey : void 0;
|
|
1738
|
+
const reportedDropped = this.droppedSinceLast;
|
|
1739
|
+
const meta = {
|
|
1740
|
+
droppedSinceLast: reportedDropped,
|
|
1741
|
+
queueDepth: this.queue.length,
|
|
1742
|
+
adapter: dest.adapter ?? "edge-core",
|
|
1743
|
+
version: SDK_VERSION
|
|
1744
|
+
};
|
|
1745
|
+
const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
|
|
1746
|
+
const response = await withBudget(
|
|
1747
|
+
sdkFetch(`${dest.apiBaseUrl}/verification-activity/edge-events`, {
|
|
1748
|
+
method: "POST",
|
|
1749
|
+
headers: {
|
|
1750
|
+
"Content-Type": "application/json",
|
|
1751
|
+
...useKey && { Authorization: `Bearer ${useKey}` }
|
|
1752
|
+
},
|
|
1753
|
+
body: JSON.stringify({ events: batch.map((q) => q.item), meta }),
|
|
1754
|
+
...controller && { signal: controller.signal }
|
|
1755
|
+
}),
|
|
1756
|
+
this.opts.requestTimeoutMs
|
|
1757
|
+
);
|
|
1758
|
+
if (!response) controller?.abort();
|
|
1759
|
+
const status = response?.status;
|
|
1760
|
+
for (const q of batch) q.inFlight = false;
|
|
1761
|
+
if (status !== void 0 && status >= 200 && status < 300) {
|
|
1762
|
+
this.droppedSinceLast = Math.max(0, this.droppedSinceLast - reportedDropped);
|
|
1763
|
+
this.deliveredTotal += batch.length;
|
|
1764
|
+
return true;
|
|
1765
|
+
}
|
|
1766
|
+
if (status === 401 && useKey) {
|
|
1767
|
+
this.rejectedKeys.add(useKey);
|
|
1768
|
+
this.requeue(batch, false);
|
|
1769
|
+
return true;
|
|
1770
|
+
}
|
|
1771
|
+
if (status === 429) {
|
|
1772
|
+
const retryAfter = Number(response?.headers.get("retry-after"));
|
|
1773
|
+
const waitMs = Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(retryAfter, 120) * 1e3 : 3e4;
|
|
1774
|
+
this.backoffUntil = this.opts.now() + waitMs;
|
|
1775
|
+
this.requeue(batch, false);
|
|
1776
|
+
return false;
|
|
1777
|
+
}
|
|
1778
|
+
if (status !== void 0 && status >= 400 && status < 500) {
|
|
1779
|
+
this.countDropped(batch.length);
|
|
1780
|
+
return true;
|
|
1781
|
+
}
|
|
1782
|
+
this.backoffUntil = this.opts.now() + this.opts.retryBackoffMs;
|
|
1783
|
+
this.requeue(batch, true);
|
|
1784
|
+
return false;
|
|
1785
|
+
}
|
|
1786
|
+
/** Put a failed batch back at the head of the queue (order preserved). */
|
|
1787
|
+
requeue(batch, countAttempt) {
|
|
1788
|
+
const keep = [];
|
|
1789
|
+
for (const q of batch) {
|
|
1790
|
+
if (countAttempt) q.attempts += 1;
|
|
1791
|
+
if (q.attempts >= this.opts.maxAttempts) this.countDropped(1);
|
|
1792
|
+
else keep.push(q);
|
|
1793
|
+
}
|
|
1794
|
+
this.queue = [...keep, ...this.queue];
|
|
1795
|
+
while (this.queue.length > this.opts.maxItems) this.dropOldest();
|
|
1796
|
+
}
|
|
1797
|
+
};
|
|
1798
|
+
var beaconBuffer = new BeaconBuffer();
|
|
1473
1799
|
function sampled(rate, random = Math.random) {
|
|
1474
1800
|
if (rate <= 0) return false;
|
|
1475
1801
|
if (rate >= 1) return true;
|
|
@@ -1646,12 +1972,19 @@ function defaultOnDenied(result, _req, res) {
|
|
|
1646
1972
|
});
|
|
1647
1973
|
return;
|
|
1648
1974
|
}
|
|
1649
|
-
|
|
1975
|
+
if (isVerificationUnavailable(result)) {
|
|
1976
|
+
res.setHeader("X-Astra-Gateway-Mode", "enforced");
|
|
1977
|
+
res.setHeader(VERIFICATION_STATUS_HEADER, "unavailable");
|
|
1978
|
+
res.setHeader("Retry-After", String(unavailableRetryAfterSeconds(result)));
|
|
1979
|
+
res.status(503).json(buildVerificationUnavailableBody(result));
|
|
1980
|
+
return;
|
|
1981
|
+
}
|
|
1982
|
+
const { status: statusCode, code } = denyStatusFor(result, "INSUFFICIENT_ACCESS");
|
|
1650
1983
|
res.setHeader("X-Astra-Gateway-Mode", "enforced");
|
|
1651
1984
|
res.status(statusCode).json({
|
|
1652
1985
|
success: false,
|
|
1653
1986
|
error: {
|
|
1654
|
-
code
|
|
1987
|
+
code,
|
|
1655
1988
|
message: result.denialReasons?.[0] || "Access denied",
|
|
1656
1989
|
guidance: result.guidance,
|
|
1657
1990
|
// Round-10: aggregated per-dimension detail + correlation handle.
|
|
@@ -1899,6 +2232,21 @@ function createMiddleware(options) {
|
|
|
1899
2232
|
result.stepUpOutcome = outcome;
|
|
1900
2233
|
}
|
|
1901
2234
|
}
|
|
2235
|
+
if (!result.policyAllowed && isVerificationUnavailable(result) && (!shouldEnforce || isAnonymousRead(req.method, credentials))) {
|
|
2236
|
+
res.setHeader(VERIFICATION_STATUS_HEADER, "unavailable");
|
|
2237
|
+
return next();
|
|
2238
|
+
}
|
|
2239
|
+
if (!shouldEnforce) {
|
|
2240
|
+
if (config.setPassThroughHeader) {
|
|
2241
|
+
res.setHeader("X-Astra-Gateway-Mode", "enforced");
|
|
2242
|
+
res.setHeader("X-Astra-Gateway-Reason", "evaluated-not-enforced");
|
|
2243
|
+
}
|
|
2244
|
+
if (shouldRecordDecisions && sessionId) {
|
|
2245
|
+
recordDecision(config, sessionId, "granted").catch(() => {
|
|
2246
|
+
});
|
|
2247
|
+
}
|
|
2248
|
+
return next();
|
|
2249
|
+
}
|
|
1902
2250
|
if (!result.policyAllowed) {
|
|
1903
2251
|
if (shouldRecordDecisions && sessionId) {
|
|
1904
2252
|
recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
|
|
@@ -1918,17 +2266,6 @@ function createMiddleware(options) {
|
|
|
1918
2266
|
onDenied(result, req, res);
|
|
1919
2267
|
return;
|
|
1920
2268
|
}
|
|
1921
|
-
if (!shouldEnforce) {
|
|
1922
|
-
if (config.setPassThroughHeader) {
|
|
1923
|
-
res.setHeader("X-Astra-Gateway-Mode", "enforced");
|
|
1924
|
-
res.setHeader("X-Astra-Gateway-Reason", "evaluated-not-enforced");
|
|
1925
|
-
}
|
|
1926
|
-
if (shouldRecordDecisions && sessionId) {
|
|
1927
|
-
recordDecision(config, sessionId, "granted").catch(() => {
|
|
1928
|
-
});
|
|
1929
|
-
}
|
|
1930
|
-
return next();
|
|
1931
|
-
}
|
|
1932
2269
|
if (routeConfig.minTrustScore && result.agent) {
|
|
1933
2270
|
if (result.agent.trustScore < routeConfig.minTrustScore) {
|
|
1934
2271
|
const trustFailure = {
|
|
@@ -1984,14 +2321,6 @@ function createMiddleware(options) {
|
|
|
1984
2321
|
verifiedAt: /* @__PURE__ */ new Date(),
|
|
1985
2322
|
correlationId
|
|
1986
2323
|
};
|
|
1987
|
-
const catchUrls = config.apiBaseUrl ? await getWellKnownUrls(config.apiBaseUrl).catch(() => void 0) : void 0;
|
|
1988
|
-
if (catchUrls) {
|
|
1989
|
-
result.guidance = {
|
|
1990
|
-
message: `Middleware threw ${errorClass} \u2014 failing closed`,
|
|
1991
|
-
registrationUrl: catchUrls.registrationUrl,
|
|
1992
|
-
documentationUrl: catchUrls.documentationUrl
|
|
1993
|
-
};
|
|
1994
|
-
}
|
|
1995
2324
|
dedupeFailures(result);
|
|
1996
2325
|
return onDenied(result, req, res);
|
|
1997
2326
|
}
|