@astrasyncai/verification-gateway 2.4.14 → 2.5.0

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.
@@ -85,7 +85,7 @@ interface ParsedMcpRequest {
85
85
  * (canonical SDK location) OR `params.arguments.purpose` (legacy /
86
86
  * conventional callers). The discriminator is on `purposeSourceFromBody`.
87
87
  * Adapter combines this with the `X-Astra-Purpose` header (header wins)
88
- * before mapping; final fallback at `mcpToPdlss` is `'mcp_invoke'`.
88
+ * before mapping; final fallback at `mcpToPdlss` is `undefined`.
89
89
  */
90
90
  purposeFromBody?: string;
91
91
  /** Which body location resolved `purposeFromBody`. */
@@ -120,60 +120,45 @@ declare function parseMcpJsonRpc(body: unknown): ParsedMcpRequest | null;
120
120
  /**
121
121
  * PDLSS mapping for an MCP request. The platform's PDLSS taxonomy is
122
122
  * `purpose / action / resource`; for MCP traffic the audit-useful dimensions
123
- * are the JSON-RPC method and (for `tools/call`) the tool name. Doc-stable so
124
- * dashboards and audits can correlate consistently across cohort-3 partners.
123
+ * are the JSON-RPC method and (for `tools/call`) the tool name.
125
124
  *
126
- * Rules:
127
- * - `purpose` is always `mcp_invoke` for MCP traffic sets the high-level
128
- * PDLSS bucket.
129
- * - `action` is the JSON-RPC method, optionally `:tool_name` suffixed for
130
- * `tools/call`. Lets PDLSS allowlist specific tools.
131
- * - `resource` is `mcp:tool/<name>` for `tools/call`, `mcp:method/<method>`
132
- * otherwise. Lets PDLSS scope on tool identity.
125
+ * v2.5.0 rules:
126
+ * - `resource` defaults to the HTTP request path (e.g. `/mcp`). Per-tool
127
+ * overrides via `toolGates` config let merchants map tools to specific
128
+ * backend resources (e.g. `/api/catalog`).
129
+ * - `purpose` defaults to `undefined` (backend evaluator applies
130
+ * skip-when-undefined). Per-tool overrides via `toolGates` config let
131
+ * merchants declare the semantic purpose each tool fulfils.
132
+ * - `action` is the bare tool name for `tools/call`, the JSON-RPC method
133
+ * otherwise. Header/body overrides available.
133
134
  */
134
135
  interface McpPdlssMapping {
135
- purpose: string;
136
+ purpose: string | undefined;
136
137
  action: string;
137
138
  resource: string;
138
- /**
139
- * Round-13 (R13-1 / R13-2): the resolution channel for purpose and
140
- * action — disjoint enums sharing the same structure per
141
- * `feedback_symmetric_fallbacks_for_symmetric_concepts.md`. Adapters
142
- * log both at debug level so partners can confirm header / body
143
- * pass-through, support can triage tickets, and we can watch the
144
- * `default_*` / `transport_layer` decay over time as integrations
145
- * mature.
146
- *
147
- * Round-12 (F19) used a narrower `purposeSource: 'header' |
148
- * 'tool_argument' | 'default_mcp_invoke'` — round-13 widens to also
149
- * carry the `_meta` source distinct from `tool_argument`, so the round-13
150
- * R13-1 fallback (which now reads both `_meta.astrasync.purpose` AND
151
- * `params.arguments.purpose`) can report WHICH body location resolved.
152
- */
153
- purposeSource: 'header' | 'meta' | 'tool_argument' | 'default_mcp_invoke';
139
+ purposeSource: 'header' | 'meta' | 'tool_argument' | 'tool_gate' | undefined;
154
140
  actionSource: 'header' | 'meta' | 'tool_argument' | 'transport_layer';
155
141
  }
156
142
  /**
157
- * Round-13 (R13-1 + R13-2) canonical precedence chain documented ONCE,
158
- * applied identically to both purpose and action. Per
159
- * `feedback_symmetric_fallbacks_for_symmetric_concepts.md` — drift
160
- * between two paired concepts generates the same support-ticket class
161
- * repeatedly, so the resolution order is:
143
+ * v2.5.0 PDLSS field derivation for MCP requests.
144
+ *
145
+ * Purpose precedence:
146
+ * - If `toolGate.purpose` is provided, it is authoritative (header/body ignored).
147
+ * - Otherwise: header body `_meta` → body `arguments` → `undefined`.
162
148
  *
163
- * 1. `X-Astra-<concept>` HTTP header (caller's explicit override)
164
- * 2. `params._meta.astrasync.<concept>` body field (canonical SDK location)
165
- * 3. `params.arguments.<concept>` body field (legacy / conventional)
166
- * 4. Transport-layer default:
167
- * purpose → 'mcp_invoke'
168
- * action → '<method>:<toolName>' (or just '<method>')
149
+ * Resource precedence:
150
+ * - `toolGate.resource` if provided, else `requestPath`.
169
151
  *
170
- * The header tier is the adapter's job (it has `req.headers` access);
171
- * this function takes the resolved `headerPurpose` / `headerAction`
172
- * inputs and combines them with the body extraction already done in
173
- * `parseMcpJsonRpc`. The discriminator is reported on `purposeSource` /
174
- * `actionSource` for debug logging.
152
+ * Action precedence (unchanged from v2.4.14):
153
+ * - header body `_meta` body `arguments` → bare tool name / method.
154
+ *
155
+ * @param requestPath The HTTP request path (e.g. '/mcp'). Required.
156
+ * @param toolGate Resolved per-tool config from `toolGates`, if present.
175
157
  */
176
- declare function mcpToPdlss(parsed: ParsedMcpRequest, headerPurpose?: string, headerAction?: string): McpPdlssMapping;
158
+ declare function mcpToPdlss(parsed: ParsedMcpRequest, requestPath: string, headerPurpose?: string, headerAction?: string, toolGate?: {
159
+ purpose?: string;
160
+ resource?: string;
161
+ }): McpPdlssMapping;
177
162
  /**
178
163
  * Recommended minimum access level per method type. The MCP middleware uses
179
164
  * this to split low-risk handshake / introspection traffic from high-risk
@@ -200,10 +185,9 @@ declare function mcpRiskTier(parsed: ParsedMcpRequest): AccessLevel;
200
185
  * from `tools/call start_checkout` (high risk). This middleware peels
201
186
  * the JSON-RPC body and applies a per-method risk tier.
202
187
  *
203
- * (b) **PDLSS mapping**. Forwards `purpose=mcp_invoke`,
204
- * `action=method[:tool]`, `resource=mcp:tool/<name>` so audit traces
205
- * are stable across cohort-3 partners. See `transport/mcp-server.ts`
206
- * for the exact mapping.
188
+ * (b) **PDLSS mapping**. Derives purpose, action, and resource from
189
+ * `toolGates` config and request context. See `transport/mcp-server.ts`
190
+ * `mcpToPdlss()` for the exact mapping.
207
191
  *
208
192
  * (c) **Inner-hop dedupe**. Outbound responses set
209
193
  * `X-Astra-Verified-Hop` so a downstream REST endpoint that the tool
@@ -249,13 +233,51 @@ declare global {
249
233
  }
250
234
  }
251
235
  }
236
+ /**
237
+ * Extended per-tool gate with optional PDLSS purpose + resource overrides.
238
+ *
239
+ * When `purpose` is set, it is authoritative for that tool — the agent's
240
+ * `X-Astra-Purpose` header is ignored. This lets the merchant declare what
241
+ * semantic purpose each tool fulfils rather than trusting agent self-declaration.
242
+ *
243
+ * When `resource` is set, it overrides the default (`req.path`) for that
244
+ * tool's verify-access call — e.g. mapping `list_products` to `/api/catalog`.
245
+ */
246
+ interface ToolGateConfig {
247
+ minAccessLevel: AccessLevel;
248
+ purpose?: string;
249
+ resource?: string;
250
+ }
252
251
  interface McpMiddlewareOptions extends GatewayConfig {
253
252
  /**
254
- * Per-tool override for the minimum access level. Tools not listed inherit
255
- * the default tier from `mcpRiskTier` (`tools/call` → `'standard'`). Use
256
- * for high-risk tools that demand `'full'` or low-risk read-only tools.
253
+ * Per-tool gating for `tools/call` invocations. Tools not listed inherit
254
+ * the default tier from `mcpRiskTier` (`tools/call` → `'standard'`).
255
+ *
256
+ * Accepts both the shorthand access-level string and the full object shape:
257
+ * ```typescript
258
+ * toolGates: {
259
+ * browse_catalog: 'read-only', // shorthand
260
+ * list_products: { minAccessLevel: 'none', // full shape
261
+ * purpose: 'shopping.search',
262
+ * resource: '/api/catalog' },
263
+ * start_checkout: { minAccessLevel: 'standard',
264
+ * purpose: 'shopping.purchase',
265
+ * resource: '/api/checkout/*' },
266
+ * }
267
+ * ```
268
+ *
269
+ * When `tools/call` arrives for a tool not declared in `toolGates`, the SDK
270
+ * falls back to a risk-tier default based on the method classification
271
+ * (`mcpRiskTier`). For `tools/call` the fallback is `'standard'`. Best
272
+ * practice: declare every tool you expose explicitly.
273
+ *
274
+ * The action axis for non-tools/call MCP methods (`tools/list`,
275
+ * `resources/list`, `prompts/list`, etc.) is the literal JSON-RPC method
276
+ * name (e.g. `'tools/list'`). Merchants gating MCP traffic by action should
277
+ * declare per-tool gates in `toolGates`, not endpoint-level `allowedActions`
278
+ * — the latter applies to REST-style action values, not MCP method strings.
257
279
  */
258
- toolGates?: Record<string, AccessLevel>;
280
+ toolGates?: Record<string, AccessLevel | ToolGateConfig>;
259
281
  /**
260
282
  * Per-method override (e.g. tighten `tools/list` to `'read-only'` if you
261
283
  * don't want unregistered probes seeing your tool catalogue). Matches by
@@ -323,4 +345,4 @@ interface McpMiddlewareOptions extends GatewayConfig {
323
345
  */
324
346
  declare function createMcpMiddleware(options: McpMiddlewareOptions): RequestHandler;
325
347
 
326
- export { MCP_VERIFIED_HOP_HEADER, type McpMiddlewareOptions, type ParsedMcpRequest, createMcpMiddleware, isVerifiedHopValidFor, mcpRiskTier, mcpToPdlss, parseMcpJsonRpc, parseVerifiedHop, serializeVerifiedHop };
348
+ export { MCP_VERIFIED_HOP_HEADER, type McpMiddlewareOptions, type ParsedMcpRequest, type ToolGateConfig, createMcpMiddleware, isVerifiedHopValidFor, mcpRiskTier, mcpToPdlss, parseMcpJsonRpc, parseVerifiedHop, serializeVerifiedHop };
@@ -85,7 +85,7 @@ interface ParsedMcpRequest {
85
85
  * (canonical SDK location) OR `params.arguments.purpose` (legacy /
86
86
  * conventional callers). The discriminator is on `purposeSourceFromBody`.
87
87
  * Adapter combines this with the `X-Astra-Purpose` header (header wins)
88
- * before mapping; final fallback at `mcpToPdlss` is `'mcp_invoke'`.
88
+ * before mapping; final fallback at `mcpToPdlss` is `undefined`.
89
89
  */
90
90
  purposeFromBody?: string;
91
91
  /** Which body location resolved `purposeFromBody`. */
@@ -120,60 +120,45 @@ declare function parseMcpJsonRpc(body: unknown): ParsedMcpRequest | null;
120
120
  /**
121
121
  * PDLSS mapping for an MCP request. The platform's PDLSS taxonomy is
122
122
  * `purpose / action / resource`; for MCP traffic the audit-useful dimensions
123
- * are the JSON-RPC method and (for `tools/call`) the tool name. Doc-stable so
124
- * dashboards and audits can correlate consistently across cohort-3 partners.
123
+ * are the JSON-RPC method and (for `tools/call`) the tool name.
125
124
  *
126
- * Rules:
127
- * - `purpose` is always `mcp_invoke` for MCP traffic sets the high-level
128
- * PDLSS bucket.
129
- * - `action` is the JSON-RPC method, optionally `:tool_name` suffixed for
130
- * `tools/call`. Lets PDLSS allowlist specific tools.
131
- * - `resource` is `mcp:tool/<name>` for `tools/call`, `mcp:method/<method>`
132
- * otherwise. Lets PDLSS scope on tool identity.
125
+ * v2.5.0 rules:
126
+ * - `resource` defaults to the HTTP request path (e.g. `/mcp`). Per-tool
127
+ * overrides via `toolGates` config let merchants map tools to specific
128
+ * backend resources (e.g. `/api/catalog`).
129
+ * - `purpose` defaults to `undefined` (backend evaluator applies
130
+ * skip-when-undefined). Per-tool overrides via `toolGates` config let
131
+ * merchants declare the semantic purpose each tool fulfils.
132
+ * - `action` is the bare tool name for `tools/call`, the JSON-RPC method
133
+ * otherwise. Header/body overrides available.
133
134
  */
134
135
  interface McpPdlssMapping {
135
- purpose: string;
136
+ purpose: string | undefined;
136
137
  action: string;
137
138
  resource: string;
138
- /**
139
- * Round-13 (R13-1 / R13-2): the resolution channel for purpose and
140
- * action — disjoint enums sharing the same structure per
141
- * `feedback_symmetric_fallbacks_for_symmetric_concepts.md`. Adapters
142
- * log both at debug level so partners can confirm header / body
143
- * pass-through, support can triage tickets, and we can watch the
144
- * `default_*` / `transport_layer` decay over time as integrations
145
- * mature.
146
- *
147
- * Round-12 (F19) used a narrower `purposeSource: 'header' |
148
- * 'tool_argument' | 'default_mcp_invoke'` — round-13 widens to also
149
- * carry the `_meta` source distinct from `tool_argument`, so the round-13
150
- * R13-1 fallback (which now reads both `_meta.astrasync.purpose` AND
151
- * `params.arguments.purpose`) can report WHICH body location resolved.
152
- */
153
- purposeSource: 'header' | 'meta' | 'tool_argument' | 'default_mcp_invoke';
139
+ purposeSource: 'header' | 'meta' | 'tool_argument' | 'tool_gate' | undefined;
154
140
  actionSource: 'header' | 'meta' | 'tool_argument' | 'transport_layer';
155
141
  }
156
142
  /**
157
- * Round-13 (R13-1 + R13-2) canonical precedence chain documented ONCE,
158
- * applied identically to both purpose and action. Per
159
- * `feedback_symmetric_fallbacks_for_symmetric_concepts.md` — drift
160
- * between two paired concepts generates the same support-ticket class
161
- * repeatedly, so the resolution order is:
143
+ * v2.5.0 PDLSS field derivation for MCP requests.
144
+ *
145
+ * Purpose precedence:
146
+ * - If `toolGate.purpose` is provided, it is authoritative (header/body ignored).
147
+ * - Otherwise: header body `_meta` → body `arguments` → `undefined`.
162
148
  *
163
- * 1. `X-Astra-<concept>` HTTP header (caller's explicit override)
164
- * 2. `params._meta.astrasync.<concept>` body field (canonical SDK location)
165
- * 3. `params.arguments.<concept>` body field (legacy / conventional)
166
- * 4. Transport-layer default:
167
- * purpose → 'mcp_invoke'
168
- * action → '<method>:<toolName>' (or just '<method>')
149
+ * Resource precedence:
150
+ * - `toolGate.resource` if provided, else `requestPath`.
169
151
  *
170
- * The header tier is the adapter's job (it has `req.headers` access);
171
- * this function takes the resolved `headerPurpose` / `headerAction`
172
- * inputs and combines them with the body extraction already done in
173
- * `parseMcpJsonRpc`. The discriminator is reported on `purposeSource` /
174
- * `actionSource` for debug logging.
152
+ * Action precedence (unchanged from v2.4.14):
153
+ * - header body `_meta` body `arguments` → bare tool name / method.
154
+ *
155
+ * @param requestPath The HTTP request path (e.g. '/mcp'). Required.
156
+ * @param toolGate Resolved per-tool config from `toolGates`, if present.
175
157
  */
176
- declare function mcpToPdlss(parsed: ParsedMcpRequest, headerPurpose?: string, headerAction?: string): McpPdlssMapping;
158
+ declare function mcpToPdlss(parsed: ParsedMcpRequest, requestPath: string, headerPurpose?: string, headerAction?: string, toolGate?: {
159
+ purpose?: string;
160
+ resource?: string;
161
+ }): McpPdlssMapping;
177
162
  /**
178
163
  * Recommended minimum access level per method type. The MCP middleware uses
179
164
  * this to split low-risk handshake / introspection traffic from high-risk
@@ -200,10 +185,9 @@ declare function mcpRiskTier(parsed: ParsedMcpRequest): AccessLevel;
200
185
  * from `tools/call start_checkout` (high risk). This middleware peels
201
186
  * the JSON-RPC body and applies a per-method risk tier.
202
187
  *
203
- * (b) **PDLSS mapping**. Forwards `purpose=mcp_invoke`,
204
- * `action=method[:tool]`, `resource=mcp:tool/<name>` so audit traces
205
- * are stable across cohort-3 partners. See `transport/mcp-server.ts`
206
- * for the exact mapping.
188
+ * (b) **PDLSS mapping**. Derives purpose, action, and resource from
189
+ * `toolGates` config and request context. See `transport/mcp-server.ts`
190
+ * `mcpToPdlss()` for the exact mapping.
207
191
  *
208
192
  * (c) **Inner-hop dedupe**. Outbound responses set
209
193
  * `X-Astra-Verified-Hop` so a downstream REST endpoint that the tool
@@ -249,13 +233,51 @@ declare global {
249
233
  }
250
234
  }
251
235
  }
236
+ /**
237
+ * Extended per-tool gate with optional PDLSS purpose + resource overrides.
238
+ *
239
+ * When `purpose` is set, it is authoritative for that tool — the agent's
240
+ * `X-Astra-Purpose` header is ignored. This lets the merchant declare what
241
+ * semantic purpose each tool fulfils rather than trusting agent self-declaration.
242
+ *
243
+ * When `resource` is set, it overrides the default (`req.path`) for that
244
+ * tool's verify-access call — e.g. mapping `list_products` to `/api/catalog`.
245
+ */
246
+ interface ToolGateConfig {
247
+ minAccessLevel: AccessLevel;
248
+ purpose?: string;
249
+ resource?: string;
250
+ }
252
251
  interface McpMiddlewareOptions extends GatewayConfig {
253
252
  /**
254
- * Per-tool override for the minimum access level. Tools not listed inherit
255
- * the default tier from `mcpRiskTier` (`tools/call` → `'standard'`). Use
256
- * for high-risk tools that demand `'full'` or low-risk read-only tools.
253
+ * Per-tool gating for `tools/call` invocations. Tools not listed inherit
254
+ * the default tier from `mcpRiskTier` (`tools/call` → `'standard'`).
255
+ *
256
+ * Accepts both the shorthand access-level string and the full object shape:
257
+ * ```typescript
258
+ * toolGates: {
259
+ * browse_catalog: 'read-only', // shorthand
260
+ * list_products: { minAccessLevel: 'none', // full shape
261
+ * purpose: 'shopping.search',
262
+ * resource: '/api/catalog' },
263
+ * start_checkout: { minAccessLevel: 'standard',
264
+ * purpose: 'shopping.purchase',
265
+ * resource: '/api/checkout/*' },
266
+ * }
267
+ * ```
268
+ *
269
+ * When `tools/call` arrives for a tool not declared in `toolGates`, the SDK
270
+ * falls back to a risk-tier default based on the method classification
271
+ * (`mcpRiskTier`). For `tools/call` the fallback is `'standard'`. Best
272
+ * practice: declare every tool you expose explicitly.
273
+ *
274
+ * The action axis for non-tools/call MCP methods (`tools/list`,
275
+ * `resources/list`, `prompts/list`, etc.) is the literal JSON-RPC method
276
+ * name (e.g. `'tools/list'`). Merchants gating MCP traffic by action should
277
+ * declare per-tool gates in `toolGates`, not endpoint-level `allowedActions`
278
+ * — the latter applies to REST-style action values, not MCP method strings.
257
279
  */
258
- toolGates?: Record<string, AccessLevel>;
280
+ toolGates?: Record<string, AccessLevel | ToolGateConfig>;
259
281
  /**
260
282
  * Per-method override (e.g. tighten `tools/list` to `'read-only'` if you
261
283
  * don't want unregistered probes seeing your tool catalogue). Matches by
@@ -323,4 +345,4 @@ interface McpMiddlewareOptions extends GatewayConfig {
323
345
  */
324
346
  declare function createMcpMiddleware(options: McpMiddlewareOptions): RequestHandler;
325
347
 
326
- export { MCP_VERIFIED_HOP_HEADER, type McpMiddlewareOptions, type ParsedMcpRequest, createMcpMiddleware, isVerifiedHopValidFor, mcpRiskTier, mcpToPdlss, parseMcpJsonRpc, parseVerifiedHop, serializeVerifiedHop };
348
+ export { MCP_VERIFIED_HOP_HEADER, type McpMiddlewareOptions, type ParsedMcpRequest, type ToolGateConfig, createMcpMiddleware, isVerifiedHopValidFor, mcpRiskTier, mcpToPdlss, parseMcpJsonRpc, parseVerifiedHop, serializeVerifiedHop };
@@ -53,6 +53,65 @@ function hasMinimumAccess(actual, required) {
53
53
  // src/version.ts
54
54
  var SDK_VERSION = "2.4.13";
55
55
 
56
+ // src/well-known.ts
57
+ var CACHE_TTL_MS = 60 * 60 * 1e3;
58
+ var cache = /* @__PURE__ */ new Map();
59
+ var inflight = /* @__PURE__ */ new Map();
60
+ function wellKnownUrl(apiBaseUrl) {
61
+ const base = apiBaseUrl.replace(/\/api\/?$/, "");
62
+ return `${base}/.well-known/agentic-commerce`;
63
+ }
64
+ async function fetchWellKnown(apiBaseUrl) {
65
+ const url = wellKnownUrl(apiBaseUrl);
66
+ const response = await fetch(url, {
67
+ method: "GET",
68
+ headers: { Accept: "application/json" },
69
+ signal: AbortSignal.timeout(5e3)
70
+ });
71
+ if (!response.ok) {
72
+ throw new Error(
73
+ `AstraSync platform must expose /.well-known/agentic-commerce; got ${response.status} from ${url}. SDK cannot initialise without it.`
74
+ );
75
+ }
76
+ const data = await response.json();
77
+ if (!data.registrationUrl || !data.documentationUrl || !data.verifyAccessUrl) {
78
+ throw new Error(
79
+ `/.well-known/agentic-commerce response missing required fields (registrationUrl, documentationUrl, verifyAccessUrl).`
80
+ );
81
+ }
82
+ return data;
83
+ }
84
+ function prefetchWellKnown(apiBaseUrl) {
85
+ const existing = inflight.get(apiBaseUrl);
86
+ if (existing) return existing;
87
+ const promise = fetchWellKnown(apiBaseUrl).then((data) => {
88
+ cache.set(apiBaseUrl, { data, fetchedAt: Date.now() });
89
+ inflight.delete(apiBaseUrl);
90
+ return data;
91
+ }).catch((err) => {
92
+ inflight.delete(apiBaseUrl);
93
+ throw err;
94
+ });
95
+ inflight.set(apiBaseUrl, promise);
96
+ return promise;
97
+ }
98
+ async function getWellKnownUrls(apiBaseUrl) {
99
+ const entry = cache.get(apiBaseUrl);
100
+ if (entry) {
101
+ if (Date.now() - entry.fetchedAt > CACHE_TTL_MS) {
102
+ prefetchWellKnown(apiBaseUrl).catch(() => {
103
+ });
104
+ }
105
+ return entry.data;
106
+ }
107
+ const pending = inflight.get(apiBaseUrl);
108
+ if (pending) return pending;
109
+ return prefetchWellKnown(apiBaseUrl);
110
+ }
111
+ function getCachedWellKnownUrls(apiBaseUrl) {
112
+ return cache.get(apiBaseUrl)?.data;
113
+ }
114
+
56
115
  // src/verify.ts
57
116
  var DEFAULT_CONFIG = {
58
117
  apiBaseUrl: "https://astrasync.ai/api",
@@ -188,21 +247,22 @@ function extractCredentials(headers, query) {
188
247
  }
189
248
  return credentials;
190
249
  }
191
- function createGuidanceResponse(config, reason, options = {}) {
250
+ function createGuidanceResponse(_config, reason, options = {}) {
192
251
  const source = options.source ?? "no_credentials";
193
252
  const isApiError = source === "api_error";
253
+ const urls = options.urls;
194
254
  const guidance = isApiError ? {
195
255
  message: "Verification is temporarily unavailable. Retry with exponential backoff; if the issue persists, contact support with the correlationId.",
196
- registrationUrl: `${config.apiBaseUrl.replace("/api", "")}/agents/register`,
197
- documentationUrl: `${config.apiBaseUrl.replace("/api", "")}/docs/agent-access`,
256
+ registrationUrl: urls?.registrationUrl ?? "",
257
+ documentationUrl: urls?.documentationUrl ?? "",
198
258
  steps: [
199
259
  "Retry the request with exponential backoff",
200
260
  "If failures persist, share the correlationId with support"
201
261
  ]
202
262
  } : {
203
263
  message: "This service verifies AI agents before granting access. Please register your agent with AstraSync.",
204
- registrationUrl: `${config.apiBaseUrl.replace("/api", "")}/agents/register`,
205
- documentationUrl: `${config.apiBaseUrl.replace("/api", "")}/docs/agent-access`,
264
+ registrationUrl: urls?.registrationUrl ?? "",
265
+ documentationUrl: urls?.documentationUrl ?? "",
206
266
  steps: [
207
267
  "Register for an AstraSync account",
208
268
  "Create and register your agent",
@@ -244,7 +304,7 @@ async function callVerifyAccessAPI(config, request) {
244
304
  const { credentials, ...requestData } = request;
245
305
  const body = {
246
306
  ...credentials.astraId && { agentId: credentials.astraId },
247
- purpose: requestData.purpose || "general"
307
+ ...requestData.purpose && { purpose: requestData.purpose }
248
308
  };
249
309
  if (requestData.action) body.action = requestData.action;
250
310
  if (requestData.resourceType) body.resourceType = requestData.resourceType;
@@ -324,6 +384,7 @@ async function callVerifyAccessAPI(config, request) {
324
384
  }
325
385
  async function verify(config, request) {
326
386
  const mergedConfig = { ...DEFAULT_CONFIG, ...config };
387
+ const urls = mergedConfig.apiBaseUrl ? getCachedWellKnownUrls(mergedConfig.apiBaseUrl) : void 0;
327
388
  if (!initCheckPerformed && !mergedConfig.disableInitChecks && mergedConfig.apiBaseUrl) {
328
389
  if (mergedConfig.strictInit) {
329
390
  await performInitCheck(mergedConfig.apiBaseUrl, mergedConfig.debug, true);
@@ -360,7 +421,8 @@ async function verify(config, request) {
360
421
  if (!apiResponse.success) {
361
422
  return createGuidanceResponse(mergedConfig, apiResponse.error, {
362
423
  source: "api_error",
363
- correlationId: apiResponse.correlationId
424
+ correlationId: apiResponse.correlationId,
425
+ urls
364
426
  });
365
427
  }
366
428
  if (!apiResponse.access?.allowed) {
@@ -383,8 +445,8 @@ async function verify(config, request) {
383
445
  requiresApproval: apiResponse.access?.requiresApproval,
384
446
  guidance: {
385
447
  message: apiResponse.access?.reason || "Access denied by PDLSS policy",
386
- registrationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/agents/register`,
387
- documentationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/docs/pdlss`
448
+ registrationUrl: urls?.registrationUrl ?? "",
449
+ documentationUrl: urls?.documentationUrl ?? ""
388
450
  },
389
451
  verifiedAt: /* @__PURE__ */ new Date(),
390
452
  // Extract sessionId so decisions can be recorded for denials too
@@ -455,12 +517,12 @@ async function verify(config, request) {
455
517
  ];
456
518
  result.guidance = result.runtimeChallenge ? {
457
519
  message: `Verification failed: ${result.runtimeChallenge.reason || "runtime challenge failed"}`,
458
- registrationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/agents/register`,
459
- documentationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/docs/runtime-challenge`
520
+ registrationUrl: urls?.registrationUrl ?? "",
521
+ documentationUrl: urls?.documentationUrl ?? ""
460
522
  } : {
461
523
  message: result.recommendationReasons?.[0] || "Access denied by AstraSync recommendation",
462
- registrationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/agents/register`,
463
- documentationUrl: `${mergedConfig.apiBaseUrl?.replace("/api", "")}/docs/pdlss`
524
+ registrationUrl: urls?.registrationUrl ?? "",
525
+ documentationUrl: urls?.documentationUrl ?? ""
464
526
  };
465
527
  } else if (result.recommendation === "step_up_required") {
466
528
  result.requiresStepUp = true;
@@ -596,19 +658,22 @@ function extractFromMcpBody(astrasyncMeta, args, key) {
596
658
  }
597
659
  return { value: void 0, source: void 0 };
598
660
  }
599
- function mcpToPdlss(parsed, headerPurpose, headerAction) {
600
- const resource = parsed.toolName ? `mcp:tool/${parsed.toolName}` : `mcp:method/${parsed.method}`;
661
+ function mcpToPdlss(parsed, requestPath, headerPurpose, headerAction, toolGate) {
662
+ const resource = toolGate?.resource ?? requestPath;
601
663
  let purpose;
602
664
  let purposeSource;
603
- if (headerPurpose) {
665
+ if (toolGate?.purpose !== void 0) {
666
+ purpose = toolGate.purpose;
667
+ purposeSource = "tool_gate";
668
+ } else if (headerPurpose) {
604
669
  purpose = headerPurpose;
605
670
  purposeSource = "header";
606
671
  } else if (parsed.purposeFromBody && parsed.purposeSourceFromBody) {
607
672
  purpose = parsed.purposeFromBody;
608
673
  purposeSource = parsed.purposeSourceFromBody;
609
674
  } else {
610
- purpose = "mcp_invoke";
611
- purposeSource = "default_mcp_invoke";
675
+ purpose = void 0;
676
+ purposeSource = void 0;
612
677
  }
613
678
  let action;
614
679
  let actionSource;
@@ -633,6 +698,9 @@ function mcpRiskTier(parsed) {
633
698
  }
634
699
 
635
700
  // src/adapters/mcp.ts
701
+ function normalizeToolGate(gate) {
702
+ return typeof gate === "string" ? { minAccessLevel: gate } : gate;
703
+ }
636
704
  function readSingleHeader(value) {
637
705
  if (typeof value === "string") return value;
638
706
  if (Array.isArray(value)) return value[0];
@@ -648,6 +716,9 @@ function dedupeFailures(result) {
648
716
  return true;
649
717
  });
650
718
  }
719
+ if (result.denialReasons && result.denialReasons.length > 1) {
720
+ result.denialReasons = [...new Set(result.denialReasons)];
721
+ }
651
722
  }
652
723
  function defaultMcpDenied(result, req, res) {
653
724
  const id = req.body?.id ?? null;
@@ -673,11 +744,17 @@ function defaultMcpDenied(result, req, res) {
673
744
  });
674
745
  }
675
746
  function resolveMinAccessLevel(parsed, opts) {
676
- if (parsed.toolName && opts.toolGates && opts.toolGates[parsed.toolName] !== void 0) {
677
- return { level: opts.toolGates[parsed.toolName], source: "toolGate" };
747
+ if (!parsed.toolName) {
748
+ if (opts.methodGates && opts.methodGates[parsed.method] !== void 0) {
749
+ return { level: opts.methodGates[parsed.method], source: "methodGate" };
750
+ }
751
+ return { level: "none", source: "discovery_default" };
678
752
  }
679
- if (opts.methodGates && opts.methodGates[parsed.method] !== void 0) {
680
- return { level: opts.methodGates[parsed.method], source: "methodGate" };
753
+ if (opts.toolGates && opts.toolGates[parsed.toolName] !== void 0) {
754
+ return {
755
+ level: normalizeToolGate(opts.toolGates[parsed.toolName]).minAccessLevel,
756
+ source: "toolGate"
757
+ };
681
758
  }
682
759
  return { level: mcpRiskTier(parsed), source: "tier" };
683
760
  }
@@ -695,6 +772,10 @@ function createMcpMiddleware(options) {
695
772
  failOnError = "open",
696
773
  ...config
697
774
  } = options;
775
+ if (config.apiBaseUrl) {
776
+ prefetchWellKnown(config.apiBaseUrl).catch(() => {
777
+ });
778
+ }
698
779
  return async (req, res, next) => {
699
780
  try {
700
781
  if (skip) return next();
@@ -707,6 +788,7 @@ function createMcpMiddleware(options) {
707
788
  return next();
708
789
  }
709
790
  req.mcpRequest = parsed;
791
+ const wellKnownUrls = config.apiBaseUrl ? await getWellKnownUrls(config.apiBaseUrl).catch(() => void 0) : void 0;
710
792
  const headerRaw = req.headers["x-astra-id"] ?? req.headers["x-astra-agentid"];
711
793
  const headerAstraId = typeof headerRaw === "string" ? headerRaw : Array.isArray(headerRaw) ? headerRaw[0] : void 0;
712
794
  const bodyAstraId = parsed.agentIdFromBody;
@@ -760,9 +842,17 @@ function createMcpMiddleware(options) {
760
842
  }
761
843
  return next();
762
844
  }
845
+ const rawGate = parsed.toolName && toolGates?.[parsed.toolName];
846
+ const gate = rawGate ? normalizeToolGate(rawGate) : void 0;
763
847
  const headerPurpose = readSingleHeader(req.headers["x-astra-purpose"]);
764
848
  const headerAction = readSingleHeader(req.headers["x-astra-action"]);
765
- const pdlss = mcpToPdlss(parsed, headerPurpose, headerAction);
849
+ const pdlss = mcpToPdlss(
850
+ parsed,
851
+ req.path,
852
+ headerPurpose,
853
+ headerAction,
854
+ gate ? { purpose: gate.purpose, resource: gate.resource } : void 0
855
+ );
766
856
  if (config.debug) {
767
857
  console.debug("[mcp-middleware] pdlss resolved", {
768
858
  purpose_source: pdlss.purposeSource,
@@ -823,6 +913,13 @@ function createMcpMiddleware(options) {
823
913
  };
824
914
  result.failures = [...result.failures ?? [], insufficientFailure];
825
915
  result.denialReasons = [...result.denialReasons ?? [], insufficientFailure.message];
916
+ if (!result.guidance && wellKnownUrls) {
917
+ result.guidance = {
918
+ message: insufficientFailure.message,
919
+ registrationUrl: wellKnownUrls.registrationUrl,
920
+ documentationUrl: wellKnownUrls.documentationUrl
921
+ };
922
+ }
826
923
  if (shouldRecordDecisions) {
827
924
  const overrideKind = gateSource === "toolGate" ? "toolGate" : gateSource === "methodGate" ? "methodGate" : "other";
828
925
  const override = {
@@ -891,6 +988,14 @@ function createMcpMiddleware(options) {
891
988
  verifiedAt: /* @__PURE__ */ new Date(),
892
989
  correlationId
893
990
  };
991
+ const catchUrls = config.apiBaseUrl ? await getWellKnownUrls(config.apiBaseUrl).catch(() => void 0) : void 0;
992
+ if (catchUrls) {
993
+ result.guidance = {
994
+ message: `Middleware threw ${errorClass} \u2014 failing closed`,
995
+ registrationUrl: catchUrls.registrationUrl,
996
+ documentationUrl: catchUrls.documentationUrl
997
+ };
998
+ }
894
999
  dedupeFailures(result);
895
1000
  return onDenied(result, req, res);
896
1001
  }