@astrasyncai/verification-gateway 5.11.0 → 5.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/adapters/express.d.mts +1 -1
- package/dist/adapters/express.d.ts +1 -1
- package/dist/adapters/express.js +394 -39
- package/dist/adapters/express.js.map +1 -1
- package/dist/adapters/express.mjs +394 -39
- package/dist/adapters/express.mjs.map +1 -1
- package/dist/adapters/mcp.d.mts +5 -1
- package/dist/adapters/mcp.d.ts +5 -1
- package/dist/adapters/mcp.js +193 -49
- package/dist/adapters/mcp.js.map +1 -1
- package/dist/adapters/mcp.mjs +193 -49
- package/dist/adapters/mcp.mjs.map +1 -1
- package/dist/adapters/nextjs.d.mts +1 -1
- package/dist/adapters/nextjs.d.ts +1 -1
- package/dist/adapters/nextjs.js +387 -22
- package/dist/adapters/nextjs.js.map +1 -1
- package/dist/adapters/nextjs.mjs +387 -22
- package/dist/adapters/nextjs.mjs.map +1 -1
- package/dist/adapters/sdk.d.mts +9 -1
- package/dist/adapters/sdk.d.ts +9 -1
- package/dist/adapters/sdk.js +125 -16
- package/dist/adapters/sdk.js.map +1 -1
- package/dist/adapters/sdk.mjs +125 -16
- package/dist/adapters/sdk.mjs.map +1 -1
- package/dist/agent/index.js +1 -1
- package/dist/agent/index.js.map +1 -1
- package/dist/agent/index.mjs +1 -1
- package/dist/agent/index.mjs.map +1 -1
- package/dist/bin/astrasync-claude-hook.js +309 -15
- package/dist/bin/astrasync-codex-hook.js +309 -15
- package/dist/bin/astrasync-guard.js +309 -15
- package/dist/bin/astrasync.js +322 -17
- package/dist/browser/background.js +309 -15
- package/dist/browser/background.js.map +1 -1
- package/dist/browser/background.mjs +309 -15
- package/dist/browser/background.mjs.map +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +1 -1
- package/dist/cli/index.mjs.map +1 -1
- package/dist/codex/index.js +309 -15
- package/dist/codex/index.js.map +1 -1
- package/dist/codex/index.mjs +309 -15
- package/dist/codex/index.mjs.map +1 -1
- package/dist/cursor/extension.js +309 -15
- package/dist/cursor/extension.js.map +1 -1
- package/dist/cursor/extension.mjs +309 -15
- package/dist/cursor/extension.mjs.map +1 -1
- package/dist/edge-config.d.mts +1 -1
- package/dist/edge-config.d.ts +1 -1
- package/dist/edge-config.js +1 -1
- package/dist/edge-config.js.map +1 -1
- package/dist/edge-config.mjs +1 -1
- package/dist/edge-config.mjs.map +1 -1
- package/dist/edge-core/index.d.mts +128 -13
- package/dist/edge-core/index.d.ts +128 -13
- package/dist/edge-core/index.js +362 -63
- package/dist/edge-core/index.js.map +1 -1
- package/dist/edge-core/index.mjs +359 -63
- package/dist/edge-core/index.mjs.map +1 -1
- package/dist/gateway/gateway.js +309 -15
- package/dist/gateway/gateway.js.map +1 -1
- package/dist/gateway/gateway.mjs +309 -15
- package/dist/gateway/gateway.mjs.map +1 -1
- package/dist/git-trigger/git-hooks.d.mts +1 -1
- package/dist/git-trigger/git-hooks.d.ts +1 -1
- package/dist/index.d.mts +118 -15
- package/dist/index.d.ts +118 -15
- package/dist/index.js +521 -72
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +515 -72
- package/dist/index.mjs.map +1 -1
- package/dist/platform-signatures.js +4 -1
- package/dist/platform-signatures.js.map +1 -1
- package/dist/platform-signatures.mjs +4 -1
- package/dist/platform-signatures.mjs.map +1 -1
- package/dist/registration/index.js +1 -1
- package/dist/registration/index.js.map +1 -1
- package/dist/registration/index.mjs +1 -1
- package/dist/registration/index.mjs.map +1 -1
- package/dist/transport/index.js +1 -1
- package/dist/transport/index.js.map +1 -1
- package/dist/transport/index.mjs +1 -1
- package/dist/transport/index.mjs.map +1 -1
- package/dist/{types-CTmUpNpS.d.mts → types-BCkwBA84.d.mts} +12 -0
- package/dist/{types-CTmUpNpS.d.ts → types-BCkwBA84.d.ts} +12 -0
- package/dist/{types-CtFXmDzm.d.ts → types-C_b4QTyO.d.ts} +18 -0
- package/dist/{types-DQrn_kei.d.mts → types-DlshIIR-.d.mts} +18 -0
- package/dist/ui/index.d.mts +1 -1
- package/dist/ui/index.d.ts +1 -1
- package/dist/verify.d.mts +35 -14
- package/dist/verify.d.ts +35 -14
- package/dist/verify.js +165 -25
- package/dist/verify.js.map +1 -1
- package/dist/verify.mjs +164 -25
- package/dist/verify.mjs.map +1 -1
- package/package.json +3 -2
package/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.13.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",
|
|
@@ -183,7 +236,7 @@ var DEFAULT_CONFIG = {
|
|
|
183
236
|
};
|
|
184
237
|
var initCheckPerformed = false;
|
|
185
238
|
var deprecationWarningShown = false;
|
|
186
|
-
async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey) {
|
|
239
|
+
async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey, counterparty) {
|
|
187
240
|
initCheckPerformed = true;
|
|
188
241
|
try {
|
|
189
242
|
const probeUrl = `${apiBaseUrl}/agents/verify-access`;
|
|
@@ -201,7 +254,8 @@ async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey) {
|
|
|
201
254
|
);
|
|
202
255
|
}
|
|
203
256
|
if (apiKey) {
|
|
204
|
-
const
|
|
257
|
+
const q = counterparty?.id ? `?counterpartyId=${encodeURIComponent(counterparty.id)}` : counterparty?.url ? `?counterpartyUrl=${encodeURIComponent(counterparty.url)}` : "";
|
|
258
|
+
const selfCheck = await sdkFetch(`${apiBaseUrl}/agents/verify-access/self-check${q}`, {
|
|
205
259
|
headers: { Authorization: `Bearer ${apiKey}` }
|
|
206
260
|
});
|
|
207
261
|
if (selfCheck.status === 401) {
|
|
@@ -213,6 +267,13 @@ async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey) {
|
|
|
213
267
|
console.error(message);
|
|
214
268
|
} else if (selfCheck.ok) {
|
|
215
269
|
const body = await selfCheck.json().catch(() => ({}));
|
|
270
|
+
if (body.counterparty && body.counterparty.canVerify === false) {
|
|
271
|
+
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.`;
|
|
272
|
+
if (strictInit) {
|
|
273
|
+
throw new Error(`${message} (strictInit=true)`);
|
|
274
|
+
}
|
|
275
|
+
console.error(message);
|
|
276
|
+
}
|
|
216
277
|
if (typeof body.daysToExpiry === "number" && body.daysToExpiry <= 7) {
|
|
217
278
|
console.warn(
|
|
218
279
|
`[VerificationGateway] the configured AstraSync API key expires in ${body.daysToExpiry} day(s) \u2014 rotate it at Settings \u2192 Developer Tools before integrations start failing.`
|
|
@@ -351,11 +412,11 @@ function createGuidanceResponse(_config, reason, options = {}) {
|
|
|
351
412
|
const isMisconfig = source === "merchant_misconfig";
|
|
352
413
|
const urls = options.urls;
|
|
353
414
|
const guidance = isMisconfig ? {
|
|
354
|
-
message: "This site's AstraSync integration is misconfigured (its API key was revoked, expired, or
|
|
415
|
+
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.",
|
|
355
416
|
registrationUrl: urls?.registrationUrl ?? "",
|
|
356
417
|
documentationUrl: options.remediation?.docsUrl ?? urls?.documentationUrl ?? "",
|
|
357
418
|
steps: [
|
|
358
|
-
"Report the failure to the site operator \u2014 their AstraSync API key needs
|
|
419
|
+
"Report the failure to the site operator \u2014 their AstraSync API key needs replacing",
|
|
359
420
|
"Do not retry: the failure is deterministic until the operator updates their key"
|
|
360
421
|
]
|
|
361
422
|
} : isApiError ? {
|
|
@@ -416,11 +477,44 @@ function createGuidanceResponse(_config, reason, options = {}) {
|
|
|
416
477
|
// coercing the absent recommendation to a policy 'deny'. Misconfig is
|
|
417
478
|
// deterministic and intentionally NOT marked retryable.
|
|
418
479
|
...isApiError && { retryable: true },
|
|
480
|
+
...options.httpStatus !== void 0 && { httpStatus: options.httpStatus },
|
|
481
|
+
...options.retryAfterMs !== void 0 && { retryAfterMs: options.retryAfterMs },
|
|
419
482
|
correlationId: options.correlationId,
|
|
420
483
|
attemptId: options.attemptId,
|
|
421
484
|
verifiedAt: /* @__PURE__ */ new Date()
|
|
422
485
|
};
|
|
423
486
|
}
|
|
487
|
+
async function readJsonSafe(response) {
|
|
488
|
+
try {
|
|
489
|
+
const parsed = await response.json();
|
|
490
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
491
|
+
} catch {
|
|
492
|
+
return {};
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
function rejectedIdentityGuidance(failures) {
|
|
496
|
+
const f = failures?.find((x) => REJECTED_IDENTITY_DIMENSIONS.has(x.dimension));
|
|
497
|
+
if (!f) return void 0;
|
|
498
|
+
if (f.dimension === "agent.lookup") {
|
|
499
|
+
return `The presented agent identifier is not registered with AstraSync. ${UNKNOWN_AGENT_GUIDANCE}`;
|
|
500
|
+
}
|
|
501
|
+
return f.guidance ? `${f.message} ${f.guidance}` : f.message;
|
|
502
|
+
}
|
|
503
|
+
var MAX_RETRY_AFTER_MS = 15 * 60 * 1e3;
|
|
504
|
+
function parseRetryAfterMs(response) {
|
|
505
|
+
const raw = response.headers?.get?.("retry-after");
|
|
506
|
+
if (!raw) return void 0;
|
|
507
|
+
const trimmed = raw.trim();
|
|
508
|
+
let ms;
|
|
509
|
+
if (/^\d+(\.\d+)?$/.test(trimmed)) {
|
|
510
|
+
ms = Number(trimmed) * 1e3;
|
|
511
|
+
} else {
|
|
512
|
+
const at = Date.parse(trimmed);
|
|
513
|
+
if (Number.isNaN(at)) return void 0;
|
|
514
|
+
ms = at - Date.now();
|
|
515
|
+
}
|
|
516
|
+
return Math.min(MAX_RETRY_AFTER_MS, Math.max(0, Math.round(ms)));
|
|
517
|
+
}
|
|
424
518
|
async function callVerifyAccessAPI(config, request) {
|
|
425
519
|
const { credentials, ...requestData } = request;
|
|
426
520
|
const body = {
|
|
@@ -477,7 +571,8 @@ async function callVerifyAccessAPI(config, request) {
|
|
|
477
571
|
body: JSON.stringify(body),
|
|
478
572
|
signal: AbortSignal.timeout(config.verifyTimeoutMs ?? 1e4)
|
|
479
573
|
});
|
|
480
|
-
const data = await response
|
|
574
|
+
const data = await readJsonSafe(response);
|
|
575
|
+
const retryAfterMs = parseRetryAfterMs(response);
|
|
481
576
|
if (response.status === 410) {
|
|
482
577
|
return {
|
|
483
578
|
success: true,
|
|
@@ -515,13 +610,31 @@ async function callVerifyAccessAPI(config, request) {
|
|
|
515
610
|
}
|
|
516
611
|
};
|
|
517
612
|
}
|
|
613
|
+
if (response.status === 404 && credentials.astraId && (data?.error === "AGENT_NOT_FOUND" || typeof data?.message === "string" && /agent/i.test(data.message))) {
|
|
614
|
+
return {
|
|
615
|
+
success: true,
|
|
616
|
+
attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0,
|
|
617
|
+
access: {
|
|
618
|
+
allowed: false,
|
|
619
|
+
reason: "unknown_agent",
|
|
620
|
+
failures: [
|
|
621
|
+
{
|
|
622
|
+
dimension: "identity.unknown_agent",
|
|
623
|
+
message: "The presented agent identifier is not registered with AstraSync.",
|
|
624
|
+
guidance: UNKNOWN_AGENT_GUIDANCE
|
|
625
|
+
}
|
|
626
|
+
]
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
}
|
|
518
630
|
if (!response.ok) {
|
|
519
|
-
|
|
631
|
+
const keyMismatch = response.status === 403 && data?.error === "COUNTERPARTY_KEY_MISMATCH";
|
|
632
|
+
if (response.status === 401 || keyMismatch) {
|
|
520
633
|
return {
|
|
521
634
|
success: false,
|
|
522
635
|
error: data.message || data.error || "Integration API key rejected",
|
|
523
636
|
misconfig: true,
|
|
524
|
-
misconfigCode: typeof data?.code === "string" ? data.code : void 0,
|
|
637
|
+
misconfigCode: keyMismatch ? "counterparty_key_mismatch" : typeof data?.code === "string" ? data.code : void 0,
|
|
525
638
|
remediation: data?.remediation && typeof data.remediation === "object" ? data.remediation : void 0,
|
|
526
639
|
correlationId: typeof data?.correlationId === "string" ? data.correlationId : void 0,
|
|
527
640
|
attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0
|
|
@@ -529,9 +642,11 @@ async function callVerifyAccessAPI(config, request) {
|
|
|
529
642
|
}
|
|
530
643
|
return {
|
|
531
644
|
success: false,
|
|
532
|
-
error: data.message || data.error || `API returned ${response.status}`,
|
|
645
|
+
error: typeof data?.message === "string" && data.message || typeof data?.error === "string" && data.error || `API returned ${response.status}`,
|
|
533
646
|
correlationId: typeof data?.correlationId === "string" ? data.correlationId : void 0,
|
|
534
|
-
attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0
|
|
647
|
+
attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0,
|
|
648
|
+
httpStatus: response.status,
|
|
649
|
+
...retryAfterMs !== void 0 && { retryAfterMs }
|
|
535
650
|
};
|
|
536
651
|
}
|
|
537
652
|
return data;
|
|
@@ -552,14 +667,22 @@ async function verify(config, request, options) {
|
|
|
552
667
|
mergedConfig.apiBaseUrl,
|
|
553
668
|
mergedConfig.debug,
|
|
554
669
|
true,
|
|
555
|
-
mergedConfig.apiKey
|
|
670
|
+
mergedConfig.apiKey,
|
|
671
|
+
{
|
|
672
|
+
id: mergedConfig.counterpartyId,
|
|
673
|
+
url: mergedConfig.counterpartyUrl ?? request.counterpartyUrl
|
|
674
|
+
}
|
|
556
675
|
);
|
|
557
676
|
} else {
|
|
558
677
|
void performInitCheck(
|
|
559
678
|
mergedConfig.apiBaseUrl,
|
|
560
679
|
mergedConfig.debug,
|
|
561
680
|
false,
|
|
562
|
-
mergedConfig.apiKey
|
|
681
|
+
mergedConfig.apiKey,
|
|
682
|
+
{
|
|
683
|
+
id: mergedConfig.counterpartyId,
|
|
684
|
+
url: mergedConfig.counterpartyUrl ?? request.counterpartyUrl
|
|
685
|
+
}
|
|
563
686
|
);
|
|
564
687
|
}
|
|
565
688
|
}
|
|
@@ -603,12 +726,14 @@ async function verify(config, request, options) {
|
|
|
603
726
|
correlationId: apiResponse.correlationId,
|
|
604
727
|
attemptId: apiResponse.attemptId,
|
|
605
728
|
remediation: apiResponse.remediation,
|
|
729
|
+
httpStatus: apiResponse.httpStatus,
|
|
730
|
+
retryAfterMs: apiResponse.retryAfterMs,
|
|
606
731
|
urls
|
|
607
732
|
});
|
|
608
733
|
}
|
|
609
734
|
if (!apiResponse.access?.allowed) {
|
|
610
735
|
const aggregatedFailures = apiResponse.access?.failures;
|
|
611
|
-
const idVerifiedFromBackend = apiResponse.verificationContext?.idVerified === true;
|
|
736
|
+
const idVerifiedFromBackend = apiResponse.verificationContext?.idVerified === true && !aggregatedFailures?.some((f) => REJECTED_IDENTITY_DIMENSIONS.has(f.dimension));
|
|
612
737
|
const result2 = {
|
|
613
738
|
identityVerified: idVerifiedFromBackend,
|
|
614
739
|
policyAllowed: false,
|
|
@@ -620,7 +745,10 @@ async function verify(config, request, options) {
|
|
|
620
745
|
requiresStepUp: apiResponse.access?.requiresStepUp,
|
|
621
746
|
requiresApproval: apiResponse.access?.requiresApproval,
|
|
622
747
|
guidance: {
|
|
623
|
-
|
|
748
|
+
// 5.12.0: a presented-but-rejected id (malformed / unknown) carries
|
|
749
|
+
// actionable registration guidance on its failure — surface it
|
|
750
|
+
// instead of the bare machine reason ('unknown_agent').
|
|
751
|
+
message: rejectedIdentityGuidance(aggregatedFailures) ?? (apiResponse.access?.reason || "Access denied by PDLSS policy"),
|
|
624
752
|
registrationUrl: urls?.registrationUrl ?? "",
|
|
625
753
|
documentationUrl: urls?.documentationUrl ?? "",
|
|
626
754
|
// 5.1.0: anonymous callers land on this branch — steer MCP-hosted
|
|
@@ -720,7 +848,7 @@ async function verify(config, request, options) {
|
|
|
720
848
|
registrationUrl: urls?.registrationUrl ?? "",
|
|
721
849
|
documentationUrl: urls?.documentationUrl ?? ""
|
|
722
850
|
};
|
|
723
|
-
} else if (result.recommendation === "step_up_required") {
|
|
851
|
+
} else if (result.recommendation === "step_up_required" && result.identityVerified) {
|
|
724
852
|
result.requiresStepUp = true;
|
|
725
853
|
result.denialReasons = serverDenialReasons ?? result.recommendationReasons ?? ["Step-up verification required"];
|
|
726
854
|
}
|
|
@@ -743,6 +871,7 @@ async function recordDecision(config, sessionId, decision, reason) {
|
|
|
743
871
|
}).catch(() => {
|
|
744
872
|
});
|
|
745
873
|
}
|
|
874
|
+
var refusedPolicyReads = /* @__PURE__ */ new Set();
|
|
746
875
|
async function fetchRoutes(config, counterpartyId) {
|
|
747
876
|
if (!counterpartyId) return null;
|
|
748
877
|
const headers = { "Content-Type": "application/json" };
|
|
@@ -755,7 +884,15 @@ async function fetchRoutes(config, counterpartyId) {
|
|
|
755
884
|
`${config.apiBaseUrl}/endpoints/${encodeURIComponent(counterpartyId)}/routes`,
|
|
756
885
|
{ method: "GET", headers }
|
|
757
886
|
);
|
|
758
|
-
if (!response.ok)
|
|
887
|
+
if (!response.ok) {
|
|
888
|
+
if ((response.status === 401 || response.status === 403 || response.status === 404) && !refusedPolicyReads.has(counterpartyId)) {
|
|
889
|
+
refusedPolicyReads.add(counterpartyId);
|
|
890
|
+
console.error(
|
|
891
|
+
`[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.`
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
return null;
|
|
895
|
+
}
|
|
759
896
|
const body = await response.json();
|
|
760
897
|
return body.data?.routes ?? [];
|
|
761
898
|
} catch {
|
|
@@ -772,7 +909,10 @@ async function reportUnregisteredAttempt(config, data) {
|
|
|
772
909
|
};
|
|
773
910
|
await sdkFetch(`${apiBaseUrl}/verification-activity/unregistered-attempt`, {
|
|
774
911
|
method: "POST",
|
|
775
|
-
headers: {
|
|
912
|
+
headers: {
|
|
913
|
+
"Content-Type": "application/json",
|
|
914
|
+
...config.apiKey && { Authorization: `Bearer ${config.apiKey}` }
|
|
915
|
+
},
|
|
776
916
|
body: JSON.stringify(body)
|
|
777
917
|
}).catch(() => {
|
|
778
918
|
});
|
|
@@ -1362,7 +1502,10 @@ var PLATFORM_AGENT_SIGNATURES = [
|
|
|
1362
1502
|
{
|
|
1363
1503
|
vendor: "chatgpt",
|
|
1364
1504
|
displayName: "OpenAI ChatGPT",
|
|
1365
|
-
|
|
1505
|
+
// GPTBot (training crawler) and OAI-SearchBot (ChatGPT search) are
|
|
1506
|
+
// OpenAI's declared crawler UAs — `gpt-?\d` never matched them, so they
|
|
1507
|
+
// classified as unattributed crawlers (round-27.4 W5).
|
|
1508
|
+
uaPatterns: [/chatgpt/i, /openai/i, /gpt-?\d/i, /\bGPTBot\b/i, /\bOAI-SearchBot\b/i],
|
|
1366
1509
|
agentCardPatterns: [/openai\.com/i, /chatgpt\.com/i],
|
|
1367
1510
|
apiKeyFormatPatterns: [/^sk-proj/i, /^sk-svcacct/i],
|
|
1368
1511
|
headerPatterns: [/^openai-organization/i, /^openai-project/i, /^openai-version/i]
|
|
@@ -1497,6 +1640,215 @@ var BROWSER_UA_PATTERN = /mozilla\/\d/i;
|
|
|
1497
1640
|
var _patterns = { STATIC_EXTENSIONS, BOT_UA_PATTERN, BROWSER_UA_PATTERN, SELF_UA_PATTERN };
|
|
1498
1641
|
|
|
1499
1642
|
// src/edge-core/events.ts
|
|
1643
|
+
async function withBudget(promise, ms) {
|
|
1644
|
+
let timer;
|
|
1645
|
+
try {
|
|
1646
|
+
return await Promise.race([
|
|
1647
|
+
promise,
|
|
1648
|
+
new Promise((resolve) => {
|
|
1649
|
+
timer = setTimeout(() => resolve(void 0), ms);
|
|
1650
|
+
})
|
|
1651
|
+
]);
|
|
1652
|
+
} catch {
|
|
1653
|
+
return void 0;
|
|
1654
|
+
} finally {
|
|
1655
|
+
if (timer) clearTimeout(timer);
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
var destKeyOf = (d) => `${d.apiBaseUrl}\0${d.apiKey ?? ""}\0${d.counterparty}\0${d.adapter ?? ""}`;
|
|
1659
|
+
var BeaconBuffer = class {
|
|
1660
|
+
constructor(options = {}) {
|
|
1661
|
+
this.queue = [];
|
|
1662
|
+
this.droppedSinceLast = 0;
|
|
1663
|
+
this.droppedTotal = 0;
|
|
1664
|
+
this.deliveredTotal = 0;
|
|
1665
|
+
this.backoffUntil = 0;
|
|
1666
|
+
this.collapse = /* @__PURE__ */ new Map();
|
|
1667
|
+
/** Keys the backend answered 401 for — later batches go unauthenticated. */
|
|
1668
|
+
this.rejectedKeys = /* @__PURE__ */ new Set();
|
|
1669
|
+
this.opts = {
|
|
1670
|
+
maxItems: options.maxItems ?? 1e3,
|
|
1671
|
+
maxBatch: options.maxBatch ?? 100,
|
|
1672
|
+
maxAttempts: options.maxAttempts ?? 3,
|
|
1673
|
+
collapseWindowMs: options.collapseWindowMs ?? 6e4,
|
|
1674
|
+
collapseMaxKeys: options.collapseMaxKeys ?? 1e3,
|
|
1675
|
+
requestTimeoutMs: options.requestTimeoutMs ?? 1e4,
|
|
1676
|
+
retryBackoffMs: options.retryBackoffMs ?? 1e3,
|
|
1677
|
+
now: options.now ?? Date.now
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
/**
|
|
1681
|
+
* Queue one beacon. With a `collapseKey`, a repeat inside the window is
|
|
1682
|
+
* merged into a held aggregate (sent when the window closes) instead of
|
|
1683
|
+
* becoming its own item.
|
|
1684
|
+
*/
|
|
1685
|
+
enqueue(dest, item, collapseKey) {
|
|
1686
|
+
const now = this.opts.now();
|
|
1687
|
+
if (collapseKey) {
|
|
1688
|
+
const entry = this.collapse.get(collapseKey);
|
|
1689
|
+
if (entry && now - entry.firstSeenAt < this.opts.collapseWindowMs) {
|
|
1690
|
+
for (const target of [entry.first, entry.held]) {
|
|
1691
|
+
if (target && !target.inFlight && this.queue.includes(target)) {
|
|
1692
|
+
target.item.repeatCount = (target.item.repeatCount ?? 1) + 1;
|
|
1693
|
+
return;
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
const held = this.push(
|
|
1697
|
+
dest,
|
|
1698
|
+
{ ...item, repeatCount: 1 },
|
|
1699
|
+
entry.firstSeenAt + this.opts.collapseWindowMs
|
|
1700
|
+
);
|
|
1701
|
+
entry.held = held;
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
this.collapse.delete(collapseKey);
|
|
1705
|
+
if (this.collapse.size >= this.opts.collapseMaxKeys) {
|
|
1706
|
+
const oldest = this.collapse.keys().next().value;
|
|
1707
|
+
if (oldest !== void 0) this.collapse.delete(oldest);
|
|
1708
|
+
}
|
|
1709
|
+
const fresh = { firstSeenAt: now };
|
|
1710
|
+
this.collapse.set(collapseKey, fresh);
|
|
1711
|
+
fresh.first = this.push(dest, item, now);
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1714
|
+
this.push(dest, item, now);
|
|
1715
|
+
}
|
|
1716
|
+
/**
|
|
1717
|
+
* Deliver every eligible queued item. Concurrent callers share ONE
|
|
1718
|
+
* in-flight drain (it survives the caller's budget and a Lambda freeze),
|
|
1719
|
+
* so nothing in flight is ever sent twice; items queued while it runs are
|
|
1720
|
+
* picked up by the same drain.
|
|
1721
|
+
*/
|
|
1722
|
+
flush() {
|
|
1723
|
+
if (!this.inFlight) {
|
|
1724
|
+
this.inFlight = this.drain().finally(() => {
|
|
1725
|
+
this.inFlight = void 0;
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1728
|
+
return this.inFlight;
|
|
1729
|
+
}
|
|
1730
|
+
/** Counters for tests and diagnostics. */
|
|
1731
|
+
stats() {
|
|
1732
|
+
return {
|
|
1733
|
+
queueDepth: this.queue.length,
|
|
1734
|
+
droppedSinceLast: this.droppedSinceLast,
|
|
1735
|
+
droppedTotal: this.droppedTotal,
|
|
1736
|
+
deliveredTotal: this.deliveredTotal,
|
|
1737
|
+
flushing: this.inFlight !== void 0
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
push(dest, item, eligibleAt) {
|
|
1741
|
+
while (this.queue.length >= this.opts.maxItems) this.dropOldest();
|
|
1742
|
+
const entry = {
|
|
1743
|
+
dest,
|
|
1744
|
+
destKey: destKeyOf(dest),
|
|
1745
|
+
item,
|
|
1746
|
+
attempts: 0,
|
|
1747
|
+
eligibleAt,
|
|
1748
|
+
inFlight: false
|
|
1749
|
+
};
|
|
1750
|
+
this.queue.push(entry);
|
|
1751
|
+
return entry;
|
|
1752
|
+
}
|
|
1753
|
+
dropOldest() {
|
|
1754
|
+
if (this.queue.shift()) this.countDropped(1);
|
|
1755
|
+
}
|
|
1756
|
+
countDropped(n) {
|
|
1757
|
+
this.droppedSinceLast += n;
|
|
1758
|
+
this.droppedTotal += n;
|
|
1759
|
+
}
|
|
1760
|
+
async drain() {
|
|
1761
|
+
for (; ; ) {
|
|
1762
|
+
const now = this.opts.now();
|
|
1763
|
+
if (now < this.backoffUntil) return;
|
|
1764
|
+
const batch = this.takeBatch(now);
|
|
1765
|
+
if (batch.length === 0) return;
|
|
1766
|
+
const ok = await this.send(batch);
|
|
1767
|
+
if (!ok) return;
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
/** Oldest eligible item + up to maxBatch-1 more for the same destination. */
|
|
1771
|
+
takeBatch(now) {
|
|
1772
|
+
const first = this.queue.find((q) => q.eligibleAt <= now);
|
|
1773
|
+
if (!first) return [];
|
|
1774
|
+
const batch = [];
|
|
1775
|
+
const rest = [];
|
|
1776
|
+
for (const q of this.queue) {
|
|
1777
|
+
if (batch.length < this.opts.maxBatch && q.destKey === first.destKey && q.eligibleAt <= now) {
|
|
1778
|
+
q.inFlight = true;
|
|
1779
|
+
batch.push(q);
|
|
1780
|
+
} else {
|
|
1781
|
+
rest.push(q);
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
this.queue = rest;
|
|
1785
|
+
return batch;
|
|
1786
|
+
}
|
|
1787
|
+
/** POST one batch. Returns false when the drain should pause (backoff). */
|
|
1788
|
+
async send(batch) {
|
|
1789
|
+
const dest = batch[0].dest;
|
|
1790
|
+
const useKey = dest.apiKey && !this.rejectedKeys.has(dest.apiKey) ? dest.apiKey : void 0;
|
|
1791
|
+
const reportedDropped = this.droppedSinceLast;
|
|
1792
|
+
const meta = {
|
|
1793
|
+
droppedSinceLast: reportedDropped,
|
|
1794
|
+
queueDepth: this.queue.length,
|
|
1795
|
+
adapter: dest.adapter ?? "edge-core",
|
|
1796
|
+
version: SDK_VERSION
|
|
1797
|
+
};
|
|
1798
|
+
const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
|
|
1799
|
+
const response = await withBudget(
|
|
1800
|
+
sdkFetch(`${dest.apiBaseUrl}/verification-activity/edge-events`, {
|
|
1801
|
+
method: "POST",
|
|
1802
|
+
headers: {
|
|
1803
|
+
"Content-Type": "application/json",
|
|
1804
|
+
...useKey && { Authorization: `Bearer ${useKey}` }
|
|
1805
|
+
},
|
|
1806
|
+
body: JSON.stringify({ events: batch.map((q) => q.item), meta }),
|
|
1807
|
+
...controller && { signal: controller.signal }
|
|
1808
|
+
}),
|
|
1809
|
+
this.opts.requestTimeoutMs
|
|
1810
|
+
);
|
|
1811
|
+
if (!response) controller?.abort();
|
|
1812
|
+
const status = response?.status;
|
|
1813
|
+
for (const q of batch) q.inFlight = false;
|
|
1814
|
+
if (status !== void 0 && status >= 200 && status < 300) {
|
|
1815
|
+
this.droppedSinceLast = Math.max(0, this.droppedSinceLast - reportedDropped);
|
|
1816
|
+
this.deliveredTotal += batch.length;
|
|
1817
|
+
return true;
|
|
1818
|
+
}
|
|
1819
|
+
if (status === 401 && useKey) {
|
|
1820
|
+
this.rejectedKeys.add(useKey);
|
|
1821
|
+
this.requeue(batch, false);
|
|
1822
|
+
return true;
|
|
1823
|
+
}
|
|
1824
|
+
if (status === 429) {
|
|
1825
|
+
const retryAfter = Number(response?.headers.get("retry-after"));
|
|
1826
|
+
const waitMs = Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(retryAfter, 120) * 1e3 : 3e4;
|
|
1827
|
+
this.backoffUntil = this.opts.now() + waitMs;
|
|
1828
|
+
this.requeue(batch, false);
|
|
1829
|
+
return false;
|
|
1830
|
+
}
|
|
1831
|
+
if (status !== void 0 && status >= 400 && status < 500) {
|
|
1832
|
+
this.countDropped(batch.length);
|
|
1833
|
+
return true;
|
|
1834
|
+
}
|
|
1835
|
+
this.backoffUntil = this.opts.now() + this.opts.retryBackoffMs;
|
|
1836
|
+
this.requeue(batch, true);
|
|
1837
|
+
return false;
|
|
1838
|
+
}
|
|
1839
|
+
/** Put a failed batch back at the head of the queue (order preserved). */
|
|
1840
|
+
requeue(batch, countAttempt) {
|
|
1841
|
+
const keep = [];
|
|
1842
|
+
for (const q of batch) {
|
|
1843
|
+
if (countAttempt) q.attempts += 1;
|
|
1844
|
+
if (q.attempts >= this.opts.maxAttempts) this.countDropped(1);
|
|
1845
|
+
else keep.push(q);
|
|
1846
|
+
}
|
|
1847
|
+
this.queue = [...keep, ...this.queue];
|
|
1848
|
+
while (this.queue.length > this.opts.maxItems) this.dropOldest();
|
|
1849
|
+
}
|
|
1850
|
+
};
|
|
1851
|
+
var beaconBuffer = new BeaconBuffer();
|
|
1500
1852
|
function sampled(rate, random = Math.random) {
|
|
1501
1853
|
if (rate <= 0) return false;
|
|
1502
1854
|
if (rate >= 1) return true;
|
|
@@ -1673,12 +2025,19 @@ function defaultOnDenied(result, _req, res) {
|
|
|
1673
2025
|
});
|
|
1674
2026
|
return;
|
|
1675
2027
|
}
|
|
1676
|
-
|
|
2028
|
+
if (isVerificationUnavailable(result)) {
|
|
2029
|
+
res.setHeader("X-Astra-Gateway-Mode", "enforced");
|
|
2030
|
+
res.setHeader(VERIFICATION_STATUS_HEADER, "unavailable");
|
|
2031
|
+
res.setHeader("Retry-After", String(unavailableRetryAfterSeconds(result)));
|
|
2032
|
+
res.status(503).json(buildVerificationUnavailableBody(result));
|
|
2033
|
+
return;
|
|
2034
|
+
}
|
|
2035
|
+
const { status: statusCode, code } = denyStatusFor(result, "INSUFFICIENT_ACCESS");
|
|
1677
2036
|
res.setHeader("X-Astra-Gateway-Mode", "enforced");
|
|
1678
2037
|
res.status(statusCode).json({
|
|
1679
2038
|
success: false,
|
|
1680
2039
|
error: {
|
|
1681
|
-
code
|
|
2040
|
+
code,
|
|
1682
2041
|
message: result.denialReasons?.[0] || "Access denied",
|
|
1683
2042
|
guidance: result.guidance,
|
|
1684
2043
|
// Round-10: aggregated per-dimension detail + correlation handle.
|
|
@@ -1926,6 +2285,21 @@ function createMiddleware(options) {
|
|
|
1926
2285
|
result.stepUpOutcome = outcome;
|
|
1927
2286
|
}
|
|
1928
2287
|
}
|
|
2288
|
+
if (!result.policyAllowed && isVerificationUnavailable(result) && (!shouldEnforce || isAnonymousRead(req.method, credentials))) {
|
|
2289
|
+
res.setHeader(VERIFICATION_STATUS_HEADER, "unavailable");
|
|
2290
|
+
return next();
|
|
2291
|
+
}
|
|
2292
|
+
if (!shouldEnforce) {
|
|
2293
|
+
if (config.setPassThroughHeader) {
|
|
2294
|
+
res.setHeader("X-Astra-Gateway-Mode", "enforced");
|
|
2295
|
+
res.setHeader("X-Astra-Gateway-Reason", "evaluated-not-enforced");
|
|
2296
|
+
}
|
|
2297
|
+
if (shouldRecordDecisions && sessionId) {
|
|
2298
|
+
recordDecision(config, sessionId, "granted").catch(() => {
|
|
2299
|
+
});
|
|
2300
|
+
}
|
|
2301
|
+
return next();
|
|
2302
|
+
}
|
|
1929
2303
|
if (!result.policyAllowed) {
|
|
1930
2304
|
if (shouldRecordDecisions && sessionId) {
|
|
1931
2305
|
recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
|
|
@@ -1945,17 +2319,6 @@ function createMiddleware(options) {
|
|
|
1945
2319
|
onDenied(result, req, res);
|
|
1946
2320
|
return;
|
|
1947
2321
|
}
|
|
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
2322
|
if (routeConfig.minTrustScore && result.agent) {
|
|
1960
2323
|
if (result.agent.trustScore < routeConfig.minTrustScore) {
|
|
1961
2324
|
const trustFailure = {
|
|
@@ -2011,14 +2374,6 @@ function createMiddleware(options) {
|
|
|
2011
2374
|
verifiedAt: /* @__PURE__ */ new Date(),
|
|
2012
2375
|
correlationId
|
|
2013
2376
|
};
|
|
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
2377
|
dedupeFailures(result);
|
|
2023
2378
|
return onDenied(result, req, res);
|
|
2024
2379
|
}
|