@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.mjs
CHANGED
|
@@ -126,7 +126,7 @@ function getCapabilities(accessLevel) {
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
// src/version.ts
|
|
129
|
-
var SDK_VERSION = "2.4.
|
|
129
|
+
var SDK_VERSION = "2.4.13";
|
|
130
130
|
|
|
131
131
|
// src/verify.ts
|
|
132
132
|
var DEFAULT_CONFIG = {
|
|
@@ -145,22 +145,27 @@ var DEFAULT_CONFIG = {
|
|
|
145
145
|
};
|
|
146
146
|
var initCheckPerformed = false;
|
|
147
147
|
var deprecationWarningShown = false;
|
|
148
|
-
async function performInitCheck(apiBaseUrl, debug) {
|
|
148
|
+
async function performInitCheck(apiBaseUrl, debug, strictInit) {
|
|
149
149
|
initCheckPerformed = true;
|
|
150
150
|
try {
|
|
151
151
|
const probeUrl = `${apiBaseUrl}/agents/verify-access`;
|
|
152
152
|
const response = await fetch(probeUrl, { method: "HEAD" });
|
|
153
153
|
const contentType = response.headers.get("content-type") ?? "";
|
|
154
154
|
if (contentType.startsWith("text/html")) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
155
|
+
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).`;
|
|
156
|
+
if (strictInit) {
|
|
157
|
+
throw new Error(`${message} (strictInit=true)`);
|
|
158
|
+
}
|
|
159
|
+
console.warn(`${message} Set disableInitChecks: true on GatewayConfig to silence.`);
|
|
158
160
|
} else if (debug) {
|
|
159
161
|
console.log(
|
|
160
162
|
`[VerificationGateway] init check passed for ${apiBaseUrl} (content-type: ${contentType})`
|
|
161
163
|
);
|
|
162
164
|
}
|
|
163
165
|
} catch (err) {
|
|
166
|
+
if (strictInit) {
|
|
167
|
+
throw err;
|
|
168
|
+
}
|
|
164
169
|
if (debug) {
|
|
165
170
|
console.log(`[VerificationGateway] init check failed (non-blocking): ${String(err)}`);
|
|
166
171
|
}
|
|
@@ -184,7 +189,23 @@ function getCacheKey(request) {
|
|
|
184
189
|
request.counterpartyType || "",
|
|
185
190
|
request.isSubAgentRequest ? "1" : "0",
|
|
186
191
|
request.parentAgentId || "",
|
|
187
|
-
request.subAgentDepth ?? ""
|
|
192
|
+
request.subAgentDepth ?? "",
|
|
193
|
+
// Audit F-A1-07: previously-missing dimensions that DO affect the
|
|
194
|
+
// backend verdict. Without these, two requests with different
|
|
195
|
+
// durations (e.g. 60s vs 86400s) collided on the same cache key and
|
|
196
|
+
// the shorter-duration allow served the longer-duration request.
|
|
197
|
+
request.durationRequired ?? "",
|
|
198
|
+
request.invocationProtocol || "",
|
|
199
|
+
request.enableRuntimeChallenge ? "1" : "0",
|
|
200
|
+
// callerMetadata fields contribute to risk model; include the ones
|
|
201
|
+
// backend reads. sourceIp/userAgent/forwardedFor change per-request
|
|
202
|
+
// so their inclusion effectively forces a re-check for any varying
|
|
203
|
+
// client (the right behavior — IP-driven anomaly scoring shouldn't
|
|
204
|
+
// be cached across IPs).
|
|
205
|
+
request.callerMetadata?.sourceIp || "",
|
|
206
|
+
request.callerMetadata?.userAgent || "",
|
|
207
|
+
request.callerMetadata?.forwardedFor || "",
|
|
208
|
+
request.callerMetadata?.agentCardUrl || ""
|
|
188
209
|
].join("|");
|
|
189
210
|
}
|
|
190
211
|
function getCachedResult(request) {
|
|
@@ -213,9 +234,13 @@ function clearCache() {
|
|
|
213
234
|
}
|
|
214
235
|
function extractCredentials(headers, query) {
|
|
215
236
|
const credentials = {};
|
|
237
|
+
const ASTRA_ID_PATTERN = /^ASTRAE?-[A-Za-z0-9_-]{1,64}$/;
|
|
216
238
|
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"];
|
|
217
239
|
if (astraIdHeader) {
|
|
218
|
-
|
|
240
|
+
const raw = Array.isArray(astraIdHeader) ? astraIdHeader[0] : typeof astraIdHeader === "string" ? astraIdHeader : void 0;
|
|
241
|
+
if (typeof raw === "string" && ASTRA_ID_PATTERN.test(raw)) {
|
|
242
|
+
credentials.astraId = raw;
|
|
243
|
+
}
|
|
219
244
|
}
|
|
220
245
|
const apiKeyHeader = headers["x-api-key"] || headers["X-Api-Key"] || headers["X-API-KEY"];
|
|
221
246
|
if (apiKeyHeader) {
|
|
@@ -224,9 +249,11 @@ function extractCredentials(headers, query) {
|
|
|
224
249
|
const authHeader = headers["authorization"] || headers["Authorization"];
|
|
225
250
|
if (authHeader) {
|
|
226
251
|
const authValue = Array.isArray(authHeader) ? authHeader[0] : authHeader;
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
252
|
+
if (typeof authValue === "string") {
|
|
253
|
+
credentials.authorizationHeader = authValue;
|
|
254
|
+
if (authValue.startsWith("Bearer ")) {
|
|
255
|
+
credentials.jwt = authValue.slice(7);
|
|
256
|
+
}
|
|
230
257
|
}
|
|
231
258
|
}
|
|
232
259
|
if (query) {
|
|
@@ -247,7 +274,7 @@ function createGuidanceResponse(config, reason, options = {}) {
|
|
|
247
274
|
const isApiError = source === "api_error";
|
|
248
275
|
const guidance = isApiError ? {
|
|
249
276
|
message: "Verification is temporarily unavailable. Retry with exponential backoff; if the issue persists, contact support with the correlationId.",
|
|
250
|
-
registrationUrl: `${config.apiBaseUrl.replace("/api", "")}/register`,
|
|
277
|
+
registrationUrl: `${config.apiBaseUrl.replace("/api", "")}/agents/register`,
|
|
251
278
|
documentationUrl: `${config.apiBaseUrl.replace("/api", "")}/docs/agent-access`,
|
|
252
279
|
steps: [
|
|
253
280
|
"Retry the request with exponential backoff",
|
|
@@ -255,7 +282,7 @@ function createGuidanceResponse(config, reason, options = {}) {
|
|
|
255
282
|
]
|
|
256
283
|
} : {
|
|
257
284
|
message: "This service verifies AI agents before granting access. Please register your agent with AstraSync.",
|
|
258
|
-
registrationUrl: `${config.apiBaseUrl.replace("/api", "")}/register`,
|
|
285
|
+
registrationUrl: `${config.apiBaseUrl.replace("/api", "")}/agents/register`,
|
|
259
286
|
documentationUrl: `${config.apiBaseUrl.replace("/api", "")}/docs/agent-access`,
|
|
260
287
|
steps: [
|
|
261
288
|
"Register for an AstraSync account",
|
|
@@ -332,12 +359,8 @@ async function callVerifyAccessAPI(config, request) {
|
|
|
332
359
|
"Content-Type": "application/json",
|
|
333
360
|
...config.customHeaders
|
|
334
361
|
};
|
|
335
|
-
if (credentials.authorizationHeader) {
|
|
336
|
-
headers["Authorization"] = credentials.authorizationHeader;
|
|
337
|
-
} else if (config.apiKey) {
|
|
338
|
-
headers["Authorization"] = `Bearer ${config.apiKey}`;
|
|
339
|
-
}
|
|
340
362
|
if (config.apiKey) {
|
|
363
|
+
headers["Authorization"] = `Bearer ${config.apiKey}`;
|
|
341
364
|
headers["X-API-Key"] = config.apiKey;
|
|
342
365
|
}
|
|
343
366
|
try {
|
|
@@ -383,7 +406,11 @@ async function callVerifyAccessAPI(config, request) {
|
|
|
383
406
|
async function verify(config, request) {
|
|
384
407
|
const mergedConfig = { ...DEFAULT_CONFIG, ...config };
|
|
385
408
|
if (!initCheckPerformed && !mergedConfig.disableInitChecks && mergedConfig.apiBaseUrl) {
|
|
386
|
-
|
|
409
|
+
if (mergedConfig.strictInit) {
|
|
410
|
+
await performInitCheck(mergedConfig.apiBaseUrl, mergedConfig.debug, true);
|
|
411
|
+
} else {
|
|
412
|
+
void performInitCheck(mergedConfig.apiBaseUrl, mergedConfig.debug, false);
|
|
413
|
+
}
|
|
387
414
|
}
|
|
388
415
|
if (!deprecationWarningShown && (config.minTrustScore !== void 0 || config.minTrustScoreForFull !== void 0)) {
|
|
389
416
|
deprecationWarningShown = true;
|
|
@@ -437,7 +464,7 @@ async function verify(config, request) {
|
|
|
437
464
|
requiresApproval: apiResponse.access?.requiresApproval,
|
|
438
465
|
guidance: {
|
|
439
466
|
message: apiResponse.access?.reason || "Access denied by PDLSS policy",
|
|
440
|
-
registrationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/register`,
|
|
467
|
+
registrationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/agents/register`,
|
|
441
468
|
documentationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/docs/pdlss`
|
|
442
469
|
},
|
|
443
470
|
verifiedAt: /* @__PURE__ */ new Date(),
|
|
@@ -507,13 +534,15 @@ async function verify(config, request) {
|
|
|
507
534
|
result.denialReasons = result.recommendationReasons || [
|
|
508
535
|
"Access denied by AstraSync recommendation"
|
|
509
536
|
];
|
|
510
|
-
|
|
511
|
-
result.
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
537
|
+
result.guidance = result.runtimeChallenge ? {
|
|
538
|
+
message: `Verification failed: ${result.runtimeChallenge.reason || "runtime challenge failed"}`,
|
|
539
|
+
registrationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/agents/register`,
|
|
540
|
+
documentationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/docs/runtime-challenge`
|
|
541
|
+
} : {
|
|
542
|
+
message: result.recommendationReasons?.[0] || "Access denied by AstraSync recommendation",
|
|
543
|
+
registrationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/agents/register`,
|
|
544
|
+
documentationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/docs/pdlss`
|
|
545
|
+
};
|
|
517
546
|
} else if (result.recommendation === "step_up_required") {
|
|
518
547
|
result.requiresStepUp = true;
|
|
519
548
|
if (ACCESS_LEVEL_HIERARCHY[result.accessLevel] > ACCESS_LEVEL_HIERARCHY["read-only"]) {
|
|
@@ -762,18 +791,40 @@ function defaultExtractPurpose(req) {
|
|
|
762
791
|
return "general";
|
|
763
792
|
}
|
|
764
793
|
}
|
|
765
|
-
function matchRoute(pattern, path) {
|
|
794
|
+
function matchRoute(pattern, path, opts) {
|
|
766
795
|
const regexPattern = pattern.replace(/\*/g, ".*").replace(/\//g, "\\/");
|
|
767
|
-
const
|
|
768
|
-
|
|
796
|
+
const caseSensitiveRegex = new RegExp(`^${regexPattern}$`);
|
|
797
|
+
const caseSensitiveResult = caseSensitiveRegex.test(path);
|
|
798
|
+
if (!opts?.caseInsensitive && !opts?.logShadowDivergence) {
|
|
799
|
+
return caseSensitiveResult;
|
|
800
|
+
}
|
|
801
|
+
const caseInsensitiveRegex = new RegExp(`^${regexPattern}$`, "i");
|
|
802
|
+
const caseInsensitiveResult = caseInsensitiveRegex.test(path);
|
|
803
|
+
if (opts?.logShadowDivergence && caseSensitiveResult !== caseInsensitiveResult) {
|
|
804
|
+
console.warn(
|
|
805
|
+
`[SHADOW] matchRoute case-insensitive would change result: pattern=${pattern} path=${path} caseSensitive=${caseSensitiveResult} caseInsensitive=${caseInsensitiveResult} correlationId=${opts.correlationId ?? "unknown"}`
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
return opts?.caseInsensitive ? caseInsensitiveResult : caseSensitiveResult;
|
|
769
809
|
}
|
|
770
|
-
function findRouteConfig(routes, path, method) {
|
|
810
|
+
function findRouteConfig(routes, path, method, opts) {
|
|
771
811
|
return routes.find((route) => {
|
|
772
812
|
const methodMatches = route.method === "*" || route.method.toUpperCase() === method.toUpperCase();
|
|
773
|
-
const pathMatches = matchRoute(route.pattern, path);
|
|
813
|
+
const pathMatches = matchRoute(route.pattern, path, opts);
|
|
774
814
|
return methodMatches && pathMatches;
|
|
775
815
|
});
|
|
776
816
|
}
|
|
817
|
+
function dedupeFailures(result) {
|
|
818
|
+
if (result.failures && result.failures.length > 1) {
|
|
819
|
+
const seen = /* @__PURE__ */ new Set();
|
|
820
|
+
result.failures = result.failures.filter((f) => {
|
|
821
|
+
const key = `${f.dimension}|${f.message}|${f.guidance ?? ""}`;
|
|
822
|
+
if (seen.has(key)) return false;
|
|
823
|
+
seen.add(key);
|
|
824
|
+
return true;
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
}
|
|
777
828
|
function defaultOnDenied(result, _req, res) {
|
|
778
829
|
const statusCode = !result.identityVerified ? 401 : 403;
|
|
779
830
|
res.setHeader("X-Astra-Gateway-Mode", "enforced");
|
|
@@ -800,6 +851,8 @@ function createMiddleware(options) {
|
|
|
800
851
|
recordDecisions,
|
|
801
852
|
enableRuntimeChallenge = true,
|
|
802
853
|
routesRefreshMs = DEFAULT_ROUTES_REFRESH_MS,
|
|
854
|
+
failOnError = "open",
|
|
855
|
+
caseInsensitiveRouteMatch = false,
|
|
803
856
|
...config
|
|
804
857
|
} = options;
|
|
805
858
|
let cachedRoutes = [];
|
|
@@ -822,7 +875,7 @@ function createMiddleware(options) {
|
|
|
822
875
|
cachedRoutes = fetched;
|
|
823
876
|
lastFetchAt = Date.now();
|
|
824
877
|
if (cachedRoutes.length === 0 && !warnedEmptyRoutes) {
|
|
825
|
-
const dashboard = config.dashboardUrl ?? "https://
|
|
878
|
+
const dashboard = config.dashboardUrl ?? "https://astrasync.ai/dashboard";
|
|
826
879
|
console.warn(
|
|
827
880
|
`[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`
|
|
828
881
|
);
|
|
@@ -848,7 +901,12 @@ function createMiddleware(options) {
|
|
|
848
901
|
refreshing = null;
|
|
849
902
|
});
|
|
850
903
|
}
|
|
851
|
-
const
|
|
904
|
+
const correlationId = req.headers["x-request-id"] || req.headers["x-correlation-id"];
|
|
905
|
+
const routeConfig = findRouteConfig(cachedRoutes, req.path, req.method, {
|
|
906
|
+
caseInsensitive: caseInsensitiveRouteMatch,
|
|
907
|
+
logShadowDivergence: true,
|
|
908
|
+
correlationId
|
|
909
|
+
});
|
|
852
910
|
if (!routeConfig) {
|
|
853
911
|
if (config.setPassThroughHeader) {
|
|
854
912
|
res.setHeader("X-Astra-Gateway-Mode", "unenforced");
|
|
@@ -880,7 +938,7 @@ function createMiddleware(options) {
|
|
|
880
938
|
denialReasons: preCheckFailures.map((f) => f.message),
|
|
881
939
|
guidance: {
|
|
882
940
|
message: "Request exceeds counterparty-defined PDLSS limits.",
|
|
883
|
-
registrationUrl: `${config.apiBaseUrl?.replace("/api", "")}/register`,
|
|
941
|
+
registrationUrl: `${config.apiBaseUrl?.replace("/api", "")}/agents/register`,
|
|
884
942
|
documentationUrl: `${config.apiBaseUrl?.replace("/api", "")}/docs/pdlss`
|
|
885
943
|
},
|
|
886
944
|
verifiedAt: /* @__PURE__ */ new Date()
|
|
@@ -895,13 +953,19 @@ function createMiddleware(options) {
|
|
|
895
953
|
requestMethod: req.method
|
|
896
954
|
}).catch(() => {
|
|
897
955
|
});
|
|
956
|
+
dedupeFailures(result2);
|
|
898
957
|
onDenied(result2, req, res);
|
|
899
958
|
return;
|
|
900
959
|
}
|
|
901
960
|
const shouldRecordDecisions = recordDecisions !== false;
|
|
902
961
|
const forwardedFor = req.headers["x-forwarded-for"];
|
|
903
962
|
const forwardedForStr = Array.isArray(forwardedFor) ? forwardedFor.join(", ") : forwardedFor;
|
|
904
|
-
const originalClientIp = forwardedForStr ? forwardedForStr.split(",")[0].trim() :
|
|
963
|
+
const originalClientIp = req.ip ?? (forwardedForStr ? forwardedForStr.split(",")[0].trim() : void 0);
|
|
964
|
+
if (!req.ip && forwardedForStr) {
|
|
965
|
+
console.warn(
|
|
966
|
+
"[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."
|
|
967
|
+
);
|
|
968
|
+
}
|
|
905
969
|
const agentCardUrl = typeof req.headers["x-astrasync-agent-card"] === "string" ? req.headers["x-astrasync-agent-card"] : void 0;
|
|
906
970
|
const result = await verify(config, {
|
|
907
971
|
credentials,
|
|
@@ -932,6 +996,7 @@ function createMiddleware(options) {
|
|
|
932
996
|
recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
|
|
933
997
|
});
|
|
934
998
|
}
|
|
999
|
+
dedupeFailures(result);
|
|
935
1000
|
onDenied(result, req, res);
|
|
936
1001
|
return;
|
|
937
1002
|
}
|
|
@@ -958,6 +1023,7 @@ function createMiddleware(options) {
|
|
|
958
1023
|
recordDecision(config, sessionId, "denied", insufficientFailure.message).catch(() => {
|
|
959
1024
|
});
|
|
960
1025
|
}
|
|
1026
|
+
dedupeFailures(result);
|
|
961
1027
|
onDenied(result, req, res);
|
|
962
1028
|
return;
|
|
963
1029
|
}
|
|
@@ -974,6 +1040,7 @@ function createMiddleware(options) {
|
|
|
974
1040
|
recordDecision(config, sessionId, "denied", trustFailure.message).catch(() => {
|
|
975
1041
|
});
|
|
976
1042
|
}
|
|
1043
|
+
dedupeFailures(result);
|
|
977
1044
|
onDenied(result, req, res);
|
|
978
1045
|
return;
|
|
979
1046
|
}
|
|
@@ -988,7 +1055,30 @@ function createMiddleware(options) {
|
|
|
988
1055
|
}
|
|
989
1056
|
next();
|
|
990
1057
|
} catch (error) {
|
|
1058
|
+
const errorClass = error instanceof Error ? error.constructor.name : typeof error;
|
|
1059
|
+
const correlationId = req.headers["x-request-id"] || req.headers["x-correlation-id"] || `gen-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
991
1060
|
console.error("[VerificationGateway] Middleware error:", error);
|
|
1061
|
+
console.warn(
|
|
1062
|
+
`[SHADOW] would-have-denied: errorClass=${errorClass} route=${req.method}:${req.path} merchantId=${config.counterpartyId ?? "unknown"} correlationId=${correlationId}`
|
|
1063
|
+
);
|
|
1064
|
+
if (failOnError === "closed") {
|
|
1065
|
+
const result = {
|
|
1066
|
+
identityVerified: false,
|
|
1067
|
+
policyAllowed: false,
|
|
1068
|
+
accessLevel: "none",
|
|
1069
|
+
denialReasons: [`Verification middleware internal error: ${errorClass}`],
|
|
1070
|
+
failures: [
|
|
1071
|
+
{
|
|
1072
|
+
dimension: "middleware.internal_error",
|
|
1073
|
+
message: `Middleware threw ${errorClass} \u2014 failing closed`
|
|
1074
|
+
}
|
|
1075
|
+
],
|
|
1076
|
+
verifiedAt: /* @__PURE__ */ new Date(),
|
|
1077
|
+
correlationId
|
|
1078
|
+
};
|
|
1079
|
+
dedupeFailures(result);
|
|
1080
|
+
return onDenied(result, req, res);
|
|
1081
|
+
}
|
|
992
1082
|
next();
|
|
993
1083
|
}
|
|
994
1084
|
};
|
|
@@ -1000,6 +1090,18 @@ __export(nextjs_exports, {
|
|
|
1000
1090
|
createMatcherConfig: () => createMatcherConfig,
|
|
1001
1091
|
createMiddleware: () => createMiddleware2
|
|
1002
1092
|
});
|
|
1093
|
+
function escapeHtml(value) {
|
|
1094
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1095
|
+
}
|
|
1096
|
+
function sanitizeUrl(value, fallback) {
|
|
1097
|
+
if (typeof value !== "string" || value.length === 0) return escapeHtml(fallback);
|
|
1098
|
+
const trimmed = value.trim();
|
|
1099
|
+
if (/^javascript:|^data:|^vbscript:/i.test(trimmed)) return escapeHtml(fallback);
|
|
1100
|
+
if (/^https?:\/\//i.test(trimmed) || trimmed.startsWith("/")) {
|
|
1101
|
+
return escapeHtml(trimmed);
|
|
1102
|
+
}
|
|
1103
|
+
return escapeHtml(fallback);
|
|
1104
|
+
}
|
|
1003
1105
|
function extractCredentialsFromNextRequest(request) {
|
|
1004
1106
|
const credentials = {};
|
|
1005
1107
|
const astraId = request.headers.get("x-astra-id") || request.headers.get("X-Astra-Id");
|
|
@@ -1071,10 +1173,18 @@ function extractPurpose(request) {
|
|
|
1071
1173
|
}
|
|
1072
1174
|
}
|
|
1073
1175
|
function generateCommerceShieldHtml(result, options) {
|
|
1074
|
-
const title = options.commerceShield?.title || "AstraSync Agent Verification";
|
|
1075
|
-
const message =
|
|
1076
|
-
|
|
1077
|
-
|
|
1176
|
+
const title = escapeHtml(options.commerceShield?.title || "AstraSync Agent Verification");
|
|
1177
|
+
const message = escapeHtml(
|
|
1178
|
+
options.commerceShield?.message || result.guidance?.message || "This site verifies AI agents before granting access. We noticed you're visiting without AstraSync credentials."
|
|
1179
|
+
);
|
|
1180
|
+
const registrationUrl = sanitizeUrl(
|
|
1181
|
+
result.guidance?.registrationUrl,
|
|
1182
|
+
"https://astrasync.ai/register"
|
|
1183
|
+
);
|
|
1184
|
+
const docsUrl = sanitizeUrl(
|
|
1185
|
+
result.guidance?.documentationUrl,
|
|
1186
|
+
"https://astrasync.ai/docs/agent-access"
|
|
1187
|
+
);
|
|
1078
1188
|
const allowGuest = options.commerceShield?.allowGuestAccess ?? true;
|
|
1079
1189
|
return `
|
|
1080
1190
|
<!DOCTYPE html>
|
|
@@ -1196,7 +1306,7 @@ function generateCommerceShieldHtml(result, options) {
|
|
|
1196
1306
|
<div class="shield-steps">
|
|
1197
1307
|
<h3>To get verified access:</h3>
|
|
1198
1308
|
<ol>
|
|
1199
|
-
<li>Register at <a href="${registrationUrl}">astrasync.ai/register</a></li>
|
|
1309
|
+
<li>Register at <a href="${registrationUrl}">astrasync.ai/agents/register</a></li>
|
|
1200
1310
|
<li>Create and register your agent</li>
|
|
1201
1311
|
<li>Add your ASTRA-ID to request headers</li>
|
|
1202
1312
|
<li>Refresh this page</li>
|
|
@@ -1284,7 +1394,7 @@ function createMiddleware2(options) {
|
|
|
1284
1394
|
denialReasons: preCheckFailures.map((f) => f.message),
|
|
1285
1395
|
guidance: {
|
|
1286
1396
|
message: "Request exceeds counterparty-defined PDLSS limits.",
|
|
1287
|
-
registrationUrl: `${config.apiBaseUrl?.replace("/api", "")}/register`,
|
|
1397
|
+
registrationUrl: `${config.apiBaseUrl?.replace("/api", "")}/agents/register`,
|
|
1288
1398
|
documentationUrl: `${config.apiBaseUrl?.replace("/api", "")}/docs/pdlss`
|
|
1289
1399
|
},
|
|
1290
1400
|
verifiedAt: /* @__PURE__ */ new Date()
|
|
@@ -1945,12 +2055,45 @@ function bufferToBase64(bytes) {
|
|
|
1945
2055
|
|
|
1946
2056
|
// src/transport/rfc9421-verify.ts
|
|
1947
2057
|
import { httpbis } from "http-message-signatures";
|
|
2058
|
+
|
|
2059
|
+
// src/transport/nonce-store.ts
|
|
2060
|
+
var InMemoryNonceStore = class {
|
|
2061
|
+
constructor(capacity = 1e4) {
|
|
2062
|
+
this.entries = /* @__PURE__ */ new Map();
|
|
2063
|
+
this.lastSweepMs = 0;
|
|
2064
|
+
this.capacity = capacity;
|
|
2065
|
+
}
|
|
2066
|
+
seen(key, expiresAtMs) {
|
|
2067
|
+
const nowMs = Date.now();
|
|
2068
|
+
if (nowMs - this.lastSweepMs > 1e3) {
|
|
2069
|
+
for (const [k, exp] of this.entries) {
|
|
2070
|
+
if (exp <= nowMs) this.entries.delete(k);
|
|
2071
|
+
}
|
|
2072
|
+
this.lastSweepMs = nowMs;
|
|
2073
|
+
}
|
|
2074
|
+
const existing = this.entries.get(key);
|
|
2075
|
+
if (existing !== void 0 && existing > nowMs) {
|
|
2076
|
+
return true;
|
|
2077
|
+
}
|
|
2078
|
+
if (this.entries.size >= this.capacity) {
|
|
2079
|
+
const oldest = this.entries.keys().next().value;
|
|
2080
|
+
if (oldest !== void 0) this.entries.delete(oldest);
|
|
2081
|
+
}
|
|
2082
|
+
this.entries.set(key, expiresAtMs);
|
|
2083
|
+
return false;
|
|
2084
|
+
}
|
|
2085
|
+
};
|
|
2086
|
+
var defaultNonceStore = new InMemoryNonceStore();
|
|
2087
|
+
|
|
2088
|
+
// src/transport/rfc9421-verify.ts
|
|
1948
2089
|
async function verifyRFC9421(request, options) {
|
|
1949
2090
|
const { resolver } = options;
|
|
1950
|
-
const tolerance = options.clockSkewSec ??
|
|
2091
|
+
const tolerance = options.clockSkewSec ?? 60;
|
|
1951
2092
|
const nowSec = options.now ? options.now() : Math.floor(Date.now() / 1e3);
|
|
2093
|
+
const nonceStore = options.nonceStore ?? defaultNonceStore;
|
|
1952
2094
|
let resolvedKid;
|
|
1953
2095
|
let resolvedAlg;
|
|
2096
|
+
let replayDetected = false;
|
|
1954
2097
|
const keyLookup = async (parameters) => {
|
|
1955
2098
|
const kid = typeof parameters.keyid === "string" ? parameters.keyid : void 0;
|
|
1956
2099
|
if (!kid) return null;
|
|
@@ -1964,6 +2107,14 @@ async function verifyRFC9421(request, options) {
|
|
|
1964
2107
|
const expires = toUnixSeconds(parameters.expires);
|
|
1965
2108
|
if (created !== void 0 && Math.abs(nowSec - created) > tolerance) return null;
|
|
1966
2109
|
if (expires !== void 0 && nowSec > expires + tolerance) return null;
|
|
2110
|
+
const nonce = typeof parameters.nonce === "string" ? parameters.nonce : void 0;
|
|
2111
|
+
if (nonce) {
|
|
2112
|
+
const expiresAtMs = (expires !== void 0 ? expires + tolerance : nowSec + tolerance) * 1e3;
|
|
2113
|
+
if (nonceStore.seen(`rfc9421:${kid}:${nonce}`, expiresAtMs)) {
|
|
2114
|
+
replayDetected = true;
|
|
2115
|
+
return null;
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
1967
2118
|
return jwkToVerifyingKey(kid, jwk, alg);
|
|
1968
2119
|
};
|
|
1969
2120
|
try {
|
|
@@ -1986,7 +2137,7 @@ async function verifyRFC9421(request, options) {
|
|
|
1986
2137
|
kid: resolvedKid,
|
|
1987
2138
|
registry: resolver.name,
|
|
1988
2139
|
algorithm: resolvedAlg,
|
|
1989
|
-
error: result === false ? "signature invalid" : "no signature found"
|
|
2140
|
+
error: replayDetected ? "RFC9421 signature replay \u2014 already seen within tolerance window" : result === false ? "signature invalid" : "no signature found"
|
|
1990
2141
|
};
|
|
1991
2142
|
} catch (err) {
|
|
1992
2143
|
return {
|
|
@@ -2811,14 +2962,26 @@ function sha256Sync2(data) {
|
|
|
2811
2962
|
function verifyAP2Chain(input) {
|
|
2812
2963
|
const { triple } = input;
|
|
2813
2964
|
const errors = [];
|
|
2965
|
+
const toleranceSec = input.clockSkewSec ?? 60;
|
|
2966
|
+
const nonceStore = input.nonceStore ?? defaultNonceStore;
|
|
2814
2967
|
const intentPresent = triple.intent !== void 0;
|
|
2815
2968
|
const cartRefOk = checkCartRef(triple, errors);
|
|
2816
2969
|
const paymentRefOk = checkPaymentRef(triple, errors);
|
|
2817
2970
|
const { ok: agentIdContinuity, agentId } = checkAgentContinuity(triple, errors);
|
|
2818
2971
|
const paymentMethodAllowed = checkPaymentMethod(triple, errors);
|
|
2819
2972
|
const totalsConsistent = checkTotals(triple, errors);
|
|
2820
|
-
const expiryOk = checkExpiries(triple,
|
|
2821
|
-
|
|
2973
|
+
const expiryOk = checkExpiries(triple, toleranceSec, input.now, errors);
|
|
2974
|
+
let replayOk = true;
|
|
2975
|
+
const replayId = triple.payment?.raw?.id ?? triple.cart?.raw?.id;
|
|
2976
|
+
if (typeof replayId === "string" && replayId.length > 0) {
|
|
2977
|
+
const now = input.now ? input.now() : Math.floor(Date.now() / 1e3);
|
|
2978
|
+
const expiresAt = (now + toleranceSec) * 1e3;
|
|
2979
|
+
if (nonceStore.seen(`ap2:${replayId}`, expiresAt)) {
|
|
2980
|
+
errors.push(`AP2 chain replay \u2014 mandate ${replayId} already seen within tolerance window`);
|
|
2981
|
+
replayOk = false;
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
const ok = cartRefOk && paymentRefOk && agentIdContinuity && paymentMethodAllowed && totalsConsistent && expiryOk && replayOk;
|
|
2822
2985
|
return {
|
|
2823
2986
|
ok,
|
|
2824
2987
|
checks: {
|
|
@@ -2864,7 +3027,10 @@ function checkAgentContinuity(triple, errors) {
|
|
|
2864
3027
|
const ids = [triple.intent?.agent_id, triple.cart?.agent_id, triple.payment?.agent_id].filter(
|
|
2865
3028
|
(id) => typeof id === "string" && id.length > 0
|
|
2866
3029
|
);
|
|
2867
|
-
if (ids.length === 0)
|
|
3030
|
+
if (ids.length === 0) {
|
|
3031
|
+
errors.push("agent_id missing across all three mandates (intent/cart/payment)");
|
|
3032
|
+
return { ok: false };
|
|
3033
|
+
}
|
|
2868
3034
|
const unique = new Set(ids);
|
|
2869
3035
|
if (unique.size > 1) {
|
|
2870
3036
|
errors.push(`agent_id mismatch across mandates: ${Array.from(unique).join(", ")}`);
|
|
@@ -2873,9 +3039,16 @@ function checkAgentContinuity(triple, errors) {
|
|
|
2873
3039
|
return { ok: true, agentId: ids[0] };
|
|
2874
3040
|
}
|
|
2875
3041
|
function checkPaymentMethod(triple, errors) {
|
|
2876
|
-
const paymentMethod = triple.payment?.payment_method;
|
|
2877
3042
|
const allowed = triple.intent?.paymentMethods;
|
|
2878
|
-
if (!
|
|
3043
|
+
if (!allowed || allowed.length === 0) return true;
|
|
3044
|
+
if (!triple.payment) return true;
|
|
3045
|
+
const paymentMethod = triple.payment.payment_method;
|
|
3046
|
+
if (!paymentMethod) {
|
|
3047
|
+
errors.push(
|
|
3048
|
+
`payment.payment_method missing but intent declares allowlist [${allowed.join(", ")}]`
|
|
3049
|
+
);
|
|
3050
|
+
return false;
|
|
3051
|
+
}
|
|
2879
3052
|
if (!allowed.includes(paymentMethod)) {
|
|
2880
3053
|
errors.push(
|
|
2881
3054
|
`payment_method "${paymentMethod}" not in intent.paymentMethods [${allowed.join(", ")}]`
|
|
@@ -2909,19 +3082,24 @@ function checkTotals(triple, errors) {
|
|
|
2909
3082
|
function checkExpiries(triple, toleranceSec, nowFn, errors) {
|
|
2910
3083
|
const now = nowFn ? nowFn() : Math.floor(Date.now() / 1e3);
|
|
2911
3084
|
let ok = true;
|
|
2912
|
-
|
|
2913
|
-
["intent", triple.intent],
|
|
2914
|
-
["cart", triple.cart]
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
3085
|
+
const layers = [
|
|
3086
|
+
["intent", triple.intent?.expires],
|
|
3087
|
+
["cart", triple.cart?.expires],
|
|
3088
|
+
[
|
|
3089
|
+
"payment",
|
|
3090
|
+
typeof triple.payment?.raw?.expires === "string" ? triple.payment.raw.expires : typeof triple.payment?.raw?.exp === "string" ? triple.payment.raw.exp : void 0
|
|
3091
|
+
]
|
|
3092
|
+
];
|
|
3093
|
+
for (const [name, expires] of layers) {
|
|
3094
|
+
if (!expires) continue;
|
|
3095
|
+
const parsed = parseExpiry(expires);
|
|
2918
3096
|
if (parsed === null) {
|
|
2919
3097
|
errors.push(`${name}.expires unparseable`);
|
|
2920
3098
|
ok = false;
|
|
2921
3099
|
continue;
|
|
2922
3100
|
}
|
|
2923
3101
|
if (now > parsed + toleranceSec) {
|
|
2924
|
-
errors.push(`${name} mandate expired at ${
|
|
3102
|
+
errors.push(`${name} mandate expired at ${expires}`);
|
|
2925
3103
|
ok = false;
|
|
2926
3104
|
}
|
|
2927
3105
|
}
|
|
@@ -2948,10 +3126,21 @@ async function verifyACPSignature(input) {
|
|
|
2948
3126
|
if (!input.signatureHeader) {
|
|
2949
3127
|
return { ok: false, error: "missing Signature header" };
|
|
2950
3128
|
}
|
|
2951
|
-
const
|
|
3129
|
+
const tolerance = input.clockSkewSec ?? 60;
|
|
3130
|
+
const nonceStore = input.nonceStore ?? defaultNonceStore;
|
|
3131
|
+
const freshness = checkTimestamp(input.timestampHeader, tolerance, input.now);
|
|
2952
3132
|
if (!freshness.ok) {
|
|
2953
3133
|
return { ok: false, error: freshness.error, timestampStale: true };
|
|
2954
3134
|
}
|
|
3135
|
+
const nowSec = input.now ? input.now() : Math.floor(Date.now() / 1e3);
|
|
3136
|
+
const expiresAtMs = (nowSec + tolerance) * 1e3;
|
|
3137
|
+
const replayKey = `acp:${input.signatureHeader}:${input.timestampHeader ?? ""}`;
|
|
3138
|
+
if (nonceStore.seen(replayKey, expiresAtMs)) {
|
|
3139
|
+
return {
|
|
3140
|
+
ok: false,
|
|
3141
|
+
error: "ACP signature replay \u2014 already seen within tolerance window"
|
|
3142
|
+
};
|
|
3143
|
+
}
|
|
2955
3144
|
const signatureBytes = decodeBase64(input.signatureHeader);
|
|
2956
3145
|
if (!signatureBytes) {
|
|
2957
3146
|
return { ok: false, error: "signature header is not valid base64" };
|
|
@@ -3169,8 +3358,9 @@ function coerceString6(v) {
|
|
|
3169
3358
|
import { BodyDigest } from "mppx";
|
|
3170
3359
|
function verifyMPP(input) {
|
|
3171
3360
|
const { context } = input;
|
|
3172
|
-
const tolerance = input.clockSkewSec ??
|
|
3361
|
+
const tolerance = input.clockSkewSec ?? 60;
|
|
3173
3362
|
const nowSec = input.now ? input.now() : Math.floor(Date.now() / 1e3);
|
|
3363
|
+
const nonceStore = input.nonceStore ?? defaultNonceStore;
|
|
3174
3364
|
const challenge = context.credential?.challenge ?? (context.challenges && context.challenges[0]);
|
|
3175
3365
|
const source = context.credential?.source;
|
|
3176
3366
|
const method = challenge?.method;
|
|
@@ -3193,21 +3383,38 @@ function verifyMPP(input) {
|
|
|
3193
3383
|
}
|
|
3194
3384
|
}
|
|
3195
3385
|
let bodyDigestOk = null;
|
|
3196
|
-
if (
|
|
3197
|
-
|
|
3198
|
-
|
|
3386
|
+
if (input.rawBody !== void 0) {
|
|
3387
|
+
if (!challenge?.digest) {
|
|
3388
|
+
bodyDigestOk = false;
|
|
3389
|
+
} else {
|
|
3390
|
+
try {
|
|
3391
|
+
if (!/^sha-256=/.test(challenge.digest)) {
|
|
3392
|
+
bodyDigestOk = false;
|
|
3393
|
+
} else {
|
|
3394
|
+
bodyDigestOk = BodyDigest.verify(challenge.digest, input.rawBody);
|
|
3395
|
+
}
|
|
3396
|
+
} catch {
|
|
3199
3397
|
bodyDigestOk = false;
|
|
3200
|
-
} else {
|
|
3201
|
-
bodyDigestOk = BodyDigest.verify(challenge.digest, input.rawBody);
|
|
3202
3398
|
}
|
|
3203
|
-
} catch {
|
|
3204
|
-
bodyDigestOk = false;
|
|
3205
3399
|
}
|
|
3206
3400
|
}
|
|
3207
|
-
|
|
3401
|
+
let replayOk = true;
|
|
3402
|
+
if (challenge?.digest && expiryOk) {
|
|
3403
|
+
const replayKey = `mpp:${challenge.digest}:${challenge.nonce ?? ""}`;
|
|
3404
|
+
const expiresAt = (nowSec + tolerance) * 1e3;
|
|
3405
|
+
if (nonceStore.seen(replayKey, expiresAt)) {
|
|
3406
|
+
replayOk = false;
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
const ok = expiryOk && (bodyDigestOk === null || bodyDigestOk === true) && replayOk;
|
|
3208
3410
|
const errors = [];
|
|
3209
3411
|
if (!expiryOk) errors.push("challenge expired");
|
|
3210
|
-
if (bodyDigestOk === false)
|
|
3412
|
+
if (bodyDigestOk === false) {
|
|
3413
|
+
errors.push(
|
|
3414
|
+
input.rawBody !== void 0 && !challenge?.digest ? "body digest required when rawBody present" : "body digest mismatch"
|
|
3415
|
+
);
|
|
3416
|
+
}
|
|
3417
|
+
if (!replayOk) errors.push("MPP challenge replay \u2014 already seen within tolerance window");
|
|
3211
3418
|
return {
|
|
3212
3419
|
ok,
|
|
3213
3420
|
expiryOk,
|
|
@@ -3371,14 +3578,32 @@ function readHeader4(headers, name) {
|
|
|
3371
3578
|
import { createHash as createHash3, webcrypto } from "crypto";
|
|
3372
3579
|
async function verifyVIChain(input) {
|
|
3373
3580
|
const errors = [];
|
|
3374
|
-
const tolerance = input.clockSkewSec ??
|
|
3581
|
+
const tolerance = input.clockSkewSec ?? 60;
|
|
3375
3582
|
const now = input.now ? input.now() : Math.floor(Date.now() / 1e3);
|
|
3376
3583
|
const { l1, l2, l3a, l3b } = input.layers;
|
|
3584
|
+
const nonceStore = input.nonceStore ?? defaultNonceStore;
|
|
3585
|
+
if (!l1) {
|
|
3586
|
+
if (!input.allowUnboundChain) {
|
|
3587
|
+
errors.push(
|
|
3588
|
+
"L1 missing \u2014 chain root unbound (set allowUnboundChain + expectedL2Key to override)"
|
|
3589
|
+
);
|
|
3590
|
+
} else if (!input.expectedL2Key) {
|
|
3591
|
+
errors.push("allowUnboundChain set but expectedL2Key missing");
|
|
3592
|
+
}
|
|
3593
|
+
}
|
|
3377
3594
|
const l1SigOk = l1 ? await input.verifySignature(l1, null) : null;
|
|
3378
3595
|
if (l1 && !l1SigOk) errors.push("L1 signature invalid");
|
|
3379
3596
|
const l1Cnf = extractCnfJwk(l1?.payload);
|
|
3380
|
-
const
|
|
3597
|
+
const l2ExpectedKey = l1Cnf ?? input.expectedL2Key ?? null;
|
|
3598
|
+
const l2SigOk = await input.verifySignature(l2, l2ExpectedKey);
|
|
3381
3599
|
if (!l2SigOk) errors.push("L2 signature invalid");
|
|
3600
|
+
if (l2SigOk) {
|
|
3601
|
+
const replayKey = `vi:l2:${l2.compact}`;
|
|
3602
|
+
const expiresAt = now * 1e3 + tolerance * 1e3;
|
|
3603
|
+
if (nonceStore.seen(replayKey, expiresAt)) {
|
|
3604
|
+
errors.push("L2 signature replay \u2014 already seen within tolerance window");
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3382
3607
|
const l2Cnf = extractCnfJwk(l2.payload);
|
|
3383
3608
|
const l3aSigOk = l3a ? await input.verifySignature(l3a, l2Cnf ?? null) : null;
|
|
3384
3609
|
if (l3a && !l3aSigOk) errors.push("L3a signature invalid");
|
|
@@ -3422,7 +3647,10 @@ async function verifyVIChain(input) {
|
|
|
3422
3647
|
}
|
|
3423
3648
|
}
|
|
3424
3649
|
const expiryOk = checkExpiryAcross([l1, l2, l3a, l3b], tolerance, now, errors);
|
|
3425
|
-
const
|
|
3650
|
+
const noUnboundChainOrReplayErrors = !errors.some(
|
|
3651
|
+
(e) => e.startsWith("L1 missing") || e.startsWith("allowUnboundChain set") || e.startsWith("L2 signature replay")
|
|
3652
|
+
);
|
|
3653
|
+
const ok = l1SigOk !== false && l2SigOk && l3aSigOk !== false && l3bSigOk !== false && l1BindsL2 && l2BindsL3 && l3aL3bTxnIdMatch !== false && checkoutHashOk !== false && expiryOk && noUnboundChainOrReplayErrors;
|
|
3426
3654
|
return {
|
|
3427
3655
|
ok,
|
|
3428
3656
|
checks: {
|
|
@@ -4077,7 +4305,7 @@ function mcpToPdlss(parsed, headerPurpose, headerAction) {
|
|
|
4077
4305
|
action = parsed.actionFromBody;
|
|
4078
4306
|
actionSource = parsed.actionSourceFromBody;
|
|
4079
4307
|
} else {
|
|
4080
|
-
action = parsed.toolName ? `${parsed.method}:${parsed.toolName}` : parsed.method;
|
|
4308
|
+
action = parsed.toolName ? parsed.method === "tools/call" ? parsed.toolName : `${parsed.method}:${parsed.toolName}` : parsed.method;
|
|
4081
4309
|
actionSource = "transport_layer";
|
|
4082
4310
|
}
|
|
4083
4311
|
return { purpose, action, resource, purposeSource, actionSource };
|
|
@@ -4096,6 +4324,17 @@ function readSingleHeader(value) {
|
|
|
4096
4324
|
if (Array.isArray(value)) return value[0];
|
|
4097
4325
|
return void 0;
|
|
4098
4326
|
}
|
|
4327
|
+
function dedupeFailures2(result) {
|
|
4328
|
+
if (result.failures && result.failures.length > 1) {
|
|
4329
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4330
|
+
result.failures = result.failures.filter((f) => {
|
|
4331
|
+
const key = `${f.dimension}|${f.message}|${f.guidance ?? ""}`;
|
|
4332
|
+
if (seen.has(key)) return false;
|
|
4333
|
+
seen.add(key);
|
|
4334
|
+
return true;
|
|
4335
|
+
});
|
|
4336
|
+
}
|
|
4337
|
+
}
|
|
4099
4338
|
function defaultMcpDenied(result, req, res) {
|
|
4100
4339
|
const id = req.body?.id ?? null;
|
|
4101
4340
|
const status = !result.identityVerified ? 401 : 403;
|
|
@@ -4135,10 +4374,11 @@ function createMcpMiddleware(options) {
|
|
|
4135
4374
|
onAgentIdMismatch = "reject",
|
|
4136
4375
|
skip = false,
|
|
4137
4376
|
onDenied = defaultMcpDenied,
|
|
4138
|
-
trustVerifiedHop =
|
|
4377
|
+
trustVerifiedHop = false,
|
|
4139
4378
|
verifiedHopMaxAgeMs,
|
|
4140
4379
|
recordDecisions,
|
|
4141
4380
|
enableRuntimeChallenge = true,
|
|
4381
|
+
failOnError = "open",
|
|
4142
4382
|
...config
|
|
4143
4383
|
} = options;
|
|
4144
4384
|
return async (req, res, next) => {
|
|
@@ -4246,6 +4486,7 @@ function createMcpMiddleware(options) {
|
|
|
4246
4486
|
recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
|
|
4247
4487
|
});
|
|
4248
4488
|
}
|
|
4489
|
+
dedupeFailures2(result);
|
|
4249
4490
|
onDenied(result, req, res);
|
|
4250
4491
|
return;
|
|
4251
4492
|
}
|
|
@@ -4291,6 +4532,7 @@ function createMcpMiddleware(options) {
|
|
|
4291
4532
|
});
|
|
4292
4533
|
}
|
|
4293
4534
|
}
|
|
4535
|
+
dedupeFailures2(result);
|
|
4294
4536
|
onDenied(result, req, res);
|
|
4295
4537
|
return;
|
|
4296
4538
|
}
|
|
@@ -4314,7 +4556,30 @@ function createMcpMiddleware(options) {
|
|
|
4314
4556
|
}
|
|
4315
4557
|
next();
|
|
4316
4558
|
} catch (error) {
|
|
4559
|
+
const errorClass = error instanceof Error ? error.constructor.name : typeof error;
|
|
4560
|
+
const correlationId = req.headers["x-request-id"] || req.headers["x-correlation-id"] || `gen-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
4317
4561
|
console.error("[VerificationGateway/MCP] Middleware error:", error);
|
|
4562
|
+
console.warn(
|
|
4563
|
+
`[SHADOW] would-have-denied: errorClass=${errorClass} route=${req.method}:${req.path} merchantId=${config.counterpartyId ?? "unknown"} correlationId=${correlationId}`
|
|
4564
|
+
);
|
|
4565
|
+
if (failOnError === "closed") {
|
|
4566
|
+
const result = {
|
|
4567
|
+
identityVerified: false,
|
|
4568
|
+
policyAllowed: false,
|
|
4569
|
+
accessLevel: "none",
|
|
4570
|
+
denialReasons: [`MCP middleware internal error: ${errorClass}`],
|
|
4571
|
+
failures: [
|
|
4572
|
+
{
|
|
4573
|
+
dimension: "middleware.internal_error",
|
|
4574
|
+
message: `Middleware threw ${errorClass} \u2014 failing closed`
|
|
4575
|
+
}
|
|
4576
|
+
],
|
|
4577
|
+
verifiedAt: /* @__PURE__ */ new Date(),
|
|
4578
|
+
correlationId
|
|
4579
|
+
};
|
|
4580
|
+
dedupeFailures2(result);
|
|
4581
|
+
return onDenied(result, req, res);
|
|
4582
|
+
}
|
|
4318
4583
|
next();
|
|
4319
4584
|
}
|
|
4320
4585
|
};
|