@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
|
@@ -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}`;
|
|
@@ -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",
|
|
@@ -156,7 +209,7 @@ var DEFAULT_CONFIG = {
|
|
|
156
209
|
};
|
|
157
210
|
var initCheckPerformed = false;
|
|
158
211
|
var deprecationWarningShown = false;
|
|
159
|
-
async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey) {
|
|
212
|
+
async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey, counterparty) {
|
|
160
213
|
initCheckPerformed = true;
|
|
161
214
|
try {
|
|
162
215
|
const probeUrl = `${apiBaseUrl}/agents/verify-access`;
|
|
@@ -174,7 +227,8 @@ async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey) {
|
|
|
174
227
|
);
|
|
175
228
|
}
|
|
176
229
|
if (apiKey) {
|
|
177
|
-
const
|
|
230
|
+
const q = counterparty?.id ? `?counterpartyId=${encodeURIComponent(counterparty.id)}` : counterparty?.url ? `?counterpartyUrl=${encodeURIComponent(counterparty.url)}` : "";
|
|
231
|
+
const selfCheck = await sdkFetch(`${apiBaseUrl}/agents/verify-access/self-check${q}`, {
|
|
178
232
|
headers: { Authorization: `Bearer ${apiKey}` }
|
|
179
233
|
});
|
|
180
234
|
if (selfCheck.status === 401) {
|
|
@@ -186,6 +240,13 @@ async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey) {
|
|
|
186
240
|
console.error(message);
|
|
187
241
|
} else if (selfCheck.ok) {
|
|
188
242
|
const body = await selfCheck.json().catch(() => ({}));
|
|
243
|
+
if (body.counterparty && body.counterparty.canVerify === false) {
|
|
244
|
+
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.`;
|
|
245
|
+
if (strictInit) {
|
|
246
|
+
throw new Error(`${message} (strictInit=true)`);
|
|
247
|
+
}
|
|
248
|
+
console.error(message);
|
|
249
|
+
}
|
|
189
250
|
if (typeof body.daysToExpiry === "number" && body.daysToExpiry <= 7) {
|
|
190
251
|
console.warn(
|
|
191
252
|
`[VerificationGateway] the configured AstraSync API key expires in ${body.daysToExpiry} day(s) \u2014 rotate it at Settings \u2192 Developer Tools before integrations start failing.`
|
|
@@ -324,11 +385,11 @@ function createGuidanceResponse(_config, reason, options = {}) {
|
|
|
324
385
|
const isMisconfig = source === "merchant_misconfig";
|
|
325
386
|
const urls = options.urls;
|
|
326
387
|
const guidance = isMisconfig ? {
|
|
327
|
-
message: "This site's AstraSync integration is misconfigured (its API key was revoked, expired, or
|
|
388
|
+
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.",
|
|
328
389
|
registrationUrl: urls?.registrationUrl ?? "",
|
|
329
390
|
documentationUrl: options.remediation?.docsUrl ?? urls?.documentationUrl ?? "",
|
|
330
391
|
steps: [
|
|
331
|
-
"Report the failure to the site operator \u2014 their AstraSync API key needs
|
|
392
|
+
"Report the failure to the site operator \u2014 their AstraSync API key needs replacing",
|
|
332
393
|
"Do not retry: the failure is deterministic until the operator updates their key"
|
|
333
394
|
]
|
|
334
395
|
} : isApiError ? {
|
|
@@ -389,11 +450,44 @@ function createGuidanceResponse(_config, reason, options = {}) {
|
|
|
389
450
|
// coercing the absent recommendation to a policy 'deny'. Misconfig is
|
|
390
451
|
// deterministic and intentionally NOT marked retryable.
|
|
391
452
|
...isApiError && { retryable: true },
|
|
453
|
+
...options.httpStatus !== void 0 && { httpStatus: options.httpStatus },
|
|
454
|
+
...options.retryAfterMs !== void 0 && { retryAfterMs: options.retryAfterMs },
|
|
392
455
|
correlationId: options.correlationId,
|
|
393
456
|
attemptId: options.attemptId,
|
|
394
457
|
verifiedAt: /* @__PURE__ */ new Date()
|
|
395
458
|
};
|
|
396
459
|
}
|
|
460
|
+
async function readJsonSafe(response) {
|
|
461
|
+
try {
|
|
462
|
+
const parsed = await response.json();
|
|
463
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
464
|
+
} catch {
|
|
465
|
+
return {};
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
function rejectedIdentityGuidance(failures) {
|
|
469
|
+
const f = failures?.find((x) => REJECTED_IDENTITY_DIMENSIONS.has(x.dimension));
|
|
470
|
+
if (!f) return void 0;
|
|
471
|
+
if (f.dimension === "agent.lookup") {
|
|
472
|
+
return `The presented agent identifier is not registered with AstraSync. ${UNKNOWN_AGENT_GUIDANCE}`;
|
|
473
|
+
}
|
|
474
|
+
return f.guidance ? `${f.message} ${f.guidance}` : f.message;
|
|
475
|
+
}
|
|
476
|
+
var MAX_RETRY_AFTER_MS = 15 * 60 * 1e3;
|
|
477
|
+
function parseRetryAfterMs(response) {
|
|
478
|
+
const raw = response.headers?.get?.("retry-after");
|
|
479
|
+
if (!raw) return void 0;
|
|
480
|
+
const trimmed = raw.trim();
|
|
481
|
+
let ms;
|
|
482
|
+
if (/^\d+(\.\d+)?$/.test(trimmed)) {
|
|
483
|
+
ms = Number(trimmed) * 1e3;
|
|
484
|
+
} else {
|
|
485
|
+
const at = Date.parse(trimmed);
|
|
486
|
+
if (Number.isNaN(at)) return void 0;
|
|
487
|
+
ms = at - Date.now();
|
|
488
|
+
}
|
|
489
|
+
return Math.min(MAX_RETRY_AFTER_MS, Math.max(0, Math.round(ms)));
|
|
490
|
+
}
|
|
397
491
|
async function callVerifyAccessAPI(config, request) {
|
|
398
492
|
const { credentials, ...requestData } = request;
|
|
399
493
|
const body = {
|
|
@@ -450,7 +544,8 @@ async function callVerifyAccessAPI(config, request) {
|
|
|
450
544
|
body: JSON.stringify(body),
|
|
451
545
|
signal: AbortSignal.timeout(config.verifyTimeoutMs ?? 1e4)
|
|
452
546
|
});
|
|
453
|
-
const data = await response
|
|
547
|
+
const data = await readJsonSafe(response);
|
|
548
|
+
const retryAfterMs = parseRetryAfterMs(response);
|
|
454
549
|
if (response.status === 410) {
|
|
455
550
|
return {
|
|
456
551
|
success: true,
|
|
@@ -488,13 +583,31 @@ async function callVerifyAccessAPI(config, request) {
|
|
|
488
583
|
}
|
|
489
584
|
};
|
|
490
585
|
}
|
|
586
|
+
if (response.status === 404 && credentials.astraId && (data?.error === "AGENT_NOT_FOUND" || typeof data?.message === "string" && /agent/i.test(data.message))) {
|
|
587
|
+
return {
|
|
588
|
+
success: true,
|
|
589
|
+
attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0,
|
|
590
|
+
access: {
|
|
591
|
+
allowed: false,
|
|
592
|
+
reason: "unknown_agent",
|
|
593
|
+
failures: [
|
|
594
|
+
{
|
|
595
|
+
dimension: "identity.unknown_agent",
|
|
596
|
+
message: "The presented agent identifier is not registered with AstraSync.",
|
|
597
|
+
guidance: UNKNOWN_AGENT_GUIDANCE
|
|
598
|
+
}
|
|
599
|
+
]
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
}
|
|
491
603
|
if (!response.ok) {
|
|
492
|
-
|
|
604
|
+
const keyMismatch = response.status === 403 && data?.error === "COUNTERPARTY_KEY_MISMATCH";
|
|
605
|
+
if (response.status === 401 || keyMismatch) {
|
|
493
606
|
return {
|
|
494
607
|
success: false,
|
|
495
608
|
error: data.message || data.error || "Integration API key rejected",
|
|
496
609
|
misconfig: true,
|
|
497
|
-
misconfigCode: typeof data?.code === "string" ? data.code : void 0,
|
|
610
|
+
misconfigCode: keyMismatch ? "counterparty_key_mismatch" : typeof data?.code === "string" ? data.code : void 0,
|
|
498
611
|
remediation: data?.remediation && typeof data.remediation === "object" ? data.remediation : void 0,
|
|
499
612
|
correlationId: typeof data?.correlationId === "string" ? data.correlationId : void 0,
|
|
500
613
|
attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0
|
|
@@ -502,9 +615,11 @@ async function callVerifyAccessAPI(config, request) {
|
|
|
502
615
|
}
|
|
503
616
|
return {
|
|
504
617
|
success: false,
|
|
505
|
-
error: data.message || data.error || `API returned ${response.status}`,
|
|
618
|
+
error: typeof data?.message === "string" && data.message || typeof data?.error === "string" && data.error || `API returned ${response.status}`,
|
|
506
619
|
correlationId: typeof data?.correlationId === "string" ? data.correlationId : void 0,
|
|
507
|
-
attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0
|
|
620
|
+
attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0,
|
|
621
|
+
httpStatus: response.status,
|
|
622
|
+
...retryAfterMs !== void 0 && { retryAfterMs }
|
|
508
623
|
};
|
|
509
624
|
}
|
|
510
625
|
return data;
|
|
@@ -525,14 +640,22 @@ async function verify(config, request, options) {
|
|
|
525
640
|
mergedConfig.apiBaseUrl,
|
|
526
641
|
mergedConfig.debug,
|
|
527
642
|
true,
|
|
528
|
-
mergedConfig.apiKey
|
|
643
|
+
mergedConfig.apiKey,
|
|
644
|
+
{
|
|
645
|
+
id: mergedConfig.counterpartyId,
|
|
646
|
+
url: mergedConfig.counterpartyUrl ?? request.counterpartyUrl
|
|
647
|
+
}
|
|
529
648
|
);
|
|
530
649
|
} else {
|
|
531
650
|
void performInitCheck(
|
|
532
651
|
mergedConfig.apiBaseUrl,
|
|
533
652
|
mergedConfig.debug,
|
|
534
653
|
false,
|
|
535
|
-
mergedConfig.apiKey
|
|
654
|
+
mergedConfig.apiKey,
|
|
655
|
+
{
|
|
656
|
+
id: mergedConfig.counterpartyId,
|
|
657
|
+
url: mergedConfig.counterpartyUrl ?? request.counterpartyUrl
|
|
658
|
+
}
|
|
536
659
|
);
|
|
537
660
|
}
|
|
538
661
|
}
|
|
@@ -576,12 +699,14 @@ async function verify(config, request, options) {
|
|
|
576
699
|
correlationId: apiResponse.correlationId,
|
|
577
700
|
attemptId: apiResponse.attemptId,
|
|
578
701
|
remediation: apiResponse.remediation,
|
|
702
|
+
httpStatus: apiResponse.httpStatus,
|
|
703
|
+
retryAfterMs: apiResponse.retryAfterMs,
|
|
579
704
|
urls
|
|
580
705
|
});
|
|
581
706
|
}
|
|
582
707
|
if (!apiResponse.access?.allowed) {
|
|
583
708
|
const aggregatedFailures = apiResponse.access?.failures;
|
|
584
|
-
const idVerifiedFromBackend = apiResponse.verificationContext?.idVerified === true;
|
|
709
|
+
const idVerifiedFromBackend = apiResponse.verificationContext?.idVerified === true && !aggregatedFailures?.some((f) => REJECTED_IDENTITY_DIMENSIONS.has(f.dimension));
|
|
585
710
|
const result2 = {
|
|
586
711
|
identityVerified: idVerifiedFromBackend,
|
|
587
712
|
policyAllowed: false,
|
|
@@ -593,7 +718,10 @@ async function verify(config, request, options) {
|
|
|
593
718
|
requiresStepUp: apiResponse.access?.requiresStepUp,
|
|
594
719
|
requiresApproval: apiResponse.access?.requiresApproval,
|
|
595
720
|
guidance: {
|
|
596
|
-
|
|
721
|
+
// 5.12.0: a presented-but-rejected id (malformed / unknown) carries
|
|
722
|
+
// actionable registration guidance on its failure — surface it
|
|
723
|
+
// instead of the bare machine reason ('unknown_agent').
|
|
724
|
+
message: rejectedIdentityGuidance(aggregatedFailures) ?? (apiResponse.access?.reason || "Access denied by PDLSS policy"),
|
|
597
725
|
registrationUrl: urls?.registrationUrl ?? "",
|
|
598
726
|
documentationUrl: urls?.documentationUrl ?? "",
|
|
599
727
|
// 5.1.0: anonymous callers land on this branch — steer MCP-hosted
|
|
@@ -693,7 +821,7 @@ async function verify(config, request, options) {
|
|
|
693
821
|
registrationUrl: urls?.registrationUrl ?? "",
|
|
694
822
|
documentationUrl: urls?.documentationUrl ?? ""
|
|
695
823
|
};
|
|
696
|
-
} else if (result.recommendation === "step_up_required") {
|
|
824
|
+
} else if (result.recommendation === "step_up_required" && result.identityVerified) {
|
|
697
825
|
result.requiresStepUp = true;
|
|
698
826
|
result.denialReasons = serverDenialReasons ?? result.recommendationReasons ?? ["Step-up verification required"];
|
|
699
827
|
}
|
|
@@ -716,6 +844,7 @@ async function recordDecision(config, sessionId, decision, reason) {
|
|
|
716
844
|
}).catch(() => {
|
|
717
845
|
});
|
|
718
846
|
}
|
|
847
|
+
var refusedPolicyReads = /* @__PURE__ */ new Set();
|
|
719
848
|
async function fetchRoutes(config, counterpartyId) {
|
|
720
849
|
if (!counterpartyId) return null;
|
|
721
850
|
const headers = { "Content-Type": "application/json" };
|
|
@@ -728,7 +857,15 @@ async function fetchRoutes(config, counterpartyId) {
|
|
|
728
857
|
`${config.apiBaseUrl}/endpoints/${encodeURIComponent(counterpartyId)}/routes`,
|
|
729
858
|
{ method: "GET", headers }
|
|
730
859
|
);
|
|
731
|
-
if (!response.ok)
|
|
860
|
+
if (!response.ok) {
|
|
861
|
+
if ((response.status === 401 || response.status === 403 || response.status === 404) && !refusedPolicyReads.has(counterpartyId)) {
|
|
862
|
+
refusedPolicyReads.add(counterpartyId);
|
|
863
|
+
console.error(
|
|
864
|
+
`[VerificationGateway] the configured API key cannot read the route policy for ${counterpartyId} (HTTP ${response.status}). The key belongs to an account that does not own this endpoint, or the counterpartyId is wrong. The gateway is in PASS-THROUGH mode: no agent is being verified.`
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
return null;
|
|
868
|
+
}
|
|
732
869
|
const body = await response.json();
|
|
733
870
|
return body.data?.routes ?? [];
|
|
734
871
|
} catch {
|
|
@@ -745,7 +882,10 @@ async function reportUnregisteredAttempt(config, data) {
|
|
|
745
882
|
};
|
|
746
883
|
await sdkFetch(`${apiBaseUrl}/verification-activity/unregistered-attempt`, {
|
|
747
884
|
method: "POST",
|
|
748
|
-
headers: {
|
|
885
|
+
headers: {
|
|
886
|
+
"Content-Type": "application/json",
|
|
887
|
+
...config.apiKey && { Authorization: `Bearer ${config.apiKey}` }
|
|
888
|
+
},
|
|
749
889
|
body: JSON.stringify(body)
|
|
750
890
|
}).catch(() => {
|
|
751
891
|
});
|
|
@@ -1335,7 +1475,10 @@ var PLATFORM_AGENT_SIGNATURES = [
|
|
|
1335
1475
|
{
|
|
1336
1476
|
vendor: "chatgpt",
|
|
1337
1477
|
displayName: "OpenAI ChatGPT",
|
|
1338
|
-
|
|
1478
|
+
// GPTBot (training crawler) and OAI-SearchBot (ChatGPT search) are
|
|
1479
|
+
// OpenAI's declared crawler UAs — `gpt-?\d` never matched them, so they
|
|
1480
|
+
// classified as unattributed crawlers (round-27.4 W5).
|
|
1481
|
+
uaPatterns: [/chatgpt/i, /openai/i, /gpt-?\d/i, /\bGPTBot\b/i, /\bOAI-SearchBot\b/i],
|
|
1339
1482
|
agentCardPatterns: [/openai\.com/i, /chatgpt\.com/i],
|
|
1340
1483
|
apiKeyFormatPatterns: [/^sk-proj/i, /^sk-svcacct/i],
|
|
1341
1484
|
headerPatterns: [/^openai-organization/i, /^openai-project/i, /^openai-version/i]
|
|
@@ -1470,6 +1613,215 @@ var BROWSER_UA_PATTERN = /mozilla\/\d/i;
|
|
|
1470
1613
|
var _patterns = { STATIC_EXTENSIONS, BOT_UA_PATTERN, BROWSER_UA_PATTERN, SELF_UA_PATTERN };
|
|
1471
1614
|
|
|
1472
1615
|
// src/edge-core/events.ts
|
|
1616
|
+
async function withBudget(promise, ms) {
|
|
1617
|
+
let timer;
|
|
1618
|
+
try {
|
|
1619
|
+
return await Promise.race([
|
|
1620
|
+
promise,
|
|
1621
|
+
new Promise((resolve) => {
|
|
1622
|
+
timer = setTimeout(() => resolve(void 0), ms);
|
|
1623
|
+
})
|
|
1624
|
+
]);
|
|
1625
|
+
} catch {
|
|
1626
|
+
return void 0;
|
|
1627
|
+
} finally {
|
|
1628
|
+
if (timer) clearTimeout(timer);
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
var destKeyOf = (d) => `${d.apiBaseUrl}\0${d.apiKey ?? ""}\0${d.counterparty}\0${d.adapter ?? ""}`;
|
|
1632
|
+
var BeaconBuffer = class {
|
|
1633
|
+
constructor(options = {}) {
|
|
1634
|
+
this.queue = [];
|
|
1635
|
+
this.droppedSinceLast = 0;
|
|
1636
|
+
this.droppedTotal = 0;
|
|
1637
|
+
this.deliveredTotal = 0;
|
|
1638
|
+
this.backoffUntil = 0;
|
|
1639
|
+
this.collapse = /* @__PURE__ */ new Map();
|
|
1640
|
+
/** Keys the backend answered 401 for — later batches go unauthenticated. */
|
|
1641
|
+
this.rejectedKeys = /* @__PURE__ */ new Set();
|
|
1642
|
+
this.opts = {
|
|
1643
|
+
maxItems: options.maxItems ?? 1e3,
|
|
1644
|
+
maxBatch: options.maxBatch ?? 100,
|
|
1645
|
+
maxAttempts: options.maxAttempts ?? 3,
|
|
1646
|
+
collapseWindowMs: options.collapseWindowMs ?? 6e4,
|
|
1647
|
+
collapseMaxKeys: options.collapseMaxKeys ?? 1e3,
|
|
1648
|
+
requestTimeoutMs: options.requestTimeoutMs ?? 1e4,
|
|
1649
|
+
retryBackoffMs: options.retryBackoffMs ?? 1e3,
|
|
1650
|
+
now: options.now ?? Date.now
|
|
1651
|
+
};
|
|
1652
|
+
}
|
|
1653
|
+
/**
|
|
1654
|
+
* Queue one beacon. With a `collapseKey`, a repeat inside the window is
|
|
1655
|
+
* merged into a held aggregate (sent when the window closes) instead of
|
|
1656
|
+
* becoming its own item.
|
|
1657
|
+
*/
|
|
1658
|
+
enqueue(dest, item, collapseKey) {
|
|
1659
|
+
const now = this.opts.now();
|
|
1660
|
+
if (collapseKey) {
|
|
1661
|
+
const entry = this.collapse.get(collapseKey);
|
|
1662
|
+
if (entry && now - entry.firstSeenAt < this.opts.collapseWindowMs) {
|
|
1663
|
+
for (const target of [entry.first, entry.held]) {
|
|
1664
|
+
if (target && !target.inFlight && this.queue.includes(target)) {
|
|
1665
|
+
target.item.repeatCount = (target.item.repeatCount ?? 1) + 1;
|
|
1666
|
+
return;
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
const held = this.push(
|
|
1670
|
+
dest,
|
|
1671
|
+
{ ...item, repeatCount: 1 },
|
|
1672
|
+
entry.firstSeenAt + this.opts.collapseWindowMs
|
|
1673
|
+
);
|
|
1674
|
+
entry.held = held;
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1677
|
+
this.collapse.delete(collapseKey);
|
|
1678
|
+
if (this.collapse.size >= this.opts.collapseMaxKeys) {
|
|
1679
|
+
const oldest = this.collapse.keys().next().value;
|
|
1680
|
+
if (oldest !== void 0) this.collapse.delete(oldest);
|
|
1681
|
+
}
|
|
1682
|
+
const fresh = { firstSeenAt: now };
|
|
1683
|
+
this.collapse.set(collapseKey, fresh);
|
|
1684
|
+
fresh.first = this.push(dest, item, now);
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1687
|
+
this.push(dest, item, now);
|
|
1688
|
+
}
|
|
1689
|
+
/**
|
|
1690
|
+
* Deliver every eligible queued item. Concurrent callers share ONE
|
|
1691
|
+
* in-flight drain (it survives the caller's budget and a Lambda freeze),
|
|
1692
|
+
* so nothing in flight is ever sent twice; items queued while it runs are
|
|
1693
|
+
* picked up by the same drain.
|
|
1694
|
+
*/
|
|
1695
|
+
flush() {
|
|
1696
|
+
if (!this.inFlight) {
|
|
1697
|
+
this.inFlight = this.drain().finally(() => {
|
|
1698
|
+
this.inFlight = void 0;
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
return this.inFlight;
|
|
1702
|
+
}
|
|
1703
|
+
/** Counters for tests and diagnostics. */
|
|
1704
|
+
stats() {
|
|
1705
|
+
return {
|
|
1706
|
+
queueDepth: this.queue.length,
|
|
1707
|
+
droppedSinceLast: this.droppedSinceLast,
|
|
1708
|
+
droppedTotal: this.droppedTotal,
|
|
1709
|
+
deliveredTotal: this.deliveredTotal,
|
|
1710
|
+
flushing: this.inFlight !== void 0
|
|
1711
|
+
};
|
|
1712
|
+
}
|
|
1713
|
+
push(dest, item, eligibleAt) {
|
|
1714
|
+
while (this.queue.length >= this.opts.maxItems) this.dropOldest();
|
|
1715
|
+
const entry = {
|
|
1716
|
+
dest,
|
|
1717
|
+
destKey: destKeyOf(dest),
|
|
1718
|
+
item,
|
|
1719
|
+
attempts: 0,
|
|
1720
|
+
eligibleAt,
|
|
1721
|
+
inFlight: false
|
|
1722
|
+
};
|
|
1723
|
+
this.queue.push(entry);
|
|
1724
|
+
return entry;
|
|
1725
|
+
}
|
|
1726
|
+
dropOldest() {
|
|
1727
|
+
if (this.queue.shift()) this.countDropped(1);
|
|
1728
|
+
}
|
|
1729
|
+
countDropped(n) {
|
|
1730
|
+
this.droppedSinceLast += n;
|
|
1731
|
+
this.droppedTotal += n;
|
|
1732
|
+
}
|
|
1733
|
+
async drain() {
|
|
1734
|
+
for (; ; ) {
|
|
1735
|
+
const now = this.opts.now();
|
|
1736
|
+
if (now < this.backoffUntil) return;
|
|
1737
|
+
const batch = this.takeBatch(now);
|
|
1738
|
+
if (batch.length === 0) return;
|
|
1739
|
+
const ok = await this.send(batch);
|
|
1740
|
+
if (!ok) return;
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
/** Oldest eligible item + up to maxBatch-1 more for the same destination. */
|
|
1744
|
+
takeBatch(now) {
|
|
1745
|
+
const first = this.queue.find((q) => q.eligibleAt <= now);
|
|
1746
|
+
if (!first) return [];
|
|
1747
|
+
const batch = [];
|
|
1748
|
+
const rest = [];
|
|
1749
|
+
for (const q of this.queue) {
|
|
1750
|
+
if (batch.length < this.opts.maxBatch && q.destKey === first.destKey && q.eligibleAt <= now) {
|
|
1751
|
+
q.inFlight = true;
|
|
1752
|
+
batch.push(q);
|
|
1753
|
+
} else {
|
|
1754
|
+
rest.push(q);
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
this.queue = rest;
|
|
1758
|
+
return batch;
|
|
1759
|
+
}
|
|
1760
|
+
/** POST one batch. Returns false when the drain should pause (backoff). */
|
|
1761
|
+
async send(batch) {
|
|
1762
|
+
const dest = batch[0].dest;
|
|
1763
|
+
const useKey = dest.apiKey && !this.rejectedKeys.has(dest.apiKey) ? dest.apiKey : void 0;
|
|
1764
|
+
const reportedDropped = this.droppedSinceLast;
|
|
1765
|
+
const meta = {
|
|
1766
|
+
droppedSinceLast: reportedDropped,
|
|
1767
|
+
queueDepth: this.queue.length,
|
|
1768
|
+
adapter: dest.adapter ?? "edge-core",
|
|
1769
|
+
version: SDK_VERSION
|
|
1770
|
+
};
|
|
1771
|
+
const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
|
|
1772
|
+
const response = await withBudget(
|
|
1773
|
+
sdkFetch(`${dest.apiBaseUrl}/verification-activity/edge-events`, {
|
|
1774
|
+
method: "POST",
|
|
1775
|
+
headers: {
|
|
1776
|
+
"Content-Type": "application/json",
|
|
1777
|
+
...useKey && { Authorization: `Bearer ${useKey}` }
|
|
1778
|
+
},
|
|
1779
|
+
body: JSON.stringify({ events: batch.map((q) => q.item), meta }),
|
|
1780
|
+
...controller && { signal: controller.signal }
|
|
1781
|
+
}),
|
|
1782
|
+
this.opts.requestTimeoutMs
|
|
1783
|
+
);
|
|
1784
|
+
if (!response) controller?.abort();
|
|
1785
|
+
const status = response?.status;
|
|
1786
|
+
for (const q of batch) q.inFlight = false;
|
|
1787
|
+
if (status !== void 0 && status >= 200 && status < 300) {
|
|
1788
|
+
this.droppedSinceLast = Math.max(0, this.droppedSinceLast - reportedDropped);
|
|
1789
|
+
this.deliveredTotal += batch.length;
|
|
1790
|
+
return true;
|
|
1791
|
+
}
|
|
1792
|
+
if (status === 401 && useKey) {
|
|
1793
|
+
this.rejectedKeys.add(useKey);
|
|
1794
|
+
this.requeue(batch, false);
|
|
1795
|
+
return true;
|
|
1796
|
+
}
|
|
1797
|
+
if (status === 429) {
|
|
1798
|
+
const retryAfter = Number(response?.headers.get("retry-after"));
|
|
1799
|
+
const waitMs = Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(retryAfter, 120) * 1e3 : 3e4;
|
|
1800
|
+
this.backoffUntil = this.opts.now() + waitMs;
|
|
1801
|
+
this.requeue(batch, false);
|
|
1802
|
+
return false;
|
|
1803
|
+
}
|
|
1804
|
+
if (status !== void 0 && status >= 400 && status < 500) {
|
|
1805
|
+
this.countDropped(batch.length);
|
|
1806
|
+
return true;
|
|
1807
|
+
}
|
|
1808
|
+
this.backoffUntil = this.opts.now() + this.opts.retryBackoffMs;
|
|
1809
|
+
this.requeue(batch, true);
|
|
1810
|
+
return false;
|
|
1811
|
+
}
|
|
1812
|
+
/** Put a failed batch back at the head of the queue (order preserved). */
|
|
1813
|
+
requeue(batch, countAttempt) {
|
|
1814
|
+
const keep = [];
|
|
1815
|
+
for (const q of batch) {
|
|
1816
|
+
if (countAttempt) q.attempts += 1;
|
|
1817
|
+
if (q.attempts >= this.opts.maxAttempts) this.countDropped(1);
|
|
1818
|
+
else keep.push(q);
|
|
1819
|
+
}
|
|
1820
|
+
this.queue = [...keep, ...this.queue];
|
|
1821
|
+
while (this.queue.length > this.opts.maxItems) this.dropOldest();
|
|
1822
|
+
}
|
|
1823
|
+
};
|
|
1824
|
+
var beaconBuffer = new BeaconBuffer();
|
|
1473
1825
|
function sampled(rate, random = Math.random) {
|
|
1474
1826
|
if (rate <= 0) return false;
|
|
1475
1827
|
if (rate >= 1) return true;
|
|
@@ -1646,12 +1998,19 @@ function defaultOnDenied(result, _req, res) {
|
|
|
1646
1998
|
});
|
|
1647
1999
|
return;
|
|
1648
2000
|
}
|
|
1649
|
-
|
|
2001
|
+
if (isVerificationUnavailable(result)) {
|
|
2002
|
+
res.setHeader("X-Astra-Gateway-Mode", "enforced");
|
|
2003
|
+
res.setHeader(VERIFICATION_STATUS_HEADER, "unavailable");
|
|
2004
|
+
res.setHeader("Retry-After", String(unavailableRetryAfterSeconds(result)));
|
|
2005
|
+
res.status(503).json(buildVerificationUnavailableBody(result));
|
|
2006
|
+
return;
|
|
2007
|
+
}
|
|
2008
|
+
const { status: statusCode, code } = denyStatusFor(result, "INSUFFICIENT_ACCESS");
|
|
1650
2009
|
res.setHeader("X-Astra-Gateway-Mode", "enforced");
|
|
1651
2010
|
res.status(statusCode).json({
|
|
1652
2011
|
success: false,
|
|
1653
2012
|
error: {
|
|
1654
|
-
code
|
|
2013
|
+
code,
|
|
1655
2014
|
message: result.denialReasons?.[0] || "Access denied",
|
|
1656
2015
|
guidance: result.guidance,
|
|
1657
2016
|
// Round-10: aggregated per-dimension detail + correlation handle.
|
|
@@ -1899,6 +2258,21 @@ function createMiddleware(options) {
|
|
|
1899
2258
|
result.stepUpOutcome = outcome;
|
|
1900
2259
|
}
|
|
1901
2260
|
}
|
|
2261
|
+
if (!result.policyAllowed && isVerificationUnavailable(result) && (!shouldEnforce || isAnonymousRead(req.method, credentials))) {
|
|
2262
|
+
res.setHeader(VERIFICATION_STATUS_HEADER, "unavailable");
|
|
2263
|
+
return next();
|
|
2264
|
+
}
|
|
2265
|
+
if (!shouldEnforce) {
|
|
2266
|
+
if (config.setPassThroughHeader) {
|
|
2267
|
+
res.setHeader("X-Astra-Gateway-Mode", "enforced");
|
|
2268
|
+
res.setHeader("X-Astra-Gateway-Reason", "evaluated-not-enforced");
|
|
2269
|
+
}
|
|
2270
|
+
if (shouldRecordDecisions && sessionId) {
|
|
2271
|
+
recordDecision(config, sessionId, "granted").catch(() => {
|
|
2272
|
+
});
|
|
2273
|
+
}
|
|
2274
|
+
return next();
|
|
2275
|
+
}
|
|
1902
2276
|
if (!result.policyAllowed) {
|
|
1903
2277
|
if (shouldRecordDecisions && sessionId) {
|
|
1904
2278
|
recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
|
|
@@ -1918,17 +2292,6 @@ function createMiddleware(options) {
|
|
|
1918
2292
|
onDenied(result, req, res);
|
|
1919
2293
|
return;
|
|
1920
2294
|
}
|
|
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
2295
|
if (routeConfig.minTrustScore && result.agent) {
|
|
1933
2296
|
if (result.agent.trustScore < routeConfig.minTrustScore) {
|
|
1934
2297
|
const trustFailure = {
|
|
@@ -1984,14 +2347,6 @@ function createMiddleware(options) {
|
|
|
1984
2347
|
verifiedAt: /* @__PURE__ */ new Date(),
|
|
1985
2348
|
correlationId
|
|
1986
2349
|
};
|
|
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
2350
|
dedupeFailures(result);
|
|
1996
2351
|
return onDenied(result, req, res);
|
|
1997
2352
|
}
|