@astrasyncai/verification-gateway 2.4.11 → 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 +129 -36
- package/dist/adapters/express.js.map +1 -1
- package/dist/adapters/express.mjs +129 -36
- 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 +75 -29
- package/dist/adapters/nextjs.js.map +1 -1
- package/dist/adapters/nextjs.mjs +75 -29
- 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 +344 -73
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +344 -73
- 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,18 +1016,27 @@ 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,
|
|
971
1035
|
purpose,
|
|
972
|
-
|
|
1036
|
+
// RFC 7230 § 3.1.1 — HTTP method tokens uppercase by IANA convention.
|
|
1037
|
+
// Backend evaluator tolerates either case as defense-in-depth
|
|
1038
|
+
// (round-18.6 batch 2); SDK emits canonical form.
|
|
1039
|
+
action: req.method.toUpperCase(),
|
|
973
1040
|
resource: req.path,
|
|
974
1041
|
createSession: shouldRecordDecisions,
|
|
975
1042
|
counterpartyUrl,
|
|
@@ -992,6 +1059,7 @@ function createMiddleware(options) {
|
|
|
992
1059
|
recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
|
|
993
1060
|
});
|
|
994
1061
|
}
|
|
1062
|
+
dedupeFailures(result);
|
|
995
1063
|
onDenied(result, req, res);
|
|
996
1064
|
return;
|
|
997
1065
|
}
|
|
@@ -1018,6 +1086,7 @@ function createMiddleware(options) {
|
|
|
1018
1086
|
recordDecision(config, sessionId, "denied", insufficientFailure.message).catch(() => {
|
|
1019
1087
|
});
|
|
1020
1088
|
}
|
|
1089
|
+
dedupeFailures(result);
|
|
1021
1090
|
onDenied(result, req, res);
|
|
1022
1091
|
return;
|
|
1023
1092
|
}
|
|
@@ -1034,6 +1103,7 @@ function createMiddleware(options) {
|
|
|
1034
1103
|
recordDecision(config, sessionId, "denied", trustFailure.message).catch(() => {
|
|
1035
1104
|
});
|
|
1036
1105
|
}
|
|
1106
|
+
dedupeFailures(result);
|
|
1037
1107
|
onDenied(result, req, res);
|
|
1038
1108
|
return;
|
|
1039
1109
|
}
|
|
@@ -1048,7 +1118,30 @@ function createMiddleware(options) {
|
|
|
1048
1118
|
}
|
|
1049
1119
|
next();
|
|
1050
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)}`;
|
|
1051
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
|
+
}
|
|
1052
1145
|
next();
|
|
1053
1146
|
}
|
|
1054
1147
|
};
|
|
@@ -1060,6 +1153,18 @@ __export(nextjs_exports, {
|
|
|
1060
1153
|
createMatcherConfig: () => createMatcherConfig,
|
|
1061
1154
|
createMiddleware: () => createMiddleware2
|
|
1062
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
|
+
}
|
|
1063
1168
|
function extractCredentialsFromNextRequest(request) {
|
|
1064
1169
|
const credentials = {};
|
|
1065
1170
|
const astraId = request.headers.get("x-astra-id") || request.headers.get("X-Astra-Id");
|
|
@@ -1131,10 +1236,18 @@ function extractPurpose(request) {
|
|
|
1131
1236
|
}
|
|
1132
1237
|
}
|
|
1133
1238
|
function generateCommerceShieldHtml(result, options) {
|
|
1134
|
-
const title = options.commerceShield?.title || "AstraSync Agent Verification";
|
|
1135
|
-
const message =
|
|
1136
|
-
|
|
1137
|
-
|
|
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
|
+
);
|
|
1138
1251
|
const allowGuest = options.commerceShield?.allowGuestAccess ?? true;
|
|
1139
1252
|
return `
|
|
1140
1253
|
<!DOCTYPE html>
|
|
@@ -1256,7 +1369,7 @@ function generateCommerceShieldHtml(result, options) {
|
|
|
1256
1369
|
<div class="shield-steps">
|
|
1257
1370
|
<h3>To get verified access:</h3>
|
|
1258
1371
|
<ol>
|
|
1259
|
-
<li>Register at <a href="${registrationUrl}">astrasync.ai/register</a></li>
|
|
1372
|
+
<li>Register at <a href="${registrationUrl}">astrasync.ai/agents/register</a></li>
|
|
1260
1373
|
<li>Create and register your agent</li>
|
|
1261
1374
|
<li>Add your ASTRA-ID to request headers</li>
|
|
1262
1375
|
<li>Refresh this page</li>
|
|
@@ -1344,7 +1457,7 @@ function createMiddleware2(options) {
|
|
|
1344
1457
|
denialReasons: preCheckFailures.map((f) => f.message),
|
|
1345
1458
|
guidance: {
|
|
1346
1459
|
message: "Request exceeds counterparty-defined PDLSS limits.",
|
|
1347
|
-
registrationUrl: `${config.apiBaseUrl?.replace("/api", "")}/register`,
|
|
1460
|
+
registrationUrl: `${config.apiBaseUrl?.replace("/api", "")}/agents/register`,
|
|
1348
1461
|
documentationUrl: `${config.apiBaseUrl?.replace("/api", "")}/docs/pdlss`
|
|
1349
1462
|
},
|
|
1350
1463
|
verifiedAt: /* @__PURE__ */ new Date()
|
|
@@ -1387,7 +1500,10 @@ function createMiddleware2(options) {
|
|
|
1387
1500
|
const result = await verify(config, {
|
|
1388
1501
|
credentials,
|
|
1389
1502
|
purpose,
|
|
1390
|
-
|
|
1503
|
+
// RFC 7230 § 3.1.1 — HTTP method tokens uppercase by IANA convention.
|
|
1504
|
+
// Backend evaluator tolerates either case as defense-in-depth
|
|
1505
|
+
// (round-18.6 batch 2); SDK emits canonical form.
|
|
1506
|
+
action: request.method.toUpperCase(),
|
|
1391
1507
|
resource: pathname,
|
|
1392
1508
|
counterpartyUrl,
|
|
1393
1509
|
counterpartyType: config.counterpartyType || "website",
|
|
@@ -2002,12 +2118,45 @@ function bufferToBase64(bytes) {
|
|
|
2002
2118
|
|
|
2003
2119
|
// src/transport/rfc9421-verify.ts
|
|
2004
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
|
|
2005
2152
|
async function verifyRFC9421(request, options) {
|
|
2006
2153
|
const { resolver } = options;
|
|
2007
|
-
const tolerance = options.clockSkewSec ??
|
|
2154
|
+
const tolerance = options.clockSkewSec ?? 60;
|
|
2008
2155
|
const nowSec = options.now ? options.now() : Math.floor(Date.now() / 1e3);
|
|
2156
|
+
const nonceStore = options.nonceStore ?? defaultNonceStore;
|
|
2009
2157
|
let resolvedKid;
|
|
2010
2158
|
let resolvedAlg;
|
|
2159
|
+
let replayDetected = false;
|
|
2011
2160
|
const keyLookup = async (parameters) => {
|
|
2012
2161
|
const kid = typeof parameters.keyid === "string" ? parameters.keyid : void 0;
|
|
2013
2162
|
if (!kid) return null;
|
|
@@ -2021,6 +2170,14 @@ async function verifyRFC9421(request, options) {
|
|
|
2021
2170
|
const expires = toUnixSeconds(parameters.expires);
|
|
2022
2171
|
if (created !== void 0 && Math.abs(nowSec - created) > tolerance) return null;
|
|
2023
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
|
+
}
|
|
2024
2181
|
return jwkToVerifyingKey(kid, jwk, alg);
|
|
2025
2182
|
};
|
|
2026
2183
|
try {
|
|
@@ -2043,7 +2200,7 @@ async function verifyRFC9421(request, options) {
|
|
|
2043
2200
|
kid: resolvedKid,
|
|
2044
2201
|
registry: resolver.name,
|
|
2045
2202
|
algorithm: resolvedAlg,
|
|
2046
|
-
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"
|
|
2047
2204
|
};
|
|
2048
2205
|
} catch (err) {
|
|
2049
2206
|
return {
|
|
@@ -2868,14 +3025,26 @@ function sha256Sync2(data) {
|
|
|
2868
3025
|
function verifyAP2Chain(input) {
|
|
2869
3026
|
const { triple } = input;
|
|
2870
3027
|
const errors = [];
|
|
3028
|
+
const toleranceSec = input.clockSkewSec ?? 60;
|
|
3029
|
+
const nonceStore = input.nonceStore ?? defaultNonceStore;
|
|
2871
3030
|
const intentPresent = triple.intent !== void 0;
|
|
2872
3031
|
const cartRefOk = checkCartRef(triple, errors);
|
|
2873
3032
|
const paymentRefOk = checkPaymentRef(triple, errors);
|
|
2874
3033
|
const { ok: agentIdContinuity, agentId } = checkAgentContinuity(triple, errors);
|
|
2875
3034
|
const paymentMethodAllowed = checkPaymentMethod(triple, errors);
|
|
2876
3035
|
const totalsConsistent = checkTotals(triple, errors);
|
|
2877
|
-
const expiryOk = checkExpiries(triple,
|
|
2878
|
-
|
|
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;
|
|
2879
3048
|
return {
|
|
2880
3049
|
ok,
|
|
2881
3050
|
checks: {
|
|
@@ -2921,7 +3090,10 @@ function checkAgentContinuity(triple, errors) {
|
|
|
2921
3090
|
const ids = [triple.intent?.agent_id, triple.cart?.agent_id, triple.payment?.agent_id].filter(
|
|
2922
3091
|
(id) => typeof id === "string" && id.length > 0
|
|
2923
3092
|
);
|
|
2924
|
-
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
|
+
}
|
|
2925
3097
|
const unique = new Set(ids);
|
|
2926
3098
|
if (unique.size > 1) {
|
|
2927
3099
|
errors.push(`agent_id mismatch across mandates: ${Array.from(unique).join(", ")}`);
|
|
@@ -2930,9 +3102,16 @@ function checkAgentContinuity(triple, errors) {
|
|
|
2930
3102
|
return { ok: true, agentId: ids[0] };
|
|
2931
3103
|
}
|
|
2932
3104
|
function checkPaymentMethod(triple, errors) {
|
|
2933
|
-
const paymentMethod = triple.payment?.payment_method;
|
|
2934
3105
|
const allowed = triple.intent?.paymentMethods;
|
|
2935
|
-
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
|
+
}
|
|
2936
3115
|
if (!allowed.includes(paymentMethod)) {
|
|
2937
3116
|
errors.push(
|
|
2938
3117
|
`payment_method "${paymentMethod}" not in intent.paymentMethods [${allowed.join(", ")}]`
|
|
@@ -2966,19 +3145,24 @@ function checkTotals(triple, errors) {
|
|
|
2966
3145
|
function checkExpiries(triple, toleranceSec, nowFn, errors) {
|
|
2967
3146
|
const now = nowFn ? nowFn() : Math.floor(Date.now() / 1e3);
|
|
2968
3147
|
let ok = true;
|
|
2969
|
-
|
|
2970
|
-
["intent", triple.intent],
|
|
2971
|
-
["cart", triple.cart]
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
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);
|
|
2975
3159
|
if (parsed === null) {
|
|
2976
3160
|
errors.push(`${name}.expires unparseable`);
|
|
2977
3161
|
ok = false;
|
|
2978
3162
|
continue;
|
|
2979
3163
|
}
|
|
2980
3164
|
if (now > parsed + toleranceSec) {
|
|
2981
|
-
errors.push(`${name} mandate expired at ${
|
|
3165
|
+
errors.push(`${name} mandate expired at ${expires}`);
|
|
2982
3166
|
ok = false;
|
|
2983
3167
|
}
|
|
2984
3168
|
}
|
|
@@ -3005,10 +3189,21 @@ async function verifyACPSignature(input) {
|
|
|
3005
3189
|
if (!input.signatureHeader) {
|
|
3006
3190
|
return { ok: false, error: "missing Signature header" };
|
|
3007
3191
|
}
|
|
3008
|
-
const
|
|
3192
|
+
const tolerance = input.clockSkewSec ?? 60;
|
|
3193
|
+
const nonceStore = input.nonceStore ?? defaultNonceStore;
|
|
3194
|
+
const freshness = checkTimestamp(input.timestampHeader, tolerance, input.now);
|
|
3009
3195
|
if (!freshness.ok) {
|
|
3010
3196
|
return { ok: false, error: freshness.error, timestampStale: true };
|
|
3011
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
|
+
}
|
|
3012
3207
|
const signatureBytes = decodeBase64(input.signatureHeader);
|
|
3013
3208
|
if (!signatureBytes) {
|
|
3014
3209
|
return { ok: false, error: "signature header is not valid base64" };
|
|
@@ -3226,8 +3421,9 @@ function coerceString6(v) {
|
|
|
3226
3421
|
var import_mppx2 = require("mppx");
|
|
3227
3422
|
function verifyMPP(input) {
|
|
3228
3423
|
const { context } = input;
|
|
3229
|
-
const tolerance = input.clockSkewSec ??
|
|
3424
|
+
const tolerance = input.clockSkewSec ?? 60;
|
|
3230
3425
|
const nowSec = input.now ? input.now() : Math.floor(Date.now() / 1e3);
|
|
3426
|
+
const nonceStore = input.nonceStore ?? defaultNonceStore;
|
|
3231
3427
|
const challenge = context.credential?.challenge ?? (context.challenges && context.challenges[0]);
|
|
3232
3428
|
const source = context.credential?.source;
|
|
3233
3429
|
const method = challenge?.method;
|
|
@@ -3250,21 +3446,38 @@ function verifyMPP(input) {
|
|
|
3250
3446
|
}
|
|
3251
3447
|
}
|
|
3252
3448
|
let bodyDigestOk = null;
|
|
3253
|
-
if (
|
|
3254
|
-
|
|
3255
|
-
|
|
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 {
|
|
3256
3460
|
bodyDigestOk = false;
|
|
3257
|
-
} else {
|
|
3258
|
-
bodyDigestOk = import_mppx2.BodyDigest.verify(challenge.digest, input.rawBody);
|
|
3259
3461
|
}
|
|
3260
|
-
} catch {
|
|
3261
|
-
bodyDigestOk = false;
|
|
3262
3462
|
}
|
|
3263
3463
|
}
|
|
3264
|
-
|
|
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;
|
|
3265
3473
|
const errors = [];
|
|
3266
3474
|
if (!expiryOk) errors.push("challenge expired");
|
|
3267
|
-
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");
|
|
3268
3481
|
return {
|
|
3269
3482
|
ok,
|
|
3270
3483
|
expiryOk,
|
|
@@ -3425,14 +3638,32 @@ function readHeader4(headers, name) {
|
|
|
3425
3638
|
var import_node_crypto4 = require("crypto");
|
|
3426
3639
|
async function verifyVIChain(input) {
|
|
3427
3640
|
const errors = [];
|
|
3428
|
-
const tolerance = input.clockSkewSec ??
|
|
3641
|
+
const tolerance = input.clockSkewSec ?? 60;
|
|
3429
3642
|
const now = input.now ? input.now() : Math.floor(Date.now() / 1e3);
|
|
3430
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
|
+
}
|
|
3431
3654
|
const l1SigOk = l1 ? await input.verifySignature(l1, null) : null;
|
|
3432
3655
|
if (l1 && !l1SigOk) errors.push("L1 signature invalid");
|
|
3433
3656
|
const l1Cnf = extractCnfJwk(l1?.payload);
|
|
3434
|
-
const
|
|
3657
|
+
const l2ExpectedKey = l1Cnf ?? input.expectedL2Key ?? null;
|
|
3658
|
+
const l2SigOk = await input.verifySignature(l2, l2ExpectedKey);
|
|
3435
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
|
+
}
|
|
3436
3667
|
const l2Cnf = extractCnfJwk(l2.payload);
|
|
3437
3668
|
const l3aSigOk = l3a ? await input.verifySignature(l3a, l2Cnf ?? null) : null;
|
|
3438
3669
|
if (l3a && !l3aSigOk) errors.push("L3a signature invalid");
|
|
@@ -3476,7 +3707,10 @@ async function verifyVIChain(input) {
|
|
|
3476
3707
|
}
|
|
3477
3708
|
}
|
|
3478
3709
|
const expiryOk = checkExpiryAcross([l1, l2, l3a, l3b], tolerance, now, errors);
|
|
3479
|
-
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;
|
|
3480
3714
|
return {
|
|
3481
3715
|
ok,
|
|
3482
3716
|
checks: {
|
|
@@ -4131,7 +4365,7 @@ function mcpToPdlss(parsed, headerPurpose, headerAction) {
|
|
|
4131
4365
|
action = parsed.actionFromBody;
|
|
4132
4366
|
actionSource = parsed.actionSourceFromBody;
|
|
4133
4367
|
} else {
|
|
4134
|
-
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;
|
|
4135
4369
|
actionSource = "transport_layer";
|
|
4136
4370
|
}
|
|
4137
4371
|
return { purpose, action, resource, purposeSource, actionSource };
|
|
@@ -4150,6 +4384,17 @@ function readSingleHeader(value) {
|
|
|
4150
4384
|
if (Array.isArray(value)) return value[0];
|
|
4151
4385
|
return void 0;
|
|
4152
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
|
+
}
|
|
4153
4398
|
function defaultMcpDenied(result, req, res) {
|
|
4154
4399
|
const id = req.body?.id ?? null;
|
|
4155
4400
|
const status = !result.identityVerified ? 401 : 403;
|
|
@@ -4189,10 +4434,11 @@ function createMcpMiddleware(options) {
|
|
|
4189
4434
|
onAgentIdMismatch = "reject",
|
|
4190
4435
|
skip = false,
|
|
4191
4436
|
onDenied = defaultMcpDenied,
|
|
4192
|
-
trustVerifiedHop =
|
|
4437
|
+
trustVerifiedHop = false,
|
|
4193
4438
|
verifiedHopMaxAgeMs,
|
|
4194
4439
|
recordDecisions,
|
|
4195
4440
|
enableRuntimeChallenge = true,
|
|
4441
|
+
failOnError = "open",
|
|
4196
4442
|
...config
|
|
4197
4443
|
} = options;
|
|
4198
4444
|
return async (req, res, next) => {
|
|
@@ -4300,6 +4546,7 @@ function createMcpMiddleware(options) {
|
|
|
4300
4546
|
recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
|
|
4301
4547
|
});
|
|
4302
4548
|
}
|
|
4549
|
+
dedupeFailures2(result);
|
|
4303
4550
|
onDenied(result, req, res);
|
|
4304
4551
|
return;
|
|
4305
4552
|
}
|
|
@@ -4345,6 +4592,7 @@ function createMcpMiddleware(options) {
|
|
|
4345
4592
|
});
|
|
4346
4593
|
}
|
|
4347
4594
|
}
|
|
4595
|
+
dedupeFailures2(result);
|
|
4348
4596
|
onDenied(result, req, res);
|
|
4349
4597
|
return;
|
|
4350
4598
|
}
|
|
@@ -4368,7 +4616,30 @@ function createMcpMiddleware(options) {
|
|
|
4368
4616
|
}
|
|
4369
4617
|
next();
|
|
4370
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)}`;
|
|
4371
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
|
+
}
|
|
4372
4643
|
next();
|
|
4373
4644
|
}
|
|
4374
4645
|
};
|