@astrasyncai/verification-gateway 2.3.4 → 2.3.7
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 +59 -21
- package/dist/adapters/express.js.map +1 -1
- package/dist/adapters/express.mjs +58 -18
- package/dist/adapters/express.mjs.map +1 -1
- package/dist/adapters/mcp.d.mts +245 -0
- package/dist/adapters/mcp.d.ts +245 -0
- package/dist/adapters/mcp.js +589 -0
- package/dist/adapters/mcp.js.map +1 -0
- package/dist/adapters/mcp.mjs +555 -0
- package/dist/adapters/mcp.mjs.map +1 -0
- package/dist/adapters/nextjs.d.mts +2 -2
- package/dist/adapters/nextjs.d.ts +2 -2
- package/dist/adapters/nextjs.js +57 -3
- package/dist/adapters/nextjs.js.map +1 -1
- package/dist/adapters/nextjs.mjs +57 -3
- 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 +3 -1
- package/dist/adapters/sdk.js.map +1 -1
- package/dist/adapters/sdk.mjs +3 -1
- 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/browser/background.js +9 -1
- package/dist/browser/background.js.map +1 -1
- package/dist/browser/background.mjs +9 -1
- 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 +9 -1
- package/dist/cursor/extension.js.map +1 -1
- package/dist/cursor/extension.mjs +9 -1
- package/dist/cursor/extension.mjs.map +1 -1
- package/dist/{express-DtvJ6BGt.d.mts → express-D9oRsseg.d.mts} +17 -14
- package/dist/{express-CraCA8_t.d.ts → express-DMSIl20m.d.ts} +17 -14
- package/dist/gateway/gateway.d.mts +2 -2
- package/dist/gateway/gateway.d.ts +2 -2
- package/dist/gateway/gateway.js +9 -1
- package/dist/gateway/gateway.js.map +1 -1
- package/dist/gateway/gateway.mjs +9 -1
- 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-BZ85CeEr.d.mts → index-Bn_7eGjb.d.mts} +1 -1
- package/dist/{index--KzVRa32.d.ts → index-BtU9yFda.d.ts} +1 -1
- package/dist/{index-BzAFmemy.d.ts → index-EwUWXC5T.d.ts} +1 -1
- package/dist/{index-SEgnWzkf.d.mts → index-YNPs800Z.d.mts} +1 -1
- package/dist/index.d.mts +7 -7
- package/dist/index.d.ts +7 -7
- package/dist/index.js +93 -20
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +93 -20
- 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/{nextjs-B8o9C0t6.d.ts → nextjs-B5ZBpHra.d.ts} +8 -2
- package/dist/{nextjs-DZHAn9j-.d.mts → nextjs-BLtjRbc-.d.mts} +8 -2
- package/dist/{sdk-CRSUFQH2.d.mts → sdk-BhkxvqnK.d.mts} +1 -1
- package/dist/{sdk-BQ3olp3v.d.ts → sdk-YmE3RG8n.d.ts} +1 -1
- package/dist/transport/index.d.mts +2 -2
- package/dist/transport/index.d.ts +2 -2
- package/dist/{types-osMd_dpT.d.ts → types-BecRpozv.d.ts} +1 -1
- package/dist/{types-JMgPake9.d.mts → types-Bxqj1sKY.d.mts} +48 -6
- package/dist/{types-JMgPake9.d.ts → types-Bxqj1sKY.d.ts} +48 -6
- package/dist/{types-aN1UHhyy.d.mts → types-DxY5zt4z.d.mts} +1 -1
- package/dist/ui/index.d.mts +1 -1
- package/dist/ui/index.d.ts +1 -1
- package/package.json +6 -1
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
// src/access-levels.ts
|
|
2
|
+
var ACCESS_LEVEL_HIERARCHY = {
|
|
3
|
+
none: 0,
|
|
4
|
+
guidance: 1,
|
|
5
|
+
"read-only": 2,
|
|
6
|
+
standard: 3,
|
|
7
|
+
full: 4,
|
|
8
|
+
internal: 5
|
|
9
|
+
};
|
|
10
|
+
function getTrustLevel(score) {
|
|
11
|
+
if (score >= 80) return "PLATINUM";
|
|
12
|
+
if (score >= 60) return "GOLD";
|
|
13
|
+
if (score >= 40) return "SILVER";
|
|
14
|
+
return "BRONZE";
|
|
15
|
+
}
|
|
16
|
+
function hasMinimumAccess(actual, required) {
|
|
17
|
+
return ACCESS_LEVEL_HIERARCHY[actual] >= ACCESS_LEVEL_HIERARCHY[required];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// src/verify.ts
|
|
21
|
+
var DEFAULT_CONFIG = {
|
|
22
|
+
apiBaseUrl: "https://astrasync.ai/api",
|
|
23
|
+
defaultAccessLevel: "guidance",
|
|
24
|
+
// minTrustScore + minTrustScoreForFull deprecated in v2.3.0 — server decides.
|
|
25
|
+
cacheTtl: 300,
|
|
26
|
+
// 5 minutes
|
|
27
|
+
debug: false
|
|
28
|
+
};
|
|
29
|
+
var initCheckPerformed = false;
|
|
30
|
+
var deprecationWarningShown = false;
|
|
31
|
+
async function performInitCheck(apiBaseUrl, debug) {
|
|
32
|
+
initCheckPerformed = true;
|
|
33
|
+
try {
|
|
34
|
+
const probeUrl = `${apiBaseUrl}/agents/verify-access`;
|
|
35
|
+
const response = await fetch(probeUrl, { method: "HEAD" });
|
|
36
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
37
|
+
if (contentType.startsWith("text/html")) {
|
|
38
|
+
console.warn(
|
|
39
|
+
`[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). Set disableInitChecks: true on GatewayConfig to silence this warning.`
|
|
40
|
+
);
|
|
41
|
+
} else if (debug) {
|
|
42
|
+
console.log(
|
|
43
|
+
`[VerificationGateway] init check passed for ${apiBaseUrl} (content-type: ${contentType})`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
} catch (err) {
|
|
47
|
+
if (debug) {
|
|
48
|
+
console.log(`[VerificationGateway] init check failed (non-blocking): ${String(err)}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
var verificationCache = /* @__PURE__ */ new Map();
|
|
53
|
+
function getCacheKey(credentials) {
|
|
54
|
+
return `${credentials.astraId || ""}-${credentials.apiKey || ""}-${credentials.jwt || ""}`;
|
|
55
|
+
}
|
|
56
|
+
function getCachedResult(credentials) {
|
|
57
|
+
const key = getCacheKey(credentials);
|
|
58
|
+
const cached = verificationCache.get(key);
|
|
59
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
60
|
+
return cached.result;
|
|
61
|
+
}
|
|
62
|
+
if (cached) {
|
|
63
|
+
verificationCache.delete(key);
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
function cacheResult(credentials, result, ttlSeconds) {
|
|
68
|
+
const key = getCacheKey(credentials);
|
|
69
|
+
verificationCache.set(key, {
|
|
70
|
+
result,
|
|
71
|
+
expiresAt: Date.now() + ttlSeconds * 1e3
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
function extractCredentials(headers, query) {
|
|
75
|
+
const credentials = {};
|
|
76
|
+
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"];
|
|
77
|
+
if (astraIdHeader) {
|
|
78
|
+
credentials.astraId = Array.isArray(astraIdHeader) ? astraIdHeader[0] : astraIdHeader;
|
|
79
|
+
}
|
|
80
|
+
const apiKeyHeader = headers["x-api-key"] || headers["X-Api-Key"] || headers["X-API-KEY"];
|
|
81
|
+
if (apiKeyHeader) {
|
|
82
|
+
credentials.apiKey = Array.isArray(apiKeyHeader) ? apiKeyHeader[0] : apiKeyHeader;
|
|
83
|
+
}
|
|
84
|
+
const authHeader = headers["authorization"] || headers["Authorization"];
|
|
85
|
+
if (authHeader) {
|
|
86
|
+
const authValue = Array.isArray(authHeader) ? authHeader[0] : authHeader;
|
|
87
|
+
credentials.authorizationHeader = authValue;
|
|
88
|
+
if (authValue.startsWith("Bearer ")) {
|
|
89
|
+
credentials.jwt = authValue.slice(7);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (query) {
|
|
93
|
+
if (query.astraId && !credentials.astraId) {
|
|
94
|
+
credentials.astraId = query.astraId;
|
|
95
|
+
}
|
|
96
|
+
if (query.apiKey && !credentials.apiKey) {
|
|
97
|
+
credentials.apiKey = query.apiKey;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return credentials;
|
|
101
|
+
}
|
|
102
|
+
function createGuidanceResponse(config, reason) {
|
|
103
|
+
const guidance = {
|
|
104
|
+
message: "This service verifies AI agents before granting access. Please register your agent with AstraSync.",
|
|
105
|
+
registrationUrl: `${config.apiBaseUrl.replace("/api", "")}/register`,
|
|
106
|
+
documentationUrl: `${config.apiBaseUrl.replace("/api", "")}/docs/agent-access`,
|
|
107
|
+
steps: [
|
|
108
|
+
"Register for an AstraSync account",
|
|
109
|
+
"Create and register your agent",
|
|
110
|
+
"Add your ASTRA-ID to request headers",
|
|
111
|
+
"Retry your request"
|
|
112
|
+
]
|
|
113
|
+
};
|
|
114
|
+
return {
|
|
115
|
+
verified: false,
|
|
116
|
+
accessLevel: "guidance",
|
|
117
|
+
guidance,
|
|
118
|
+
denialReasons: reason ? [reason] : ["No valid agent credentials provided"],
|
|
119
|
+
verifiedAt: /* @__PURE__ */ new Date()
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
async function callVerifyAccessAPI(config, request) {
|
|
123
|
+
const { credentials, ...requestData } = request;
|
|
124
|
+
const body = {
|
|
125
|
+
...credentials.astraId && { agentId: credentials.astraId },
|
|
126
|
+
purpose: requestData.purpose || "general"
|
|
127
|
+
};
|
|
128
|
+
if (requestData.action) body.action = requestData.action;
|
|
129
|
+
if (requestData.resourceType) body.resourceType = requestData.resourceType;
|
|
130
|
+
if (requestData.resource) body.resource = requestData.resource;
|
|
131
|
+
if (requestData.jurisdiction) body.jurisdiction = requestData.jurisdiction;
|
|
132
|
+
if (requestData.transactionValue) body.transactionValue = requestData.transactionValue;
|
|
133
|
+
if (requestData.currency) body.currency = requestData.currency;
|
|
134
|
+
if (requestData.isSubAgentRequest) body.isSubAgentRequest = requestData.isSubAgentRequest;
|
|
135
|
+
if (requestData.parentAgentId) body.parentAgentId = requestData.parentAgentId;
|
|
136
|
+
if (requestData.subAgentDepth !== void 0) body.subAgentDepth = requestData.subAgentDepth;
|
|
137
|
+
if (requestData.enableRuntimeChallenge)
|
|
138
|
+
body.enableRuntimeChallenge = requestData.enableRuntimeChallenge;
|
|
139
|
+
if (requestData.createSession) body.createSession = requestData.createSession;
|
|
140
|
+
if (requestData.durationRequired) body.durationRequired = requestData.durationRequired;
|
|
141
|
+
if (requestData.counterpartyType) body.counterpartyType = requestData.counterpartyType;
|
|
142
|
+
if (requestData.counterpartyUrl) body.counterpartyUrl = requestData.counterpartyUrl;
|
|
143
|
+
if (config.counterpartyId) body.counterpartyId = config.counterpartyId;
|
|
144
|
+
if (requestData.runtimeChallengeOptions)
|
|
145
|
+
body.runtimeChallengeOptions = requestData.runtimeChallengeOptions;
|
|
146
|
+
if (requestData.callerMetadata || requestData.clientIp || requestData.userAgent) {
|
|
147
|
+
const meta = {
|
|
148
|
+
...requestData.clientIp && { sourceIp: requestData.clientIp },
|
|
149
|
+
...requestData.userAgent && { userAgent: requestData.userAgent },
|
|
150
|
+
...requestData.callerMetadata
|
|
151
|
+
};
|
|
152
|
+
if (Object.keys(meta).length > 0) body.callerMetadata = meta;
|
|
153
|
+
}
|
|
154
|
+
const headers = {
|
|
155
|
+
"Content-Type": "application/json",
|
|
156
|
+
...config.customHeaders
|
|
157
|
+
};
|
|
158
|
+
if (credentials.authorizationHeader) {
|
|
159
|
+
headers["Authorization"] = credentials.authorizationHeader;
|
|
160
|
+
} else if (config.apiKey) {
|
|
161
|
+
headers["Authorization"] = `Bearer ${config.apiKey}`;
|
|
162
|
+
}
|
|
163
|
+
if (config.apiKey) {
|
|
164
|
+
headers["X-API-Key"] = config.apiKey;
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
const response = await fetch(`${config.apiBaseUrl}/agents/verify-access`, {
|
|
168
|
+
method: "POST",
|
|
169
|
+
headers,
|
|
170
|
+
body: JSON.stringify(body)
|
|
171
|
+
});
|
|
172
|
+
const data = await response.json();
|
|
173
|
+
if (!response.ok) {
|
|
174
|
+
return {
|
|
175
|
+
success: false,
|
|
176
|
+
error: data.message || data.error || `API returned ${response.status}`
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
return data;
|
|
180
|
+
} catch (error) {
|
|
181
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
182
|
+
return {
|
|
183
|
+
success: false,
|
|
184
|
+
error: `Failed to call verify-access API: ${message}`
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async function verify(config, request) {
|
|
189
|
+
const mergedConfig = { ...DEFAULT_CONFIG, ...config };
|
|
190
|
+
if (!initCheckPerformed && !mergedConfig.disableInitChecks && mergedConfig.apiBaseUrl) {
|
|
191
|
+
void performInitCheck(mergedConfig.apiBaseUrl, mergedConfig.debug);
|
|
192
|
+
}
|
|
193
|
+
if (!deprecationWarningShown && (config.minTrustScore !== void 0 || config.minTrustScoreForFull !== void 0)) {
|
|
194
|
+
deprecationWarningShown = true;
|
|
195
|
+
console.warn(
|
|
196
|
+
"[VerificationGateway] minTrustScore / minTrustScoreForFull are deprecated in v2.3.0 and have no effect. Server is now the single source of truth for access-level decisions (the SDK reads access.accessLevel from the verify-access response). To gate access to an endpoint, configure the endpoint's trust_score_requirement server-side."
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
if (mergedConfig.cacheTtl && mergedConfig.cacheTtl > 0) {
|
|
200
|
+
const cached = getCachedResult(request.credentials);
|
|
201
|
+
if (cached) {
|
|
202
|
+
if (mergedConfig.debug) {
|
|
203
|
+
console.log("[VerificationGateway] Returning cached result");
|
|
204
|
+
}
|
|
205
|
+
return cached;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const enrichedRequest = { ...request };
|
|
209
|
+
if (!enrichedRequest.counterpartyUrl && mergedConfig.counterpartyUrl) {
|
|
210
|
+
enrichedRequest.counterpartyUrl = mergedConfig.counterpartyUrl;
|
|
211
|
+
}
|
|
212
|
+
if (!enrichedRequest.counterpartyType && mergedConfig.counterpartyType) {
|
|
213
|
+
enrichedRequest.counterpartyType = mergedConfig.counterpartyType;
|
|
214
|
+
}
|
|
215
|
+
if (mergedConfig.debug) {
|
|
216
|
+
console.log("[VerificationGateway] Calling verify-access API");
|
|
217
|
+
}
|
|
218
|
+
const apiResponse = await callVerifyAccessAPI(mergedConfig, enrichedRequest);
|
|
219
|
+
if (!apiResponse.success) {
|
|
220
|
+
return createGuidanceResponse(mergedConfig, apiResponse.error);
|
|
221
|
+
}
|
|
222
|
+
if (!apiResponse.access?.allowed) {
|
|
223
|
+
const aggregatedFailures = apiResponse.access?.failures;
|
|
224
|
+
const result2 = {
|
|
225
|
+
verified: false,
|
|
226
|
+
accessLevel: "guidance",
|
|
227
|
+
denialReasons: aggregatedFailures && aggregatedFailures.length > 0 ? aggregatedFailures.map((f) => f.message) : apiResponse.access?.reason ? [apiResponse.access.reason] : ["Access denied"],
|
|
228
|
+
failures: aggregatedFailures,
|
|
229
|
+
requiresStepUp: apiResponse.access?.requiresStepUp,
|
|
230
|
+
requiresApproval: apiResponse.access?.requiresApproval,
|
|
231
|
+
guidance: {
|
|
232
|
+
message: apiResponse.access?.reason || "Access denied by PDLSS policy",
|
|
233
|
+
registrationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/register`,
|
|
234
|
+
documentationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/docs/pdlss`
|
|
235
|
+
},
|
|
236
|
+
verifiedAt: /* @__PURE__ */ new Date(),
|
|
237
|
+
// Extract sessionId so decisions can be recorded for denials too
|
|
238
|
+
sessionId: apiResponse.sessionId,
|
|
239
|
+
recommendation: apiResponse.recommendation,
|
|
240
|
+
recommendationReasons: apiResponse.recommendationReasons
|
|
241
|
+
};
|
|
242
|
+
return result2;
|
|
243
|
+
}
|
|
244
|
+
const agent = apiResponse.agent ? {
|
|
245
|
+
astraId: apiResponse.agent.astraId,
|
|
246
|
+
name: apiResponse.agent.name,
|
|
247
|
+
trustScore: apiResponse.agent.trustScore,
|
|
248
|
+
trustLevel: getTrustLevel(apiResponse.agent.trustScore),
|
|
249
|
+
blockchainVerified: apiResponse.agent.blockchainStatus === "verified",
|
|
250
|
+
status: apiResponse.agent.agentStatus
|
|
251
|
+
} : void 0;
|
|
252
|
+
const developer = apiResponse.developer ? {
|
|
253
|
+
astradId: apiResponse.developer.kyaOwnerId,
|
|
254
|
+
name: apiResponse.developer.fullName,
|
|
255
|
+
trustScore: apiResponse.developer.trustScore || 0,
|
|
256
|
+
verified: apiResponse.developer.identityVerified
|
|
257
|
+
} : void 0;
|
|
258
|
+
const organization = apiResponse.organization ? {
|
|
259
|
+
name: apiResponse.organization.name,
|
|
260
|
+
verified: apiResponse.organization.verified,
|
|
261
|
+
trustScore: apiResponse.organization.trustScore
|
|
262
|
+
} : void 0;
|
|
263
|
+
const verificationContext = apiResponse.verificationContext;
|
|
264
|
+
const accessLevel = apiResponse.access?.accessLevel ?? "standard";
|
|
265
|
+
const result = {
|
|
266
|
+
verified: true,
|
|
267
|
+
accessLevel,
|
|
268
|
+
agent,
|
|
269
|
+
developer,
|
|
270
|
+
organization,
|
|
271
|
+
appliedPolicy: apiResponse.access?.appliedPolicy,
|
|
272
|
+
verificationContext,
|
|
273
|
+
requiresStepUp: apiResponse.access?.requiresStepUp,
|
|
274
|
+
requiresApproval: apiResponse.access?.requiresApproval,
|
|
275
|
+
verifiedAt: /* @__PURE__ */ new Date(),
|
|
276
|
+
cacheTtl: mergedConfig.cacheTtl,
|
|
277
|
+
// Handshake Protocol v10 enhanced fields (present when backend returns them)
|
|
278
|
+
sessionId: apiResponse.sessionId,
|
|
279
|
+
runtimeChallenge: apiResponse.runtimeChallenge,
|
|
280
|
+
tokenGuidance: apiResponse.tokenGuidance,
|
|
281
|
+
recommendation: apiResponse.recommendation,
|
|
282
|
+
recommendationReasons: apiResponse.recommendationReasons
|
|
283
|
+
};
|
|
284
|
+
if (result.recommendation === "deny") {
|
|
285
|
+
result.verified = false;
|
|
286
|
+
result.accessLevel = "none";
|
|
287
|
+
result.denialReasons = result.recommendationReasons || [
|
|
288
|
+
"Access denied by AstraSync recommendation"
|
|
289
|
+
];
|
|
290
|
+
if (result.runtimeChallenge) {
|
|
291
|
+
result.guidance = {
|
|
292
|
+
message: `Verification failed: ${result.runtimeChallenge.reason || "runtime challenge failed"}`,
|
|
293
|
+
registrationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/register`,
|
|
294
|
+
documentationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/docs/runtime-challenge`
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
} else if (result.recommendation === "step_up_required") {
|
|
298
|
+
result.requiresStepUp = true;
|
|
299
|
+
if (ACCESS_LEVEL_HIERARCHY[result.accessLevel] > ACCESS_LEVEL_HIERARCHY["read-only"]) {
|
|
300
|
+
result.accessLevel = "read-only";
|
|
301
|
+
}
|
|
302
|
+
result.denialReasons = result.recommendationReasons || ["Step-up verification required"];
|
|
303
|
+
}
|
|
304
|
+
if (mergedConfig.cacheTtl && mergedConfig.cacheTtl > 0 && result.recommendation !== "deny") {
|
|
305
|
+
cacheResult(request.credentials, result, mergedConfig.cacheTtl);
|
|
306
|
+
}
|
|
307
|
+
return result;
|
|
308
|
+
}
|
|
309
|
+
async function recordDecision(config, sessionId, decision, reason) {
|
|
310
|
+
const headers = { "Content-Type": "application/json" };
|
|
311
|
+
if (config.apiKey) {
|
|
312
|
+
headers["Authorization"] = `Bearer ${config.apiKey}`;
|
|
313
|
+
headers["X-API-Key"] = config.apiKey;
|
|
314
|
+
}
|
|
315
|
+
await fetch(`${config.apiBaseUrl}/agents/verify-access/${sessionId}/decision`, {
|
|
316
|
+
method: "POST",
|
|
317
|
+
headers,
|
|
318
|
+
body: JSON.stringify({ decision, reason })
|
|
319
|
+
}).catch(() => {
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// src/transport/mcp-server.ts
|
|
324
|
+
var MCP_VERIFIED_HOP_HEADER = "X-Astra-Verified-Hop";
|
|
325
|
+
var MCP_VERIFIED_HOP_MAX_AGE_MS = 6e4;
|
|
326
|
+
function serializeVerifiedHop(marker) {
|
|
327
|
+
return `${marker.astraId};${marker.sessionId ?? ""};${marker.checkedAt}`;
|
|
328
|
+
}
|
|
329
|
+
function parseVerifiedHop(value) {
|
|
330
|
+
if (!value) return null;
|
|
331
|
+
const parts = value.split(";");
|
|
332
|
+
if (parts.length !== 3) return null;
|
|
333
|
+
const [astraId, sessionId, checkedAtRaw] = parts;
|
|
334
|
+
if (!astraId) return null;
|
|
335
|
+
const checkedAt = Number(checkedAtRaw);
|
|
336
|
+
if (!Number.isFinite(checkedAt) || checkedAt <= 0) return null;
|
|
337
|
+
return {
|
|
338
|
+
astraId,
|
|
339
|
+
...sessionId ? { sessionId } : {},
|
|
340
|
+
checkedAt
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
function isVerifiedHopValidFor(marker, expectedAstraId, opts = {}) {
|
|
344
|
+
if (!marker) return false;
|
|
345
|
+
if (marker.astraId !== expectedAstraId) return false;
|
|
346
|
+
const maxAge = opts.maxAgeMs ?? MCP_VERIFIED_HOP_MAX_AGE_MS;
|
|
347
|
+
const now = opts.now ?? Date.now();
|
|
348
|
+
return now - marker.checkedAt <= maxAge && now >= marker.checkedAt;
|
|
349
|
+
}
|
|
350
|
+
function parseMcpJsonRpc(body) {
|
|
351
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return null;
|
|
352
|
+
const obj = body;
|
|
353
|
+
if (obj.jsonrpc !== "2.0" && obj.jsonrpc !== "1.0") return null;
|
|
354
|
+
const method = typeof obj.method === "string" ? obj.method : null;
|
|
355
|
+
if (!method) return null;
|
|
356
|
+
const params = obj.params;
|
|
357
|
+
let toolName;
|
|
358
|
+
if (method === "tools/call" && params && typeof params.name === "string") {
|
|
359
|
+
toolName = params.name;
|
|
360
|
+
}
|
|
361
|
+
let protocolVersion;
|
|
362
|
+
if (method === "initialize" && params && typeof params.protocolVersion === "string") {
|
|
363
|
+
protocolVersion = params.protocolVersion;
|
|
364
|
+
}
|
|
365
|
+
let agentIdFromBody;
|
|
366
|
+
const meta = params?._meta;
|
|
367
|
+
const astrasyncMeta = meta?.astrasync;
|
|
368
|
+
if (astrasyncMeta && typeof astrasyncMeta.agentId === "string") {
|
|
369
|
+
agentIdFromBody = astrasyncMeta.agentId;
|
|
370
|
+
} else {
|
|
371
|
+
const args = params?.arguments;
|
|
372
|
+
if (args && typeof args.agent_id === "string") agentIdFromBody = args.agent_id;
|
|
373
|
+
}
|
|
374
|
+
const isInitialize = method === "initialize";
|
|
375
|
+
const isToolCall = method === "tools/call";
|
|
376
|
+
const isIntrospection = method === "tools/list" || method === "prompts/list" || method === "resources/list" || method === "ping" || method === "notifications/initialized";
|
|
377
|
+
return {
|
|
378
|
+
method,
|
|
379
|
+
...toolName ? { toolName } : {},
|
|
380
|
+
...protocolVersion ? { protocolVersion } : {},
|
|
381
|
+
...agentIdFromBody ? { agentIdFromBody } : {},
|
|
382
|
+
isInitialize,
|
|
383
|
+
isToolCall,
|
|
384
|
+
isIntrospection
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
function mcpToPdlss(parsed) {
|
|
388
|
+
const action = parsed.toolName ? `${parsed.method}:${parsed.toolName}` : parsed.method;
|
|
389
|
+
const resource = parsed.toolName ? `mcp:tool/${parsed.toolName}` : `mcp:method/${parsed.method}`;
|
|
390
|
+
return {
|
|
391
|
+
purpose: "mcp_invoke",
|
|
392
|
+
action,
|
|
393
|
+
resource
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
function mcpRiskTier(parsed) {
|
|
397
|
+
if (parsed.isInitialize || parsed.method === "notifications/initialized") return "none";
|
|
398
|
+
if (parsed.isIntrospection) return "none";
|
|
399
|
+
if (parsed.method === "resources/read") return "read-only";
|
|
400
|
+
if (parsed.isToolCall) return "standard";
|
|
401
|
+
return "standard";
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// src/adapters/mcp.ts
|
|
405
|
+
function defaultMcpDenied(result, req, res) {
|
|
406
|
+
const id = req.body?.id ?? null;
|
|
407
|
+
const status = result.verified ? 403 : 401;
|
|
408
|
+
res.status(status).json({
|
|
409
|
+
jsonrpc: "2.0",
|
|
410
|
+
id,
|
|
411
|
+
error: {
|
|
412
|
+
code: result.verified ? -32001 : -32e3,
|
|
413
|
+
message: result.denialReasons?.[0] ?? "Access denied",
|
|
414
|
+
data: {
|
|
415
|
+
accessLevel: result.accessLevel,
|
|
416
|
+
guidance: result.guidance
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
function resolveMinAccessLevel(parsed, opts) {
|
|
422
|
+
if (parsed.toolName && opts.toolGates && opts.toolGates[parsed.toolName] !== void 0) {
|
|
423
|
+
return opts.toolGates[parsed.toolName];
|
|
424
|
+
}
|
|
425
|
+
if (opts.methodGates && opts.methodGates[parsed.method] !== void 0) {
|
|
426
|
+
return opts.methodGates[parsed.method];
|
|
427
|
+
}
|
|
428
|
+
return mcpRiskTier(parsed);
|
|
429
|
+
}
|
|
430
|
+
function createMcpMiddleware(options) {
|
|
431
|
+
const {
|
|
432
|
+
toolGates,
|
|
433
|
+
methodGates,
|
|
434
|
+
onAgentIdMismatch = "reject",
|
|
435
|
+
skip = false,
|
|
436
|
+
onDenied = defaultMcpDenied,
|
|
437
|
+
trustVerifiedHop = true,
|
|
438
|
+
verifiedHopMaxAgeMs,
|
|
439
|
+
recordDecisions,
|
|
440
|
+
enableRuntimeChallenge = true,
|
|
441
|
+
...config
|
|
442
|
+
} = options;
|
|
443
|
+
return async (req, res, next) => {
|
|
444
|
+
try {
|
|
445
|
+
if (skip) return next();
|
|
446
|
+
const parsed = parseMcpJsonRpc(req.body);
|
|
447
|
+
if (!parsed) {
|
|
448
|
+
return next();
|
|
449
|
+
}
|
|
450
|
+
req.mcpRequest = parsed;
|
|
451
|
+
const headerRaw = req.headers["x-astra-id"] ?? req.headers["x-astra-agentid"];
|
|
452
|
+
const headerAstraId = typeof headerRaw === "string" ? headerRaw : Array.isArray(headerRaw) ? headerRaw[0] : void 0;
|
|
453
|
+
const bodyAstraId = parsed.agentIdFromBody;
|
|
454
|
+
let effectiveAstraId;
|
|
455
|
+
if (headerAstraId && bodyAstraId && headerAstraId !== bodyAstraId) {
|
|
456
|
+
if (onAgentIdMismatch === "reject") {
|
|
457
|
+
const id = req.body?.id ?? null;
|
|
458
|
+
res.status(400).json({
|
|
459
|
+
jsonrpc: "2.0",
|
|
460
|
+
id,
|
|
461
|
+
error: {
|
|
462
|
+
code: -32602,
|
|
463
|
+
message: "AGENT_ID_MISMATCH",
|
|
464
|
+
data: {
|
|
465
|
+
detail: "The agent id in the X-Astra-Id header disagrees with params._meta.astrasync.agentId / params.arguments.agent_id. Reconcile before calling the tool \u2014 see https://astrasync.ai/docs/mcp-integration#identity-reconciliation.",
|
|
466
|
+
headerAstraId,
|
|
467
|
+
bodyAstraId
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
effectiveAstraId = onAgentIdMismatch === "prefer-header" ? headerAstraId : bodyAstraId;
|
|
474
|
+
} else {
|
|
475
|
+
effectiveAstraId = headerAstraId ?? bodyAstraId;
|
|
476
|
+
}
|
|
477
|
+
if (trustVerifiedHop && effectiveAstraId) {
|
|
478
|
+
const hopRaw = req.headers[MCP_VERIFIED_HOP_HEADER.toLowerCase()];
|
|
479
|
+
const hopValue = typeof hopRaw === "string" ? hopRaw : Array.isArray(hopRaw) ? hopRaw[0] : void 0;
|
|
480
|
+
const marker = parseVerifiedHop(hopValue);
|
|
481
|
+
if (isVerifiedHopValidFor(marker, effectiveAstraId, {
|
|
482
|
+
...verifiedHopMaxAgeMs !== void 0 ? { maxAgeMs: verifiedHopMaxAgeMs } : {}
|
|
483
|
+
})) {
|
|
484
|
+
return next();
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
const minAccessLevel = resolveMinAccessLevel(parsed, { toolGates, methodGates });
|
|
488
|
+
if (minAccessLevel === "none") {
|
|
489
|
+
return next();
|
|
490
|
+
}
|
|
491
|
+
const credentials = extractCredentials(
|
|
492
|
+
req.headers,
|
|
493
|
+
req.query
|
|
494
|
+
);
|
|
495
|
+
if (effectiveAstraId) credentials.astraId = effectiveAstraId;
|
|
496
|
+
const pdlss = mcpToPdlss(parsed);
|
|
497
|
+
const counterpartyUrl = config.counterpartyUrl || `${req.protocol}://${req.get("host")}${req.path}`;
|
|
498
|
+
const shouldRecordDecisions = recordDecisions !== false;
|
|
499
|
+
const result = await verify(config, {
|
|
500
|
+
credentials,
|
|
501
|
+
purpose: pdlss.purpose,
|
|
502
|
+
action: pdlss.action,
|
|
503
|
+
resource: pdlss.resource,
|
|
504
|
+
createSession: shouldRecordDecisions,
|
|
505
|
+
counterpartyUrl,
|
|
506
|
+
counterpartyType: config.counterpartyType || "mcp_server",
|
|
507
|
+
enableRuntimeChallenge,
|
|
508
|
+
callerMetadata: {
|
|
509
|
+
sourceIp: req.ip,
|
|
510
|
+
userAgent: req.headers["user-agent"],
|
|
511
|
+
host: req.headers.host
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
req.agentVerification = result;
|
|
515
|
+
const sessionId = result.sessionId;
|
|
516
|
+
if (!hasMinimumAccess(result.accessLevel, minAccessLevel)) {
|
|
517
|
+
if (shouldRecordDecisions && sessionId) {
|
|
518
|
+
recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
onDenied(result, req, res);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
if (effectiveAstraId) {
|
|
525
|
+
res.setHeader(
|
|
526
|
+
MCP_VERIFIED_HOP_HEADER,
|
|
527
|
+
serializeVerifiedHop({
|
|
528
|
+
astraId: effectiveAstraId,
|
|
529
|
+
...sessionId ? { sessionId } : {},
|
|
530
|
+
checkedAt: Date.now()
|
|
531
|
+
})
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
if (shouldRecordDecisions && sessionId) {
|
|
535
|
+
recordDecision(config, sessionId, "granted").catch(() => {
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
next();
|
|
539
|
+
} catch (error) {
|
|
540
|
+
console.error("[VerificationGateway/MCP] Middleware error:", error);
|
|
541
|
+
next();
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
export {
|
|
546
|
+
MCP_VERIFIED_HOP_HEADER,
|
|
547
|
+
createMcpMiddleware,
|
|
548
|
+
isVerifiedHopValidFor,
|
|
549
|
+
mcpRiskTier,
|
|
550
|
+
mcpToPdlss,
|
|
551
|
+
parseMcpJsonRpc,
|
|
552
|
+
parseVerifiedHop,
|
|
553
|
+
serializeVerifiedHop
|
|
554
|
+
};
|
|
555
|
+
//# sourceMappingURL=mcp.mjs.map
|