@astrasyncai/verification-gateway 5.11.0 → 5.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/adapters/express.d.mts +1 -1
- package/dist/adapters/express.d.ts +1 -1
- package/dist/adapters/express.js +394 -39
- package/dist/adapters/express.js.map +1 -1
- package/dist/adapters/express.mjs +394 -39
- package/dist/adapters/express.mjs.map +1 -1
- package/dist/adapters/mcp.d.mts +5 -1
- package/dist/adapters/mcp.d.ts +5 -1
- package/dist/adapters/mcp.js +193 -49
- package/dist/adapters/mcp.js.map +1 -1
- package/dist/adapters/mcp.mjs +193 -49
- package/dist/adapters/mcp.mjs.map +1 -1
- package/dist/adapters/nextjs.d.mts +1 -1
- package/dist/adapters/nextjs.d.ts +1 -1
- package/dist/adapters/nextjs.js +387 -22
- package/dist/adapters/nextjs.js.map +1 -1
- package/dist/adapters/nextjs.mjs +387 -22
- package/dist/adapters/nextjs.mjs.map +1 -1
- package/dist/adapters/sdk.d.mts +9 -1
- package/dist/adapters/sdk.d.ts +9 -1
- package/dist/adapters/sdk.js +125 -16
- package/dist/adapters/sdk.js.map +1 -1
- package/dist/adapters/sdk.mjs +125 -16
- package/dist/adapters/sdk.mjs.map +1 -1
- package/dist/agent/index.js +1 -1
- package/dist/agent/index.js.map +1 -1
- package/dist/agent/index.mjs +1 -1
- package/dist/agent/index.mjs.map +1 -1
- package/dist/bin/astrasync-claude-hook.js +309 -15
- package/dist/bin/astrasync-codex-hook.js +309 -15
- package/dist/bin/astrasync-guard.js +309 -15
- package/dist/bin/astrasync.js +322 -17
- package/dist/browser/background.js +309 -15
- package/dist/browser/background.js.map +1 -1
- package/dist/browser/background.mjs +309 -15
- package/dist/browser/background.mjs.map +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +1 -1
- package/dist/cli/index.mjs.map +1 -1
- package/dist/codex/index.js +309 -15
- package/dist/codex/index.js.map +1 -1
- package/dist/codex/index.mjs +309 -15
- package/dist/codex/index.mjs.map +1 -1
- package/dist/cursor/extension.js +309 -15
- package/dist/cursor/extension.js.map +1 -1
- package/dist/cursor/extension.mjs +309 -15
- package/dist/cursor/extension.mjs.map +1 -1
- package/dist/edge-config.d.mts +1 -1
- package/dist/edge-config.d.ts +1 -1
- package/dist/edge-config.js +1 -1
- package/dist/edge-config.js.map +1 -1
- package/dist/edge-config.mjs +1 -1
- package/dist/edge-config.mjs.map +1 -1
- package/dist/edge-core/index.d.mts +128 -13
- package/dist/edge-core/index.d.ts +128 -13
- package/dist/edge-core/index.js +362 -63
- package/dist/edge-core/index.js.map +1 -1
- package/dist/edge-core/index.mjs +359 -63
- package/dist/edge-core/index.mjs.map +1 -1
- package/dist/gateway/gateway.js +309 -15
- package/dist/gateway/gateway.js.map +1 -1
- package/dist/gateway/gateway.mjs +309 -15
- package/dist/gateway/gateway.mjs.map +1 -1
- package/dist/git-trigger/git-hooks.d.mts +1 -1
- package/dist/git-trigger/git-hooks.d.ts +1 -1
- package/dist/index.d.mts +118 -15
- package/dist/index.d.ts +118 -15
- package/dist/index.js +521 -72
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +515 -72
- package/dist/index.mjs.map +1 -1
- package/dist/platform-signatures.js +4 -1
- package/dist/platform-signatures.js.map +1 -1
- package/dist/platform-signatures.mjs +4 -1
- package/dist/platform-signatures.mjs.map +1 -1
- package/dist/registration/index.js +1 -1
- package/dist/registration/index.js.map +1 -1
- package/dist/registration/index.mjs +1 -1
- package/dist/registration/index.mjs.map +1 -1
- package/dist/transport/index.js +1 -1
- package/dist/transport/index.js.map +1 -1
- package/dist/transport/index.mjs +1 -1
- package/dist/transport/index.mjs.map +1 -1
- package/dist/{types-CTmUpNpS.d.mts → types-BCkwBA84.d.mts} +12 -0
- package/dist/{types-CTmUpNpS.d.ts → types-BCkwBA84.d.ts} +12 -0
- package/dist/{types-CtFXmDzm.d.ts → types-C_b4QTyO.d.ts} +18 -0
- package/dist/{types-DQrn_kei.d.mts → types-DlshIIR-.d.mts} +18 -0
- package/dist/ui/index.d.mts +1 -1
- package/dist/ui/index.d.ts +1 -1
- package/dist/verify.d.mts +35 -14
- package/dist/verify.d.ts +35 -14
- package/dist/verify.js +165 -25
- package/dist/verify.js.map +1 -1
- package/dist/verify.mjs +164 -25
- package/dist/verify.mjs.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -51,6 +51,7 @@ __export(src_exports, {
|
|
|
51
51
|
RegistrationTimeoutError: () => RegistrationTimeoutError,
|
|
52
52
|
SDK_USER_AGENT: () => SDK_USER_AGENT,
|
|
53
53
|
TRUST_LEVEL_RANGES: () => TRUST_LEVEL_RANGES,
|
|
54
|
+
VERIFICATION_STATUS_HEADER: () => VERIFICATION_STATUS_HEADER,
|
|
54
55
|
VERSION: () => SDK_VERSION,
|
|
55
56
|
_resetEdgeConfigCache: () => _resetEdgeConfigCache,
|
|
56
57
|
agent: () => agent_exports,
|
|
@@ -58,9 +59,11 @@ __export(src_exports, {
|
|
|
58
59
|
awaitStepUpApproval: () => awaitStepUpApproval,
|
|
59
60
|
buildGuidance: () => buildGuidance,
|
|
60
61
|
buildSdkObservedMetadata: () => buildSdkObservedMetadata,
|
|
62
|
+
buildVerificationUnavailableBody: () => buildVerificationUnavailableBody,
|
|
61
63
|
clearCache: () => clearCache,
|
|
62
64
|
createMcpMiddleware: () => createMcpMiddleware,
|
|
63
65
|
degradeToObserve: () => degradeToObserve,
|
|
66
|
+
denyStatusFor: () => denyStatusFor,
|
|
64
67
|
deriveConnectionFromHeaders: () => deriveConnectionFromHeaders,
|
|
65
68
|
detectPlatformFingerprint: () => detectPlatformFingerprint,
|
|
66
69
|
detectRuntime: () => detectRuntime,
|
|
@@ -77,6 +80,8 @@ __export(src_exports, {
|
|
|
77
80
|
getWellKnownUrls: () => getWellKnownUrls,
|
|
78
81
|
hasCredentials: () => hasCredentials,
|
|
79
82
|
isEdgeConfig: () => isEdgeConfig,
|
|
83
|
+
isRejectedIdentityClaim: () => isRejectedIdentityClaim,
|
|
84
|
+
isVerificationUnavailable: () => isVerificationUnavailable,
|
|
80
85
|
isVerifiedHopValidFor: () => isVerifiedHopValidFor,
|
|
81
86
|
matchEdgePathRule: () => matchEdgePathRule,
|
|
82
87
|
matchPlatformSignature: () => matchPlatformSignature,
|
|
@@ -96,6 +101,7 @@ __export(src_exports, {
|
|
|
96
101
|
serializeVerifiedHop: () => serializeVerifiedHop,
|
|
97
102
|
setMcpMeta: () => setMcpMeta,
|
|
98
103
|
transport: () => transport_exports,
|
|
104
|
+
unavailableRetryAfterSeconds: () => unavailableRetryAfterSeconds,
|
|
99
105
|
validateLocalPolicy: () => validatePolicy,
|
|
100
106
|
verify: () => verify
|
|
101
107
|
});
|
|
@@ -116,7 +122,7 @@ function getTrustLevel(score) {
|
|
|
116
122
|
}
|
|
117
123
|
|
|
118
124
|
// src/version.ts
|
|
119
|
-
var SDK_VERSION = "5.
|
|
125
|
+
var SDK_VERSION = "5.13.0";
|
|
120
126
|
|
|
121
127
|
// src/http.ts
|
|
122
128
|
var SDK_USER_AGENT = `astrasync-sdk/${SDK_VERSION}`;
|
|
@@ -195,6 +201,59 @@ function getCachedWellKnownUrls(apiBaseUrl) {
|
|
|
195
201
|
return cache.get(apiBaseUrl)?.data;
|
|
196
202
|
}
|
|
197
203
|
|
|
204
|
+
// src/failure-posture.ts
|
|
205
|
+
var VERIFICATION_STATUS_HEADER = "X-Astra-Verification";
|
|
206
|
+
var DEFAULT_UNAVAILABLE_RETRY_AFTER_S = 30;
|
|
207
|
+
var REJECTED_IDENTITY_DIMENSIONS = /* @__PURE__ */ new Set([
|
|
208
|
+
"identity.invalid_agent_id",
|
|
209
|
+
"identity.unknown_agent",
|
|
210
|
+
"agent.lookup"
|
|
211
|
+
]);
|
|
212
|
+
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.";
|
|
213
|
+
var UNAVAILABLE_DIMENSIONS = /* @__PURE__ */ new Set([
|
|
214
|
+
"verify_access.api_error",
|
|
215
|
+
"middleware.internal_error"
|
|
216
|
+
]);
|
|
217
|
+
function isVerificationUnavailable(result) {
|
|
218
|
+
if (result.misconfigured) return false;
|
|
219
|
+
return (result.failures ?? []).some((f) => UNAVAILABLE_DIMENSIONS.has(f.dimension));
|
|
220
|
+
}
|
|
221
|
+
function unavailableRetryAfterSeconds(result) {
|
|
222
|
+
if (typeof result.retryAfterMs === "number" && Number.isFinite(result.retryAfterMs)) {
|
|
223
|
+
return Math.max(1, Math.ceil(result.retryAfterMs / 1e3));
|
|
224
|
+
}
|
|
225
|
+
return DEFAULT_UNAVAILABLE_RETRY_AFTER_S;
|
|
226
|
+
}
|
|
227
|
+
function isAnonymousRead(method, credentials) {
|
|
228
|
+
const m = (method ?? "").toUpperCase();
|
|
229
|
+
if (m !== "GET" && m !== "HEAD") return false;
|
|
230
|
+
return !credentials.astraId && !credentials.jwt;
|
|
231
|
+
}
|
|
232
|
+
function buildVerificationUnavailableBody(result) {
|
|
233
|
+
return {
|
|
234
|
+
success: false,
|
|
235
|
+
error: {
|
|
236
|
+
code: "VERIFICATION_UNAVAILABLE",
|
|
237
|
+
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.",
|
|
238
|
+
retryable: true,
|
|
239
|
+
retryAfterSeconds: unavailableRetryAfterSeconds(result),
|
|
240
|
+
...result.failures && { failures: result.failures },
|
|
241
|
+
...result.correlationId && { correlationId: result.correlationId },
|
|
242
|
+
guidance: {
|
|
243
|
+
message: "Retry the request after the Retry-After interval, with exponential backoff; the condition is transient."
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function isRejectedIdentityClaim(result) {
|
|
249
|
+
return (result.failures ?? []).some((f) => REJECTED_IDENTITY_DIMENSIONS.has(f.dimension));
|
|
250
|
+
}
|
|
251
|
+
function denyStatusFor(result, policyDeniedCode) {
|
|
252
|
+
if (isRejectedIdentityClaim(result)) return { status: 403, code: "AGENT_IDENTITY_REJECTED" };
|
|
253
|
+
if (result.identityVerified) return { status: 403, code: policyDeniedCode };
|
|
254
|
+
return { status: 401, code: "UNAUTHORIZED" };
|
|
255
|
+
}
|
|
256
|
+
|
|
198
257
|
// src/verify.ts
|
|
199
258
|
var DEFAULT_CONFIG = {
|
|
200
259
|
apiBaseUrl: "https://astrasync.ai/api",
|
|
@@ -207,7 +266,7 @@ var DEFAULT_CONFIG = {
|
|
|
207
266
|
};
|
|
208
267
|
var initCheckPerformed = false;
|
|
209
268
|
var deprecationWarningShown = false;
|
|
210
|
-
async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey) {
|
|
269
|
+
async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey, counterparty) {
|
|
211
270
|
initCheckPerformed = true;
|
|
212
271
|
try {
|
|
213
272
|
const probeUrl = `${apiBaseUrl}/agents/verify-access`;
|
|
@@ -225,7 +284,8 @@ async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey) {
|
|
|
225
284
|
);
|
|
226
285
|
}
|
|
227
286
|
if (apiKey) {
|
|
228
|
-
const
|
|
287
|
+
const q = counterparty?.id ? `?counterpartyId=${encodeURIComponent(counterparty.id)}` : counterparty?.url ? `?counterpartyUrl=${encodeURIComponent(counterparty.url)}` : "";
|
|
288
|
+
const selfCheck = await sdkFetch(`${apiBaseUrl}/agents/verify-access/self-check${q}`, {
|
|
229
289
|
headers: { Authorization: `Bearer ${apiKey}` }
|
|
230
290
|
});
|
|
231
291
|
if (selfCheck.status === 401) {
|
|
@@ -237,6 +297,13 @@ async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey) {
|
|
|
237
297
|
console.error(message);
|
|
238
298
|
} else if (selfCheck.ok) {
|
|
239
299
|
const body = await selfCheck.json().catch(() => ({}));
|
|
300
|
+
if (body.counterparty && body.counterparty.canVerify === false) {
|
|
301
|
+
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.`;
|
|
302
|
+
if (strictInit) {
|
|
303
|
+
throw new Error(`${message} (strictInit=true)`);
|
|
304
|
+
}
|
|
305
|
+
console.error(message);
|
|
306
|
+
}
|
|
240
307
|
if (typeof body.daysToExpiry === "number" && body.daysToExpiry <= 7) {
|
|
241
308
|
console.warn(
|
|
242
309
|
`[VerificationGateway] the configured AstraSync API key expires in ${body.daysToExpiry} day(s) \u2014 rotate it at Settings \u2192 Developer Tools before integrations start failing.`
|
|
@@ -381,11 +448,11 @@ function createGuidanceResponse(_config, reason, options = {}) {
|
|
|
381
448
|
const isMisconfig = source === "merchant_misconfig";
|
|
382
449
|
const urls = options.urls;
|
|
383
450
|
const guidance = isMisconfig ? {
|
|
384
|
-
message: "This site's AstraSync integration is misconfigured (its API key was revoked, expired, or
|
|
451
|
+
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.",
|
|
385
452
|
registrationUrl: urls?.registrationUrl ?? "",
|
|
386
453
|
documentationUrl: options.remediation?.docsUrl ?? urls?.documentationUrl ?? "",
|
|
387
454
|
steps: [
|
|
388
|
-
"Report the failure to the site operator \u2014 their AstraSync API key needs
|
|
455
|
+
"Report the failure to the site operator \u2014 their AstraSync API key needs replacing",
|
|
389
456
|
"Do not retry: the failure is deterministic until the operator updates their key"
|
|
390
457
|
]
|
|
391
458
|
} : isApiError ? {
|
|
@@ -446,11 +513,44 @@ function createGuidanceResponse(_config, reason, options = {}) {
|
|
|
446
513
|
// coercing the absent recommendation to a policy 'deny'. Misconfig is
|
|
447
514
|
// deterministic and intentionally NOT marked retryable.
|
|
448
515
|
...isApiError && { retryable: true },
|
|
516
|
+
...options.httpStatus !== void 0 && { httpStatus: options.httpStatus },
|
|
517
|
+
...options.retryAfterMs !== void 0 && { retryAfterMs: options.retryAfterMs },
|
|
449
518
|
correlationId: options.correlationId,
|
|
450
519
|
attemptId: options.attemptId,
|
|
451
520
|
verifiedAt: /* @__PURE__ */ new Date()
|
|
452
521
|
};
|
|
453
522
|
}
|
|
523
|
+
async function readJsonSafe(response) {
|
|
524
|
+
try {
|
|
525
|
+
const parsed = await response.json();
|
|
526
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
527
|
+
} catch {
|
|
528
|
+
return {};
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
function rejectedIdentityGuidance(failures) {
|
|
532
|
+
const f = failures?.find((x) => REJECTED_IDENTITY_DIMENSIONS.has(x.dimension));
|
|
533
|
+
if (!f) return void 0;
|
|
534
|
+
if (f.dimension === "agent.lookup") {
|
|
535
|
+
return `The presented agent identifier is not registered with AstraSync. ${UNKNOWN_AGENT_GUIDANCE}`;
|
|
536
|
+
}
|
|
537
|
+
return f.guidance ? `${f.message} ${f.guidance}` : f.message;
|
|
538
|
+
}
|
|
539
|
+
var MAX_RETRY_AFTER_MS = 15 * 60 * 1e3;
|
|
540
|
+
function parseRetryAfterMs(response) {
|
|
541
|
+
const raw = response.headers?.get?.("retry-after");
|
|
542
|
+
if (!raw) return void 0;
|
|
543
|
+
const trimmed = raw.trim();
|
|
544
|
+
let ms;
|
|
545
|
+
if (/^\d+(\.\d+)?$/.test(trimmed)) {
|
|
546
|
+
ms = Number(trimmed) * 1e3;
|
|
547
|
+
} else {
|
|
548
|
+
const at = Date.parse(trimmed);
|
|
549
|
+
if (Number.isNaN(at)) return void 0;
|
|
550
|
+
ms = at - Date.now();
|
|
551
|
+
}
|
|
552
|
+
return Math.min(MAX_RETRY_AFTER_MS, Math.max(0, Math.round(ms)));
|
|
553
|
+
}
|
|
454
554
|
async function callVerifyAccessAPI(config, request) {
|
|
455
555
|
const { credentials, ...requestData } = request;
|
|
456
556
|
const body = {
|
|
@@ -507,7 +607,8 @@ async function callVerifyAccessAPI(config, request) {
|
|
|
507
607
|
body: JSON.stringify(body),
|
|
508
608
|
signal: AbortSignal.timeout(config.verifyTimeoutMs ?? 1e4)
|
|
509
609
|
});
|
|
510
|
-
const data = await response
|
|
610
|
+
const data = await readJsonSafe(response);
|
|
611
|
+
const retryAfterMs = parseRetryAfterMs(response);
|
|
511
612
|
if (response.status === 410) {
|
|
512
613
|
return {
|
|
513
614
|
success: true,
|
|
@@ -545,13 +646,31 @@ async function callVerifyAccessAPI(config, request) {
|
|
|
545
646
|
}
|
|
546
647
|
};
|
|
547
648
|
}
|
|
649
|
+
if (response.status === 404 && credentials.astraId && (data?.error === "AGENT_NOT_FOUND" || typeof data?.message === "string" && /agent/i.test(data.message))) {
|
|
650
|
+
return {
|
|
651
|
+
success: true,
|
|
652
|
+
attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0,
|
|
653
|
+
access: {
|
|
654
|
+
allowed: false,
|
|
655
|
+
reason: "unknown_agent",
|
|
656
|
+
failures: [
|
|
657
|
+
{
|
|
658
|
+
dimension: "identity.unknown_agent",
|
|
659
|
+
message: "The presented agent identifier is not registered with AstraSync.",
|
|
660
|
+
guidance: UNKNOWN_AGENT_GUIDANCE
|
|
661
|
+
}
|
|
662
|
+
]
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
}
|
|
548
666
|
if (!response.ok) {
|
|
549
|
-
|
|
667
|
+
const keyMismatch = response.status === 403 && data?.error === "COUNTERPARTY_KEY_MISMATCH";
|
|
668
|
+
if (response.status === 401 || keyMismatch) {
|
|
550
669
|
return {
|
|
551
670
|
success: false,
|
|
552
671
|
error: data.message || data.error || "Integration API key rejected",
|
|
553
672
|
misconfig: true,
|
|
554
|
-
misconfigCode: typeof data?.code === "string" ? data.code : void 0,
|
|
673
|
+
misconfigCode: keyMismatch ? "counterparty_key_mismatch" : typeof data?.code === "string" ? data.code : void 0,
|
|
555
674
|
remediation: data?.remediation && typeof data.remediation === "object" ? data.remediation : void 0,
|
|
556
675
|
correlationId: typeof data?.correlationId === "string" ? data.correlationId : void 0,
|
|
557
676
|
attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0
|
|
@@ -559,9 +678,11 @@ async function callVerifyAccessAPI(config, request) {
|
|
|
559
678
|
}
|
|
560
679
|
return {
|
|
561
680
|
success: false,
|
|
562
|
-
error: data.message || data.error || `API returned ${response.status}`,
|
|
681
|
+
error: typeof data?.message === "string" && data.message || typeof data?.error === "string" && data.error || `API returned ${response.status}`,
|
|
563
682
|
correlationId: typeof data?.correlationId === "string" ? data.correlationId : void 0,
|
|
564
|
-
attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0
|
|
683
|
+
attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0,
|
|
684
|
+
httpStatus: response.status,
|
|
685
|
+
...retryAfterMs !== void 0 && { retryAfterMs }
|
|
565
686
|
};
|
|
566
687
|
}
|
|
567
688
|
return data;
|
|
@@ -582,14 +703,22 @@ async function verify(config, request, options) {
|
|
|
582
703
|
mergedConfig.apiBaseUrl,
|
|
583
704
|
mergedConfig.debug,
|
|
584
705
|
true,
|
|
585
|
-
mergedConfig.apiKey
|
|
706
|
+
mergedConfig.apiKey,
|
|
707
|
+
{
|
|
708
|
+
id: mergedConfig.counterpartyId,
|
|
709
|
+
url: mergedConfig.counterpartyUrl ?? request.counterpartyUrl
|
|
710
|
+
}
|
|
586
711
|
);
|
|
587
712
|
} else {
|
|
588
713
|
void performInitCheck(
|
|
589
714
|
mergedConfig.apiBaseUrl,
|
|
590
715
|
mergedConfig.debug,
|
|
591
716
|
false,
|
|
592
|
-
mergedConfig.apiKey
|
|
717
|
+
mergedConfig.apiKey,
|
|
718
|
+
{
|
|
719
|
+
id: mergedConfig.counterpartyId,
|
|
720
|
+
url: mergedConfig.counterpartyUrl ?? request.counterpartyUrl
|
|
721
|
+
}
|
|
593
722
|
);
|
|
594
723
|
}
|
|
595
724
|
}
|
|
@@ -633,12 +762,14 @@ async function verify(config, request, options) {
|
|
|
633
762
|
correlationId: apiResponse.correlationId,
|
|
634
763
|
attemptId: apiResponse.attemptId,
|
|
635
764
|
remediation: apiResponse.remediation,
|
|
765
|
+
httpStatus: apiResponse.httpStatus,
|
|
766
|
+
retryAfterMs: apiResponse.retryAfterMs,
|
|
636
767
|
urls
|
|
637
768
|
});
|
|
638
769
|
}
|
|
639
770
|
if (!apiResponse.access?.allowed) {
|
|
640
771
|
const aggregatedFailures = apiResponse.access?.failures;
|
|
641
|
-
const idVerifiedFromBackend = apiResponse.verificationContext?.idVerified === true;
|
|
772
|
+
const idVerifiedFromBackend = apiResponse.verificationContext?.idVerified === true && !aggregatedFailures?.some((f) => REJECTED_IDENTITY_DIMENSIONS.has(f.dimension));
|
|
642
773
|
const result2 = {
|
|
643
774
|
identityVerified: idVerifiedFromBackend,
|
|
644
775
|
policyAllowed: false,
|
|
@@ -650,7 +781,10 @@ async function verify(config, request, options) {
|
|
|
650
781
|
requiresStepUp: apiResponse.access?.requiresStepUp,
|
|
651
782
|
requiresApproval: apiResponse.access?.requiresApproval,
|
|
652
783
|
guidance: {
|
|
653
|
-
|
|
784
|
+
// 5.12.0: a presented-but-rejected id (malformed / unknown) carries
|
|
785
|
+
// actionable registration guidance on its failure — surface it
|
|
786
|
+
// instead of the bare machine reason ('unknown_agent').
|
|
787
|
+
message: rejectedIdentityGuidance(aggregatedFailures) ?? (apiResponse.access?.reason || "Access denied by PDLSS policy"),
|
|
654
788
|
registrationUrl: urls?.registrationUrl ?? "",
|
|
655
789
|
documentationUrl: urls?.documentationUrl ?? "",
|
|
656
790
|
// 5.1.0: anonymous callers land on this branch — steer MCP-hosted
|
|
@@ -750,7 +884,7 @@ async function verify(config, request, options) {
|
|
|
750
884
|
registrationUrl: urls?.registrationUrl ?? "",
|
|
751
885
|
documentationUrl: urls?.documentationUrl ?? ""
|
|
752
886
|
};
|
|
753
|
-
} else if (result.recommendation === "step_up_required") {
|
|
887
|
+
} else if (result.recommendation === "step_up_required" && result.identityVerified) {
|
|
754
888
|
result.requiresStepUp = true;
|
|
755
889
|
result.denialReasons = serverDenialReasons ?? result.recommendationReasons ?? ["Step-up verification required"];
|
|
756
890
|
}
|
|
@@ -773,7 +907,7 @@ async function recordDecision(config, sessionId, decision, reason) {
|
|
|
773
907
|
}).catch(() => {
|
|
774
908
|
});
|
|
775
909
|
}
|
|
776
|
-
async function reportAttempt(config, report) {
|
|
910
|
+
async function reportAttempt(config, report, options = {}) {
|
|
777
911
|
const apiBaseUrl = config.apiBaseUrl || DEFAULT_CONFIG.apiBaseUrl;
|
|
778
912
|
const headers = { "Content-Type": "application/json" };
|
|
779
913
|
if (config.apiKey) {
|
|
@@ -784,13 +918,38 @@ async function reportAttempt(config, report) {
|
|
|
784
918
|
...report,
|
|
785
919
|
...!report.counterpartyId && config.counterpartyId && { counterpartyId: config.counterpartyId }
|
|
786
920
|
};
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
921
|
+
const url = `${apiBaseUrl}/agents/verify-access/attempt-report`;
|
|
922
|
+
const init = { method: "POST", headers, body: JSON.stringify(body) };
|
|
923
|
+
const fail = (failure) => {
|
|
924
|
+
try {
|
|
925
|
+
options.onFailure?.(failure);
|
|
926
|
+
} catch {
|
|
927
|
+
}
|
|
928
|
+
};
|
|
929
|
+
let response;
|
|
930
|
+
try {
|
|
931
|
+
response = await sdkFetch(url, init);
|
|
932
|
+
} catch (err) {
|
|
933
|
+
fail({ error: err instanceof Error ? err.message : String(err), retried: false });
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
if (response.ok) return;
|
|
937
|
+
if (response.status !== 429 && response.status < 500) {
|
|
938
|
+
fail({ status: response.status, retried: false });
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
const delayMs = Math.min(ATTEMPT_REPORT_MAX_RETRY_DELAY_MS, parseRetryAfterMs(response) ?? 500);
|
|
942
|
+
const timer = setTimeout(() => {
|
|
943
|
+
sdkFetch(url, init).then((retry) => {
|
|
944
|
+
if (!retry.ok) fail({ status: retry.status, retried: true });
|
|
945
|
+
}).catch((err) => {
|
|
946
|
+
fail({ error: err instanceof Error ? err.message : String(err), retried: true });
|
|
947
|
+
});
|
|
948
|
+
}, delayMs);
|
|
949
|
+
timer.unref?.();
|
|
793
950
|
}
|
|
951
|
+
var ATTEMPT_REPORT_MAX_RETRY_DELAY_MS = 2e3;
|
|
952
|
+
var refusedPolicyReads = /* @__PURE__ */ new Set();
|
|
794
953
|
async function fetchRoutes(config, counterpartyId) {
|
|
795
954
|
if (!counterpartyId) return null;
|
|
796
955
|
const headers = { "Content-Type": "application/json" };
|
|
@@ -803,7 +962,15 @@ async function fetchRoutes(config, counterpartyId) {
|
|
|
803
962
|
`${config.apiBaseUrl}/endpoints/${encodeURIComponent(counterpartyId)}/routes`,
|
|
804
963
|
{ method: "GET", headers }
|
|
805
964
|
);
|
|
806
|
-
if (!response.ok)
|
|
965
|
+
if (!response.ok) {
|
|
966
|
+
if ((response.status === 401 || response.status === 403 || response.status === 404) && !refusedPolicyReads.has(counterpartyId)) {
|
|
967
|
+
refusedPolicyReads.add(counterpartyId);
|
|
968
|
+
console.error(
|
|
969
|
+
`[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.`
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
return null;
|
|
973
|
+
}
|
|
807
974
|
const body = await response.json();
|
|
808
975
|
return body.data?.routes ?? [];
|
|
809
976
|
} catch {
|
|
@@ -820,7 +987,10 @@ async function reportUnregisteredAttempt(config, data) {
|
|
|
820
987
|
};
|
|
821
988
|
await sdkFetch(`${apiBaseUrl}/verification-activity/unregistered-attempt`, {
|
|
822
989
|
method: "POST",
|
|
823
|
-
headers: {
|
|
990
|
+
headers: {
|
|
991
|
+
"Content-Type": "application/json",
|
|
992
|
+
...config.apiKey && { Authorization: `Bearer ${config.apiKey}` }
|
|
993
|
+
},
|
|
824
994
|
body: JSON.stringify(body)
|
|
825
995
|
}).catch(() => {
|
|
826
996
|
});
|
|
@@ -842,7 +1012,11 @@ async function quickVerify(config, credentials) {
|
|
|
842
1012
|
return {
|
|
843
1013
|
identityVerified: result.identityVerified,
|
|
844
1014
|
policyAllowed: result.policyAllowed,
|
|
845
|
-
reason: result.denialReasons?.[0]
|
|
1015
|
+
reason: result.denialReasons?.[0],
|
|
1016
|
+
...isVerificationUnavailable(result) && {
|
|
1017
|
+
unavailable: true,
|
|
1018
|
+
retryAfterMs: unavailableRetryAfterSeconds(result) * 1e3
|
|
1019
|
+
}
|
|
846
1020
|
};
|
|
847
1021
|
}
|
|
848
1022
|
|
|
@@ -1590,7 +1764,10 @@ var PLATFORM_AGENT_SIGNATURES = [
|
|
|
1590
1764
|
{
|
|
1591
1765
|
vendor: "chatgpt",
|
|
1592
1766
|
displayName: "OpenAI ChatGPT",
|
|
1593
|
-
|
|
1767
|
+
// GPTBot (training crawler) and OAI-SearchBot (ChatGPT search) are
|
|
1768
|
+
// OpenAI's declared crawler UAs — `gpt-?\d` never matched them, so they
|
|
1769
|
+
// classified as unattributed crawlers (round-27.4 W5).
|
|
1770
|
+
uaPatterns: [/chatgpt/i, /openai/i, /gpt-?\d/i, /\bGPTBot\b/i, /\bOAI-SearchBot\b/i],
|
|
1594
1771
|
agentCardPatterns: [/openai\.com/i, /chatgpt\.com/i],
|
|
1595
1772
|
apiKeyFormatPatterns: [/^sk-proj/i, /^sk-svcacct/i],
|
|
1596
1773
|
headerPatterns: [/^openai-organization/i, /^openai-project/i, /^openai-version/i]
|
|
@@ -1725,6 +1902,215 @@ var BROWSER_UA_PATTERN = /mozilla\/\d/i;
|
|
|
1725
1902
|
var _patterns = { STATIC_EXTENSIONS, BOT_UA_PATTERN, BROWSER_UA_PATTERN, SELF_UA_PATTERN };
|
|
1726
1903
|
|
|
1727
1904
|
// src/edge-core/events.ts
|
|
1905
|
+
async function withBudget(promise, ms) {
|
|
1906
|
+
let timer;
|
|
1907
|
+
try {
|
|
1908
|
+
return await Promise.race([
|
|
1909
|
+
promise,
|
|
1910
|
+
new Promise((resolve) => {
|
|
1911
|
+
timer = setTimeout(() => resolve(void 0), ms);
|
|
1912
|
+
})
|
|
1913
|
+
]);
|
|
1914
|
+
} catch {
|
|
1915
|
+
return void 0;
|
|
1916
|
+
} finally {
|
|
1917
|
+
if (timer) clearTimeout(timer);
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
var destKeyOf = (d) => `${d.apiBaseUrl}\0${d.apiKey ?? ""}\0${d.counterparty}\0${d.adapter ?? ""}`;
|
|
1921
|
+
var BeaconBuffer = class {
|
|
1922
|
+
constructor(options = {}) {
|
|
1923
|
+
this.queue = [];
|
|
1924
|
+
this.droppedSinceLast = 0;
|
|
1925
|
+
this.droppedTotal = 0;
|
|
1926
|
+
this.deliveredTotal = 0;
|
|
1927
|
+
this.backoffUntil = 0;
|
|
1928
|
+
this.collapse = /* @__PURE__ */ new Map();
|
|
1929
|
+
/** Keys the backend answered 401 for — later batches go unauthenticated. */
|
|
1930
|
+
this.rejectedKeys = /* @__PURE__ */ new Set();
|
|
1931
|
+
this.opts = {
|
|
1932
|
+
maxItems: options.maxItems ?? 1e3,
|
|
1933
|
+
maxBatch: options.maxBatch ?? 100,
|
|
1934
|
+
maxAttempts: options.maxAttempts ?? 3,
|
|
1935
|
+
collapseWindowMs: options.collapseWindowMs ?? 6e4,
|
|
1936
|
+
collapseMaxKeys: options.collapseMaxKeys ?? 1e3,
|
|
1937
|
+
requestTimeoutMs: options.requestTimeoutMs ?? 1e4,
|
|
1938
|
+
retryBackoffMs: options.retryBackoffMs ?? 1e3,
|
|
1939
|
+
now: options.now ?? Date.now
|
|
1940
|
+
};
|
|
1941
|
+
}
|
|
1942
|
+
/**
|
|
1943
|
+
* Queue one beacon. With a `collapseKey`, a repeat inside the window is
|
|
1944
|
+
* merged into a held aggregate (sent when the window closes) instead of
|
|
1945
|
+
* becoming its own item.
|
|
1946
|
+
*/
|
|
1947
|
+
enqueue(dest, item, collapseKey) {
|
|
1948
|
+
const now = this.opts.now();
|
|
1949
|
+
if (collapseKey) {
|
|
1950
|
+
const entry = this.collapse.get(collapseKey);
|
|
1951
|
+
if (entry && now - entry.firstSeenAt < this.opts.collapseWindowMs) {
|
|
1952
|
+
for (const target of [entry.first, entry.held]) {
|
|
1953
|
+
if (target && !target.inFlight && this.queue.includes(target)) {
|
|
1954
|
+
target.item.repeatCount = (target.item.repeatCount ?? 1) + 1;
|
|
1955
|
+
return;
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
const held = this.push(
|
|
1959
|
+
dest,
|
|
1960
|
+
{ ...item, repeatCount: 1 },
|
|
1961
|
+
entry.firstSeenAt + this.opts.collapseWindowMs
|
|
1962
|
+
);
|
|
1963
|
+
entry.held = held;
|
|
1964
|
+
return;
|
|
1965
|
+
}
|
|
1966
|
+
this.collapse.delete(collapseKey);
|
|
1967
|
+
if (this.collapse.size >= this.opts.collapseMaxKeys) {
|
|
1968
|
+
const oldest = this.collapse.keys().next().value;
|
|
1969
|
+
if (oldest !== void 0) this.collapse.delete(oldest);
|
|
1970
|
+
}
|
|
1971
|
+
const fresh = { firstSeenAt: now };
|
|
1972
|
+
this.collapse.set(collapseKey, fresh);
|
|
1973
|
+
fresh.first = this.push(dest, item, now);
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1976
|
+
this.push(dest, item, now);
|
|
1977
|
+
}
|
|
1978
|
+
/**
|
|
1979
|
+
* Deliver every eligible queued item. Concurrent callers share ONE
|
|
1980
|
+
* in-flight drain (it survives the caller's budget and a Lambda freeze),
|
|
1981
|
+
* so nothing in flight is ever sent twice; items queued while it runs are
|
|
1982
|
+
* picked up by the same drain.
|
|
1983
|
+
*/
|
|
1984
|
+
flush() {
|
|
1985
|
+
if (!this.inFlight) {
|
|
1986
|
+
this.inFlight = this.drain().finally(() => {
|
|
1987
|
+
this.inFlight = void 0;
|
|
1988
|
+
});
|
|
1989
|
+
}
|
|
1990
|
+
return this.inFlight;
|
|
1991
|
+
}
|
|
1992
|
+
/** Counters for tests and diagnostics. */
|
|
1993
|
+
stats() {
|
|
1994
|
+
return {
|
|
1995
|
+
queueDepth: this.queue.length,
|
|
1996
|
+
droppedSinceLast: this.droppedSinceLast,
|
|
1997
|
+
droppedTotal: this.droppedTotal,
|
|
1998
|
+
deliveredTotal: this.deliveredTotal,
|
|
1999
|
+
flushing: this.inFlight !== void 0
|
|
2000
|
+
};
|
|
2001
|
+
}
|
|
2002
|
+
push(dest, item, eligibleAt) {
|
|
2003
|
+
while (this.queue.length >= this.opts.maxItems) this.dropOldest();
|
|
2004
|
+
const entry = {
|
|
2005
|
+
dest,
|
|
2006
|
+
destKey: destKeyOf(dest),
|
|
2007
|
+
item,
|
|
2008
|
+
attempts: 0,
|
|
2009
|
+
eligibleAt,
|
|
2010
|
+
inFlight: false
|
|
2011
|
+
};
|
|
2012
|
+
this.queue.push(entry);
|
|
2013
|
+
return entry;
|
|
2014
|
+
}
|
|
2015
|
+
dropOldest() {
|
|
2016
|
+
if (this.queue.shift()) this.countDropped(1);
|
|
2017
|
+
}
|
|
2018
|
+
countDropped(n) {
|
|
2019
|
+
this.droppedSinceLast += n;
|
|
2020
|
+
this.droppedTotal += n;
|
|
2021
|
+
}
|
|
2022
|
+
async drain() {
|
|
2023
|
+
for (; ; ) {
|
|
2024
|
+
const now = this.opts.now();
|
|
2025
|
+
if (now < this.backoffUntil) return;
|
|
2026
|
+
const batch = this.takeBatch(now);
|
|
2027
|
+
if (batch.length === 0) return;
|
|
2028
|
+
const ok = await this.send(batch);
|
|
2029
|
+
if (!ok) return;
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
/** Oldest eligible item + up to maxBatch-1 more for the same destination. */
|
|
2033
|
+
takeBatch(now) {
|
|
2034
|
+
const first = this.queue.find((q) => q.eligibleAt <= now);
|
|
2035
|
+
if (!first) return [];
|
|
2036
|
+
const batch = [];
|
|
2037
|
+
const rest = [];
|
|
2038
|
+
for (const q of this.queue) {
|
|
2039
|
+
if (batch.length < this.opts.maxBatch && q.destKey === first.destKey && q.eligibleAt <= now) {
|
|
2040
|
+
q.inFlight = true;
|
|
2041
|
+
batch.push(q);
|
|
2042
|
+
} else {
|
|
2043
|
+
rest.push(q);
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
this.queue = rest;
|
|
2047
|
+
return batch;
|
|
2048
|
+
}
|
|
2049
|
+
/** POST one batch. Returns false when the drain should pause (backoff). */
|
|
2050
|
+
async send(batch) {
|
|
2051
|
+
const dest = batch[0].dest;
|
|
2052
|
+
const useKey = dest.apiKey && !this.rejectedKeys.has(dest.apiKey) ? dest.apiKey : void 0;
|
|
2053
|
+
const reportedDropped = this.droppedSinceLast;
|
|
2054
|
+
const meta = {
|
|
2055
|
+
droppedSinceLast: reportedDropped,
|
|
2056
|
+
queueDepth: this.queue.length,
|
|
2057
|
+
adapter: dest.adapter ?? "edge-core",
|
|
2058
|
+
version: SDK_VERSION
|
|
2059
|
+
};
|
|
2060
|
+
const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
|
|
2061
|
+
const response = await withBudget(
|
|
2062
|
+
sdkFetch(`${dest.apiBaseUrl}/verification-activity/edge-events`, {
|
|
2063
|
+
method: "POST",
|
|
2064
|
+
headers: {
|
|
2065
|
+
"Content-Type": "application/json",
|
|
2066
|
+
...useKey && { Authorization: `Bearer ${useKey}` }
|
|
2067
|
+
},
|
|
2068
|
+
body: JSON.stringify({ events: batch.map((q) => q.item), meta }),
|
|
2069
|
+
...controller && { signal: controller.signal }
|
|
2070
|
+
}),
|
|
2071
|
+
this.opts.requestTimeoutMs
|
|
2072
|
+
);
|
|
2073
|
+
if (!response) controller?.abort();
|
|
2074
|
+
const status = response?.status;
|
|
2075
|
+
for (const q of batch) q.inFlight = false;
|
|
2076
|
+
if (status !== void 0 && status >= 200 && status < 300) {
|
|
2077
|
+
this.droppedSinceLast = Math.max(0, this.droppedSinceLast - reportedDropped);
|
|
2078
|
+
this.deliveredTotal += batch.length;
|
|
2079
|
+
return true;
|
|
2080
|
+
}
|
|
2081
|
+
if (status === 401 && useKey) {
|
|
2082
|
+
this.rejectedKeys.add(useKey);
|
|
2083
|
+
this.requeue(batch, false);
|
|
2084
|
+
return true;
|
|
2085
|
+
}
|
|
2086
|
+
if (status === 429) {
|
|
2087
|
+
const retryAfter = Number(response?.headers.get("retry-after"));
|
|
2088
|
+
const waitMs = Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(retryAfter, 120) * 1e3 : 3e4;
|
|
2089
|
+
this.backoffUntil = this.opts.now() + waitMs;
|
|
2090
|
+
this.requeue(batch, false);
|
|
2091
|
+
return false;
|
|
2092
|
+
}
|
|
2093
|
+
if (status !== void 0 && status >= 400 && status < 500) {
|
|
2094
|
+
this.countDropped(batch.length);
|
|
2095
|
+
return true;
|
|
2096
|
+
}
|
|
2097
|
+
this.backoffUntil = this.opts.now() + this.opts.retryBackoffMs;
|
|
2098
|
+
this.requeue(batch, true);
|
|
2099
|
+
return false;
|
|
2100
|
+
}
|
|
2101
|
+
/** Put a failed batch back at the head of the queue (order preserved). */
|
|
2102
|
+
requeue(batch, countAttempt) {
|
|
2103
|
+
const keep = [];
|
|
2104
|
+
for (const q of batch) {
|
|
2105
|
+
if (countAttempt) q.attempts += 1;
|
|
2106
|
+
if (q.attempts >= this.opts.maxAttempts) this.countDropped(1);
|
|
2107
|
+
else keep.push(q);
|
|
2108
|
+
}
|
|
2109
|
+
this.queue = [...keep, ...this.queue];
|
|
2110
|
+
while (this.queue.length > this.opts.maxItems) this.dropOldest();
|
|
2111
|
+
}
|
|
2112
|
+
};
|
|
2113
|
+
var beaconBuffer = new BeaconBuffer();
|
|
1728
2114
|
function sampled(rate, random = Math.random) {
|
|
1729
2115
|
if (rate <= 0) return false;
|
|
1730
2116
|
if (rate >= 1) return true;
|
|
@@ -1901,12 +2287,19 @@ function defaultOnDenied(result, _req, res) {
|
|
|
1901
2287
|
});
|
|
1902
2288
|
return;
|
|
1903
2289
|
}
|
|
1904
|
-
|
|
2290
|
+
if (isVerificationUnavailable(result)) {
|
|
2291
|
+
res.setHeader("X-Astra-Gateway-Mode", "enforced");
|
|
2292
|
+
res.setHeader(VERIFICATION_STATUS_HEADER, "unavailable");
|
|
2293
|
+
res.setHeader("Retry-After", String(unavailableRetryAfterSeconds(result)));
|
|
2294
|
+
res.status(503).json(buildVerificationUnavailableBody(result));
|
|
2295
|
+
return;
|
|
2296
|
+
}
|
|
2297
|
+
const { status: statusCode, code } = denyStatusFor(result, "INSUFFICIENT_ACCESS");
|
|
1905
2298
|
res.setHeader("X-Astra-Gateway-Mode", "enforced");
|
|
1906
2299
|
res.status(statusCode).json({
|
|
1907
2300
|
success: false,
|
|
1908
2301
|
error: {
|
|
1909
|
-
code
|
|
2302
|
+
code,
|
|
1910
2303
|
message: result.denialReasons?.[0] || "Access denied",
|
|
1911
2304
|
guidance: result.guidance,
|
|
1912
2305
|
// Round-10: aggregated per-dimension detail + correlation handle.
|
|
@@ -2154,6 +2547,21 @@ function createMiddleware(options) {
|
|
|
2154
2547
|
result.stepUpOutcome = outcome;
|
|
2155
2548
|
}
|
|
2156
2549
|
}
|
|
2550
|
+
if (!result.policyAllowed && isVerificationUnavailable(result) && (!shouldEnforce || isAnonymousRead(req.method, credentials))) {
|
|
2551
|
+
res.setHeader(VERIFICATION_STATUS_HEADER, "unavailable");
|
|
2552
|
+
return next();
|
|
2553
|
+
}
|
|
2554
|
+
if (!shouldEnforce) {
|
|
2555
|
+
if (config.setPassThroughHeader) {
|
|
2556
|
+
res.setHeader("X-Astra-Gateway-Mode", "enforced");
|
|
2557
|
+
res.setHeader("X-Astra-Gateway-Reason", "evaluated-not-enforced");
|
|
2558
|
+
}
|
|
2559
|
+
if (shouldRecordDecisions && sessionId) {
|
|
2560
|
+
recordDecision(config, sessionId, "granted").catch(() => {
|
|
2561
|
+
});
|
|
2562
|
+
}
|
|
2563
|
+
return next();
|
|
2564
|
+
}
|
|
2157
2565
|
if (!result.policyAllowed) {
|
|
2158
2566
|
if (shouldRecordDecisions && sessionId) {
|
|
2159
2567
|
recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
|
|
@@ -2173,17 +2581,6 @@ function createMiddleware(options) {
|
|
|
2173
2581
|
onDenied(result, req, res);
|
|
2174
2582
|
return;
|
|
2175
2583
|
}
|
|
2176
|
-
if (!shouldEnforce) {
|
|
2177
|
-
if (config.setPassThroughHeader) {
|
|
2178
|
-
res.setHeader("X-Astra-Gateway-Mode", "enforced");
|
|
2179
|
-
res.setHeader("X-Astra-Gateway-Reason", "evaluated-not-enforced");
|
|
2180
|
-
}
|
|
2181
|
-
if (shouldRecordDecisions && sessionId) {
|
|
2182
|
-
recordDecision(config, sessionId, "granted").catch(() => {
|
|
2183
|
-
});
|
|
2184
|
-
}
|
|
2185
|
-
return next();
|
|
2186
|
-
}
|
|
2187
2584
|
if (routeConfig.minTrustScore && result.agent) {
|
|
2188
2585
|
if (result.agent.trustScore < routeConfig.minTrustScore) {
|
|
2189
2586
|
const trustFailure = {
|
|
@@ -2239,14 +2636,6 @@ function createMiddleware(options) {
|
|
|
2239
2636
|
verifiedAt: /* @__PURE__ */ new Date(),
|
|
2240
2637
|
correlationId
|
|
2241
2638
|
};
|
|
2242
|
-
const catchUrls = config.apiBaseUrl ? await getWellKnownUrls(config.apiBaseUrl).catch(() => void 0) : void 0;
|
|
2243
|
-
if (catchUrls) {
|
|
2244
|
-
result.guidance = {
|
|
2245
|
-
message: `Middleware threw ${errorClass} \u2014 failing closed`,
|
|
2246
|
-
registrationUrl: catchUrls.registrationUrl,
|
|
2247
|
-
documentationUrl: catchUrls.documentationUrl
|
|
2248
|
-
};
|
|
2249
|
-
}
|
|
2250
2639
|
dedupeFailures(result);
|
|
2251
2640
|
return onDenied(result, req, res);
|
|
2252
2641
|
}
|
|
@@ -2666,6 +3055,20 @@ function createMiddleware2(options) {
|
|
|
2666
3055
|
}
|
|
2667
3056
|
const approvalRequired = result.policyAllowed && requiresHumanApproval(result);
|
|
2668
3057
|
if (approvalRequired) annotateApprovalRequired(result);
|
|
3058
|
+
if (!result.policyAllowed && isVerificationUnavailable(result)) {
|
|
3059
|
+
if (isAnonymousRead(request.method, credentials)) {
|
|
3060
|
+
const passThrough = NextResponse.next();
|
|
3061
|
+
passThrough.headers.set(VERIFICATION_STATUS_HEADER, "unavailable");
|
|
3062
|
+
return passThrough;
|
|
3063
|
+
}
|
|
3064
|
+
return NextResponse.json(buildVerificationUnavailableBody(result), {
|
|
3065
|
+
status: 503,
|
|
3066
|
+
headers: {
|
|
3067
|
+
"Retry-After": String(unavailableRetryAfterSeconds(result)),
|
|
3068
|
+
[VERIFICATION_STATUS_HEADER]: "unavailable"
|
|
3069
|
+
}
|
|
3070
|
+
});
|
|
3071
|
+
}
|
|
2669
3072
|
if (!result.policyAllowed || approvalRequired) {
|
|
2670
3073
|
if (result.misconfigured && pathname.startsWith("/api/")) {
|
|
2671
3074
|
return NextResponse.json(
|
|
@@ -2683,13 +3086,12 @@ function createMiddleware2(options) {
|
|
|
2683
3086
|
);
|
|
2684
3087
|
}
|
|
2685
3088
|
if (pathname.startsWith("/api/")) {
|
|
3089
|
+
const denyStatus = denyStatusFor(result, "POLICY_DENIED");
|
|
2686
3090
|
return NextResponse.json(
|
|
2687
3091
|
{
|
|
2688
3092
|
success: false,
|
|
2689
3093
|
error: {
|
|
2690
|
-
|
|
2691
|
-
// OK, policy denied (update PDLSS / step up).
|
|
2692
|
-
code: !result.identityVerified ? "UNAUTHORIZED" : "POLICY_DENIED",
|
|
3094
|
+
code: denyStatus.code,
|
|
2693
3095
|
message: result.denialReasons?.[0] || "Access denied",
|
|
2694
3096
|
guidance: result.guidance,
|
|
2695
3097
|
failures: result.failures,
|
|
@@ -2699,7 +3101,7 @@ function createMiddleware2(options) {
|
|
|
2699
3101
|
}
|
|
2700
3102
|
},
|
|
2701
3103
|
{
|
|
2702
|
-
status:
|
|
3104
|
+
status: denyStatus.status,
|
|
2703
3105
|
...stepUpHoldOutcome && {
|
|
2704
3106
|
headers: { "X-Astra-Step-Up": stepUpHoldOutcome }
|
|
2705
3107
|
}
|
|
@@ -2826,6 +3228,12 @@ var VerificationGatewayClient = class {
|
|
|
2826
3228
|
* fulfil against it only once the order is settled / pending_merchant. An
|
|
2827
3229
|
* explicit `buyerEmail` (here or on the bridge handoff body) wins over the
|
|
2828
3230
|
* account default.
|
|
3231
|
+
*
|
|
3232
|
+
* 5.12.0 approval binding: pass the bridge handoff body's `attemptId` (and
|
|
3233
|
+
* the same `checkoutSessionId`) so a step-up approval minted for this cart
|
|
3234
|
+
* is matched to — and only to — this confirm. A held cart's approval is
|
|
3235
|
+
* bound to its checkout session and exact value; a confirm for any other
|
|
3236
|
+
* cart is held for its own approval.
|
|
2829
3237
|
*/
|
|
2830
3238
|
async confirmCheckout(options) {
|
|
2831
3239
|
const credentials = {
|
|
@@ -2845,6 +3253,7 @@ var VerificationGatewayClient = class {
|
|
|
2845
3253
|
commercePhase: "confirm",
|
|
2846
3254
|
checkoutSessionId: options.checkoutSessionId,
|
|
2847
3255
|
checkoutItems: options.checkoutItems,
|
|
3256
|
+
...options.attemptId && { attemptId: options.attemptId },
|
|
2848
3257
|
buyerEmail: options.buyerEmail,
|
|
2849
3258
|
counterpartyUrl: options.counterpartyUrl,
|
|
2850
3259
|
counterpartyType: options.counterpartyType
|
|
@@ -5645,8 +6054,37 @@ function dedupeFailures2(result) {
|
|
|
5645
6054
|
result.denialReasons = [...new Set(result.denialReasons)];
|
|
5646
6055
|
}
|
|
5647
6056
|
}
|
|
6057
|
+
function isReadOnlyMcpMethod(parsed) {
|
|
6058
|
+
return parsed.method !== "tools/call";
|
|
6059
|
+
}
|
|
5648
6060
|
function defaultMcpDenied(result, req, res) {
|
|
5649
6061
|
const id = req.body?.id ?? null;
|
|
6062
|
+
if (isVerificationUnavailable(result)) {
|
|
6063
|
+
const retryAfterSeconds = unavailableRetryAfterSeconds(result);
|
|
6064
|
+
const body = buildVerificationUnavailableBody(result).error;
|
|
6065
|
+
res.setHeader("X-Astra-Gateway-Mode", "enforced");
|
|
6066
|
+
res.setHeader(VERIFICATION_STATUS_HEADER, "unavailable");
|
|
6067
|
+
res.setHeader("Retry-After", String(retryAfterSeconds));
|
|
6068
|
+
res.status(503).json({
|
|
6069
|
+
jsonrpc: "2.0",
|
|
6070
|
+
id,
|
|
6071
|
+
error: {
|
|
6072
|
+
// -32003 → verification temporarily unavailable (retryable)
|
|
6073
|
+
code: -32003,
|
|
6074
|
+
message: body.message,
|
|
6075
|
+
data: {
|
|
6076
|
+
code: body.code,
|
|
6077
|
+
recommendation: "error",
|
|
6078
|
+
retryable: true,
|
|
6079
|
+
retryAfterSeconds,
|
|
6080
|
+
guidance: body.guidance,
|
|
6081
|
+
failures: result.failures,
|
|
6082
|
+
correlationId: result.correlationId
|
|
6083
|
+
}
|
|
6084
|
+
}
|
|
6085
|
+
});
|
|
6086
|
+
return;
|
|
6087
|
+
}
|
|
5650
6088
|
if (result.misconfigured) {
|
|
5651
6089
|
res.setHeader("X-Astra-Gateway-Mode", "enforced");
|
|
5652
6090
|
res.status(503).json({
|
|
@@ -5665,7 +6103,7 @@ function defaultMcpDenied(result, req, res) {
|
|
|
5665
6103
|
});
|
|
5666
6104
|
return;
|
|
5667
6105
|
}
|
|
5668
|
-
const status
|
|
6106
|
+
const { status, code: denyCode } = denyStatusFor(result, "INSUFFICIENT_ACCESS");
|
|
5669
6107
|
res.setHeader("X-Astra-Gateway-Mode", "enforced");
|
|
5670
6108
|
res.status(status).json({
|
|
5671
6109
|
jsonrpc: "2.0",
|
|
@@ -5675,9 +6113,11 @@ function defaultMcpDenied(result, req, res) {
|
|
|
5675
6113
|
// -32000 → unauthorized (no identity resolved)
|
|
5676
6114
|
// -32001 → insufficient access (identity OK, policy denied)
|
|
5677
6115
|
// -32002 → merchant verification misconfigured (handled above)
|
|
6116
|
+
// -32003 → verification temporarily unavailable (handled above)
|
|
5678
6117
|
code: !result.identityVerified ? -32e3 : -32001,
|
|
5679
6118
|
message: result.denialReasons?.[0] ?? "Access denied",
|
|
5680
6119
|
data: {
|
|
6120
|
+
code: denyCode,
|
|
5681
6121
|
guidance: result.guidance,
|
|
5682
6122
|
// Round-10: aggregated per-dimension detail + correlation handle.
|
|
5683
6123
|
failures: result.failures,
|
|
@@ -5805,6 +6245,9 @@ function createMcpMiddleware(options) {
|
|
|
5805
6245
|
resolved_action: pdlss.action
|
|
5806
6246
|
});
|
|
5807
6247
|
}
|
|
6248
|
+
if (!pdlss.purpose && !shouldEnforce) {
|
|
6249
|
+
return next();
|
|
6250
|
+
}
|
|
5808
6251
|
if (!pdlss.purpose) {
|
|
5809
6252
|
const id = req.body?.id ?? null;
|
|
5810
6253
|
res.status(400).json({
|
|
@@ -5824,6 +6267,9 @@ function createMcpMiddleware(options) {
|
|
|
5824
6267
|
}
|
|
5825
6268
|
const counterpartyUrl = config.counterpartyUrl || `${req.protocol}://${req.get("host")}${req.path}`;
|
|
5826
6269
|
const shouldRecordDecisions = recordDecisions !== false;
|
|
6270
|
+
const forwardedFor = req.headers["x-forwarded-for"];
|
|
6271
|
+
const forwardedForStr = Array.isArray(forwardedFor) ? forwardedFor.join(", ") : forwardedFor;
|
|
6272
|
+
const sourceIp = req.ip ?? (forwardedForStr ? forwardedForStr.split(",")[0].trim() : void 0);
|
|
5827
6273
|
const verificationRequest = {
|
|
5828
6274
|
credentials,
|
|
5829
6275
|
purpose: pdlss.purpose,
|
|
@@ -5838,9 +6284,10 @@ function createMcpMiddleware(options) {
|
|
|
5838
6284
|
counterpartyType: config.counterpartyType || "mcp_server",
|
|
5839
6285
|
enableRuntimeChallenge,
|
|
5840
6286
|
callerMetadata: {
|
|
5841
|
-
sourceIp
|
|
6287
|
+
sourceIp,
|
|
5842
6288
|
userAgent: req.headers["user-agent"],
|
|
5843
6289
|
host: req.headers.host,
|
|
6290
|
+
...forwardedForStr && { forwardedFor: forwardedForStr },
|
|
5844
6291
|
// Capture EVERY inbound header (parity with the express adapter —
|
|
5845
6292
|
// MCP-only merchants have no edge to do it upstream) plus the
|
|
5846
6293
|
// CDN-derived connection block. Local string work, secrets
|
|
@@ -5863,6 +6310,21 @@ function createMcpMiddleware(options) {
|
|
|
5863
6310
|
result.stepUpOutcome = outcome;
|
|
5864
6311
|
}
|
|
5865
6312
|
}
|
|
6313
|
+
if (!result.policyAllowed && isVerificationUnavailable(result) && (!shouldEnforce || isReadOnlyMcpMethod(parsed) && !credentials.astraId && !credentials.jwt)) {
|
|
6314
|
+
res.setHeader(VERIFICATION_STATUS_HEADER, "unavailable");
|
|
6315
|
+
return next();
|
|
6316
|
+
}
|
|
6317
|
+
if (!shouldEnforce) {
|
|
6318
|
+
if (config.setPassThroughHeader) {
|
|
6319
|
+
res.setHeader("X-Astra-Gateway-Mode", "enforced");
|
|
6320
|
+
res.setHeader("X-Astra-Gateway-Reason", "evaluated-not-enforced");
|
|
6321
|
+
}
|
|
6322
|
+
if (shouldRecordDecisions && sessionId) {
|
|
6323
|
+
recordDecision(config, sessionId, "granted").catch(() => {
|
|
6324
|
+
});
|
|
6325
|
+
}
|
|
6326
|
+
return next();
|
|
6327
|
+
}
|
|
5866
6328
|
if (!result.policyAllowed) {
|
|
5867
6329
|
if (shouldRecordDecisions && sessionId) {
|
|
5868
6330
|
recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
|
|
@@ -5882,17 +6344,6 @@ function createMcpMiddleware(options) {
|
|
|
5882
6344
|
onDenied(result, req, res);
|
|
5883
6345
|
return;
|
|
5884
6346
|
}
|
|
5885
|
-
if (!shouldEnforce) {
|
|
5886
|
-
if (config.setPassThroughHeader) {
|
|
5887
|
-
res.setHeader("X-Astra-Gateway-Mode", "enforced");
|
|
5888
|
-
res.setHeader("X-Astra-Gateway-Reason", "evaluated-not-enforced");
|
|
5889
|
-
}
|
|
5890
|
-
if (shouldRecordDecisions && sessionId) {
|
|
5891
|
-
recordDecision(config, sessionId, "granted").catch(() => {
|
|
5892
|
-
});
|
|
5893
|
-
}
|
|
5894
|
-
return next();
|
|
5895
|
-
}
|
|
5896
6347
|
if (effectiveAstraId) {
|
|
5897
6348
|
res.setHeader(
|
|
5898
6349
|
MCP_VERIFIED_HOP_HEADER,
|
|
@@ -5933,14 +6384,6 @@ function createMcpMiddleware(options) {
|
|
|
5933
6384
|
verifiedAt: /* @__PURE__ */ new Date(),
|
|
5934
6385
|
correlationId
|
|
5935
6386
|
};
|
|
5936
|
-
const catchUrls = config.apiBaseUrl ? await getWellKnownUrls(config.apiBaseUrl).catch(() => void 0) : void 0;
|
|
5937
|
-
if (catchUrls) {
|
|
5938
|
-
result.guidance = {
|
|
5939
|
-
message: `Middleware threw ${errorClass} \u2014 failing closed`,
|
|
5940
|
-
registrationUrl: catchUrls.registrationUrl,
|
|
5941
|
-
documentationUrl: catchUrls.documentationUrl
|
|
5942
|
-
};
|
|
5943
|
-
}
|
|
5944
6387
|
dedupeFailures2(result);
|
|
5945
6388
|
return onDenied(result, req, res);
|
|
5946
6389
|
}
|
|
@@ -9948,6 +10391,7 @@ function parseYaml(yamlContent) {
|
|
|
9948
10391
|
RegistrationTimeoutError,
|
|
9949
10392
|
SDK_USER_AGENT,
|
|
9950
10393
|
TRUST_LEVEL_RANGES,
|
|
10394
|
+
VERIFICATION_STATUS_HEADER,
|
|
9951
10395
|
VERSION,
|
|
9952
10396
|
_resetEdgeConfigCache,
|
|
9953
10397
|
agent,
|
|
@@ -9955,9 +10399,11 @@ function parseYaml(yamlContent) {
|
|
|
9955
10399
|
awaitStepUpApproval,
|
|
9956
10400
|
buildGuidance,
|
|
9957
10401
|
buildSdkObservedMetadata,
|
|
10402
|
+
buildVerificationUnavailableBody,
|
|
9958
10403
|
clearCache,
|
|
9959
10404
|
createMcpMiddleware,
|
|
9960
10405
|
degradeToObserve,
|
|
10406
|
+
denyStatusFor,
|
|
9961
10407
|
deriveConnectionFromHeaders,
|
|
9962
10408
|
detectPlatformFingerprint,
|
|
9963
10409
|
detectRuntime,
|
|
@@ -9974,6 +10420,8 @@ function parseYaml(yamlContent) {
|
|
|
9974
10420
|
getWellKnownUrls,
|
|
9975
10421
|
hasCredentials,
|
|
9976
10422
|
isEdgeConfig,
|
|
10423
|
+
isRejectedIdentityClaim,
|
|
10424
|
+
isVerificationUnavailable,
|
|
9977
10425
|
isVerifiedHopValidFor,
|
|
9978
10426
|
matchEdgePathRule,
|
|
9979
10427
|
matchPlatformSignature,
|
|
@@ -9993,6 +10441,7 @@ function parseYaml(yamlContent) {
|
|
|
9993
10441
|
serializeVerifiedHop,
|
|
9994
10442
|
setMcpMeta,
|
|
9995
10443
|
transport,
|
|
10444
|
+
unavailableRetryAfterSeconds,
|
|
9996
10445
|
validateLocalPolicy,
|
|
9997
10446
|
verify
|
|
9998
10447
|
});
|