@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.mjs
CHANGED
|
@@ -19,7 +19,7 @@ function getTrustLevel(score) {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
// src/version.ts
|
|
22
|
-
var SDK_VERSION = "5.
|
|
22
|
+
var SDK_VERSION = "5.13.0";
|
|
23
23
|
|
|
24
24
|
// src/http.ts
|
|
25
25
|
var SDK_USER_AGENT = `astrasync-sdk/${SDK_VERSION}`;
|
|
@@ -98,6 +98,59 @@ function getCachedWellKnownUrls(apiBaseUrl) {
|
|
|
98
98
|
return cache.get(apiBaseUrl)?.data;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
// src/failure-posture.ts
|
|
102
|
+
var VERIFICATION_STATUS_HEADER = "X-Astra-Verification";
|
|
103
|
+
var DEFAULT_UNAVAILABLE_RETRY_AFTER_S = 30;
|
|
104
|
+
var REJECTED_IDENTITY_DIMENSIONS = /* @__PURE__ */ new Set([
|
|
105
|
+
"identity.invalid_agent_id",
|
|
106
|
+
"identity.unknown_agent",
|
|
107
|
+
"agent.lookup"
|
|
108
|
+
]);
|
|
109
|
+
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.";
|
|
110
|
+
var UNAVAILABLE_DIMENSIONS = /* @__PURE__ */ new Set([
|
|
111
|
+
"verify_access.api_error",
|
|
112
|
+
"middleware.internal_error"
|
|
113
|
+
]);
|
|
114
|
+
function isVerificationUnavailable(result) {
|
|
115
|
+
if (result.misconfigured) return false;
|
|
116
|
+
return (result.failures ?? []).some((f) => UNAVAILABLE_DIMENSIONS.has(f.dimension));
|
|
117
|
+
}
|
|
118
|
+
function unavailableRetryAfterSeconds(result) {
|
|
119
|
+
if (typeof result.retryAfterMs === "number" && Number.isFinite(result.retryAfterMs)) {
|
|
120
|
+
return Math.max(1, Math.ceil(result.retryAfterMs / 1e3));
|
|
121
|
+
}
|
|
122
|
+
return DEFAULT_UNAVAILABLE_RETRY_AFTER_S;
|
|
123
|
+
}
|
|
124
|
+
function isAnonymousRead(method, credentials) {
|
|
125
|
+
const m = (method ?? "").toUpperCase();
|
|
126
|
+
if (m !== "GET" && m !== "HEAD") return false;
|
|
127
|
+
return !credentials.astraId && !credentials.jwt;
|
|
128
|
+
}
|
|
129
|
+
function buildVerificationUnavailableBody(result) {
|
|
130
|
+
return {
|
|
131
|
+
success: false,
|
|
132
|
+
error: {
|
|
133
|
+
code: "VERIFICATION_UNAVAILABLE",
|
|
134
|
+
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.",
|
|
135
|
+
retryable: true,
|
|
136
|
+
retryAfterSeconds: unavailableRetryAfterSeconds(result),
|
|
137
|
+
...result.failures && { failures: result.failures },
|
|
138
|
+
...result.correlationId && { correlationId: result.correlationId },
|
|
139
|
+
guidance: {
|
|
140
|
+
message: "Retry the request after the Retry-After interval, with exponential backoff; the condition is transient."
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function isRejectedIdentityClaim(result) {
|
|
146
|
+
return (result.failures ?? []).some((f) => REJECTED_IDENTITY_DIMENSIONS.has(f.dimension));
|
|
147
|
+
}
|
|
148
|
+
function denyStatusFor(result, policyDeniedCode) {
|
|
149
|
+
if (isRejectedIdentityClaim(result)) return { status: 403, code: "AGENT_IDENTITY_REJECTED" };
|
|
150
|
+
if (result.identityVerified) return { status: 403, code: policyDeniedCode };
|
|
151
|
+
return { status: 401, code: "UNAUTHORIZED" };
|
|
152
|
+
}
|
|
153
|
+
|
|
101
154
|
// src/verify.ts
|
|
102
155
|
var DEFAULT_CONFIG = {
|
|
103
156
|
apiBaseUrl: "https://astrasync.ai/api",
|
|
@@ -110,7 +163,7 @@ var DEFAULT_CONFIG = {
|
|
|
110
163
|
};
|
|
111
164
|
var initCheckPerformed = false;
|
|
112
165
|
var deprecationWarningShown = false;
|
|
113
|
-
async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey) {
|
|
166
|
+
async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey, counterparty) {
|
|
114
167
|
initCheckPerformed = true;
|
|
115
168
|
try {
|
|
116
169
|
const probeUrl = `${apiBaseUrl}/agents/verify-access`;
|
|
@@ -128,7 +181,8 @@ async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey) {
|
|
|
128
181
|
);
|
|
129
182
|
}
|
|
130
183
|
if (apiKey) {
|
|
131
|
-
const
|
|
184
|
+
const q = counterparty?.id ? `?counterpartyId=${encodeURIComponent(counterparty.id)}` : counterparty?.url ? `?counterpartyUrl=${encodeURIComponent(counterparty.url)}` : "";
|
|
185
|
+
const selfCheck = await sdkFetch(`${apiBaseUrl}/agents/verify-access/self-check${q}`, {
|
|
132
186
|
headers: { Authorization: `Bearer ${apiKey}` }
|
|
133
187
|
});
|
|
134
188
|
if (selfCheck.status === 401) {
|
|
@@ -140,6 +194,13 @@ async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey) {
|
|
|
140
194
|
console.error(message);
|
|
141
195
|
} else if (selfCheck.ok) {
|
|
142
196
|
const body = await selfCheck.json().catch(() => ({}));
|
|
197
|
+
if (body.counterparty && body.counterparty.canVerify === false) {
|
|
198
|
+
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.`;
|
|
199
|
+
if (strictInit) {
|
|
200
|
+
throw new Error(`${message} (strictInit=true)`);
|
|
201
|
+
}
|
|
202
|
+
console.error(message);
|
|
203
|
+
}
|
|
143
204
|
if (typeof body.daysToExpiry === "number" && body.daysToExpiry <= 7) {
|
|
144
205
|
console.warn(
|
|
145
206
|
`[VerificationGateway] the configured AstraSync API key expires in ${body.daysToExpiry} day(s) \u2014 rotate it at Settings \u2192 Developer Tools before integrations start failing.`
|
|
@@ -284,11 +345,11 @@ function createGuidanceResponse(_config, reason, options = {}) {
|
|
|
284
345
|
const isMisconfig = source === "merchant_misconfig";
|
|
285
346
|
const urls = options.urls;
|
|
286
347
|
const guidance = isMisconfig ? {
|
|
287
|
-
message: "This site's AstraSync integration is misconfigured (its API key was revoked, expired, or
|
|
348
|
+
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.",
|
|
288
349
|
registrationUrl: urls?.registrationUrl ?? "",
|
|
289
350
|
documentationUrl: options.remediation?.docsUrl ?? urls?.documentationUrl ?? "",
|
|
290
351
|
steps: [
|
|
291
|
-
"Report the failure to the site operator \u2014 their AstraSync API key needs
|
|
352
|
+
"Report the failure to the site operator \u2014 their AstraSync API key needs replacing",
|
|
292
353
|
"Do not retry: the failure is deterministic until the operator updates their key"
|
|
293
354
|
]
|
|
294
355
|
} : isApiError ? {
|
|
@@ -349,11 +410,44 @@ function createGuidanceResponse(_config, reason, options = {}) {
|
|
|
349
410
|
// coercing the absent recommendation to a policy 'deny'. Misconfig is
|
|
350
411
|
// deterministic and intentionally NOT marked retryable.
|
|
351
412
|
...isApiError && { retryable: true },
|
|
413
|
+
...options.httpStatus !== void 0 && { httpStatus: options.httpStatus },
|
|
414
|
+
...options.retryAfterMs !== void 0 && { retryAfterMs: options.retryAfterMs },
|
|
352
415
|
correlationId: options.correlationId,
|
|
353
416
|
attemptId: options.attemptId,
|
|
354
417
|
verifiedAt: /* @__PURE__ */ new Date()
|
|
355
418
|
};
|
|
356
419
|
}
|
|
420
|
+
async function readJsonSafe(response) {
|
|
421
|
+
try {
|
|
422
|
+
const parsed = await response.json();
|
|
423
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
424
|
+
} catch {
|
|
425
|
+
return {};
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
function rejectedIdentityGuidance(failures) {
|
|
429
|
+
const f = failures?.find((x) => REJECTED_IDENTITY_DIMENSIONS.has(x.dimension));
|
|
430
|
+
if (!f) return void 0;
|
|
431
|
+
if (f.dimension === "agent.lookup") {
|
|
432
|
+
return `The presented agent identifier is not registered with AstraSync. ${UNKNOWN_AGENT_GUIDANCE}`;
|
|
433
|
+
}
|
|
434
|
+
return f.guidance ? `${f.message} ${f.guidance}` : f.message;
|
|
435
|
+
}
|
|
436
|
+
var MAX_RETRY_AFTER_MS = 15 * 60 * 1e3;
|
|
437
|
+
function parseRetryAfterMs(response) {
|
|
438
|
+
const raw = response.headers?.get?.("retry-after");
|
|
439
|
+
if (!raw) return void 0;
|
|
440
|
+
const trimmed = raw.trim();
|
|
441
|
+
let ms;
|
|
442
|
+
if (/^\d+(\.\d+)?$/.test(trimmed)) {
|
|
443
|
+
ms = Number(trimmed) * 1e3;
|
|
444
|
+
} else {
|
|
445
|
+
const at = Date.parse(trimmed);
|
|
446
|
+
if (Number.isNaN(at)) return void 0;
|
|
447
|
+
ms = at - Date.now();
|
|
448
|
+
}
|
|
449
|
+
return Math.min(MAX_RETRY_AFTER_MS, Math.max(0, Math.round(ms)));
|
|
450
|
+
}
|
|
357
451
|
async function callVerifyAccessAPI(config, request) {
|
|
358
452
|
const { credentials, ...requestData } = request;
|
|
359
453
|
const body = {
|
|
@@ -410,7 +504,8 @@ async function callVerifyAccessAPI(config, request) {
|
|
|
410
504
|
body: JSON.stringify(body),
|
|
411
505
|
signal: AbortSignal.timeout(config.verifyTimeoutMs ?? 1e4)
|
|
412
506
|
});
|
|
413
|
-
const data = await response
|
|
507
|
+
const data = await readJsonSafe(response);
|
|
508
|
+
const retryAfterMs = parseRetryAfterMs(response);
|
|
414
509
|
if (response.status === 410) {
|
|
415
510
|
return {
|
|
416
511
|
success: true,
|
|
@@ -448,13 +543,31 @@ async function callVerifyAccessAPI(config, request) {
|
|
|
448
543
|
}
|
|
449
544
|
};
|
|
450
545
|
}
|
|
546
|
+
if (response.status === 404 && credentials.astraId && (data?.error === "AGENT_NOT_FOUND" || typeof data?.message === "string" && /agent/i.test(data.message))) {
|
|
547
|
+
return {
|
|
548
|
+
success: true,
|
|
549
|
+
attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0,
|
|
550
|
+
access: {
|
|
551
|
+
allowed: false,
|
|
552
|
+
reason: "unknown_agent",
|
|
553
|
+
failures: [
|
|
554
|
+
{
|
|
555
|
+
dimension: "identity.unknown_agent",
|
|
556
|
+
message: "The presented agent identifier is not registered with AstraSync.",
|
|
557
|
+
guidance: UNKNOWN_AGENT_GUIDANCE
|
|
558
|
+
}
|
|
559
|
+
]
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
}
|
|
451
563
|
if (!response.ok) {
|
|
452
|
-
|
|
564
|
+
const keyMismatch = response.status === 403 && data?.error === "COUNTERPARTY_KEY_MISMATCH";
|
|
565
|
+
if (response.status === 401 || keyMismatch) {
|
|
453
566
|
return {
|
|
454
567
|
success: false,
|
|
455
568
|
error: data.message || data.error || "Integration API key rejected",
|
|
456
569
|
misconfig: true,
|
|
457
|
-
misconfigCode: typeof data?.code === "string" ? data.code : void 0,
|
|
570
|
+
misconfigCode: keyMismatch ? "counterparty_key_mismatch" : typeof data?.code === "string" ? data.code : void 0,
|
|
458
571
|
remediation: data?.remediation && typeof data.remediation === "object" ? data.remediation : void 0,
|
|
459
572
|
correlationId: typeof data?.correlationId === "string" ? data.correlationId : void 0,
|
|
460
573
|
attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0
|
|
@@ -462,9 +575,11 @@ async function callVerifyAccessAPI(config, request) {
|
|
|
462
575
|
}
|
|
463
576
|
return {
|
|
464
577
|
success: false,
|
|
465
|
-
error: data.message || data.error || `API returned ${response.status}`,
|
|
578
|
+
error: typeof data?.message === "string" && data.message || typeof data?.error === "string" && data.error || `API returned ${response.status}`,
|
|
466
579
|
correlationId: typeof data?.correlationId === "string" ? data.correlationId : void 0,
|
|
467
|
-
attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0
|
|
580
|
+
attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0,
|
|
581
|
+
httpStatus: response.status,
|
|
582
|
+
...retryAfterMs !== void 0 && { retryAfterMs }
|
|
468
583
|
};
|
|
469
584
|
}
|
|
470
585
|
return data;
|
|
@@ -485,14 +600,22 @@ async function verify(config, request, options) {
|
|
|
485
600
|
mergedConfig.apiBaseUrl,
|
|
486
601
|
mergedConfig.debug,
|
|
487
602
|
true,
|
|
488
|
-
mergedConfig.apiKey
|
|
603
|
+
mergedConfig.apiKey,
|
|
604
|
+
{
|
|
605
|
+
id: mergedConfig.counterpartyId,
|
|
606
|
+
url: mergedConfig.counterpartyUrl ?? request.counterpartyUrl
|
|
607
|
+
}
|
|
489
608
|
);
|
|
490
609
|
} else {
|
|
491
610
|
void performInitCheck(
|
|
492
611
|
mergedConfig.apiBaseUrl,
|
|
493
612
|
mergedConfig.debug,
|
|
494
613
|
false,
|
|
495
|
-
mergedConfig.apiKey
|
|
614
|
+
mergedConfig.apiKey,
|
|
615
|
+
{
|
|
616
|
+
id: mergedConfig.counterpartyId,
|
|
617
|
+
url: mergedConfig.counterpartyUrl ?? request.counterpartyUrl
|
|
618
|
+
}
|
|
496
619
|
);
|
|
497
620
|
}
|
|
498
621
|
}
|
|
@@ -536,12 +659,14 @@ async function verify(config, request, options) {
|
|
|
536
659
|
correlationId: apiResponse.correlationId,
|
|
537
660
|
attemptId: apiResponse.attemptId,
|
|
538
661
|
remediation: apiResponse.remediation,
|
|
662
|
+
httpStatus: apiResponse.httpStatus,
|
|
663
|
+
retryAfterMs: apiResponse.retryAfterMs,
|
|
539
664
|
urls
|
|
540
665
|
});
|
|
541
666
|
}
|
|
542
667
|
if (!apiResponse.access?.allowed) {
|
|
543
668
|
const aggregatedFailures = apiResponse.access?.failures;
|
|
544
|
-
const idVerifiedFromBackend = apiResponse.verificationContext?.idVerified === true;
|
|
669
|
+
const idVerifiedFromBackend = apiResponse.verificationContext?.idVerified === true && !aggregatedFailures?.some((f) => REJECTED_IDENTITY_DIMENSIONS.has(f.dimension));
|
|
545
670
|
const result2 = {
|
|
546
671
|
identityVerified: idVerifiedFromBackend,
|
|
547
672
|
policyAllowed: false,
|
|
@@ -553,7 +678,10 @@ async function verify(config, request, options) {
|
|
|
553
678
|
requiresStepUp: apiResponse.access?.requiresStepUp,
|
|
554
679
|
requiresApproval: apiResponse.access?.requiresApproval,
|
|
555
680
|
guidance: {
|
|
556
|
-
|
|
681
|
+
// 5.12.0: a presented-but-rejected id (malformed / unknown) carries
|
|
682
|
+
// actionable registration guidance on its failure — surface it
|
|
683
|
+
// instead of the bare machine reason ('unknown_agent').
|
|
684
|
+
message: rejectedIdentityGuidance(aggregatedFailures) ?? (apiResponse.access?.reason || "Access denied by PDLSS policy"),
|
|
557
685
|
registrationUrl: urls?.registrationUrl ?? "",
|
|
558
686
|
documentationUrl: urls?.documentationUrl ?? "",
|
|
559
687
|
// 5.1.0: anonymous callers land on this branch — steer MCP-hosted
|
|
@@ -653,7 +781,7 @@ async function verify(config, request, options) {
|
|
|
653
781
|
registrationUrl: urls?.registrationUrl ?? "",
|
|
654
782
|
documentationUrl: urls?.documentationUrl ?? ""
|
|
655
783
|
};
|
|
656
|
-
} else if (result.recommendation === "step_up_required") {
|
|
784
|
+
} else if (result.recommendation === "step_up_required" && result.identityVerified) {
|
|
657
785
|
result.requiresStepUp = true;
|
|
658
786
|
result.denialReasons = serverDenialReasons ?? result.recommendationReasons ?? ["Step-up verification required"];
|
|
659
787
|
}
|
|
@@ -676,7 +804,7 @@ async function recordDecision(config, sessionId, decision, reason) {
|
|
|
676
804
|
}).catch(() => {
|
|
677
805
|
});
|
|
678
806
|
}
|
|
679
|
-
async function reportAttempt(config, report) {
|
|
807
|
+
async function reportAttempt(config, report, options = {}) {
|
|
680
808
|
const apiBaseUrl = config.apiBaseUrl || DEFAULT_CONFIG.apiBaseUrl;
|
|
681
809
|
const headers = { "Content-Type": "application/json" };
|
|
682
810
|
if (config.apiKey) {
|
|
@@ -687,13 +815,38 @@ async function reportAttempt(config, report) {
|
|
|
687
815
|
...report,
|
|
688
816
|
...!report.counterpartyId && config.counterpartyId && { counterpartyId: config.counterpartyId }
|
|
689
817
|
};
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
818
|
+
const url = `${apiBaseUrl}/agents/verify-access/attempt-report`;
|
|
819
|
+
const init = { method: "POST", headers, body: JSON.stringify(body) };
|
|
820
|
+
const fail = (failure) => {
|
|
821
|
+
try {
|
|
822
|
+
options.onFailure?.(failure);
|
|
823
|
+
} catch {
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
let response;
|
|
827
|
+
try {
|
|
828
|
+
response = await sdkFetch(url, init);
|
|
829
|
+
} catch (err) {
|
|
830
|
+
fail({ error: err instanceof Error ? err.message : String(err), retried: false });
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
if (response.ok) return;
|
|
834
|
+
if (response.status !== 429 && response.status < 500) {
|
|
835
|
+
fail({ status: response.status, retried: false });
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
const delayMs = Math.min(ATTEMPT_REPORT_MAX_RETRY_DELAY_MS, parseRetryAfterMs(response) ?? 500);
|
|
839
|
+
const timer = setTimeout(() => {
|
|
840
|
+
sdkFetch(url, init).then((retry) => {
|
|
841
|
+
if (!retry.ok) fail({ status: retry.status, retried: true });
|
|
842
|
+
}).catch((err) => {
|
|
843
|
+
fail({ error: err instanceof Error ? err.message : String(err), retried: true });
|
|
844
|
+
});
|
|
845
|
+
}, delayMs);
|
|
846
|
+
timer.unref?.();
|
|
696
847
|
}
|
|
848
|
+
var ATTEMPT_REPORT_MAX_RETRY_DELAY_MS = 2e3;
|
|
849
|
+
var refusedPolicyReads = /* @__PURE__ */ new Set();
|
|
697
850
|
async function fetchRoutes(config, counterpartyId) {
|
|
698
851
|
if (!counterpartyId) return null;
|
|
699
852
|
const headers = { "Content-Type": "application/json" };
|
|
@@ -706,7 +859,15 @@ async function fetchRoutes(config, counterpartyId) {
|
|
|
706
859
|
`${config.apiBaseUrl}/endpoints/${encodeURIComponent(counterpartyId)}/routes`,
|
|
707
860
|
{ method: "GET", headers }
|
|
708
861
|
);
|
|
709
|
-
if (!response.ok)
|
|
862
|
+
if (!response.ok) {
|
|
863
|
+
if ((response.status === 401 || response.status === 403 || response.status === 404) && !refusedPolicyReads.has(counterpartyId)) {
|
|
864
|
+
refusedPolicyReads.add(counterpartyId);
|
|
865
|
+
console.error(
|
|
866
|
+
`[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.`
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
return null;
|
|
870
|
+
}
|
|
710
871
|
const body = await response.json();
|
|
711
872
|
return body.data?.routes ?? [];
|
|
712
873
|
} catch {
|
|
@@ -723,7 +884,10 @@ async function reportUnregisteredAttempt(config, data) {
|
|
|
723
884
|
};
|
|
724
885
|
await sdkFetch(`${apiBaseUrl}/verification-activity/unregistered-attempt`, {
|
|
725
886
|
method: "POST",
|
|
726
|
-
headers: {
|
|
887
|
+
headers: {
|
|
888
|
+
"Content-Type": "application/json",
|
|
889
|
+
...config.apiKey && { Authorization: `Bearer ${config.apiKey}` }
|
|
890
|
+
},
|
|
727
891
|
body: JSON.stringify(body)
|
|
728
892
|
}).catch(() => {
|
|
729
893
|
});
|
|
@@ -745,7 +909,11 @@ async function quickVerify(config, credentials) {
|
|
|
745
909
|
return {
|
|
746
910
|
identityVerified: result.identityVerified,
|
|
747
911
|
policyAllowed: result.policyAllowed,
|
|
748
|
-
reason: result.denialReasons?.[0]
|
|
912
|
+
reason: result.denialReasons?.[0],
|
|
913
|
+
...isVerificationUnavailable(result) && {
|
|
914
|
+
unavailable: true,
|
|
915
|
+
retryAfterMs: unavailableRetryAfterSeconds(result) * 1e3
|
|
916
|
+
}
|
|
749
917
|
};
|
|
750
918
|
}
|
|
751
919
|
|
|
@@ -1493,7 +1661,10 @@ var PLATFORM_AGENT_SIGNATURES = [
|
|
|
1493
1661
|
{
|
|
1494
1662
|
vendor: "chatgpt",
|
|
1495
1663
|
displayName: "OpenAI ChatGPT",
|
|
1496
|
-
|
|
1664
|
+
// GPTBot (training crawler) and OAI-SearchBot (ChatGPT search) are
|
|
1665
|
+
// OpenAI's declared crawler UAs — `gpt-?\d` never matched them, so they
|
|
1666
|
+
// classified as unattributed crawlers (round-27.4 W5).
|
|
1667
|
+
uaPatterns: [/chatgpt/i, /openai/i, /gpt-?\d/i, /\bGPTBot\b/i, /\bOAI-SearchBot\b/i],
|
|
1497
1668
|
agentCardPatterns: [/openai\.com/i, /chatgpt\.com/i],
|
|
1498
1669
|
apiKeyFormatPatterns: [/^sk-proj/i, /^sk-svcacct/i],
|
|
1499
1670
|
headerPatterns: [/^openai-organization/i, /^openai-project/i, /^openai-version/i]
|
|
@@ -1628,6 +1799,215 @@ var BROWSER_UA_PATTERN = /mozilla\/\d/i;
|
|
|
1628
1799
|
var _patterns = { STATIC_EXTENSIONS, BOT_UA_PATTERN, BROWSER_UA_PATTERN, SELF_UA_PATTERN };
|
|
1629
1800
|
|
|
1630
1801
|
// src/edge-core/events.ts
|
|
1802
|
+
async function withBudget(promise, ms) {
|
|
1803
|
+
let timer;
|
|
1804
|
+
try {
|
|
1805
|
+
return await Promise.race([
|
|
1806
|
+
promise,
|
|
1807
|
+
new Promise((resolve) => {
|
|
1808
|
+
timer = setTimeout(() => resolve(void 0), ms);
|
|
1809
|
+
})
|
|
1810
|
+
]);
|
|
1811
|
+
} catch {
|
|
1812
|
+
return void 0;
|
|
1813
|
+
} finally {
|
|
1814
|
+
if (timer) clearTimeout(timer);
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
var destKeyOf = (d) => `${d.apiBaseUrl}\0${d.apiKey ?? ""}\0${d.counterparty}\0${d.adapter ?? ""}`;
|
|
1818
|
+
var BeaconBuffer = class {
|
|
1819
|
+
constructor(options = {}) {
|
|
1820
|
+
this.queue = [];
|
|
1821
|
+
this.droppedSinceLast = 0;
|
|
1822
|
+
this.droppedTotal = 0;
|
|
1823
|
+
this.deliveredTotal = 0;
|
|
1824
|
+
this.backoffUntil = 0;
|
|
1825
|
+
this.collapse = /* @__PURE__ */ new Map();
|
|
1826
|
+
/** Keys the backend answered 401 for — later batches go unauthenticated. */
|
|
1827
|
+
this.rejectedKeys = /* @__PURE__ */ new Set();
|
|
1828
|
+
this.opts = {
|
|
1829
|
+
maxItems: options.maxItems ?? 1e3,
|
|
1830
|
+
maxBatch: options.maxBatch ?? 100,
|
|
1831
|
+
maxAttempts: options.maxAttempts ?? 3,
|
|
1832
|
+
collapseWindowMs: options.collapseWindowMs ?? 6e4,
|
|
1833
|
+
collapseMaxKeys: options.collapseMaxKeys ?? 1e3,
|
|
1834
|
+
requestTimeoutMs: options.requestTimeoutMs ?? 1e4,
|
|
1835
|
+
retryBackoffMs: options.retryBackoffMs ?? 1e3,
|
|
1836
|
+
now: options.now ?? Date.now
|
|
1837
|
+
};
|
|
1838
|
+
}
|
|
1839
|
+
/**
|
|
1840
|
+
* Queue one beacon. With a `collapseKey`, a repeat inside the window is
|
|
1841
|
+
* merged into a held aggregate (sent when the window closes) instead of
|
|
1842
|
+
* becoming its own item.
|
|
1843
|
+
*/
|
|
1844
|
+
enqueue(dest, item, collapseKey) {
|
|
1845
|
+
const now = this.opts.now();
|
|
1846
|
+
if (collapseKey) {
|
|
1847
|
+
const entry = this.collapse.get(collapseKey);
|
|
1848
|
+
if (entry && now - entry.firstSeenAt < this.opts.collapseWindowMs) {
|
|
1849
|
+
for (const target of [entry.first, entry.held]) {
|
|
1850
|
+
if (target && !target.inFlight && this.queue.includes(target)) {
|
|
1851
|
+
target.item.repeatCount = (target.item.repeatCount ?? 1) + 1;
|
|
1852
|
+
return;
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
const held = this.push(
|
|
1856
|
+
dest,
|
|
1857
|
+
{ ...item, repeatCount: 1 },
|
|
1858
|
+
entry.firstSeenAt + this.opts.collapseWindowMs
|
|
1859
|
+
);
|
|
1860
|
+
entry.held = held;
|
|
1861
|
+
return;
|
|
1862
|
+
}
|
|
1863
|
+
this.collapse.delete(collapseKey);
|
|
1864
|
+
if (this.collapse.size >= this.opts.collapseMaxKeys) {
|
|
1865
|
+
const oldest = this.collapse.keys().next().value;
|
|
1866
|
+
if (oldest !== void 0) this.collapse.delete(oldest);
|
|
1867
|
+
}
|
|
1868
|
+
const fresh = { firstSeenAt: now };
|
|
1869
|
+
this.collapse.set(collapseKey, fresh);
|
|
1870
|
+
fresh.first = this.push(dest, item, now);
|
|
1871
|
+
return;
|
|
1872
|
+
}
|
|
1873
|
+
this.push(dest, item, now);
|
|
1874
|
+
}
|
|
1875
|
+
/**
|
|
1876
|
+
* Deliver every eligible queued item. Concurrent callers share ONE
|
|
1877
|
+
* in-flight drain (it survives the caller's budget and a Lambda freeze),
|
|
1878
|
+
* so nothing in flight is ever sent twice; items queued while it runs are
|
|
1879
|
+
* picked up by the same drain.
|
|
1880
|
+
*/
|
|
1881
|
+
flush() {
|
|
1882
|
+
if (!this.inFlight) {
|
|
1883
|
+
this.inFlight = this.drain().finally(() => {
|
|
1884
|
+
this.inFlight = void 0;
|
|
1885
|
+
});
|
|
1886
|
+
}
|
|
1887
|
+
return this.inFlight;
|
|
1888
|
+
}
|
|
1889
|
+
/** Counters for tests and diagnostics. */
|
|
1890
|
+
stats() {
|
|
1891
|
+
return {
|
|
1892
|
+
queueDepth: this.queue.length,
|
|
1893
|
+
droppedSinceLast: this.droppedSinceLast,
|
|
1894
|
+
droppedTotal: this.droppedTotal,
|
|
1895
|
+
deliveredTotal: this.deliveredTotal,
|
|
1896
|
+
flushing: this.inFlight !== void 0
|
|
1897
|
+
};
|
|
1898
|
+
}
|
|
1899
|
+
push(dest, item, eligibleAt) {
|
|
1900
|
+
while (this.queue.length >= this.opts.maxItems) this.dropOldest();
|
|
1901
|
+
const entry = {
|
|
1902
|
+
dest,
|
|
1903
|
+
destKey: destKeyOf(dest),
|
|
1904
|
+
item,
|
|
1905
|
+
attempts: 0,
|
|
1906
|
+
eligibleAt,
|
|
1907
|
+
inFlight: false
|
|
1908
|
+
};
|
|
1909
|
+
this.queue.push(entry);
|
|
1910
|
+
return entry;
|
|
1911
|
+
}
|
|
1912
|
+
dropOldest() {
|
|
1913
|
+
if (this.queue.shift()) this.countDropped(1);
|
|
1914
|
+
}
|
|
1915
|
+
countDropped(n) {
|
|
1916
|
+
this.droppedSinceLast += n;
|
|
1917
|
+
this.droppedTotal += n;
|
|
1918
|
+
}
|
|
1919
|
+
async drain() {
|
|
1920
|
+
for (; ; ) {
|
|
1921
|
+
const now = this.opts.now();
|
|
1922
|
+
if (now < this.backoffUntil) return;
|
|
1923
|
+
const batch = this.takeBatch(now);
|
|
1924
|
+
if (batch.length === 0) return;
|
|
1925
|
+
const ok = await this.send(batch);
|
|
1926
|
+
if (!ok) return;
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
/** Oldest eligible item + up to maxBatch-1 more for the same destination. */
|
|
1930
|
+
takeBatch(now) {
|
|
1931
|
+
const first = this.queue.find((q) => q.eligibleAt <= now);
|
|
1932
|
+
if (!first) return [];
|
|
1933
|
+
const batch = [];
|
|
1934
|
+
const rest = [];
|
|
1935
|
+
for (const q of this.queue) {
|
|
1936
|
+
if (batch.length < this.opts.maxBatch && q.destKey === first.destKey && q.eligibleAt <= now) {
|
|
1937
|
+
q.inFlight = true;
|
|
1938
|
+
batch.push(q);
|
|
1939
|
+
} else {
|
|
1940
|
+
rest.push(q);
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
this.queue = rest;
|
|
1944
|
+
return batch;
|
|
1945
|
+
}
|
|
1946
|
+
/** POST one batch. Returns false when the drain should pause (backoff). */
|
|
1947
|
+
async send(batch) {
|
|
1948
|
+
const dest = batch[0].dest;
|
|
1949
|
+
const useKey = dest.apiKey && !this.rejectedKeys.has(dest.apiKey) ? dest.apiKey : void 0;
|
|
1950
|
+
const reportedDropped = this.droppedSinceLast;
|
|
1951
|
+
const meta = {
|
|
1952
|
+
droppedSinceLast: reportedDropped,
|
|
1953
|
+
queueDepth: this.queue.length,
|
|
1954
|
+
adapter: dest.adapter ?? "edge-core",
|
|
1955
|
+
version: SDK_VERSION
|
|
1956
|
+
};
|
|
1957
|
+
const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
|
|
1958
|
+
const response = await withBudget(
|
|
1959
|
+
sdkFetch(`${dest.apiBaseUrl}/verification-activity/edge-events`, {
|
|
1960
|
+
method: "POST",
|
|
1961
|
+
headers: {
|
|
1962
|
+
"Content-Type": "application/json",
|
|
1963
|
+
...useKey && { Authorization: `Bearer ${useKey}` }
|
|
1964
|
+
},
|
|
1965
|
+
body: JSON.stringify({ events: batch.map((q) => q.item), meta }),
|
|
1966
|
+
...controller && { signal: controller.signal }
|
|
1967
|
+
}),
|
|
1968
|
+
this.opts.requestTimeoutMs
|
|
1969
|
+
);
|
|
1970
|
+
if (!response) controller?.abort();
|
|
1971
|
+
const status = response?.status;
|
|
1972
|
+
for (const q of batch) q.inFlight = false;
|
|
1973
|
+
if (status !== void 0 && status >= 200 && status < 300) {
|
|
1974
|
+
this.droppedSinceLast = Math.max(0, this.droppedSinceLast - reportedDropped);
|
|
1975
|
+
this.deliveredTotal += batch.length;
|
|
1976
|
+
return true;
|
|
1977
|
+
}
|
|
1978
|
+
if (status === 401 && useKey) {
|
|
1979
|
+
this.rejectedKeys.add(useKey);
|
|
1980
|
+
this.requeue(batch, false);
|
|
1981
|
+
return true;
|
|
1982
|
+
}
|
|
1983
|
+
if (status === 429) {
|
|
1984
|
+
const retryAfter = Number(response?.headers.get("retry-after"));
|
|
1985
|
+
const waitMs = Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(retryAfter, 120) * 1e3 : 3e4;
|
|
1986
|
+
this.backoffUntil = this.opts.now() + waitMs;
|
|
1987
|
+
this.requeue(batch, false);
|
|
1988
|
+
return false;
|
|
1989
|
+
}
|
|
1990
|
+
if (status !== void 0 && status >= 400 && status < 500) {
|
|
1991
|
+
this.countDropped(batch.length);
|
|
1992
|
+
return true;
|
|
1993
|
+
}
|
|
1994
|
+
this.backoffUntil = this.opts.now() + this.opts.retryBackoffMs;
|
|
1995
|
+
this.requeue(batch, true);
|
|
1996
|
+
return false;
|
|
1997
|
+
}
|
|
1998
|
+
/** Put a failed batch back at the head of the queue (order preserved). */
|
|
1999
|
+
requeue(batch, countAttempt) {
|
|
2000
|
+
const keep = [];
|
|
2001
|
+
for (const q of batch) {
|
|
2002
|
+
if (countAttempt) q.attempts += 1;
|
|
2003
|
+
if (q.attempts >= this.opts.maxAttempts) this.countDropped(1);
|
|
2004
|
+
else keep.push(q);
|
|
2005
|
+
}
|
|
2006
|
+
this.queue = [...keep, ...this.queue];
|
|
2007
|
+
while (this.queue.length > this.opts.maxItems) this.dropOldest();
|
|
2008
|
+
}
|
|
2009
|
+
};
|
|
2010
|
+
var beaconBuffer = new BeaconBuffer();
|
|
1631
2011
|
function sampled(rate, random = Math.random) {
|
|
1632
2012
|
if (rate <= 0) return false;
|
|
1633
2013
|
if (rate >= 1) return true;
|
|
@@ -1804,12 +2184,19 @@ function defaultOnDenied(result, _req, res) {
|
|
|
1804
2184
|
});
|
|
1805
2185
|
return;
|
|
1806
2186
|
}
|
|
1807
|
-
|
|
2187
|
+
if (isVerificationUnavailable(result)) {
|
|
2188
|
+
res.setHeader("X-Astra-Gateway-Mode", "enforced");
|
|
2189
|
+
res.setHeader(VERIFICATION_STATUS_HEADER, "unavailable");
|
|
2190
|
+
res.setHeader("Retry-After", String(unavailableRetryAfterSeconds(result)));
|
|
2191
|
+
res.status(503).json(buildVerificationUnavailableBody(result));
|
|
2192
|
+
return;
|
|
2193
|
+
}
|
|
2194
|
+
const { status: statusCode, code } = denyStatusFor(result, "INSUFFICIENT_ACCESS");
|
|
1808
2195
|
res.setHeader("X-Astra-Gateway-Mode", "enforced");
|
|
1809
2196
|
res.status(statusCode).json({
|
|
1810
2197
|
success: false,
|
|
1811
2198
|
error: {
|
|
1812
|
-
code
|
|
2199
|
+
code,
|
|
1813
2200
|
message: result.denialReasons?.[0] || "Access denied",
|
|
1814
2201
|
guidance: result.guidance,
|
|
1815
2202
|
// Round-10: aggregated per-dimension detail + correlation handle.
|
|
@@ -2057,6 +2444,21 @@ function createMiddleware(options) {
|
|
|
2057
2444
|
result.stepUpOutcome = outcome;
|
|
2058
2445
|
}
|
|
2059
2446
|
}
|
|
2447
|
+
if (!result.policyAllowed && isVerificationUnavailable(result) && (!shouldEnforce || isAnonymousRead(req.method, credentials))) {
|
|
2448
|
+
res.setHeader(VERIFICATION_STATUS_HEADER, "unavailable");
|
|
2449
|
+
return next();
|
|
2450
|
+
}
|
|
2451
|
+
if (!shouldEnforce) {
|
|
2452
|
+
if (config.setPassThroughHeader) {
|
|
2453
|
+
res.setHeader("X-Astra-Gateway-Mode", "enforced");
|
|
2454
|
+
res.setHeader("X-Astra-Gateway-Reason", "evaluated-not-enforced");
|
|
2455
|
+
}
|
|
2456
|
+
if (shouldRecordDecisions && sessionId) {
|
|
2457
|
+
recordDecision(config, sessionId, "granted").catch(() => {
|
|
2458
|
+
});
|
|
2459
|
+
}
|
|
2460
|
+
return next();
|
|
2461
|
+
}
|
|
2060
2462
|
if (!result.policyAllowed) {
|
|
2061
2463
|
if (shouldRecordDecisions && sessionId) {
|
|
2062
2464
|
recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
|
|
@@ -2076,17 +2478,6 @@ function createMiddleware(options) {
|
|
|
2076
2478
|
onDenied(result, req, res);
|
|
2077
2479
|
return;
|
|
2078
2480
|
}
|
|
2079
|
-
if (!shouldEnforce) {
|
|
2080
|
-
if (config.setPassThroughHeader) {
|
|
2081
|
-
res.setHeader("X-Astra-Gateway-Mode", "enforced");
|
|
2082
|
-
res.setHeader("X-Astra-Gateway-Reason", "evaluated-not-enforced");
|
|
2083
|
-
}
|
|
2084
|
-
if (shouldRecordDecisions && sessionId) {
|
|
2085
|
-
recordDecision(config, sessionId, "granted").catch(() => {
|
|
2086
|
-
});
|
|
2087
|
-
}
|
|
2088
|
-
return next();
|
|
2089
|
-
}
|
|
2090
2481
|
if (routeConfig.minTrustScore && result.agent) {
|
|
2091
2482
|
if (result.agent.trustScore < routeConfig.minTrustScore) {
|
|
2092
2483
|
const trustFailure = {
|
|
@@ -2142,14 +2533,6 @@ function createMiddleware(options) {
|
|
|
2142
2533
|
verifiedAt: /* @__PURE__ */ new Date(),
|
|
2143
2534
|
correlationId
|
|
2144
2535
|
};
|
|
2145
|
-
const catchUrls = config.apiBaseUrl ? await getWellKnownUrls(config.apiBaseUrl).catch(() => void 0) : void 0;
|
|
2146
|
-
if (catchUrls) {
|
|
2147
|
-
result.guidance = {
|
|
2148
|
-
message: `Middleware threw ${errorClass} \u2014 failing closed`,
|
|
2149
|
-
registrationUrl: catchUrls.registrationUrl,
|
|
2150
|
-
documentationUrl: catchUrls.documentationUrl
|
|
2151
|
-
};
|
|
2152
|
-
}
|
|
2153
2536
|
dedupeFailures(result);
|
|
2154
2537
|
return onDenied(result, req, res);
|
|
2155
2538
|
}
|
|
@@ -2569,6 +2952,20 @@ function createMiddleware2(options) {
|
|
|
2569
2952
|
}
|
|
2570
2953
|
const approvalRequired = result.policyAllowed && requiresHumanApproval(result);
|
|
2571
2954
|
if (approvalRequired) annotateApprovalRequired(result);
|
|
2955
|
+
if (!result.policyAllowed && isVerificationUnavailable(result)) {
|
|
2956
|
+
if (isAnonymousRead(request.method, credentials)) {
|
|
2957
|
+
const passThrough = NextResponse.next();
|
|
2958
|
+
passThrough.headers.set(VERIFICATION_STATUS_HEADER, "unavailable");
|
|
2959
|
+
return passThrough;
|
|
2960
|
+
}
|
|
2961
|
+
return NextResponse.json(buildVerificationUnavailableBody(result), {
|
|
2962
|
+
status: 503,
|
|
2963
|
+
headers: {
|
|
2964
|
+
"Retry-After": String(unavailableRetryAfterSeconds(result)),
|
|
2965
|
+
[VERIFICATION_STATUS_HEADER]: "unavailable"
|
|
2966
|
+
}
|
|
2967
|
+
});
|
|
2968
|
+
}
|
|
2572
2969
|
if (!result.policyAllowed || approvalRequired) {
|
|
2573
2970
|
if (result.misconfigured && pathname.startsWith("/api/")) {
|
|
2574
2971
|
return NextResponse.json(
|
|
@@ -2586,13 +2983,12 @@ function createMiddleware2(options) {
|
|
|
2586
2983
|
);
|
|
2587
2984
|
}
|
|
2588
2985
|
if (pathname.startsWith("/api/")) {
|
|
2986
|
+
const denyStatus = denyStatusFor(result, "POLICY_DENIED");
|
|
2589
2987
|
return NextResponse.json(
|
|
2590
2988
|
{
|
|
2591
2989
|
success: false,
|
|
2592
2990
|
error: {
|
|
2593
|
-
|
|
2594
|
-
// OK, policy denied (update PDLSS / step up).
|
|
2595
|
-
code: !result.identityVerified ? "UNAUTHORIZED" : "POLICY_DENIED",
|
|
2991
|
+
code: denyStatus.code,
|
|
2596
2992
|
message: result.denialReasons?.[0] || "Access denied",
|
|
2597
2993
|
guidance: result.guidance,
|
|
2598
2994
|
failures: result.failures,
|
|
@@ -2602,7 +2998,7 @@ function createMiddleware2(options) {
|
|
|
2602
2998
|
}
|
|
2603
2999
|
},
|
|
2604
3000
|
{
|
|
2605
|
-
status:
|
|
3001
|
+
status: denyStatus.status,
|
|
2606
3002
|
...stepUpHoldOutcome && {
|
|
2607
3003
|
headers: { "X-Astra-Step-Up": stepUpHoldOutcome }
|
|
2608
3004
|
}
|
|
@@ -2729,6 +3125,12 @@ var VerificationGatewayClient = class {
|
|
|
2729
3125
|
* fulfil against it only once the order is settled / pending_merchant. An
|
|
2730
3126
|
* explicit `buyerEmail` (here or on the bridge handoff body) wins over the
|
|
2731
3127
|
* account default.
|
|
3128
|
+
*
|
|
3129
|
+
* 5.12.0 approval binding: pass the bridge handoff body's `attemptId` (and
|
|
3130
|
+
* the same `checkoutSessionId`) so a step-up approval minted for this cart
|
|
3131
|
+
* is matched to — and only to — this confirm. A held cart's approval is
|
|
3132
|
+
* bound to its checkout session and exact value; a confirm for any other
|
|
3133
|
+
* cart is held for its own approval.
|
|
2732
3134
|
*/
|
|
2733
3135
|
async confirmCheckout(options) {
|
|
2734
3136
|
const credentials = {
|
|
@@ -2748,6 +3150,7 @@ var VerificationGatewayClient = class {
|
|
|
2748
3150
|
commercePhase: "confirm",
|
|
2749
3151
|
checkoutSessionId: options.checkoutSessionId,
|
|
2750
3152
|
checkoutItems: options.checkoutItems,
|
|
3153
|
+
...options.attemptId && { attemptId: options.attemptId },
|
|
2751
3154
|
buyerEmail: options.buyerEmail,
|
|
2752
3155
|
counterpartyUrl: options.counterpartyUrl,
|
|
2753
3156
|
counterpartyType: options.counterpartyType
|
|
@@ -5551,8 +5954,37 @@ function dedupeFailures2(result) {
|
|
|
5551
5954
|
result.denialReasons = [...new Set(result.denialReasons)];
|
|
5552
5955
|
}
|
|
5553
5956
|
}
|
|
5957
|
+
function isReadOnlyMcpMethod(parsed) {
|
|
5958
|
+
return parsed.method !== "tools/call";
|
|
5959
|
+
}
|
|
5554
5960
|
function defaultMcpDenied(result, req, res) {
|
|
5555
5961
|
const id = req.body?.id ?? null;
|
|
5962
|
+
if (isVerificationUnavailable(result)) {
|
|
5963
|
+
const retryAfterSeconds = unavailableRetryAfterSeconds(result);
|
|
5964
|
+
const body = buildVerificationUnavailableBody(result).error;
|
|
5965
|
+
res.setHeader("X-Astra-Gateway-Mode", "enforced");
|
|
5966
|
+
res.setHeader(VERIFICATION_STATUS_HEADER, "unavailable");
|
|
5967
|
+
res.setHeader("Retry-After", String(retryAfterSeconds));
|
|
5968
|
+
res.status(503).json({
|
|
5969
|
+
jsonrpc: "2.0",
|
|
5970
|
+
id,
|
|
5971
|
+
error: {
|
|
5972
|
+
// -32003 → verification temporarily unavailable (retryable)
|
|
5973
|
+
code: -32003,
|
|
5974
|
+
message: body.message,
|
|
5975
|
+
data: {
|
|
5976
|
+
code: body.code,
|
|
5977
|
+
recommendation: "error",
|
|
5978
|
+
retryable: true,
|
|
5979
|
+
retryAfterSeconds,
|
|
5980
|
+
guidance: body.guidance,
|
|
5981
|
+
failures: result.failures,
|
|
5982
|
+
correlationId: result.correlationId
|
|
5983
|
+
}
|
|
5984
|
+
}
|
|
5985
|
+
});
|
|
5986
|
+
return;
|
|
5987
|
+
}
|
|
5556
5988
|
if (result.misconfigured) {
|
|
5557
5989
|
res.setHeader("X-Astra-Gateway-Mode", "enforced");
|
|
5558
5990
|
res.status(503).json({
|
|
@@ -5571,7 +6003,7 @@ function defaultMcpDenied(result, req, res) {
|
|
|
5571
6003
|
});
|
|
5572
6004
|
return;
|
|
5573
6005
|
}
|
|
5574
|
-
const status
|
|
6006
|
+
const { status, code: denyCode } = denyStatusFor(result, "INSUFFICIENT_ACCESS");
|
|
5575
6007
|
res.setHeader("X-Astra-Gateway-Mode", "enforced");
|
|
5576
6008
|
res.status(status).json({
|
|
5577
6009
|
jsonrpc: "2.0",
|
|
@@ -5581,9 +6013,11 @@ function defaultMcpDenied(result, req, res) {
|
|
|
5581
6013
|
// -32000 → unauthorized (no identity resolved)
|
|
5582
6014
|
// -32001 → insufficient access (identity OK, policy denied)
|
|
5583
6015
|
// -32002 → merchant verification misconfigured (handled above)
|
|
6016
|
+
// -32003 → verification temporarily unavailable (handled above)
|
|
5584
6017
|
code: !result.identityVerified ? -32e3 : -32001,
|
|
5585
6018
|
message: result.denialReasons?.[0] ?? "Access denied",
|
|
5586
6019
|
data: {
|
|
6020
|
+
code: denyCode,
|
|
5587
6021
|
guidance: result.guidance,
|
|
5588
6022
|
// Round-10: aggregated per-dimension detail + correlation handle.
|
|
5589
6023
|
failures: result.failures,
|
|
@@ -5711,6 +6145,9 @@ function createMcpMiddleware(options) {
|
|
|
5711
6145
|
resolved_action: pdlss.action
|
|
5712
6146
|
});
|
|
5713
6147
|
}
|
|
6148
|
+
if (!pdlss.purpose && !shouldEnforce) {
|
|
6149
|
+
return next();
|
|
6150
|
+
}
|
|
5714
6151
|
if (!pdlss.purpose) {
|
|
5715
6152
|
const id = req.body?.id ?? null;
|
|
5716
6153
|
res.status(400).json({
|
|
@@ -5730,6 +6167,9 @@ function createMcpMiddleware(options) {
|
|
|
5730
6167
|
}
|
|
5731
6168
|
const counterpartyUrl = config.counterpartyUrl || `${req.protocol}://${req.get("host")}${req.path}`;
|
|
5732
6169
|
const shouldRecordDecisions = recordDecisions !== false;
|
|
6170
|
+
const forwardedFor = req.headers["x-forwarded-for"];
|
|
6171
|
+
const forwardedForStr = Array.isArray(forwardedFor) ? forwardedFor.join(", ") : forwardedFor;
|
|
6172
|
+
const sourceIp = req.ip ?? (forwardedForStr ? forwardedForStr.split(",")[0].trim() : void 0);
|
|
5733
6173
|
const verificationRequest = {
|
|
5734
6174
|
credentials,
|
|
5735
6175
|
purpose: pdlss.purpose,
|
|
@@ -5744,9 +6184,10 @@ function createMcpMiddleware(options) {
|
|
|
5744
6184
|
counterpartyType: config.counterpartyType || "mcp_server",
|
|
5745
6185
|
enableRuntimeChallenge,
|
|
5746
6186
|
callerMetadata: {
|
|
5747
|
-
sourceIp
|
|
6187
|
+
sourceIp,
|
|
5748
6188
|
userAgent: req.headers["user-agent"],
|
|
5749
6189
|
host: req.headers.host,
|
|
6190
|
+
...forwardedForStr && { forwardedFor: forwardedForStr },
|
|
5750
6191
|
// Capture EVERY inbound header (parity with the express adapter —
|
|
5751
6192
|
// MCP-only merchants have no edge to do it upstream) plus the
|
|
5752
6193
|
// CDN-derived connection block. Local string work, secrets
|
|
@@ -5769,6 +6210,21 @@ function createMcpMiddleware(options) {
|
|
|
5769
6210
|
result.stepUpOutcome = outcome;
|
|
5770
6211
|
}
|
|
5771
6212
|
}
|
|
6213
|
+
if (!result.policyAllowed && isVerificationUnavailable(result) && (!shouldEnforce || isReadOnlyMcpMethod(parsed) && !credentials.astraId && !credentials.jwt)) {
|
|
6214
|
+
res.setHeader(VERIFICATION_STATUS_HEADER, "unavailable");
|
|
6215
|
+
return next();
|
|
6216
|
+
}
|
|
6217
|
+
if (!shouldEnforce) {
|
|
6218
|
+
if (config.setPassThroughHeader) {
|
|
6219
|
+
res.setHeader("X-Astra-Gateway-Mode", "enforced");
|
|
6220
|
+
res.setHeader("X-Astra-Gateway-Reason", "evaluated-not-enforced");
|
|
6221
|
+
}
|
|
6222
|
+
if (shouldRecordDecisions && sessionId) {
|
|
6223
|
+
recordDecision(config, sessionId, "granted").catch(() => {
|
|
6224
|
+
});
|
|
6225
|
+
}
|
|
6226
|
+
return next();
|
|
6227
|
+
}
|
|
5772
6228
|
if (!result.policyAllowed) {
|
|
5773
6229
|
if (shouldRecordDecisions && sessionId) {
|
|
5774
6230
|
recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
|
|
@@ -5788,17 +6244,6 @@ function createMcpMiddleware(options) {
|
|
|
5788
6244
|
onDenied(result, req, res);
|
|
5789
6245
|
return;
|
|
5790
6246
|
}
|
|
5791
|
-
if (!shouldEnforce) {
|
|
5792
|
-
if (config.setPassThroughHeader) {
|
|
5793
|
-
res.setHeader("X-Astra-Gateway-Mode", "enforced");
|
|
5794
|
-
res.setHeader("X-Astra-Gateway-Reason", "evaluated-not-enforced");
|
|
5795
|
-
}
|
|
5796
|
-
if (shouldRecordDecisions && sessionId) {
|
|
5797
|
-
recordDecision(config, sessionId, "granted").catch(() => {
|
|
5798
|
-
});
|
|
5799
|
-
}
|
|
5800
|
-
return next();
|
|
5801
|
-
}
|
|
5802
6247
|
if (effectiveAstraId) {
|
|
5803
6248
|
res.setHeader(
|
|
5804
6249
|
MCP_VERIFIED_HOP_HEADER,
|
|
@@ -5839,14 +6284,6 @@ function createMcpMiddleware(options) {
|
|
|
5839
6284
|
verifiedAt: /* @__PURE__ */ new Date(),
|
|
5840
6285
|
correlationId
|
|
5841
6286
|
};
|
|
5842
|
-
const catchUrls = config.apiBaseUrl ? await getWellKnownUrls(config.apiBaseUrl).catch(() => void 0) : void 0;
|
|
5843
|
-
if (catchUrls) {
|
|
5844
|
-
result.guidance = {
|
|
5845
|
-
message: `Middleware threw ${errorClass} \u2014 failing closed`,
|
|
5846
|
-
registrationUrl: catchUrls.registrationUrl,
|
|
5847
|
-
documentationUrl: catchUrls.documentationUrl
|
|
5848
|
-
};
|
|
5849
|
-
}
|
|
5850
6287
|
dedupeFailures2(result);
|
|
5851
6288
|
return onDenied(result, req, res);
|
|
5852
6289
|
}
|
|
@@ -9853,6 +10290,7 @@ export {
|
|
|
9853
10290
|
RegistrationTimeoutError,
|
|
9854
10291
|
SDK_USER_AGENT,
|
|
9855
10292
|
TRUST_LEVEL_RANGES,
|
|
10293
|
+
VERIFICATION_STATUS_HEADER,
|
|
9856
10294
|
SDK_VERSION as VERSION,
|
|
9857
10295
|
_resetEdgeConfigCache,
|
|
9858
10296
|
agent_exports as agent,
|
|
@@ -9860,9 +10298,11 @@ export {
|
|
|
9860
10298
|
awaitStepUpApproval,
|
|
9861
10299
|
buildGuidance,
|
|
9862
10300
|
buildSdkObservedMetadata,
|
|
10301
|
+
buildVerificationUnavailableBody,
|
|
9863
10302
|
clearCache,
|
|
9864
10303
|
createMcpMiddleware,
|
|
9865
10304
|
degradeToObserve,
|
|
10305
|
+
denyStatusFor,
|
|
9866
10306
|
deriveConnectionFromHeaders,
|
|
9867
10307
|
detectPlatformFingerprint,
|
|
9868
10308
|
detectRuntime,
|
|
@@ -9879,6 +10319,8 @@ export {
|
|
|
9879
10319
|
getWellKnownUrls,
|
|
9880
10320
|
hasCredentials,
|
|
9881
10321
|
isEdgeConfig,
|
|
10322
|
+
isRejectedIdentityClaim,
|
|
10323
|
+
isVerificationUnavailable,
|
|
9882
10324
|
isVerifiedHopValidFor,
|
|
9883
10325
|
matchEdgePathRule,
|
|
9884
10326
|
matchPlatformSignature,
|
|
@@ -9898,6 +10340,7 @@ export {
|
|
|
9898
10340
|
serializeVerifiedHop,
|
|
9899
10341
|
setMcpMeta,
|
|
9900
10342
|
transport_exports as transport,
|
|
10343
|
+
unavailableRetryAfterSeconds,
|
|
9901
10344
|
validatePolicy as validateLocalPolicy,
|
|
9902
10345
|
verify
|
|
9903
10346
|
};
|