@astrasyncai/verification-gateway 2.4.12 → 2.4.14
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/dist/adapter-interface/interface.d.mts +2 -2
- package/dist/adapter-interface/interface.d.ts +2 -2
- package/dist/adapters/express.d.mts +2 -2
- package/dist/adapters/express.d.ts +2 -2
- package/dist/adapters/express.js +125 -35
- package/dist/adapters/express.js.map +1 -1
- package/dist/adapters/express.mjs +125 -35
- package/dist/adapters/express.mjs.map +1 -1
- package/dist/adapters/mcp.d.mts +26 -4
- package/dist/adapters/mcp.d.ts +26 -4
- package/dist/adapters/mcp.js +94 -28
- package/dist/adapters/mcp.js.map +1 -1
- package/dist/adapters/mcp.mjs +94 -28
- package/dist/adapters/mcp.mjs.map +1 -1
- package/dist/adapters/nextjs.d.mts +2 -2
- package/dist/adapters/nextjs.d.ts +2 -2
- package/dist/adapters/nextjs.js +71 -28
- package/dist/adapters/nextjs.js.map +1 -1
- package/dist/adapters/nextjs.mjs +71 -28
- package/dist/adapters/nextjs.mjs.map +1 -1
- package/dist/adapters/sdk.d.mts +2 -2
- package/dist/adapters/sdk.d.ts +2 -2
- package/dist/adapters/sdk.js +45 -22
- package/dist/adapters/sdk.js.map +1 -1
- package/dist/adapters/sdk.mjs +45 -22
- package/dist/adapters/sdk.mjs.map +1 -1
- package/dist/agent/index.d.mts +2 -2
- package/dist/agent/index.d.ts +2 -2
- package/dist/agent/index.js +29 -0
- package/dist/agent/index.js.map +1 -1
- package/dist/agent/index.mjs +29 -0
- package/dist/agent/index.mjs.map +1 -1
- package/dist/browser/background.js +86 -24
- package/dist/browser/background.js.map +1 -1
- package/dist/browser/background.mjs +86 -24
- package/dist/browser/background.mjs.map +1 -1
- package/dist/browser/browser-adapter.d.mts +2 -2
- package/dist/browser/browser-adapter.d.ts +2 -2
- package/dist/cli/index.d.mts +2 -2
- package/dist/cli/index.d.ts +2 -2
- package/dist/cursor/cursor-adapter.d.mts +2 -2
- package/dist/cursor/cursor-adapter.d.ts +2 -2
- package/dist/cursor/extension.d.mts +2 -2
- package/dist/cursor/extension.d.ts +2 -2
- package/dist/cursor/extension.js +86 -24
- package/dist/cursor/extension.js.map +1 -1
- package/dist/cursor/extension.mjs +86 -24
- package/dist/cursor/extension.mjs.map +1 -1
- package/dist/{express-C1ePFB7n.d.ts → express-CrfwoNAR.d.ts} +1 -1
- package/dist/{express-4WStX3PV.d.mts → express-ienhAXps.d.mts} +1 -1
- package/dist/gateway/gateway.d.mts +2 -2
- package/dist/gateway/gateway.d.ts +2 -2
- package/dist/gateway/gateway.js +86 -24
- package/dist/gateway/gateway.js.map +1 -1
- package/dist/gateway/gateway.mjs +86 -24
- package/dist/gateway/gateway.mjs.map +1 -1
- package/dist/git-trigger/git-hooks.d.mts +2 -2
- package/dist/git-trigger/git-hooks.d.ts +2 -2
- package/dist/{index-ChPX4WHl.d.mts → index-B5e2IDWU.d.mts} +1 -1
- package/dist/{index-CzJMCgEy.d.ts → index-CCdZxvAr.d.ts} +71 -6
- package/dist/{index-D8IEntil.d.mts → index-CEg_WG6y.d.mts} +71 -6
- package/dist/{index-Cjm-zBeZ.d.ts → index-DC5f8eoQ.d.ts} +1 -1
- package/dist/index.d.mts +7 -7
- package/dist/index.d.ts +7 -7
- package/dist/index.js +336 -71
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +336 -71
- package/dist/index.mjs.map +1 -1
- package/dist/local-evaluator/evaluator.d.mts +2 -2
- package/dist/local-evaluator/evaluator.d.ts +2 -2
- package/dist/local-evaluator/evaluator.js +12 -2
- package/dist/local-evaluator/evaluator.js.map +1 -1
- package/dist/local-evaluator/evaluator.mjs +12 -2
- package/dist/local-evaluator/evaluator.mjs.map +1 -1
- package/dist/{nextjs-BIORS__0.d.ts → nextjs-66R1KW8e.d.ts} +1 -1
- package/dist/{nextjs-CjzHdaXA.d.mts → nextjs-DSpisQst.d.mts} +1 -1
- package/dist/{sdk-Chhz-FcT.d.mts → sdk-5U_CBRpr.d.mts} +1 -1
- package/dist/{sdk-CqTEQAc6.d.ts → sdk-Bm8np66n.d.ts} +1 -1
- package/dist/transport/index.d.mts +2 -2
- package/dist/transport/index.d.ts +2 -2
- package/dist/transport/index.js +146 -28
- package/dist/transport/index.js.map +1 -1
- package/dist/transport/index.mjs +146 -28
- package/dist/transport/index.mjs.map +1 -1
- package/dist/{types-L15pYd2c.d.mts → types-B3USs-Kx.d.mts} +42 -1
- package/dist/{types-L15pYd2c.d.ts → types-B3USs-Kx.d.ts} +42 -1
- package/dist/{types-DNK2BgIf.d.mts → types-CgDCUfo8.d.mts} +1 -1
- package/dist/{types-DoWIuzfj.d.ts → types-R5N4ET6x.d.ts} +1 -1
- package/dist/ui/index.d.mts +1 -1
- package/dist/ui/index.d.ts +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -189,7 +189,7 @@ function getCapabilities(accessLevel) {
|
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
// src/version.ts
|
|
192
|
-
var SDK_VERSION = "2.4.
|
|
192
|
+
var SDK_VERSION = "2.4.13";
|
|
193
193
|
|
|
194
194
|
// src/verify.ts
|
|
195
195
|
var DEFAULT_CONFIG = {
|
|
@@ -208,22 +208,27 @@ var DEFAULT_CONFIG = {
|
|
|
208
208
|
};
|
|
209
209
|
var initCheckPerformed = false;
|
|
210
210
|
var deprecationWarningShown = false;
|
|
211
|
-
async function performInitCheck(apiBaseUrl, debug) {
|
|
211
|
+
async function performInitCheck(apiBaseUrl, debug, strictInit) {
|
|
212
212
|
initCheckPerformed = true;
|
|
213
213
|
try {
|
|
214
214
|
const probeUrl = `${apiBaseUrl}/agents/verify-access`;
|
|
215
215
|
const response = await fetch(probeUrl, { method: "HEAD" });
|
|
216
216
|
const contentType = response.headers.get("content-type") ?? "";
|
|
217
217
|
if (contentType.startsWith("text/html")) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
218
|
+
const message = `[VerificationGateway] apiBaseUrl '${apiBaseUrl}' returned HTML (content-type: ${contentType}). This usually means apiBaseUrl is pointing at a marketing site instead of the API. Expected: 'https://astrasync.ai/api' (prod) or 'https://staging.astrasync.ai/api' (staging).`;
|
|
219
|
+
if (strictInit) {
|
|
220
|
+
throw new Error(`${message} (strictInit=true)`);
|
|
221
|
+
}
|
|
222
|
+
console.warn(`${message} Set disableInitChecks: true on GatewayConfig to silence.`);
|
|
221
223
|
} else if (debug) {
|
|
222
224
|
console.log(
|
|
223
225
|
`[VerificationGateway] init check passed for ${apiBaseUrl} (content-type: ${contentType})`
|
|
224
226
|
);
|
|
225
227
|
}
|
|
226
228
|
} catch (err) {
|
|
229
|
+
if (strictInit) {
|
|
230
|
+
throw err;
|
|
231
|
+
}
|
|
227
232
|
if (debug) {
|
|
228
233
|
console.log(`[VerificationGateway] init check failed (non-blocking): ${String(err)}`);
|
|
229
234
|
}
|
|
@@ -247,7 +252,23 @@ function getCacheKey(request) {
|
|
|
247
252
|
request.counterpartyType || "",
|
|
248
253
|
request.isSubAgentRequest ? "1" : "0",
|
|
249
254
|
request.parentAgentId || "",
|
|
250
|
-
request.subAgentDepth ?? ""
|
|
255
|
+
request.subAgentDepth ?? "",
|
|
256
|
+
// Audit F-A1-07: previously-missing dimensions that DO affect the
|
|
257
|
+
// backend verdict. Without these, two requests with different
|
|
258
|
+
// durations (e.g. 60s vs 86400s) collided on the same cache key and
|
|
259
|
+
// the shorter-duration allow served the longer-duration request.
|
|
260
|
+
request.durationRequired ?? "",
|
|
261
|
+
request.invocationProtocol || "",
|
|
262
|
+
request.enableRuntimeChallenge ? "1" : "0",
|
|
263
|
+
// callerMetadata fields contribute to risk model; include the ones
|
|
264
|
+
// backend reads. sourceIp/userAgent/forwardedFor change per-request
|
|
265
|
+
// so their inclusion effectively forces a re-check for any varying
|
|
266
|
+
// client (the right behavior — IP-driven anomaly scoring shouldn't
|
|
267
|
+
// be cached across IPs).
|
|
268
|
+
request.callerMetadata?.sourceIp || "",
|
|
269
|
+
request.callerMetadata?.userAgent || "",
|
|
270
|
+
request.callerMetadata?.forwardedFor || "",
|
|
271
|
+
request.callerMetadata?.agentCardUrl || ""
|
|
251
272
|
].join("|");
|
|
252
273
|
}
|
|
253
274
|
function getCachedResult(request) {
|
|
@@ -276,9 +297,13 @@ function clearCache() {
|
|
|
276
297
|
}
|
|
277
298
|
function extractCredentials(headers, query) {
|
|
278
299
|
const credentials = {};
|
|
300
|
+
const ASTRA_ID_PATTERN = /^ASTRAE?-[A-Za-z0-9_-]{1,64}$/;
|
|
279
301
|
const astraIdHeader = headers["x-astra-id"] || headers["X-Astra-Id"] || headers["X-ASTRA-ID"] || headers["x-astra-agentid"] || headers["X-Astra-AgentId"] || headers["x-astra-agent-id"] || headers["X-Astra-Agent-Id"] || headers["X-ASTRA-AGENT-ID"];
|
|
280
302
|
if (astraIdHeader) {
|
|
281
|
-
|
|
303
|
+
const raw = Array.isArray(astraIdHeader) ? astraIdHeader[0] : typeof astraIdHeader === "string" ? astraIdHeader : void 0;
|
|
304
|
+
if (typeof raw === "string" && ASTRA_ID_PATTERN.test(raw)) {
|
|
305
|
+
credentials.astraId = raw;
|
|
306
|
+
}
|
|
282
307
|
}
|
|
283
308
|
const apiKeyHeader = headers["x-api-key"] || headers["X-Api-Key"] || headers["X-API-KEY"];
|
|
284
309
|
if (apiKeyHeader) {
|
|
@@ -287,9 +312,11 @@ function extractCredentials(headers, query) {
|
|
|
287
312
|
const authHeader = headers["authorization"] || headers["Authorization"];
|
|
288
313
|
if (authHeader) {
|
|
289
314
|
const authValue = Array.isArray(authHeader) ? authHeader[0] : authHeader;
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
315
|
+
if (typeof authValue === "string") {
|
|
316
|
+
credentials.authorizationHeader = authValue;
|
|
317
|
+
if (authValue.startsWith("Bearer ")) {
|
|
318
|
+
credentials.jwt = authValue.slice(7);
|
|
319
|
+
}
|
|
293
320
|
}
|
|
294
321
|
}
|
|
295
322
|
if (query) {
|
|
@@ -310,7 +337,7 @@ function createGuidanceResponse(config, reason, options = {}) {
|
|
|
310
337
|
const isApiError = source === "api_error";
|
|
311
338
|
const guidance = isApiError ? {
|
|
312
339
|
message: "Verification is temporarily unavailable. Retry with exponential backoff; if the issue persists, contact support with the correlationId.",
|
|
313
|
-
registrationUrl: `${config.apiBaseUrl.replace("/api", "")}/register`,
|
|
340
|
+
registrationUrl: `${config.apiBaseUrl.replace("/api", "")}/agents/register`,
|
|
314
341
|
documentationUrl: `${config.apiBaseUrl.replace("/api", "")}/docs/agent-access`,
|
|
315
342
|
steps: [
|
|
316
343
|
"Retry the request with exponential backoff",
|
|
@@ -318,7 +345,7 @@ function createGuidanceResponse(config, reason, options = {}) {
|
|
|
318
345
|
]
|
|
319
346
|
} : {
|
|
320
347
|
message: "This service verifies AI agents before granting access. Please register your agent with AstraSync.",
|
|
321
|
-
registrationUrl: `${config.apiBaseUrl.replace("/api", "")}/register`,
|
|
348
|
+
registrationUrl: `${config.apiBaseUrl.replace("/api", "")}/agents/register`,
|
|
322
349
|
documentationUrl: `${config.apiBaseUrl.replace("/api", "")}/docs/agent-access`,
|
|
323
350
|
steps: [
|
|
324
351
|
"Register for an AstraSync account",
|
|
@@ -395,12 +422,8 @@ async function callVerifyAccessAPI(config, request) {
|
|
|
395
422
|
"Content-Type": "application/json",
|
|
396
423
|
...config.customHeaders
|
|
397
424
|
};
|
|
398
|
-
if (credentials.authorizationHeader) {
|
|
399
|
-
headers["Authorization"] = credentials.authorizationHeader;
|
|
400
|
-
} else if (config.apiKey) {
|
|
401
|
-
headers["Authorization"] = `Bearer ${config.apiKey}`;
|
|
402
|
-
}
|
|
403
425
|
if (config.apiKey) {
|
|
426
|
+
headers["Authorization"] = `Bearer ${config.apiKey}`;
|
|
404
427
|
headers["X-API-Key"] = config.apiKey;
|
|
405
428
|
}
|
|
406
429
|
try {
|
|
@@ -446,7 +469,11 @@ async function callVerifyAccessAPI(config, request) {
|
|
|
446
469
|
async function verify(config, request) {
|
|
447
470
|
const mergedConfig = { ...DEFAULT_CONFIG, ...config };
|
|
448
471
|
if (!initCheckPerformed && !mergedConfig.disableInitChecks && mergedConfig.apiBaseUrl) {
|
|
449
|
-
|
|
472
|
+
if (mergedConfig.strictInit) {
|
|
473
|
+
await performInitCheck(mergedConfig.apiBaseUrl, mergedConfig.debug, true);
|
|
474
|
+
} else {
|
|
475
|
+
void performInitCheck(mergedConfig.apiBaseUrl, mergedConfig.debug, false);
|
|
476
|
+
}
|
|
450
477
|
}
|
|
451
478
|
if (!deprecationWarningShown && (config.minTrustScore !== void 0 || config.minTrustScoreForFull !== void 0)) {
|
|
452
479
|
deprecationWarningShown = true;
|
|
@@ -500,7 +527,7 @@ async function verify(config, request) {
|
|
|
500
527
|
requiresApproval: apiResponse.access?.requiresApproval,
|
|
501
528
|
guidance: {
|
|
502
529
|
message: apiResponse.access?.reason || "Access denied by PDLSS policy",
|
|
503
|
-
registrationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/register`,
|
|
530
|
+
registrationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/agents/register`,
|
|
504
531
|
documentationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/docs/pdlss`
|
|
505
532
|
},
|
|
506
533
|
verifiedAt: /* @__PURE__ */ new Date(),
|
|
@@ -570,13 +597,15 @@ async function verify(config, request) {
|
|
|
570
597
|
result.denialReasons = result.recommendationReasons || [
|
|
571
598
|
"Access denied by AstraSync recommendation"
|
|
572
599
|
];
|
|
573
|
-
|
|
574
|
-
result.
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
600
|
+
result.guidance = result.runtimeChallenge ? {
|
|
601
|
+
message: `Verification failed: ${result.runtimeChallenge.reason || "runtime challenge failed"}`,
|
|
602
|
+
registrationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/agents/register`,
|
|
603
|
+
documentationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/docs/runtime-challenge`
|
|
604
|
+
} : {
|
|
605
|
+
message: result.recommendationReasons?.[0] || "Access denied by AstraSync recommendation",
|
|
606
|
+
registrationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/agents/register`,
|
|
607
|
+
documentationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/docs/pdlss`
|
|
608
|
+
};
|
|
580
609
|
} else if (result.recommendation === "step_up_required") {
|
|
581
610
|
result.requiresStepUp = true;
|
|
582
611
|
if (ACCESS_LEVEL_HIERARCHY[result.accessLevel] > ACCESS_LEVEL_HIERARCHY["read-only"]) {
|
|
@@ -825,18 +854,40 @@ function defaultExtractPurpose(req) {
|
|
|
825
854
|
return "general";
|
|
826
855
|
}
|
|
827
856
|
}
|
|
828
|
-
function matchRoute(pattern, path) {
|
|
857
|
+
function matchRoute(pattern, path, opts) {
|
|
829
858
|
const regexPattern = pattern.replace(/\*/g, ".*").replace(/\//g, "\\/");
|
|
830
|
-
const
|
|
831
|
-
|
|
859
|
+
const caseSensitiveRegex = new RegExp(`^${regexPattern}$`);
|
|
860
|
+
const caseSensitiveResult = caseSensitiveRegex.test(path);
|
|
861
|
+
if (!opts?.caseInsensitive && !opts?.logShadowDivergence) {
|
|
862
|
+
return caseSensitiveResult;
|
|
863
|
+
}
|
|
864
|
+
const caseInsensitiveRegex = new RegExp(`^${regexPattern}$`, "i");
|
|
865
|
+
const caseInsensitiveResult = caseInsensitiveRegex.test(path);
|
|
866
|
+
if (opts?.logShadowDivergence && caseSensitiveResult !== caseInsensitiveResult) {
|
|
867
|
+
console.warn(
|
|
868
|
+
`[SHADOW] matchRoute case-insensitive would change result: pattern=${pattern} path=${path} caseSensitive=${caseSensitiveResult} caseInsensitive=${caseInsensitiveResult} correlationId=${opts.correlationId ?? "unknown"}`
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
return opts?.caseInsensitive ? caseInsensitiveResult : caseSensitiveResult;
|
|
832
872
|
}
|
|
833
|
-
function findRouteConfig(routes, path, method) {
|
|
873
|
+
function findRouteConfig(routes, path, method, opts) {
|
|
834
874
|
return routes.find((route) => {
|
|
835
875
|
const methodMatches = route.method === "*" || route.method.toUpperCase() === method.toUpperCase();
|
|
836
|
-
const pathMatches = matchRoute(route.pattern, path);
|
|
876
|
+
const pathMatches = matchRoute(route.pattern, path, opts);
|
|
837
877
|
return methodMatches && pathMatches;
|
|
838
878
|
});
|
|
839
879
|
}
|
|
880
|
+
function dedupeFailures(result) {
|
|
881
|
+
if (result.failures && result.failures.length > 1) {
|
|
882
|
+
const seen = /* @__PURE__ */ new Set();
|
|
883
|
+
result.failures = result.failures.filter((f) => {
|
|
884
|
+
const key = `${f.dimension}|${f.message}|${f.guidance ?? ""}`;
|
|
885
|
+
if (seen.has(key)) return false;
|
|
886
|
+
seen.add(key);
|
|
887
|
+
return true;
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
}
|
|
840
891
|
function defaultOnDenied(result, _req, res) {
|
|
841
892
|
const statusCode = !result.identityVerified ? 401 : 403;
|
|
842
893
|
res.setHeader("X-Astra-Gateway-Mode", "enforced");
|
|
@@ -863,6 +914,8 @@ function createMiddleware(options) {
|
|
|
863
914
|
recordDecisions,
|
|
864
915
|
enableRuntimeChallenge = true,
|
|
865
916
|
routesRefreshMs = DEFAULT_ROUTES_REFRESH_MS,
|
|
917
|
+
failOnError = "open",
|
|
918
|
+
caseInsensitiveRouteMatch = false,
|
|
866
919
|
...config
|
|
867
920
|
} = options;
|
|
868
921
|
let cachedRoutes = [];
|
|
@@ -885,7 +938,7 @@ function createMiddleware(options) {
|
|
|
885
938
|
cachedRoutes = fetched;
|
|
886
939
|
lastFetchAt = Date.now();
|
|
887
940
|
if (cachedRoutes.length === 0 && !warnedEmptyRoutes) {
|
|
888
|
-
const dashboard = config.dashboardUrl ?? "https://
|
|
941
|
+
const dashboard = config.dashboardUrl ?? "https://astrasync.ai/dashboard";
|
|
889
942
|
console.warn(
|
|
890
943
|
`[VerificationGateway] No route policy configured for ${config.counterpartyId}. Gateway is in pass-through mode for ALL traffic until you add at least one route. Configure at ${dashboard}/dashboard/endpoints/${config.counterpartyId}/routes`
|
|
891
944
|
);
|
|
@@ -911,7 +964,12 @@ function createMiddleware(options) {
|
|
|
911
964
|
refreshing = null;
|
|
912
965
|
});
|
|
913
966
|
}
|
|
914
|
-
const
|
|
967
|
+
const correlationId = req.headers["x-request-id"] || req.headers["x-correlation-id"];
|
|
968
|
+
const routeConfig = findRouteConfig(cachedRoutes, req.path, req.method, {
|
|
969
|
+
caseInsensitive: caseInsensitiveRouteMatch,
|
|
970
|
+
logShadowDivergence: true,
|
|
971
|
+
correlationId
|
|
972
|
+
});
|
|
915
973
|
if (!routeConfig) {
|
|
916
974
|
if (config.setPassThroughHeader) {
|
|
917
975
|
res.setHeader("X-Astra-Gateway-Mode", "unenforced");
|
|
@@ -943,7 +1001,7 @@ function createMiddleware(options) {
|
|
|
943
1001
|
denialReasons: preCheckFailures.map((f) => f.message),
|
|
944
1002
|
guidance: {
|
|
945
1003
|
message: "Request exceeds counterparty-defined PDLSS limits.",
|
|
946
|
-
registrationUrl: `${config.apiBaseUrl?.replace("/api", "")}/register`,
|
|
1004
|
+
registrationUrl: `${config.apiBaseUrl?.replace("/api", "")}/agents/register`,
|
|
947
1005
|
documentationUrl: `${config.apiBaseUrl?.replace("/api", "")}/docs/pdlss`
|
|
948
1006
|
},
|
|
949
1007
|
verifiedAt: /* @__PURE__ */ new Date()
|
|
@@ -958,13 +1016,19 @@ function createMiddleware(options) {
|
|
|
958
1016
|
requestMethod: req.method
|
|
959
1017
|
}).catch(() => {
|
|
960
1018
|
});
|
|
1019
|
+
dedupeFailures(result2);
|
|
961
1020
|
onDenied(result2, req, res);
|
|
962
1021
|
return;
|
|
963
1022
|
}
|
|
964
1023
|
const shouldRecordDecisions = recordDecisions !== false;
|
|
965
1024
|
const forwardedFor = req.headers["x-forwarded-for"];
|
|
966
1025
|
const forwardedForStr = Array.isArray(forwardedFor) ? forwardedFor.join(", ") : forwardedFor;
|
|
967
|
-
const originalClientIp = forwardedForStr ? forwardedForStr.split(",")[0].trim() :
|
|
1026
|
+
const originalClientIp = req.ip ?? (forwardedForStr ? forwardedForStr.split(",")[0].trim() : void 0);
|
|
1027
|
+
if (!req.ip && forwardedForStr) {
|
|
1028
|
+
console.warn(
|
|
1029
|
+
"[VerificationGateway] req.ip unset \u2014 falling back to leftmost X-Forwarded-For. Configure Express trust proxy correctly to avoid spoofable client IPs in audit logs."
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
968
1032
|
const agentCardUrl = typeof req.headers["x-astrasync-agent-card"] === "string" ? req.headers["x-astrasync-agent-card"] : void 0;
|
|
969
1033
|
const result = await verify(config, {
|
|
970
1034
|
credentials,
|
|
@@ -995,6 +1059,7 @@ function createMiddleware(options) {
|
|
|
995
1059
|
recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
|
|
996
1060
|
});
|
|
997
1061
|
}
|
|
1062
|
+
dedupeFailures(result);
|
|
998
1063
|
onDenied(result, req, res);
|
|
999
1064
|
return;
|
|
1000
1065
|
}
|
|
@@ -1021,6 +1086,7 @@ function createMiddleware(options) {
|
|
|
1021
1086
|
recordDecision(config, sessionId, "denied", insufficientFailure.message).catch(() => {
|
|
1022
1087
|
});
|
|
1023
1088
|
}
|
|
1089
|
+
dedupeFailures(result);
|
|
1024
1090
|
onDenied(result, req, res);
|
|
1025
1091
|
return;
|
|
1026
1092
|
}
|
|
@@ -1037,6 +1103,7 @@ function createMiddleware(options) {
|
|
|
1037
1103
|
recordDecision(config, sessionId, "denied", trustFailure.message).catch(() => {
|
|
1038
1104
|
});
|
|
1039
1105
|
}
|
|
1106
|
+
dedupeFailures(result);
|
|
1040
1107
|
onDenied(result, req, res);
|
|
1041
1108
|
return;
|
|
1042
1109
|
}
|
|
@@ -1051,7 +1118,30 @@ function createMiddleware(options) {
|
|
|
1051
1118
|
}
|
|
1052
1119
|
next();
|
|
1053
1120
|
} catch (error) {
|
|
1121
|
+
const errorClass = error instanceof Error ? error.constructor.name : typeof error;
|
|
1122
|
+
const correlationId = req.headers["x-request-id"] || req.headers["x-correlation-id"] || `gen-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1054
1123
|
console.error("[VerificationGateway] Middleware error:", error);
|
|
1124
|
+
console.warn(
|
|
1125
|
+
`[SHADOW] would-have-denied: errorClass=${errorClass} route=${req.method}:${req.path} merchantId=${config.counterpartyId ?? "unknown"} correlationId=${correlationId}`
|
|
1126
|
+
);
|
|
1127
|
+
if (failOnError === "closed") {
|
|
1128
|
+
const result = {
|
|
1129
|
+
identityVerified: false,
|
|
1130
|
+
policyAllowed: false,
|
|
1131
|
+
accessLevel: "none",
|
|
1132
|
+
denialReasons: [`Verification middleware internal error: ${errorClass}`],
|
|
1133
|
+
failures: [
|
|
1134
|
+
{
|
|
1135
|
+
dimension: "middleware.internal_error",
|
|
1136
|
+
message: `Middleware threw ${errorClass} \u2014 failing closed`
|
|
1137
|
+
}
|
|
1138
|
+
],
|
|
1139
|
+
verifiedAt: /* @__PURE__ */ new Date(),
|
|
1140
|
+
correlationId
|
|
1141
|
+
};
|
|
1142
|
+
dedupeFailures(result);
|
|
1143
|
+
return onDenied(result, req, res);
|
|
1144
|
+
}
|
|
1055
1145
|
next();
|
|
1056
1146
|
}
|
|
1057
1147
|
};
|
|
@@ -1063,6 +1153,18 @@ __export(nextjs_exports, {
|
|
|
1063
1153
|
createMatcherConfig: () => createMatcherConfig,
|
|
1064
1154
|
createMiddleware: () => createMiddleware2
|
|
1065
1155
|
});
|
|
1156
|
+
function escapeHtml(value) {
|
|
1157
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1158
|
+
}
|
|
1159
|
+
function sanitizeUrl(value, fallback) {
|
|
1160
|
+
if (typeof value !== "string" || value.length === 0) return escapeHtml(fallback);
|
|
1161
|
+
const trimmed = value.trim();
|
|
1162
|
+
if (/^javascript:|^data:|^vbscript:/i.test(trimmed)) return escapeHtml(fallback);
|
|
1163
|
+
if (/^https?:\/\//i.test(trimmed) || trimmed.startsWith("/")) {
|
|
1164
|
+
return escapeHtml(trimmed);
|
|
1165
|
+
}
|
|
1166
|
+
return escapeHtml(fallback);
|
|
1167
|
+
}
|
|
1066
1168
|
function extractCredentialsFromNextRequest(request) {
|
|
1067
1169
|
const credentials = {};
|
|
1068
1170
|
const astraId = request.headers.get("x-astra-id") || request.headers.get("X-Astra-Id");
|
|
@@ -1134,10 +1236,18 @@ function extractPurpose(request) {
|
|
|
1134
1236
|
}
|
|
1135
1237
|
}
|
|
1136
1238
|
function generateCommerceShieldHtml(result, options) {
|
|
1137
|
-
const title = options.commerceShield?.title || "AstraSync Agent Verification";
|
|
1138
|
-
const message =
|
|
1139
|
-
|
|
1140
|
-
|
|
1239
|
+
const title = escapeHtml(options.commerceShield?.title || "AstraSync Agent Verification");
|
|
1240
|
+
const message = escapeHtml(
|
|
1241
|
+
options.commerceShield?.message || result.guidance?.message || "This site verifies AI agents before granting access. We noticed you're visiting without AstraSync credentials."
|
|
1242
|
+
);
|
|
1243
|
+
const registrationUrl = sanitizeUrl(
|
|
1244
|
+
result.guidance?.registrationUrl,
|
|
1245
|
+
"https://astrasync.ai/register"
|
|
1246
|
+
);
|
|
1247
|
+
const docsUrl = sanitizeUrl(
|
|
1248
|
+
result.guidance?.documentationUrl,
|
|
1249
|
+
"https://astrasync.ai/docs/agent-access"
|
|
1250
|
+
);
|
|
1141
1251
|
const allowGuest = options.commerceShield?.allowGuestAccess ?? true;
|
|
1142
1252
|
return `
|
|
1143
1253
|
<!DOCTYPE html>
|
|
@@ -1259,7 +1369,7 @@ function generateCommerceShieldHtml(result, options) {
|
|
|
1259
1369
|
<div class="shield-steps">
|
|
1260
1370
|
<h3>To get verified access:</h3>
|
|
1261
1371
|
<ol>
|
|
1262
|
-
<li>Register at <a href="${registrationUrl}">astrasync.ai/register</a></li>
|
|
1372
|
+
<li>Register at <a href="${registrationUrl}">astrasync.ai/agents/register</a></li>
|
|
1263
1373
|
<li>Create and register your agent</li>
|
|
1264
1374
|
<li>Add your ASTRA-ID to request headers</li>
|
|
1265
1375
|
<li>Refresh this page</li>
|
|
@@ -1347,7 +1457,7 @@ function createMiddleware2(options) {
|
|
|
1347
1457
|
denialReasons: preCheckFailures.map((f) => f.message),
|
|
1348
1458
|
guidance: {
|
|
1349
1459
|
message: "Request exceeds counterparty-defined PDLSS limits.",
|
|
1350
|
-
registrationUrl: `${config.apiBaseUrl?.replace("/api", "")}/register`,
|
|
1460
|
+
registrationUrl: `${config.apiBaseUrl?.replace("/api", "")}/agents/register`,
|
|
1351
1461
|
documentationUrl: `${config.apiBaseUrl?.replace("/api", "")}/docs/pdlss`
|
|
1352
1462
|
},
|
|
1353
1463
|
verifiedAt: /* @__PURE__ */ new Date()
|
|
@@ -2008,12 +2118,45 @@ function bufferToBase64(bytes) {
|
|
|
2008
2118
|
|
|
2009
2119
|
// src/transport/rfc9421-verify.ts
|
|
2010
2120
|
var import_http_message_signatures = require("http-message-signatures");
|
|
2121
|
+
|
|
2122
|
+
// src/transport/nonce-store.ts
|
|
2123
|
+
var InMemoryNonceStore = class {
|
|
2124
|
+
constructor(capacity = 1e4) {
|
|
2125
|
+
this.entries = /* @__PURE__ */ new Map();
|
|
2126
|
+
this.lastSweepMs = 0;
|
|
2127
|
+
this.capacity = capacity;
|
|
2128
|
+
}
|
|
2129
|
+
seen(key, expiresAtMs) {
|
|
2130
|
+
const nowMs = Date.now();
|
|
2131
|
+
if (nowMs - this.lastSweepMs > 1e3) {
|
|
2132
|
+
for (const [k, exp] of this.entries) {
|
|
2133
|
+
if (exp <= nowMs) this.entries.delete(k);
|
|
2134
|
+
}
|
|
2135
|
+
this.lastSweepMs = nowMs;
|
|
2136
|
+
}
|
|
2137
|
+
const existing = this.entries.get(key);
|
|
2138
|
+
if (existing !== void 0 && existing > nowMs) {
|
|
2139
|
+
return true;
|
|
2140
|
+
}
|
|
2141
|
+
if (this.entries.size >= this.capacity) {
|
|
2142
|
+
const oldest = this.entries.keys().next().value;
|
|
2143
|
+
if (oldest !== void 0) this.entries.delete(oldest);
|
|
2144
|
+
}
|
|
2145
|
+
this.entries.set(key, expiresAtMs);
|
|
2146
|
+
return false;
|
|
2147
|
+
}
|
|
2148
|
+
};
|
|
2149
|
+
var defaultNonceStore = new InMemoryNonceStore();
|
|
2150
|
+
|
|
2151
|
+
// src/transport/rfc9421-verify.ts
|
|
2011
2152
|
async function verifyRFC9421(request, options) {
|
|
2012
2153
|
const { resolver } = options;
|
|
2013
|
-
const tolerance = options.clockSkewSec ??
|
|
2154
|
+
const tolerance = options.clockSkewSec ?? 60;
|
|
2014
2155
|
const nowSec = options.now ? options.now() : Math.floor(Date.now() / 1e3);
|
|
2156
|
+
const nonceStore = options.nonceStore ?? defaultNonceStore;
|
|
2015
2157
|
let resolvedKid;
|
|
2016
2158
|
let resolvedAlg;
|
|
2159
|
+
let replayDetected = false;
|
|
2017
2160
|
const keyLookup = async (parameters) => {
|
|
2018
2161
|
const kid = typeof parameters.keyid === "string" ? parameters.keyid : void 0;
|
|
2019
2162
|
if (!kid) return null;
|
|
@@ -2027,6 +2170,14 @@ async function verifyRFC9421(request, options) {
|
|
|
2027
2170
|
const expires = toUnixSeconds(parameters.expires);
|
|
2028
2171
|
if (created !== void 0 && Math.abs(nowSec - created) > tolerance) return null;
|
|
2029
2172
|
if (expires !== void 0 && nowSec > expires + tolerance) return null;
|
|
2173
|
+
const nonce = typeof parameters.nonce === "string" ? parameters.nonce : void 0;
|
|
2174
|
+
if (nonce) {
|
|
2175
|
+
const expiresAtMs = (expires !== void 0 ? expires + tolerance : nowSec + tolerance) * 1e3;
|
|
2176
|
+
if (nonceStore.seen(`rfc9421:${kid}:${nonce}`, expiresAtMs)) {
|
|
2177
|
+
replayDetected = true;
|
|
2178
|
+
return null;
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2030
2181
|
return jwkToVerifyingKey(kid, jwk, alg);
|
|
2031
2182
|
};
|
|
2032
2183
|
try {
|
|
@@ -2049,7 +2200,7 @@ async function verifyRFC9421(request, options) {
|
|
|
2049
2200
|
kid: resolvedKid,
|
|
2050
2201
|
registry: resolver.name,
|
|
2051
2202
|
algorithm: resolvedAlg,
|
|
2052
|
-
error: result === false ? "signature invalid" : "no signature found"
|
|
2203
|
+
error: replayDetected ? "RFC9421 signature replay \u2014 already seen within tolerance window" : result === false ? "signature invalid" : "no signature found"
|
|
2053
2204
|
};
|
|
2054
2205
|
} catch (err) {
|
|
2055
2206
|
return {
|
|
@@ -2874,14 +3025,26 @@ function sha256Sync2(data) {
|
|
|
2874
3025
|
function verifyAP2Chain(input) {
|
|
2875
3026
|
const { triple } = input;
|
|
2876
3027
|
const errors = [];
|
|
3028
|
+
const toleranceSec = input.clockSkewSec ?? 60;
|
|
3029
|
+
const nonceStore = input.nonceStore ?? defaultNonceStore;
|
|
2877
3030
|
const intentPresent = triple.intent !== void 0;
|
|
2878
3031
|
const cartRefOk = checkCartRef(triple, errors);
|
|
2879
3032
|
const paymentRefOk = checkPaymentRef(triple, errors);
|
|
2880
3033
|
const { ok: agentIdContinuity, agentId } = checkAgentContinuity(triple, errors);
|
|
2881
3034
|
const paymentMethodAllowed = checkPaymentMethod(triple, errors);
|
|
2882
3035
|
const totalsConsistent = checkTotals(triple, errors);
|
|
2883
|
-
const expiryOk = checkExpiries(triple,
|
|
2884
|
-
|
|
3036
|
+
const expiryOk = checkExpiries(triple, toleranceSec, input.now, errors);
|
|
3037
|
+
let replayOk = true;
|
|
3038
|
+
const replayId = triple.payment?.raw?.id ?? triple.cart?.raw?.id;
|
|
3039
|
+
if (typeof replayId === "string" && replayId.length > 0) {
|
|
3040
|
+
const now = input.now ? input.now() : Math.floor(Date.now() / 1e3);
|
|
3041
|
+
const expiresAt = (now + toleranceSec) * 1e3;
|
|
3042
|
+
if (nonceStore.seen(`ap2:${replayId}`, expiresAt)) {
|
|
3043
|
+
errors.push(`AP2 chain replay \u2014 mandate ${replayId} already seen within tolerance window`);
|
|
3044
|
+
replayOk = false;
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
const ok = cartRefOk && paymentRefOk && agentIdContinuity && paymentMethodAllowed && totalsConsistent && expiryOk && replayOk;
|
|
2885
3048
|
return {
|
|
2886
3049
|
ok,
|
|
2887
3050
|
checks: {
|
|
@@ -2927,7 +3090,10 @@ function checkAgentContinuity(triple, errors) {
|
|
|
2927
3090
|
const ids = [triple.intent?.agent_id, triple.cart?.agent_id, triple.payment?.agent_id].filter(
|
|
2928
3091
|
(id) => typeof id === "string" && id.length > 0
|
|
2929
3092
|
);
|
|
2930
|
-
if (ids.length === 0)
|
|
3093
|
+
if (ids.length === 0) {
|
|
3094
|
+
errors.push("agent_id missing across all three mandates (intent/cart/payment)");
|
|
3095
|
+
return { ok: false };
|
|
3096
|
+
}
|
|
2931
3097
|
const unique = new Set(ids);
|
|
2932
3098
|
if (unique.size > 1) {
|
|
2933
3099
|
errors.push(`agent_id mismatch across mandates: ${Array.from(unique).join(", ")}`);
|
|
@@ -2936,9 +3102,16 @@ function checkAgentContinuity(triple, errors) {
|
|
|
2936
3102
|
return { ok: true, agentId: ids[0] };
|
|
2937
3103
|
}
|
|
2938
3104
|
function checkPaymentMethod(triple, errors) {
|
|
2939
|
-
const paymentMethod = triple.payment?.payment_method;
|
|
2940
3105
|
const allowed = triple.intent?.paymentMethods;
|
|
2941
|
-
if (!
|
|
3106
|
+
if (!allowed || allowed.length === 0) return true;
|
|
3107
|
+
if (!triple.payment) return true;
|
|
3108
|
+
const paymentMethod = triple.payment.payment_method;
|
|
3109
|
+
if (!paymentMethod) {
|
|
3110
|
+
errors.push(
|
|
3111
|
+
`payment.payment_method missing but intent declares allowlist [${allowed.join(", ")}]`
|
|
3112
|
+
);
|
|
3113
|
+
return false;
|
|
3114
|
+
}
|
|
2942
3115
|
if (!allowed.includes(paymentMethod)) {
|
|
2943
3116
|
errors.push(
|
|
2944
3117
|
`payment_method "${paymentMethod}" not in intent.paymentMethods [${allowed.join(", ")}]`
|
|
@@ -2972,19 +3145,24 @@ function checkTotals(triple, errors) {
|
|
|
2972
3145
|
function checkExpiries(triple, toleranceSec, nowFn, errors) {
|
|
2973
3146
|
const now = nowFn ? nowFn() : Math.floor(Date.now() / 1e3);
|
|
2974
3147
|
let ok = true;
|
|
2975
|
-
|
|
2976
|
-
["intent", triple.intent],
|
|
2977
|
-
["cart", triple.cart]
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
3148
|
+
const layers = [
|
|
3149
|
+
["intent", triple.intent?.expires],
|
|
3150
|
+
["cart", triple.cart?.expires],
|
|
3151
|
+
[
|
|
3152
|
+
"payment",
|
|
3153
|
+
typeof triple.payment?.raw?.expires === "string" ? triple.payment.raw.expires : typeof triple.payment?.raw?.exp === "string" ? triple.payment.raw.exp : void 0
|
|
3154
|
+
]
|
|
3155
|
+
];
|
|
3156
|
+
for (const [name, expires] of layers) {
|
|
3157
|
+
if (!expires) continue;
|
|
3158
|
+
const parsed = parseExpiry(expires);
|
|
2981
3159
|
if (parsed === null) {
|
|
2982
3160
|
errors.push(`${name}.expires unparseable`);
|
|
2983
3161
|
ok = false;
|
|
2984
3162
|
continue;
|
|
2985
3163
|
}
|
|
2986
3164
|
if (now > parsed + toleranceSec) {
|
|
2987
|
-
errors.push(`${name} mandate expired at ${
|
|
3165
|
+
errors.push(`${name} mandate expired at ${expires}`);
|
|
2988
3166
|
ok = false;
|
|
2989
3167
|
}
|
|
2990
3168
|
}
|
|
@@ -3011,10 +3189,21 @@ async function verifyACPSignature(input) {
|
|
|
3011
3189
|
if (!input.signatureHeader) {
|
|
3012
3190
|
return { ok: false, error: "missing Signature header" };
|
|
3013
3191
|
}
|
|
3014
|
-
const
|
|
3192
|
+
const tolerance = input.clockSkewSec ?? 60;
|
|
3193
|
+
const nonceStore = input.nonceStore ?? defaultNonceStore;
|
|
3194
|
+
const freshness = checkTimestamp(input.timestampHeader, tolerance, input.now);
|
|
3015
3195
|
if (!freshness.ok) {
|
|
3016
3196
|
return { ok: false, error: freshness.error, timestampStale: true };
|
|
3017
3197
|
}
|
|
3198
|
+
const nowSec = input.now ? input.now() : Math.floor(Date.now() / 1e3);
|
|
3199
|
+
const expiresAtMs = (nowSec + tolerance) * 1e3;
|
|
3200
|
+
const replayKey = `acp:${input.signatureHeader}:${input.timestampHeader ?? ""}`;
|
|
3201
|
+
if (nonceStore.seen(replayKey, expiresAtMs)) {
|
|
3202
|
+
return {
|
|
3203
|
+
ok: false,
|
|
3204
|
+
error: "ACP signature replay \u2014 already seen within tolerance window"
|
|
3205
|
+
};
|
|
3206
|
+
}
|
|
3018
3207
|
const signatureBytes = decodeBase64(input.signatureHeader);
|
|
3019
3208
|
if (!signatureBytes) {
|
|
3020
3209
|
return { ok: false, error: "signature header is not valid base64" };
|
|
@@ -3232,8 +3421,9 @@ function coerceString6(v) {
|
|
|
3232
3421
|
var import_mppx2 = require("mppx");
|
|
3233
3422
|
function verifyMPP(input) {
|
|
3234
3423
|
const { context } = input;
|
|
3235
|
-
const tolerance = input.clockSkewSec ??
|
|
3424
|
+
const tolerance = input.clockSkewSec ?? 60;
|
|
3236
3425
|
const nowSec = input.now ? input.now() : Math.floor(Date.now() / 1e3);
|
|
3426
|
+
const nonceStore = input.nonceStore ?? defaultNonceStore;
|
|
3237
3427
|
const challenge = context.credential?.challenge ?? (context.challenges && context.challenges[0]);
|
|
3238
3428
|
const source = context.credential?.source;
|
|
3239
3429
|
const method = challenge?.method;
|
|
@@ -3256,21 +3446,38 @@ function verifyMPP(input) {
|
|
|
3256
3446
|
}
|
|
3257
3447
|
}
|
|
3258
3448
|
let bodyDigestOk = null;
|
|
3259
|
-
if (
|
|
3260
|
-
|
|
3261
|
-
|
|
3449
|
+
if (input.rawBody !== void 0) {
|
|
3450
|
+
if (!challenge?.digest) {
|
|
3451
|
+
bodyDigestOk = false;
|
|
3452
|
+
} else {
|
|
3453
|
+
try {
|
|
3454
|
+
if (!/^sha-256=/.test(challenge.digest)) {
|
|
3455
|
+
bodyDigestOk = false;
|
|
3456
|
+
} else {
|
|
3457
|
+
bodyDigestOk = import_mppx2.BodyDigest.verify(challenge.digest, input.rawBody);
|
|
3458
|
+
}
|
|
3459
|
+
} catch {
|
|
3262
3460
|
bodyDigestOk = false;
|
|
3263
|
-
} else {
|
|
3264
|
-
bodyDigestOk = import_mppx2.BodyDigest.verify(challenge.digest, input.rawBody);
|
|
3265
3461
|
}
|
|
3266
|
-
} catch {
|
|
3267
|
-
bodyDigestOk = false;
|
|
3268
3462
|
}
|
|
3269
3463
|
}
|
|
3270
|
-
|
|
3464
|
+
let replayOk = true;
|
|
3465
|
+
if (challenge?.digest && expiryOk) {
|
|
3466
|
+
const replayKey = `mpp:${challenge.digest}:${challenge.nonce ?? ""}`;
|
|
3467
|
+
const expiresAt = (nowSec + tolerance) * 1e3;
|
|
3468
|
+
if (nonceStore.seen(replayKey, expiresAt)) {
|
|
3469
|
+
replayOk = false;
|
|
3470
|
+
}
|
|
3471
|
+
}
|
|
3472
|
+
const ok = expiryOk && (bodyDigestOk === null || bodyDigestOk === true) && replayOk;
|
|
3271
3473
|
const errors = [];
|
|
3272
3474
|
if (!expiryOk) errors.push("challenge expired");
|
|
3273
|
-
if (bodyDigestOk === false)
|
|
3475
|
+
if (bodyDigestOk === false) {
|
|
3476
|
+
errors.push(
|
|
3477
|
+
input.rawBody !== void 0 && !challenge?.digest ? "body digest required when rawBody present" : "body digest mismatch"
|
|
3478
|
+
);
|
|
3479
|
+
}
|
|
3480
|
+
if (!replayOk) errors.push("MPP challenge replay \u2014 already seen within tolerance window");
|
|
3274
3481
|
return {
|
|
3275
3482
|
ok,
|
|
3276
3483
|
expiryOk,
|
|
@@ -3431,14 +3638,32 @@ function readHeader4(headers, name) {
|
|
|
3431
3638
|
var import_node_crypto4 = require("crypto");
|
|
3432
3639
|
async function verifyVIChain(input) {
|
|
3433
3640
|
const errors = [];
|
|
3434
|
-
const tolerance = input.clockSkewSec ??
|
|
3641
|
+
const tolerance = input.clockSkewSec ?? 60;
|
|
3435
3642
|
const now = input.now ? input.now() : Math.floor(Date.now() / 1e3);
|
|
3436
3643
|
const { l1, l2, l3a, l3b } = input.layers;
|
|
3644
|
+
const nonceStore = input.nonceStore ?? defaultNonceStore;
|
|
3645
|
+
if (!l1) {
|
|
3646
|
+
if (!input.allowUnboundChain) {
|
|
3647
|
+
errors.push(
|
|
3648
|
+
"L1 missing \u2014 chain root unbound (set allowUnboundChain + expectedL2Key to override)"
|
|
3649
|
+
);
|
|
3650
|
+
} else if (!input.expectedL2Key) {
|
|
3651
|
+
errors.push("allowUnboundChain set but expectedL2Key missing");
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3437
3654
|
const l1SigOk = l1 ? await input.verifySignature(l1, null) : null;
|
|
3438
3655
|
if (l1 && !l1SigOk) errors.push("L1 signature invalid");
|
|
3439
3656
|
const l1Cnf = extractCnfJwk(l1?.payload);
|
|
3440
|
-
const
|
|
3657
|
+
const l2ExpectedKey = l1Cnf ?? input.expectedL2Key ?? null;
|
|
3658
|
+
const l2SigOk = await input.verifySignature(l2, l2ExpectedKey);
|
|
3441
3659
|
if (!l2SigOk) errors.push("L2 signature invalid");
|
|
3660
|
+
if (l2SigOk) {
|
|
3661
|
+
const replayKey = `vi:l2:${l2.compact}`;
|
|
3662
|
+
const expiresAt = now * 1e3 + tolerance * 1e3;
|
|
3663
|
+
if (nonceStore.seen(replayKey, expiresAt)) {
|
|
3664
|
+
errors.push("L2 signature replay \u2014 already seen within tolerance window");
|
|
3665
|
+
}
|
|
3666
|
+
}
|
|
3442
3667
|
const l2Cnf = extractCnfJwk(l2.payload);
|
|
3443
3668
|
const l3aSigOk = l3a ? await input.verifySignature(l3a, l2Cnf ?? null) : null;
|
|
3444
3669
|
if (l3a && !l3aSigOk) errors.push("L3a signature invalid");
|
|
@@ -3482,7 +3707,10 @@ async function verifyVIChain(input) {
|
|
|
3482
3707
|
}
|
|
3483
3708
|
}
|
|
3484
3709
|
const expiryOk = checkExpiryAcross([l1, l2, l3a, l3b], tolerance, now, errors);
|
|
3485
|
-
const
|
|
3710
|
+
const noUnboundChainOrReplayErrors = !errors.some(
|
|
3711
|
+
(e) => e.startsWith("L1 missing") || e.startsWith("allowUnboundChain set") || e.startsWith("L2 signature replay")
|
|
3712
|
+
);
|
|
3713
|
+
const ok = l1SigOk !== false && l2SigOk && l3aSigOk !== false && l3bSigOk !== false && l1BindsL2 && l2BindsL3 && l3aL3bTxnIdMatch !== false && checkoutHashOk !== false && expiryOk && noUnboundChainOrReplayErrors;
|
|
3486
3714
|
return {
|
|
3487
3715
|
ok,
|
|
3488
3716
|
checks: {
|
|
@@ -4137,7 +4365,7 @@ function mcpToPdlss(parsed, headerPurpose, headerAction) {
|
|
|
4137
4365
|
action = parsed.actionFromBody;
|
|
4138
4366
|
actionSource = parsed.actionSourceFromBody;
|
|
4139
4367
|
} else {
|
|
4140
|
-
action = parsed.toolName ? `${parsed.method}:${parsed.toolName}` : parsed.method;
|
|
4368
|
+
action = parsed.toolName ? parsed.method === "tools/call" ? parsed.toolName : `${parsed.method}:${parsed.toolName}` : parsed.method;
|
|
4141
4369
|
actionSource = "transport_layer";
|
|
4142
4370
|
}
|
|
4143
4371
|
return { purpose, action, resource, purposeSource, actionSource };
|
|
@@ -4156,6 +4384,17 @@ function readSingleHeader(value) {
|
|
|
4156
4384
|
if (Array.isArray(value)) return value[0];
|
|
4157
4385
|
return void 0;
|
|
4158
4386
|
}
|
|
4387
|
+
function dedupeFailures2(result) {
|
|
4388
|
+
if (result.failures && result.failures.length > 1) {
|
|
4389
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4390
|
+
result.failures = result.failures.filter((f) => {
|
|
4391
|
+
const key = `${f.dimension}|${f.message}|${f.guidance ?? ""}`;
|
|
4392
|
+
if (seen.has(key)) return false;
|
|
4393
|
+
seen.add(key);
|
|
4394
|
+
return true;
|
|
4395
|
+
});
|
|
4396
|
+
}
|
|
4397
|
+
}
|
|
4159
4398
|
function defaultMcpDenied(result, req, res) {
|
|
4160
4399
|
const id = req.body?.id ?? null;
|
|
4161
4400
|
const status = !result.identityVerified ? 401 : 403;
|
|
@@ -4195,10 +4434,11 @@ function createMcpMiddleware(options) {
|
|
|
4195
4434
|
onAgentIdMismatch = "reject",
|
|
4196
4435
|
skip = false,
|
|
4197
4436
|
onDenied = defaultMcpDenied,
|
|
4198
|
-
trustVerifiedHop =
|
|
4437
|
+
trustVerifiedHop = false,
|
|
4199
4438
|
verifiedHopMaxAgeMs,
|
|
4200
4439
|
recordDecisions,
|
|
4201
4440
|
enableRuntimeChallenge = true,
|
|
4441
|
+
failOnError = "open",
|
|
4202
4442
|
...config
|
|
4203
4443
|
} = options;
|
|
4204
4444
|
return async (req, res, next) => {
|
|
@@ -4306,6 +4546,7 @@ function createMcpMiddleware(options) {
|
|
|
4306
4546
|
recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
|
|
4307
4547
|
});
|
|
4308
4548
|
}
|
|
4549
|
+
dedupeFailures2(result);
|
|
4309
4550
|
onDenied(result, req, res);
|
|
4310
4551
|
return;
|
|
4311
4552
|
}
|
|
@@ -4351,6 +4592,7 @@ function createMcpMiddleware(options) {
|
|
|
4351
4592
|
});
|
|
4352
4593
|
}
|
|
4353
4594
|
}
|
|
4595
|
+
dedupeFailures2(result);
|
|
4354
4596
|
onDenied(result, req, res);
|
|
4355
4597
|
return;
|
|
4356
4598
|
}
|
|
@@ -4374,7 +4616,30 @@ function createMcpMiddleware(options) {
|
|
|
4374
4616
|
}
|
|
4375
4617
|
next();
|
|
4376
4618
|
} catch (error) {
|
|
4619
|
+
const errorClass = error instanceof Error ? error.constructor.name : typeof error;
|
|
4620
|
+
const correlationId = req.headers["x-request-id"] || req.headers["x-correlation-id"] || `gen-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
4377
4621
|
console.error("[VerificationGateway/MCP] Middleware error:", error);
|
|
4622
|
+
console.warn(
|
|
4623
|
+
`[SHADOW] would-have-denied: errorClass=${errorClass} route=${req.method}:${req.path} merchantId=${config.counterpartyId ?? "unknown"} correlationId=${correlationId}`
|
|
4624
|
+
);
|
|
4625
|
+
if (failOnError === "closed") {
|
|
4626
|
+
const result = {
|
|
4627
|
+
identityVerified: false,
|
|
4628
|
+
policyAllowed: false,
|
|
4629
|
+
accessLevel: "none",
|
|
4630
|
+
denialReasons: [`MCP middleware internal error: ${errorClass}`],
|
|
4631
|
+
failures: [
|
|
4632
|
+
{
|
|
4633
|
+
dimension: "middleware.internal_error",
|
|
4634
|
+
message: `Middleware threw ${errorClass} \u2014 failing closed`
|
|
4635
|
+
}
|
|
4636
|
+
],
|
|
4637
|
+
verifiedAt: /* @__PURE__ */ new Date(),
|
|
4638
|
+
correlationId
|
|
4639
|
+
};
|
|
4640
|
+
dedupeFailures2(result);
|
|
4641
|
+
return onDenied(result, req, res);
|
|
4642
|
+
}
|
|
4378
4643
|
next();
|
|
4379
4644
|
}
|
|
4380
4645
|
};
|