@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/adapters/mcp.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Request, Response, RequestHandler } from 'express';
|
|
2
|
-
import { d as VerificationResult, G as GatewayConfig } from '../types-
|
|
2
|
+
import { d as VerificationResult, G as GatewayConfig } from '../types-BCkwBA84.mjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* X-Astra-Verified-Hop — cross-hop verify-access dedupe marker.
|
|
@@ -384,6 +384,10 @@ interface McpMiddlewareOptions extends GatewayConfig {
|
|
|
384
384
|
* record what WOULD be denied with `failOnError: 'closed'` so merchants
|
|
385
385
|
* can grep correlationId for impact analysis during the observation
|
|
386
386
|
* window. Default flips to `'closed'` in a follow-up release.
|
|
387
|
+
*
|
|
388
|
+
* 5.12.0: `'closed'` answers the retryable verification-unavailable
|
|
389
|
+
* JSON-RPC error (HTTP 503 + Retry-After, code -32003) — never an
|
|
390
|
+
* identity-shaped 401.
|
|
387
391
|
*/
|
|
388
392
|
failOnError?: 'open' | 'closed';
|
|
389
393
|
}
|
package/dist/adapters/mcp.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Request, Response, RequestHandler } from 'express';
|
|
2
|
-
import { d as VerificationResult, G as GatewayConfig } from '../types-
|
|
2
|
+
import { d as VerificationResult, G as GatewayConfig } from '../types-BCkwBA84.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* X-Astra-Verified-Hop — cross-hop verify-access dedupe marker.
|
|
@@ -384,6 +384,10 @@ interface McpMiddlewareOptions extends GatewayConfig {
|
|
|
384
384
|
* record what WOULD be denied with `failOnError: 'closed'` so merchants
|
|
385
385
|
* can grep correlationId for impact analysis during the observation
|
|
386
386
|
* window. Default flips to `'closed'` in a follow-up release.
|
|
387
|
+
*
|
|
388
|
+
* 5.12.0: `'closed'` answers the retryable verification-unavailable
|
|
389
|
+
* JSON-RPC error (HTTP 503 + Retry-After, code -32003) — never an
|
|
390
|
+
* identity-shaped 401.
|
|
387
391
|
*/
|
|
388
392
|
failOnError?: 'open' | 'closed';
|
|
389
393
|
}
|
package/dist/adapters/mcp.js
CHANGED
|
@@ -32,7 +32,7 @@ __export(mcp_exports, {
|
|
|
32
32
|
module.exports = __toCommonJS(mcp_exports);
|
|
33
33
|
|
|
34
34
|
// src/version.ts
|
|
35
|
-
var SDK_VERSION = "5.
|
|
35
|
+
var SDK_VERSION = "5.13.0";
|
|
36
36
|
|
|
37
37
|
// src/http.ts
|
|
38
38
|
var SDK_USER_AGENT = `astrasync-sdk/${SDK_VERSION}`;
|
|
@@ -102,23 +102,58 @@ function prefetchWellKnown(apiBaseUrl) {
|
|
|
102
102
|
inflight.set(apiBaseUrl, promise);
|
|
103
103
|
return promise;
|
|
104
104
|
}
|
|
105
|
-
async function getWellKnownUrls(apiBaseUrl) {
|
|
106
|
-
const entry = cache.get(apiBaseUrl);
|
|
107
|
-
if (entry) {
|
|
108
|
-
if (Date.now() - entry.fetchedAt > CACHE_TTL_MS) {
|
|
109
|
-
prefetchWellKnown(apiBaseUrl).catch(() => {
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
return entry.data;
|
|
113
|
-
}
|
|
114
|
-
const pending = inflight.get(apiBaseUrl);
|
|
115
|
-
if (pending) return pending;
|
|
116
|
-
return prefetchWellKnown(apiBaseUrl);
|
|
117
|
-
}
|
|
118
105
|
function getCachedWellKnownUrls(apiBaseUrl) {
|
|
119
106
|
return cache.get(apiBaseUrl)?.data;
|
|
120
107
|
}
|
|
121
108
|
|
|
109
|
+
// src/failure-posture.ts
|
|
110
|
+
var VERIFICATION_STATUS_HEADER = "X-Astra-Verification";
|
|
111
|
+
var DEFAULT_UNAVAILABLE_RETRY_AFTER_S = 30;
|
|
112
|
+
var REJECTED_IDENTITY_DIMENSIONS = /* @__PURE__ */ new Set([
|
|
113
|
+
"identity.invalid_agent_id",
|
|
114
|
+
"identity.unknown_agent",
|
|
115
|
+
"agent.lookup"
|
|
116
|
+
]);
|
|
117
|
+
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.";
|
|
118
|
+
var UNAVAILABLE_DIMENSIONS = /* @__PURE__ */ new Set([
|
|
119
|
+
"verify_access.api_error",
|
|
120
|
+
"middleware.internal_error"
|
|
121
|
+
]);
|
|
122
|
+
function isVerificationUnavailable(result) {
|
|
123
|
+
if (result.misconfigured) return false;
|
|
124
|
+
return (result.failures ?? []).some((f) => UNAVAILABLE_DIMENSIONS.has(f.dimension));
|
|
125
|
+
}
|
|
126
|
+
function unavailableRetryAfterSeconds(result) {
|
|
127
|
+
if (typeof result.retryAfterMs === "number" && Number.isFinite(result.retryAfterMs)) {
|
|
128
|
+
return Math.max(1, Math.ceil(result.retryAfterMs / 1e3));
|
|
129
|
+
}
|
|
130
|
+
return DEFAULT_UNAVAILABLE_RETRY_AFTER_S;
|
|
131
|
+
}
|
|
132
|
+
function buildVerificationUnavailableBody(result) {
|
|
133
|
+
return {
|
|
134
|
+
success: false,
|
|
135
|
+
error: {
|
|
136
|
+
code: "VERIFICATION_UNAVAILABLE",
|
|
137
|
+
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.",
|
|
138
|
+
retryable: true,
|
|
139
|
+
retryAfterSeconds: unavailableRetryAfterSeconds(result),
|
|
140
|
+
...result.failures && { failures: result.failures },
|
|
141
|
+
...result.correlationId && { correlationId: result.correlationId },
|
|
142
|
+
guidance: {
|
|
143
|
+
message: "Retry the request after the Retry-After interval, with exponential backoff; the condition is transient."
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function isRejectedIdentityClaim(result) {
|
|
149
|
+
return (result.failures ?? []).some((f) => REJECTED_IDENTITY_DIMENSIONS.has(f.dimension));
|
|
150
|
+
}
|
|
151
|
+
function denyStatusFor(result, policyDeniedCode) {
|
|
152
|
+
if (isRejectedIdentityClaim(result)) return { status: 403, code: "AGENT_IDENTITY_REJECTED" };
|
|
153
|
+
if (result.identityVerified) return { status: 403, code: policyDeniedCode };
|
|
154
|
+
return { status: 401, code: "UNAUTHORIZED" };
|
|
155
|
+
}
|
|
156
|
+
|
|
122
157
|
// src/verify.ts
|
|
123
158
|
var DEFAULT_CONFIG = {
|
|
124
159
|
apiBaseUrl: "https://astrasync.ai/api",
|
|
@@ -131,7 +166,7 @@ var DEFAULT_CONFIG = {
|
|
|
131
166
|
};
|
|
132
167
|
var initCheckPerformed = false;
|
|
133
168
|
var deprecationWarningShown = false;
|
|
134
|
-
async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey) {
|
|
169
|
+
async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey, counterparty) {
|
|
135
170
|
initCheckPerformed = true;
|
|
136
171
|
try {
|
|
137
172
|
const probeUrl = `${apiBaseUrl}/agents/verify-access`;
|
|
@@ -149,7 +184,8 @@ async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey) {
|
|
|
149
184
|
);
|
|
150
185
|
}
|
|
151
186
|
if (apiKey) {
|
|
152
|
-
const
|
|
187
|
+
const q = counterparty?.id ? `?counterpartyId=${encodeURIComponent(counterparty.id)}` : counterparty?.url ? `?counterpartyUrl=${encodeURIComponent(counterparty.url)}` : "";
|
|
188
|
+
const selfCheck = await sdkFetch(`${apiBaseUrl}/agents/verify-access/self-check${q}`, {
|
|
153
189
|
headers: { Authorization: `Bearer ${apiKey}` }
|
|
154
190
|
});
|
|
155
191
|
if (selfCheck.status === 401) {
|
|
@@ -161,6 +197,13 @@ async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey) {
|
|
|
161
197
|
console.error(message);
|
|
162
198
|
} else if (selfCheck.ok) {
|
|
163
199
|
const body = await selfCheck.json().catch(() => ({}));
|
|
200
|
+
if (body.counterparty && body.counterparty.canVerify === false) {
|
|
201
|
+
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.`;
|
|
202
|
+
if (strictInit) {
|
|
203
|
+
throw new Error(`${message} (strictInit=true)`);
|
|
204
|
+
}
|
|
205
|
+
console.error(message);
|
|
206
|
+
}
|
|
164
207
|
if (typeof body.daysToExpiry === "number" && body.daysToExpiry <= 7) {
|
|
165
208
|
console.warn(
|
|
166
209
|
`[VerificationGateway] the configured AstraSync API key expires in ${body.daysToExpiry} day(s) \u2014 rotate it at Settings \u2192 Developer Tools before integrations start failing.`
|
|
@@ -299,11 +342,11 @@ function createGuidanceResponse(_config, reason, options = {}) {
|
|
|
299
342
|
const isMisconfig = source === "merchant_misconfig";
|
|
300
343
|
const urls = options.urls;
|
|
301
344
|
const guidance = isMisconfig ? {
|
|
302
|
-
message: "This site's AstraSync integration is misconfigured (its API key was revoked, expired, or
|
|
345
|
+
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.",
|
|
303
346
|
registrationUrl: urls?.registrationUrl ?? "",
|
|
304
347
|
documentationUrl: options.remediation?.docsUrl ?? urls?.documentationUrl ?? "",
|
|
305
348
|
steps: [
|
|
306
|
-
"Report the failure to the site operator \u2014 their AstraSync API key needs
|
|
349
|
+
"Report the failure to the site operator \u2014 their AstraSync API key needs replacing",
|
|
307
350
|
"Do not retry: the failure is deterministic until the operator updates their key"
|
|
308
351
|
]
|
|
309
352
|
} : isApiError ? {
|
|
@@ -364,11 +407,44 @@ function createGuidanceResponse(_config, reason, options = {}) {
|
|
|
364
407
|
// coercing the absent recommendation to a policy 'deny'. Misconfig is
|
|
365
408
|
// deterministic and intentionally NOT marked retryable.
|
|
366
409
|
...isApiError && { retryable: true },
|
|
410
|
+
...options.httpStatus !== void 0 && { httpStatus: options.httpStatus },
|
|
411
|
+
...options.retryAfterMs !== void 0 && { retryAfterMs: options.retryAfterMs },
|
|
367
412
|
correlationId: options.correlationId,
|
|
368
413
|
attemptId: options.attemptId,
|
|
369
414
|
verifiedAt: /* @__PURE__ */ new Date()
|
|
370
415
|
};
|
|
371
416
|
}
|
|
417
|
+
async function readJsonSafe(response) {
|
|
418
|
+
try {
|
|
419
|
+
const parsed = await response.json();
|
|
420
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
421
|
+
} catch {
|
|
422
|
+
return {};
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
function rejectedIdentityGuidance(failures) {
|
|
426
|
+
const f = failures?.find((x) => REJECTED_IDENTITY_DIMENSIONS.has(x.dimension));
|
|
427
|
+
if (!f) return void 0;
|
|
428
|
+
if (f.dimension === "agent.lookup") {
|
|
429
|
+
return `The presented agent identifier is not registered with AstraSync. ${UNKNOWN_AGENT_GUIDANCE}`;
|
|
430
|
+
}
|
|
431
|
+
return f.guidance ? `${f.message} ${f.guidance}` : f.message;
|
|
432
|
+
}
|
|
433
|
+
var MAX_RETRY_AFTER_MS = 15 * 60 * 1e3;
|
|
434
|
+
function parseRetryAfterMs(response) {
|
|
435
|
+
const raw = response.headers?.get?.("retry-after");
|
|
436
|
+
if (!raw) return void 0;
|
|
437
|
+
const trimmed = raw.trim();
|
|
438
|
+
let ms;
|
|
439
|
+
if (/^\d+(\.\d+)?$/.test(trimmed)) {
|
|
440
|
+
ms = Number(trimmed) * 1e3;
|
|
441
|
+
} else {
|
|
442
|
+
const at = Date.parse(trimmed);
|
|
443
|
+
if (Number.isNaN(at)) return void 0;
|
|
444
|
+
ms = at - Date.now();
|
|
445
|
+
}
|
|
446
|
+
return Math.min(MAX_RETRY_AFTER_MS, Math.max(0, Math.round(ms)));
|
|
447
|
+
}
|
|
372
448
|
async function callVerifyAccessAPI(config, request) {
|
|
373
449
|
const { credentials, ...requestData } = request;
|
|
374
450
|
const body = {
|
|
@@ -425,7 +501,8 @@ async function callVerifyAccessAPI(config, request) {
|
|
|
425
501
|
body: JSON.stringify(body),
|
|
426
502
|
signal: AbortSignal.timeout(config.verifyTimeoutMs ?? 1e4)
|
|
427
503
|
});
|
|
428
|
-
const data = await response
|
|
504
|
+
const data = await readJsonSafe(response);
|
|
505
|
+
const retryAfterMs = parseRetryAfterMs(response);
|
|
429
506
|
if (response.status === 410) {
|
|
430
507
|
return {
|
|
431
508
|
success: true,
|
|
@@ -463,13 +540,31 @@ async function callVerifyAccessAPI(config, request) {
|
|
|
463
540
|
}
|
|
464
541
|
};
|
|
465
542
|
}
|
|
543
|
+
if (response.status === 404 && credentials.astraId && (data?.error === "AGENT_NOT_FOUND" || typeof data?.message === "string" && /agent/i.test(data.message))) {
|
|
544
|
+
return {
|
|
545
|
+
success: true,
|
|
546
|
+
attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0,
|
|
547
|
+
access: {
|
|
548
|
+
allowed: false,
|
|
549
|
+
reason: "unknown_agent",
|
|
550
|
+
failures: [
|
|
551
|
+
{
|
|
552
|
+
dimension: "identity.unknown_agent",
|
|
553
|
+
message: "The presented agent identifier is not registered with AstraSync.",
|
|
554
|
+
guidance: UNKNOWN_AGENT_GUIDANCE
|
|
555
|
+
}
|
|
556
|
+
]
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
}
|
|
466
560
|
if (!response.ok) {
|
|
467
|
-
|
|
561
|
+
const keyMismatch = response.status === 403 && data?.error === "COUNTERPARTY_KEY_MISMATCH";
|
|
562
|
+
if (response.status === 401 || keyMismatch) {
|
|
468
563
|
return {
|
|
469
564
|
success: false,
|
|
470
565
|
error: data.message || data.error || "Integration API key rejected",
|
|
471
566
|
misconfig: true,
|
|
472
|
-
misconfigCode: typeof data?.code === "string" ? data.code : void 0,
|
|
567
|
+
misconfigCode: keyMismatch ? "counterparty_key_mismatch" : typeof data?.code === "string" ? data.code : void 0,
|
|
473
568
|
remediation: data?.remediation && typeof data.remediation === "object" ? data.remediation : void 0,
|
|
474
569
|
correlationId: typeof data?.correlationId === "string" ? data.correlationId : void 0,
|
|
475
570
|
attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0
|
|
@@ -477,9 +572,11 @@ async function callVerifyAccessAPI(config, request) {
|
|
|
477
572
|
}
|
|
478
573
|
return {
|
|
479
574
|
success: false,
|
|
480
|
-
error: data.message || data.error || `API returned ${response.status}`,
|
|
575
|
+
error: typeof data?.message === "string" && data.message || typeof data?.error === "string" && data.error || `API returned ${response.status}`,
|
|
481
576
|
correlationId: typeof data?.correlationId === "string" ? data.correlationId : void 0,
|
|
482
|
-
attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0
|
|
577
|
+
attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0,
|
|
578
|
+
httpStatus: response.status,
|
|
579
|
+
...retryAfterMs !== void 0 && { retryAfterMs }
|
|
483
580
|
};
|
|
484
581
|
}
|
|
485
582
|
return data;
|
|
@@ -500,14 +597,22 @@ async function verify(config, request, options) {
|
|
|
500
597
|
mergedConfig.apiBaseUrl,
|
|
501
598
|
mergedConfig.debug,
|
|
502
599
|
true,
|
|
503
|
-
mergedConfig.apiKey
|
|
600
|
+
mergedConfig.apiKey,
|
|
601
|
+
{
|
|
602
|
+
id: mergedConfig.counterpartyId,
|
|
603
|
+
url: mergedConfig.counterpartyUrl ?? request.counterpartyUrl
|
|
604
|
+
}
|
|
504
605
|
);
|
|
505
606
|
} else {
|
|
506
607
|
void performInitCheck(
|
|
507
608
|
mergedConfig.apiBaseUrl,
|
|
508
609
|
mergedConfig.debug,
|
|
509
610
|
false,
|
|
510
|
-
mergedConfig.apiKey
|
|
611
|
+
mergedConfig.apiKey,
|
|
612
|
+
{
|
|
613
|
+
id: mergedConfig.counterpartyId,
|
|
614
|
+
url: mergedConfig.counterpartyUrl ?? request.counterpartyUrl
|
|
615
|
+
}
|
|
511
616
|
);
|
|
512
617
|
}
|
|
513
618
|
}
|
|
@@ -551,12 +656,14 @@ async function verify(config, request, options) {
|
|
|
551
656
|
correlationId: apiResponse.correlationId,
|
|
552
657
|
attemptId: apiResponse.attemptId,
|
|
553
658
|
remediation: apiResponse.remediation,
|
|
659
|
+
httpStatus: apiResponse.httpStatus,
|
|
660
|
+
retryAfterMs: apiResponse.retryAfterMs,
|
|
554
661
|
urls
|
|
555
662
|
});
|
|
556
663
|
}
|
|
557
664
|
if (!apiResponse.access?.allowed) {
|
|
558
665
|
const aggregatedFailures = apiResponse.access?.failures;
|
|
559
|
-
const idVerifiedFromBackend = apiResponse.verificationContext?.idVerified === true;
|
|
666
|
+
const idVerifiedFromBackend = apiResponse.verificationContext?.idVerified === true && !aggregatedFailures?.some((f) => REJECTED_IDENTITY_DIMENSIONS.has(f.dimension));
|
|
560
667
|
const result2 = {
|
|
561
668
|
identityVerified: idVerifiedFromBackend,
|
|
562
669
|
policyAllowed: false,
|
|
@@ -568,7 +675,10 @@ async function verify(config, request, options) {
|
|
|
568
675
|
requiresStepUp: apiResponse.access?.requiresStepUp,
|
|
569
676
|
requiresApproval: apiResponse.access?.requiresApproval,
|
|
570
677
|
guidance: {
|
|
571
|
-
|
|
678
|
+
// 5.12.0: a presented-but-rejected id (malformed / unknown) carries
|
|
679
|
+
// actionable registration guidance on its failure — surface it
|
|
680
|
+
// instead of the bare machine reason ('unknown_agent').
|
|
681
|
+
message: rejectedIdentityGuidance(aggregatedFailures) ?? (apiResponse.access?.reason || "Access denied by PDLSS policy"),
|
|
572
682
|
registrationUrl: urls?.registrationUrl ?? "",
|
|
573
683
|
documentationUrl: urls?.documentationUrl ?? "",
|
|
574
684
|
// 5.1.0: anonymous callers land on this branch — steer MCP-hosted
|
|
@@ -668,7 +778,7 @@ async function verify(config, request, options) {
|
|
|
668
778
|
registrationUrl: urls?.registrationUrl ?? "",
|
|
669
779
|
documentationUrl: urls?.documentationUrl ?? ""
|
|
670
780
|
};
|
|
671
|
-
} else if (result.recommendation === "step_up_required") {
|
|
781
|
+
} else if (result.recommendation === "step_up_required" && result.identityVerified) {
|
|
672
782
|
result.requiresStepUp = true;
|
|
673
783
|
result.denialReasons = serverDenialReasons ?? result.recommendationReasons ?? ["Step-up verification required"];
|
|
674
784
|
}
|
|
@@ -1142,8 +1252,37 @@ function dedupeFailures(result) {
|
|
|
1142
1252
|
result.denialReasons = [...new Set(result.denialReasons)];
|
|
1143
1253
|
}
|
|
1144
1254
|
}
|
|
1255
|
+
function isReadOnlyMcpMethod(parsed) {
|
|
1256
|
+
return parsed.method !== "tools/call";
|
|
1257
|
+
}
|
|
1145
1258
|
function defaultMcpDenied(result, req, res) {
|
|
1146
1259
|
const id = req.body?.id ?? null;
|
|
1260
|
+
if (isVerificationUnavailable(result)) {
|
|
1261
|
+
const retryAfterSeconds = unavailableRetryAfterSeconds(result);
|
|
1262
|
+
const body = buildVerificationUnavailableBody(result).error;
|
|
1263
|
+
res.setHeader("X-Astra-Gateway-Mode", "enforced");
|
|
1264
|
+
res.setHeader(VERIFICATION_STATUS_HEADER, "unavailable");
|
|
1265
|
+
res.setHeader("Retry-After", String(retryAfterSeconds));
|
|
1266
|
+
res.status(503).json({
|
|
1267
|
+
jsonrpc: "2.0",
|
|
1268
|
+
id,
|
|
1269
|
+
error: {
|
|
1270
|
+
// -32003 → verification temporarily unavailable (retryable)
|
|
1271
|
+
code: -32003,
|
|
1272
|
+
message: body.message,
|
|
1273
|
+
data: {
|
|
1274
|
+
code: body.code,
|
|
1275
|
+
recommendation: "error",
|
|
1276
|
+
retryable: true,
|
|
1277
|
+
retryAfterSeconds,
|
|
1278
|
+
guidance: body.guidance,
|
|
1279
|
+
failures: result.failures,
|
|
1280
|
+
correlationId: result.correlationId
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
});
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1147
1286
|
if (result.misconfigured) {
|
|
1148
1287
|
res.setHeader("X-Astra-Gateway-Mode", "enforced");
|
|
1149
1288
|
res.status(503).json({
|
|
@@ -1162,7 +1301,7 @@ function defaultMcpDenied(result, req, res) {
|
|
|
1162
1301
|
});
|
|
1163
1302
|
return;
|
|
1164
1303
|
}
|
|
1165
|
-
const status
|
|
1304
|
+
const { status, code: denyCode } = denyStatusFor(result, "INSUFFICIENT_ACCESS");
|
|
1166
1305
|
res.setHeader("X-Astra-Gateway-Mode", "enforced");
|
|
1167
1306
|
res.status(status).json({
|
|
1168
1307
|
jsonrpc: "2.0",
|
|
@@ -1172,9 +1311,11 @@ function defaultMcpDenied(result, req, res) {
|
|
|
1172
1311
|
// -32000 → unauthorized (no identity resolved)
|
|
1173
1312
|
// -32001 → insufficient access (identity OK, policy denied)
|
|
1174
1313
|
// -32002 → merchant verification misconfigured (handled above)
|
|
1314
|
+
// -32003 → verification temporarily unavailable (handled above)
|
|
1175
1315
|
code: !result.identityVerified ? -32e3 : -32001,
|
|
1176
1316
|
message: result.denialReasons?.[0] ?? "Access denied",
|
|
1177
1317
|
data: {
|
|
1318
|
+
code: denyCode,
|
|
1178
1319
|
guidance: result.guidance,
|
|
1179
1320
|
// Round-10: aggregated per-dimension detail + correlation handle.
|
|
1180
1321
|
failures: result.failures,
|
|
@@ -1302,6 +1443,9 @@ function createMcpMiddleware(options) {
|
|
|
1302
1443
|
resolved_action: pdlss.action
|
|
1303
1444
|
});
|
|
1304
1445
|
}
|
|
1446
|
+
if (!pdlss.purpose && !shouldEnforce) {
|
|
1447
|
+
return next();
|
|
1448
|
+
}
|
|
1305
1449
|
if (!pdlss.purpose) {
|
|
1306
1450
|
const id = req.body?.id ?? null;
|
|
1307
1451
|
res.status(400).json({
|
|
@@ -1321,6 +1465,9 @@ function createMcpMiddleware(options) {
|
|
|
1321
1465
|
}
|
|
1322
1466
|
const counterpartyUrl = config.counterpartyUrl || `${req.protocol}://${req.get("host")}${req.path}`;
|
|
1323
1467
|
const shouldRecordDecisions = recordDecisions !== false;
|
|
1468
|
+
const forwardedFor = req.headers["x-forwarded-for"];
|
|
1469
|
+
const forwardedForStr = Array.isArray(forwardedFor) ? forwardedFor.join(", ") : forwardedFor;
|
|
1470
|
+
const sourceIp = req.ip ?? (forwardedForStr ? forwardedForStr.split(",")[0].trim() : void 0);
|
|
1324
1471
|
const verificationRequest = {
|
|
1325
1472
|
credentials,
|
|
1326
1473
|
purpose: pdlss.purpose,
|
|
@@ -1335,9 +1482,10 @@ function createMcpMiddleware(options) {
|
|
|
1335
1482
|
counterpartyType: config.counterpartyType || "mcp_server",
|
|
1336
1483
|
enableRuntimeChallenge,
|
|
1337
1484
|
callerMetadata: {
|
|
1338
|
-
sourceIp
|
|
1485
|
+
sourceIp,
|
|
1339
1486
|
userAgent: req.headers["user-agent"],
|
|
1340
1487
|
host: req.headers.host,
|
|
1488
|
+
...forwardedForStr && { forwardedFor: forwardedForStr },
|
|
1341
1489
|
// Capture EVERY inbound header (parity with the express adapter —
|
|
1342
1490
|
// MCP-only merchants have no edge to do it upstream) plus the
|
|
1343
1491
|
// CDN-derived connection block. Local string work, secrets
|
|
@@ -1360,6 +1508,21 @@ function createMcpMiddleware(options) {
|
|
|
1360
1508
|
result.stepUpOutcome = outcome;
|
|
1361
1509
|
}
|
|
1362
1510
|
}
|
|
1511
|
+
if (!result.policyAllowed && isVerificationUnavailable(result) && (!shouldEnforce || isReadOnlyMcpMethod(parsed) && !credentials.astraId && !credentials.jwt)) {
|
|
1512
|
+
res.setHeader(VERIFICATION_STATUS_HEADER, "unavailable");
|
|
1513
|
+
return next();
|
|
1514
|
+
}
|
|
1515
|
+
if (!shouldEnforce) {
|
|
1516
|
+
if (config.setPassThroughHeader) {
|
|
1517
|
+
res.setHeader("X-Astra-Gateway-Mode", "enforced");
|
|
1518
|
+
res.setHeader("X-Astra-Gateway-Reason", "evaluated-not-enforced");
|
|
1519
|
+
}
|
|
1520
|
+
if (shouldRecordDecisions && sessionId) {
|
|
1521
|
+
recordDecision(config, sessionId, "granted").catch(() => {
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1524
|
+
return next();
|
|
1525
|
+
}
|
|
1363
1526
|
if (!result.policyAllowed) {
|
|
1364
1527
|
if (shouldRecordDecisions && sessionId) {
|
|
1365
1528
|
recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
|
|
@@ -1379,17 +1542,6 @@ function createMcpMiddleware(options) {
|
|
|
1379
1542
|
onDenied(result, req, res);
|
|
1380
1543
|
return;
|
|
1381
1544
|
}
|
|
1382
|
-
if (!shouldEnforce) {
|
|
1383
|
-
if (config.setPassThroughHeader) {
|
|
1384
|
-
res.setHeader("X-Astra-Gateway-Mode", "enforced");
|
|
1385
|
-
res.setHeader("X-Astra-Gateway-Reason", "evaluated-not-enforced");
|
|
1386
|
-
}
|
|
1387
|
-
if (shouldRecordDecisions && sessionId) {
|
|
1388
|
-
recordDecision(config, sessionId, "granted").catch(() => {
|
|
1389
|
-
});
|
|
1390
|
-
}
|
|
1391
|
-
return next();
|
|
1392
|
-
}
|
|
1393
1545
|
if (effectiveAstraId) {
|
|
1394
1546
|
res.setHeader(
|
|
1395
1547
|
MCP_VERIFIED_HOP_HEADER,
|
|
@@ -1430,14 +1582,6 @@ function createMcpMiddleware(options) {
|
|
|
1430
1582
|
verifiedAt: /* @__PURE__ */ new Date(),
|
|
1431
1583
|
correlationId
|
|
1432
1584
|
};
|
|
1433
|
-
const catchUrls = config.apiBaseUrl ? await getWellKnownUrls(config.apiBaseUrl).catch(() => void 0) : void 0;
|
|
1434
|
-
if (catchUrls) {
|
|
1435
|
-
result.guidance = {
|
|
1436
|
-
message: `Middleware threw ${errorClass} \u2014 failing closed`,
|
|
1437
|
-
registrationUrl: catchUrls.registrationUrl,
|
|
1438
|
-
documentationUrl: catchUrls.documentationUrl
|
|
1439
|
-
};
|
|
1440
|
-
}
|
|
1441
1585
|
dedupeFailures(result);
|
|
1442
1586
|
return onDenied(result, req, res);
|
|
1443
1587
|
}
|