@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.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,18 +953,27 @@ 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,
|
|
908
972
|
purpose,
|
|
909
|
-
|
|
973
|
+
// RFC 7230 § 3.1.1 — HTTP method tokens uppercase by IANA convention.
|
|
974
|
+
// Backend evaluator tolerates either case as defense-in-depth
|
|
975
|
+
// (round-18.6 batch 2); SDK emits canonical form.
|
|
976
|
+
action: req.method.toUpperCase(),
|
|
910
977
|
resource: req.path,
|
|
911
978
|
createSession: shouldRecordDecisions,
|
|
912
979
|
counterpartyUrl,
|
|
@@ -929,6 +996,7 @@ function createMiddleware(options) {
|
|
|
929
996
|
recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
|
|
930
997
|
});
|
|
931
998
|
}
|
|
999
|
+
dedupeFailures(result);
|
|
932
1000
|
onDenied(result, req, res);
|
|
933
1001
|
return;
|
|
934
1002
|
}
|
|
@@ -955,6 +1023,7 @@ function createMiddleware(options) {
|
|
|
955
1023
|
recordDecision(config, sessionId, "denied", insufficientFailure.message).catch(() => {
|
|
956
1024
|
});
|
|
957
1025
|
}
|
|
1026
|
+
dedupeFailures(result);
|
|
958
1027
|
onDenied(result, req, res);
|
|
959
1028
|
return;
|
|
960
1029
|
}
|
|
@@ -971,6 +1040,7 @@ function createMiddleware(options) {
|
|
|
971
1040
|
recordDecision(config, sessionId, "denied", trustFailure.message).catch(() => {
|
|
972
1041
|
});
|
|
973
1042
|
}
|
|
1043
|
+
dedupeFailures(result);
|
|
974
1044
|
onDenied(result, req, res);
|
|
975
1045
|
return;
|
|
976
1046
|
}
|
|
@@ -985,7 +1055,30 @@ function createMiddleware(options) {
|
|
|
985
1055
|
}
|
|
986
1056
|
next();
|
|
987
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)}`;
|
|
988
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
|
+
}
|
|
989
1082
|
next();
|
|
990
1083
|
}
|
|
991
1084
|
};
|
|
@@ -997,6 +1090,18 @@ __export(nextjs_exports, {
|
|
|
997
1090
|
createMatcherConfig: () => createMatcherConfig,
|
|
998
1091
|
createMiddleware: () => createMiddleware2
|
|
999
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
|
+
}
|
|
1000
1105
|
function extractCredentialsFromNextRequest(request) {
|
|
1001
1106
|
const credentials = {};
|
|
1002
1107
|
const astraId = request.headers.get("x-astra-id") || request.headers.get("X-Astra-Id");
|
|
@@ -1068,10 +1173,18 @@ function extractPurpose(request) {
|
|
|
1068
1173
|
}
|
|
1069
1174
|
}
|
|
1070
1175
|
function generateCommerceShieldHtml(result, options) {
|
|
1071
|
-
const title = options.commerceShield?.title || "AstraSync Agent Verification";
|
|
1072
|
-
const message =
|
|
1073
|
-
|
|
1074
|
-
|
|
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
|
+
);
|
|
1075
1188
|
const allowGuest = options.commerceShield?.allowGuestAccess ?? true;
|
|
1076
1189
|
return `
|
|
1077
1190
|
<!DOCTYPE html>
|
|
@@ -1193,7 +1306,7 @@ function generateCommerceShieldHtml(result, options) {
|
|
|
1193
1306
|
<div class="shield-steps">
|
|
1194
1307
|
<h3>To get verified access:</h3>
|
|
1195
1308
|
<ol>
|
|
1196
|
-
<li>Register at <a href="${registrationUrl}">astrasync.ai/register</a></li>
|
|
1309
|
+
<li>Register at <a href="${registrationUrl}">astrasync.ai/agents/register</a></li>
|
|
1197
1310
|
<li>Create and register your agent</li>
|
|
1198
1311
|
<li>Add your ASTRA-ID to request headers</li>
|
|
1199
1312
|
<li>Refresh this page</li>
|
|
@@ -1281,7 +1394,7 @@ function createMiddleware2(options) {
|
|
|
1281
1394
|
denialReasons: preCheckFailures.map((f) => f.message),
|
|
1282
1395
|
guidance: {
|
|
1283
1396
|
message: "Request exceeds counterparty-defined PDLSS limits.",
|
|
1284
|
-
registrationUrl: `${config.apiBaseUrl?.replace("/api", "")}/register`,
|
|
1397
|
+
registrationUrl: `${config.apiBaseUrl?.replace("/api", "")}/agents/register`,
|
|
1285
1398
|
documentationUrl: `${config.apiBaseUrl?.replace("/api", "")}/docs/pdlss`
|
|
1286
1399
|
},
|
|
1287
1400
|
verifiedAt: /* @__PURE__ */ new Date()
|
|
@@ -1324,7 +1437,10 @@ function createMiddleware2(options) {
|
|
|
1324
1437
|
const result = await verify(config, {
|
|
1325
1438
|
credentials,
|
|
1326
1439
|
purpose,
|
|
1327
|
-
|
|
1440
|
+
// RFC 7230 § 3.1.1 — HTTP method tokens uppercase by IANA convention.
|
|
1441
|
+
// Backend evaluator tolerates either case as defense-in-depth
|
|
1442
|
+
// (round-18.6 batch 2); SDK emits canonical form.
|
|
1443
|
+
action: request.method.toUpperCase(),
|
|
1328
1444
|
resource: pathname,
|
|
1329
1445
|
counterpartyUrl,
|
|
1330
1446
|
counterpartyType: config.counterpartyType || "website",
|
|
@@ -1939,12 +2055,45 @@ function bufferToBase64(bytes) {
|
|
|
1939
2055
|
|
|
1940
2056
|
// src/transport/rfc9421-verify.ts
|
|
1941
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
|
|
1942
2089
|
async function verifyRFC9421(request, options) {
|
|
1943
2090
|
const { resolver } = options;
|
|
1944
|
-
const tolerance = options.clockSkewSec ??
|
|
2091
|
+
const tolerance = options.clockSkewSec ?? 60;
|
|
1945
2092
|
const nowSec = options.now ? options.now() : Math.floor(Date.now() / 1e3);
|
|
2093
|
+
const nonceStore = options.nonceStore ?? defaultNonceStore;
|
|
1946
2094
|
let resolvedKid;
|
|
1947
2095
|
let resolvedAlg;
|
|
2096
|
+
let replayDetected = false;
|
|
1948
2097
|
const keyLookup = async (parameters) => {
|
|
1949
2098
|
const kid = typeof parameters.keyid === "string" ? parameters.keyid : void 0;
|
|
1950
2099
|
if (!kid) return null;
|
|
@@ -1958,6 +2107,14 @@ async function verifyRFC9421(request, options) {
|
|
|
1958
2107
|
const expires = toUnixSeconds(parameters.expires);
|
|
1959
2108
|
if (created !== void 0 && Math.abs(nowSec - created) > tolerance) return null;
|
|
1960
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
|
+
}
|
|
1961
2118
|
return jwkToVerifyingKey(kid, jwk, alg);
|
|
1962
2119
|
};
|
|
1963
2120
|
try {
|
|
@@ -1980,7 +2137,7 @@ async function verifyRFC9421(request, options) {
|
|
|
1980
2137
|
kid: resolvedKid,
|
|
1981
2138
|
registry: resolver.name,
|
|
1982
2139
|
algorithm: resolvedAlg,
|
|
1983
|
-
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"
|
|
1984
2141
|
};
|
|
1985
2142
|
} catch (err) {
|
|
1986
2143
|
return {
|
|
@@ -2805,14 +2962,26 @@ function sha256Sync2(data) {
|
|
|
2805
2962
|
function verifyAP2Chain(input) {
|
|
2806
2963
|
const { triple } = input;
|
|
2807
2964
|
const errors = [];
|
|
2965
|
+
const toleranceSec = input.clockSkewSec ?? 60;
|
|
2966
|
+
const nonceStore = input.nonceStore ?? defaultNonceStore;
|
|
2808
2967
|
const intentPresent = triple.intent !== void 0;
|
|
2809
2968
|
const cartRefOk = checkCartRef(triple, errors);
|
|
2810
2969
|
const paymentRefOk = checkPaymentRef(triple, errors);
|
|
2811
2970
|
const { ok: agentIdContinuity, agentId } = checkAgentContinuity(triple, errors);
|
|
2812
2971
|
const paymentMethodAllowed = checkPaymentMethod(triple, errors);
|
|
2813
2972
|
const totalsConsistent = checkTotals(triple, errors);
|
|
2814
|
-
const expiryOk = checkExpiries(triple,
|
|
2815
|
-
|
|
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;
|
|
2816
2985
|
return {
|
|
2817
2986
|
ok,
|
|
2818
2987
|
checks: {
|
|
@@ -2858,7 +3027,10 @@ function checkAgentContinuity(triple, errors) {
|
|
|
2858
3027
|
const ids = [triple.intent?.agent_id, triple.cart?.agent_id, triple.payment?.agent_id].filter(
|
|
2859
3028
|
(id) => typeof id === "string" && id.length > 0
|
|
2860
3029
|
);
|
|
2861
|
-
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
|
+
}
|
|
2862
3034
|
const unique = new Set(ids);
|
|
2863
3035
|
if (unique.size > 1) {
|
|
2864
3036
|
errors.push(`agent_id mismatch across mandates: ${Array.from(unique).join(", ")}`);
|
|
@@ -2867,9 +3039,16 @@ function checkAgentContinuity(triple, errors) {
|
|
|
2867
3039
|
return { ok: true, agentId: ids[0] };
|
|
2868
3040
|
}
|
|
2869
3041
|
function checkPaymentMethod(triple, errors) {
|
|
2870
|
-
const paymentMethod = triple.payment?.payment_method;
|
|
2871
3042
|
const allowed = triple.intent?.paymentMethods;
|
|
2872
|
-
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
|
+
}
|
|
2873
3052
|
if (!allowed.includes(paymentMethod)) {
|
|
2874
3053
|
errors.push(
|
|
2875
3054
|
`payment_method "${paymentMethod}" not in intent.paymentMethods [${allowed.join(", ")}]`
|
|
@@ -2903,19 +3082,24 @@ function checkTotals(triple, errors) {
|
|
|
2903
3082
|
function checkExpiries(triple, toleranceSec, nowFn, errors) {
|
|
2904
3083
|
const now = nowFn ? nowFn() : Math.floor(Date.now() / 1e3);
|
|
2905
3084
|
let ok = true;
|
|
2906
|
-
|
|
2907
|
-
["intent", triple.intent],
|
|
2908
|
-
["cart", triple.cart]
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
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);
|
|
2912
3096
|
if (parsed === null) {
|
|
2913
3097
|
errors.push(`${name}.expires unparseable`);
|
|
2914
3098
|
ok = false;
|
|
2915
3099
|
continue;
|
|
2916
3100
|
}
|
|
2917
3101
|
if (now > parsed + toleranceSec) {
|
|
2918
|
-
errors.push(`${name} mandate expired at ${
|
|
3102
|
+
errors.push(`${name} mandate expired at ${expires}`);
|
|
2919
3103
|
ok = false;
|
|
2920
3104
|
}
|
|
2921
3105
|
}
|
|
@@ -2942,10 +3126,21 @@ async function verifyACPSignature(input) {
|
|
|
2942
3126
|
if (!input.signatureHeader) {
|
|
2943
3127
|
return { ok: false, error: "missing Signature header" };
|
|
2944
3128
|
}
|
|
2945
|
-
const
|
|
3129
|
+
const tolerance = input.clockSkewSec ?? 60;
|
|
3130
|
+
const nonceStore = input.nonceStore ?? defaultNonceStore;
|
|
3131
|
+
const freshness = checkTimestamp(input.timestampHeader, tolerance, input.now);
|
|
2946
3132
|
if (!freshness.ok) {
|
|
2947
3133
|
return { ok: false, error: freshness.error, timestampStale: true };
|
|
2948
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
|
+
}
|
|
2949
3144
|
const signatureBytes = decodeBase64(input.signatureHeader);
|
|
2950
3145
|
if (!signatureBytes) {
|
|
2951
3146
|
return { ok: false, error: "signature header is not valid base64" };
|
|
@@ -3163,8 +3358,9 @@ function coerceString6(v) {
|
|
|
3163
3358
|
import { BodyDigest } from "mppx";
|
|
3164
3359
|
function verifyMPP(input) {
|
|
3165
3360
|
const { context } = input;
|
|
3166
|
-
const tolerance = input.clockSkewSec ??
|
|
3361
|
+
const tolerance = input.clockSkewSec ?? 60;
|
|
3167
3362
|
const nowSec = input.now ? input.now() : Math.floor(Date.now() / 1e3);
|
|
3363
|
+
const nonceStore = input.nonceStore ?? defaultNonceStore;
|
|
3168
3364
|
const challenge = context.credential?.challenge ?? (context.challenges && context.challenges[0]);
|
|
3169
3365
|
const source = context.credential?.source;
|
|
3170
3366
|
const method = challenge?.method;
|
|
@@ -3187,21 +3383,38 @@ function verifyMPP(input) {
|
|
|
3187
3383
|
}
|
|
3188
3384
|
}
|
|
3189
3385
|
let bodyDigestOk = null;
|
|
3190
|
-
if (
|
|
3191
|
-
|
|
3192
|
-
|
|
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 {
|
|
3193
3397
|
bodyDigestOk = false;
|
|
3194
|
-
} else {
|
|
3195
|
-
bodyDigestOk = BodyDigest.verify(challenge.digest, input.rawBody);
|
|
3196
3398
|
}
|
|
3197
|
-
} catch {
|
|
3198
|
-
bodyDigestOk = false;
|
|
3199
3399
|
}
|
|
3200
3400
|
}
|
|
3201
|
-
|
|
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;
|
|
3202
3410
|
const errors = [];
|
|
3203
3411
|
if (!expiryOk) errors.push("challenge expired");
|
|
3204
|
-
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");
|
|
3205
3418
|
return {
|
|
3206
3419
|
ok,
|
|
3207
3420
|
expiryOk,
|
|
@@ -3365,14 +3578,32 @@ function readHeader4(headers, name) {
|
|
|
3365
3578
|
import { createHash as createHash3, webcrypto } from "crypto";
|
|
3366
3579
|
async function verifyVIChain(input) {
|
|
3367
3580
|
const errors = [];
|
|
3368
|
-
const tolerance = input.clockSkewSec ??
|
|
3581
|
+
const tolerance = input.clockSkewSec ?? 60;
|
|
3369
3582
|
const now = input.now ? input.now() : Math.floor(Date.now() / 1e3);
|
|
3370
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
|
+
}
|
|
3371
3594
|
const l1SigOk = l1 ? await input.verifySignature(l1, null) : null;
|
|
3372
3595
|
if (l1 && !l1SigOk) errors.push("L1 signature invalid");
|
|
3373
3596
|
const l1Cnf = extractCnfJwk(l1?.payload);
|
|
3374
|
-
const
|
|
3597
|
+
const l2ExpectedKey = l1Cnf ?? input.expectedL2Key ?? null;
|
|
3598
|
+
const l2SigOk = await input.verifySignature(l2, l2ExpectedKey);
|
|
3375
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
|
+
}
|
|
3376
3607
|
const l2Cnf = extractCnfJwk(l2.payload);
|
|
3377
3608
|
const l3aSigOk = l3a ? await input.verifySignature(l3a, l2Cnf ?? null) : null;
|
|
3378
3609
|
if (l3a && !l3aSigOk) errors.push("L3a signature invalid");
|
|
@@ -3416,7 +3647,10 @@ async function verifyVIChain(input) {
|
|
|
3416
3647
|
}
|
|
3417
3648
|
}
|
|
3418
3649
|
const expiryOk = checkExpiryAcross([l1, l2, l3a, l3b], tolerance, now, errors);
|
|
3419
|
-
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;
|
|
3420
3654
|
return {
|
|
3421
3655
|
ok,
|
|
3422
3656
|
checks: {
|
|
@@ -4071,7 +4305,7 @@ function mcpToPdlss(parsed, headerPurpose, headerAction) {
|
|
|
4071
4305
|
action = parsed.actionFromBody;
|
|
4072
4306
|
actionSource = parsed.actionSourceFromBody;
|
|
4073
4307
|
} else {
|
|
4074
|
-
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;
|
|
4075
4309
|
actionSource = "transport_layer";
|
|
4076
4310
|
}
|
|
4077
4311
|
return { purpose, action, resource, purposeSource, actionSource };
|
|
@@ -4090,6 +4324,17 @@ function readSingleHeader(value) {
|
|
|
4090
4324
|
if (Array.isArray(value)) return value[0];
|
|
4091
4325
|
return void 0;
|
|
4092
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
|
+
}
|
|
4093
4338
|
function defaultMcpDenied(result, req, res) {
|
|
4094
4339
|
const id = req.body?.id ?? null;
|
|
4095
4340
|
const status = !result.identityVerified ? 401 : 403;
|
|
@@ -4129,10 +4374,11 @@ function createMcpMiddleware(options) {
|
|
|
4129
4374
|
onAgentIdMismatch = "reject",
|
|
4130
4375
|
skip = false,
|
|
4131
4376
|
onDenied = defaultMcpDenied,
|
|
4132
|
-
trustVerifiedHop =
|
|
4377
|
+
trustVerifiedHop = false,
|
|
4133
4378
|
verifiedHopMaxAgeMs,
|
|
4134
4379
|
recordDecisions,
|
|
4135
4380
|
enableRuntimeChallenge = true,
|
|
4381
|
+
failOnError = "open",
|
|
4136
4382
|
...config
|
|
4137
4383
|
} = options;
|
|
4138
4384
|
return async (req, res, next) => {
|
|
@@ -4240,6 +4486,7 @@ function createMcpMiddleware(options) {
|
|
|
4240
4486
|
recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
|
|
4241
4487
|
});
|
|
4242
4488
|
}
|
|
4489
|
+
dedupeFailures2(result);
|
|
4243
4490
|
onDenied(result, req, res);
|
|
4244
4491
|
return;
|
|
4245
4492
|
}
|
|
@@ -4285,6 +4532,7 @@ function createMcpMiddleware(options) {
|
|
|
4285
4532
|
});
|
|
4286
4533
|
}
|
|
4287
4534
|
}
|
|
4535
|
+
dedupeFailures2(result);
|
|
4288
4536
|
onDenied(result, req, res);
|
|
4289
4537
|
return;
|
|
4290
4538
|
}
|
|
@@ -4308,7 +4556,30 @@ function createMcpMiddleware(options) {
|
|
|
4308
4556
|
}
|
|
4309
4557
|
next();
|
|
4310
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)}`;
|
|
4311
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
|
+
}
|
|
4312
4583
|
next();
|
|
4313
4584
|
}
|
|
4314
4585
|
};
|