@astrasyncai/verification-gateway 3.0.0 → 3.2.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.
Files changed (87) hide show
  1. package/dist/adapter-interface/interface.d.mts +2 -2
  2. package/dist/adapter-interface/interface.d.ts +2 -2
  3. package/dist/adapters/express.d.mts +2 -2
  4. package/dist/adapters/express.d.ts +2 -2
  5. package/dist/adapters/express.js +145 -93
  6. package/dist/adapters/express.js.map +1 -1
  7. package/dist/adapters/express.mjs +145 -93
  8. package/dist/adapters/express.mjs.map +1 -1
  9. package/dist/adapters/mcp.d.mts +29 -11
  10. package/dist/adapters/mcp.d.ts +29 -11
  11. package/dist/adapters/mcp.js +43 -102
  12. package/dist/adapters/mcp.js.map +1 -1
  13. package/dist/adapters/mcp.mjs +43 -102
  14. package/dist/adapters/mcp.mjs.map +1 -1
  15. package/dist/adapters/nextjs.d.mts +2 -2
  16. package/dist/adapters/nextjs.d.ts +2 -2
  17. package/dist/adapters/nextjs.js +126 -56
  18. package/dist/adapters/nextjs.js.map +1 -1
  19. package/dist/adapters/nextjs.mjs +126 -56
  20. package/dist/adapters/nextjs.mjs.map +1 -1
  21. package/dist/adapters/sdk.d.mts +2 -2
  22. package/dist/adapters/sdk.d.ts +2 -2
  23. package/dist/adapters/sdk.js +25 -14
  24. package/dist/adapters/sdk.js.map +1 -1
  25. package/dist/adapters/sdk.mjs +25 -14
  26. package/dist/adapters/sdk.mjs.map +1 -1
  27. package/dist/agent/index.d.mts +2 -2
  28. package/dist/agent/index.d.ts +2 -2
  29. package/dist/agent/index.js +3 -0
  30. package/dist/agent/index.js.map +1 -1
  31. package/dist/agent/index.mjs +3 -0
  32. package/dist/agent/index.mjs.map +1 -1
  33. package/dist/browser/background.js +18 -21
  34. package/dist/browser/background.js.map +1 -1
  35. package/dist/browser/background.mjs +18 -21
  36. package/dist/browser/background.mjs.map +1 -1
  37. package/dist/browser/browser-adapter.d.mts +2 -2
  38. package/dist/browser/browser-adapter.d.ts +2 -2
  39. package/dist/cli/index.d.mts +2 -2
  40. package/dist/cli/index.d.ts +2 -2
  41. package/dist/cursor/cursor-adapter.d.mts +2 -2
  42. package/dist/cursor/cursor-adapter.d.ts +2 -2
  43. package/dist/cursor/extension.d.mts +2 -2
  44. package/dist/cursor/extension.d.ts +2 -2
  45. package/dist/cursor/extension.js +18 -21
  46. package/dist/cursor/extension.js.map +1 -1
  47. package/dist/cursor/extension.mjs +18 -21
  48. package/dist/cursor/extension.mjs.map +1 -1
  49. package/dist/{express-CrfwoNAR.d.ts → express-BowlMHQF.d.ts} +1 -1
  50. package/dist/{express-ienhAXps.d.mts → express-CeoSdOAZ.d.mts} +1 -1
  51. package/dist/gateway/gateway.d.mts +2 -2
  52. package/dist/gateway/gateway.d.ts +2 -2
  53. package/dist/gateway/gateway.js +18 -21
  54. package/dist/gateway/gateway.js.map +1 -1
  55. package/dist/gateway/gateway.mjs +18 -21
  56. package/dist/gateway/gateway.mjs.map +1 -1
  57. package/dist/git-trigger/git-hooks.d.mts +2 -2
  58. package/dist/git-trigger/git-hooks.d.ts +2 -2
  59. package/dist/{index-CEg_WG6y.d.mts → index-B51W8gn8.d.mts} +1 -1
  60. package/dist/{index-DC5f8eoQ.d.ts → index-DBmlycVm.d.ts} +1 -1
  61. package/dist/{index-B5e2IDWU.d.mts → index-DtGziFEm.d.mts} +1 -1
  62. package/dist/{index-CCdZxvAr.d.ts → index-DzXXBuLm.d.ts} +1 -1
  63. package/dist/index.d.mts +7 -7
  64. package/dist/index.d.ts +7 -7
  65. package/dist/index.js +209 -191
  66. package/dist/index.js.map +1 -1
  67. package/dist/index.mjs +209 -191
  68. package/dist/index.mjs.map +1 -1
  69. package/dist/local-evaluator/evaluator.d.mts +2 -2
  70. package/dist/local-evaluator/evaluator.d.ts +2 -2
  71. package/dist/{nextjs-DSpisQst.d.mts → nextjs-BW1rzr1I.d.mts} +1 -1
  72. package/dist/{nextjs-66R1KW8e.d.ts → nextjs-V_K0qlAQ.d.ts} +1 -1
  73. package/dist/{sdk-5U_CBRpr.d.mts → sdk-ZYgI7G9f.d.ts} +14 -3
  74. package/dist/{sdk-Bm8np66n.d.ts → sdk-e5jg7sqW.d.mts} +14 -3
  75. package/dist/transport/index.d.mts +2 -2
  76. package/dist/transport/index.d.ts +2 -2
  77. package/dist/transport/index.js +10 -0
  78. package/dist/transport/index.js.map +1 -1
  79. package/dist/transport/index.mjs +10 -0
  80. package/dist/transport/index.mjs.map +1 -1
  81. package/dist/{types-CgDCUfo8.d.mts → types-BNiLZY0i.d.mts} +1 -1
  82. package/dist/{types-R5N4ET6x.d.ts → types-DJi-u3fz.d.ts} +1 -1
  83. package/dist/{types-B3USs-Kx.d.mts → types-rFh4VMH4.d.mts} +30 -2
  84. package/dist/{types-B3USs-Kx.d.ts → types-rFh4VMH4.d.ts} +30 -2
  85. package/dist/ui/index.d.mts +1 -1
  86. package/dist/ui/index.d.ts +1 -1
  87. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  import { Request, Response, RequestHandler } from 'express';
2
- import { A as AccessLevel, G as GatewayConfig, i as VerificationResult } from '../types-B3USs-Kx.mjs';
2
+ import { A as AccessLevel, G as GatewayConfig, i as VerificationResult } from '../types-rFh4VMH4.mjs';
3
3
 
4
4
  /**
5
5
  * MCP server-side helpers — companion to `transport/mcp.ts` (which handles the
@@ -137,7 +137,7 @@ interface McpPdlssMapping {
137
137
  action: string;
138
138
  resource: string;
139
139
  purposeSource: 'header' | 'meta' | 'tool_argument' | 'tool_gate' | undefined;
140
- actionSource: 'header' | 'meta' | 'tool_argument' | 'transport_layer';
140
+ actionSource: 'header' | 'meta' | 'tool_argument' | 'tool_gate' | 'transport_layer';
141
141
  }
142
142
  /**
143
143
  * v2.5.0 — PDLSS field derivation for MCP requests.
@@ -149,14 +149,18 @@ interface McpPdlssMapping {
149
149
  * Resource precedence:
150
150
  * - `toolGate.resource` if provided, else `requestPath`.
151
151
  *
152
- * Action precedence (unchanged from v2.4.14):
153
- * - header body `_meta` → body `arguments` → bare tool name / method.
152
+ * Action precedence (3.1.0, Bug 14 §4.6 — toolGate override added for
153
+ * symmetry with purpose):
154
+ * - `toolGate.action` authoritative → header → body `_meta` → body
155
+ * `arguments` → bare tool name / method (transport_layer, unchanged —
156
+ * bare tool names are legitimate enumerated actions, never aliased).
154
157
  *
155
158
  * @param requestPath The HTTP request path (e.g. '/mcp'). Required.
156
159
  * @param toolGate Resolved per-tool config from `toolGates`, if present.
157
160
  */
158
161
  declare function mcpToPdlss(parsed: ParsedMcpRequest, requestPath: string, headerPurpose?: string, headerAction?: string, toolGate?: {
159
162
  purpose?: string;
163
+ action?: string;
160
164
  resource?: string;
161
165
  }): McpPdlssMapping;
162
166
  /**
@@ -234,18 +238,25 @@ declare global {
234
238
  }
235
239
  }
236
240
  /**
237
- * Extended per-tool gate with optional PDLSS purpose + resource overrides.
241
+ * Extended per-tool gate with optional PDLSS purpose + action + resource
242
+ * overrides.
238
243
  *
239
244
  * When `purpose` is set, it is authoritative for that tool — the agent's
240
245
  * `X-Astra-Purpose` header is ignored. This lets the merchant declare what
241
246
  * semantic purpose each tool fulfils rather than trusting agent self-declaration.
242
247
  *
248
+ * When `action` is set (Bug 14, §4.6 — symmetric with `purpose`), it is
249
+ * authoritative over `X-Astra-Action` / body declarations, letting the
250
+ * merchant pin a dotted-verb action (e.g. `shopping.search`) for a tool
251
+ * whose callers would otherwise fall to the bare tool-name transport default.
252
+ *
243
253
  * When `resource` is set, it overrides the default (`req.path`) for that
244
254
  * tool's verify-access call — e.g. mapping `list_products` to `/api/catalog`.
245
255
  */
246
256
  interface ToolGateConfig {
247
257
  minAccessLevel: AccessLevel;
248
258
  purpose?: string;
259
+ action?: string;
249
260
  resource?: string;
250
261
  }
251
262
  interface McpMiddlewareOptions extends GatewayConfig {
@@ -253,15 +264,22 @@ interface McpMiddlewareOptions extends GatewayConfig {
253
264
  * Per-tool gating for `tools/call` invocations. Tools not listed inherit
254
265
  * the default tier from `mcpRiskTier` (`tools/call` → `'standard'`).
255
266
  *
256
- * Accepts both the shorthand access-level string and the full object shape:
267
+ * Accepts both the shorthand access-level string and the full object shape.
268
+ * NOTE (3.2.0): the access-level band no longer gates, and a gated `tools/call`
269
+ * must carry a resolvable PDLSS **purpose** (the backend requires it). So the
270
+ * bare string shorthand — which carries no purpose — only works if the caller
271
+ * supplies the purpose another way (an `X-Astra-Purpose` header, or
272
+ * `params._meta.astrasync.purpose`); otherwise the call fails fast with a
273
+ * `PDLSS_PURPOSE_REQUIRED` 400. Prefer the **object form with `purpose`**:
257
274
  * ```typescript
258
275
  * toolGates: {
259
- * browse_catalog: 'read-only', // shorthand
260
- * list_products: { minAccessLevel: 'none', // full shape
261
- * purpose: 'shopping.search',
276
+ * browse_catalog: 'read-only', // shorthand — purpose must come
277
+ * // from a header / _meta, else 400
278
+ * list_products: { purpose: 'shopping', // object form (recommended)
279
+ * action: 'shopping.search',
262
280
  * resource: '/api/catalog' },
263
- * start_checkout: { minAccessLevel: 'standard',
264
- * purpose: 'shopping.purchase',
281
+ * start_checkout: { purpose: 'shopping',
282
+ * action: 'shopping.purchase',
265
283
  * resource: '/api/checkout/*' },
266
284
  * }
267
285
  * ```
@@ -1,5 +1,5 @@
1
1
  import { Request, Response, RequestHandler } from 'express';
2
- import { A as AccessLevel, G as GatewayConfig, i as VerificationResult } from '../types-B3USs-Kx.js';
2
+ import { A as AccessLevel, G as GatewayConfig, i as VerificationResult } from '../types-rFh4VMH4.js';
3
3
 
4
4
  /**
5
5
  * MCP server-side helpers — companion to `transport/mcp.ts` (which handles the
@@ -137,7 +137,7 @@ interface McpPdlssMapping {
137
137
  action: string;
138
138
  resource: string;
139
139
  purposeSource: 'header' | 'meta' | 'tool_argument' | 'tool_gate' | undefined;
140
- actionSource: 'header' | 'meta' | 'tool_argument' | 'transport_layer';
140
+ actionSource: 'header' | 'meta' | 'tool_argument' | 'tool_gate' | 'transport_layer';
141
141
  }
142
142
  /**
143
143
  * v2.5.0 — PDLSS field derivation for MCP requests.
@@ -149,14 +149,18 @@ interface McpPdlssMapping {
149
149
  * Resource precedence:
150
150
  * - `toolGate.resource` if provided, else `requestPath`.
151
151
  *
152
- * Action precedence (unchanged from v2.4.14):
153
- * - header body `_meta` → body `arguments` → bare tool name / method.
152
+ * Action precedence (3.1.0, Bug 14 §4.6 — toolGate override added for
153
+ * symmetry with purpose):
154
+ * - `toolGate.action` authoritative → header → body `_meta` → body
155
+ * `arguments` → bare tool name / method (transport_layer, unchanged —
156
+ * bare tool names are legitimate enumerated actions, never aliased).
154
157
  *
155
158
  * @param requestPath The HTTP request path (e.g. '/mcp'). Required.
156
159
  * @param toolGate Resolved per-tool config from `toolGates`, if present.
157
160
  */
158
161
  declare function mcpToPdlss(parsed: ParsedMcpRequest, requestPath: string, headerPurpose?: string, headerAction?: string, toolGate?: {
159
162
  purpose?: string;
163
+ action?: string;
160
164
  resource?: string;
161
165
  }): McpPdlssMapping;
162
166
  /**
@@ -234,18 +238,25 @@ declare global {
234
238
  }
235
239
  }
236
240
  /**
237
- * Extended per-tool gate with optional PDLSS purpose + resource overrides.
241
+ * Extended per-tool gate with optional PDLSS purpose + action + resource
242
+ * overrides.
238
243
  *
239
244
  * When `purpose` is set, it is authoritative for that tool — the agent's
240
245
  * `X-Astra-Purpose` header is ignored. This lets the merchant declare what
241
246
  * semantic purpose each tool fulfils rather than trusting agent self-declaration.
242
247
  *
248
+ * When `action` is set (Bug 14, §4.6 — symmetric with `purpose`), it is
249
+ * authoritative over `X-Astra-Action` / body declarations, letting the
250
+ * merchant pin a dotted-verb action (e.g. `shopping.search`) for a tool
251
+ * whose callers would otherwise fall to the bare tool-name transport default.
252
+ *
243
253
  * When `resource` is set, it overrides the default (`req.path`) for that
244
254
  * tool's verify-access call — e.g. mapping `list_products` to `/api/catalog`.
245
255
  */
246
256
  interface ToolGateConfig {
247
257
  minAccessLevel: AccessLevel;
248
258
  purpose?: string;
259
+ action?: string;
249
260
  resource?: string;
250
261
  }
251
262
  interface McpMiddlewareOptions extends GatewayConfig {
@@ -253,15 +264,22 @@ interface McpMiddlewareOptions extends GatewayConfig {
253
264
  * Per-tool gating for `tools/call` invocations. Tools not listed inherit
254
265
  * the default tier from `mcpRiskTier` (`tools/call` → `'standard'`).
255
266
  *
256
- * Accepts both the shorthand access-level string and the full object shape:
267
+ * Accepts both the shorthand access-level string and the full object shape.
268
+ * NOTE (3.2.0): the access-level band no longer gates, and a gated `tools/call`
269
+ * must carry a resolvable PDLSS **purpose** (the backend requires it). So the
270
+ * bare string shorthand — which carries no purpose — only works if the caller
271
+ * supplies the purpose another way (an `X-Astra-Purpose` header, or
272
+ * `params._meta.astrasync.purpose`); otherwise the call fails fast with a
273
+ * `PDLSS_PURPOSE_REQUIRED` 400. Prefer the **object form with `purpose`**:
257
274
  * ```typescript
258
275
  * toolGates: {
259
- * browse_catalog: 'read-only', // shorthand
260
- * list_products: { minAccessLevel: 'none', // full shape
261
- * purpose: 'shopping.search',
276
+ * browse_catalog: 'read-only', // shorthand — purpose must come
277
+ * // from a header / _meta, else 400
278
+ * list_products: { purpose: 'shopping', // object form (recommended)
279
+ * action: 'shopping.search',
262
280
  * resource: '/api/catalog' },
263
- * start_checkout: { minAccessLevel: 'standard',
264
- * purpose: 'shopping.purchase',
281
+ * start_checkout: { purpose: 'shopping',
282
+ * action: 'shopping.purchase',
265
283
  * resource: '/api/checkout/*' },
266
284
  * }
267
285
  * ```
@@ -32,26 +32,15 @@ __export(mcp_exports, {
32
32
  module.exports = __toCommonJS(mcp_exports);
33
33
 
34
34
  // src/access-levels.ts
35
- var ACCESS_LEVEL_HIERARCHY = {
36
- none: 0,
37
- restricted: 1,
38
- "read-only": 2,
39
- standard: 3,
40
- full: 4,
41
- internal: 5
42
- };
43
35
  function getTrustLevel(score) {
44
36
  if (score >= 80) return "PLATINUM";
45
37
  if (score >= 60) return "GOLD";
46
38
  if (score >= 40) return "SILVER";
47
39
  return "BRONZE";
48
40
  }
49
- function hasMinimumAccess(actual, required) {
50
- return ACCESS_LEVEL_HIERARCHY[actual] >= ACCESS_LEVEL_HIERARCHY[required];
51
- }
52
41
 
53
42
  // src/version.ts
54
- var SDK_VERSION = "3.0.0";
43
+ var SDK_VERSION = "3.2.0";
55
44
 
56
45
  // src/well-known.ts
57
46
  var CACHE_TTL_MS = 60 * 60 * 1e3;
@@ -156,7 +145,7 @@ async function performInitCheck(apiBaseUrl, debug, strictInit) {
156
145
  }
157
146
  }
158
147
  var verificationCache = /* @__PURE__ */ new Map();
159
- function getCacheKey(request) {
148
+ function getCacheKey(request, counterpartyId) {
160
149
  const c = request.credentials;
161
150
  return [
162
151
  c.astraId || "",
@@ -169,6 +158,14 @@ function getCacheKey(request) {
169
158
  request.jurisdiction || "",
170
159
  request.transactionValue ?? "",
171
160
  request.currency || "",
161
+ // SECURITY (cross-merchant cache leak): the merchant identity is sent via
162
+ // `config.counterpartyId`, NOT on the request, so it was previously absent
163
+ // from the key — two verifies for the SAME agent/purpose/action/value but
164
+ // DIFFERENT merchants collided, and a grant at a permissive merchant (low
165
+ // trust floor) was served for a stricter one. Same bug class as the
166
+ // duration omission (F-A1-07). counterpartyId affects the backend verdict
167
+ // (trust floor / per-route policy), so it MUST key the cache.
168
+ counterpartyId || "",
172
169
  request.counterpartyUrl || "",
173
170
  request.counterpartyType || "",
174
171
  request.isSubAgentRequest ? "1" : "0",
@@ -192,8 +189,8 @@ function getCacheKey(request) {
192
189
  request.callerMetadata?.agentCardUrl || ""
193
190
  ].join("|");
194
191
  }
195
- function getCachedResult(request) {
196
- const key = getCacheKey(request);
192
+ function getCachedResult(request, counterpartyId) {
193
+ const key = getCacheKey(request, counterpartyId);
197
194
  const cached = verificationCache.get(key);
198
195
  if (cached && cached.expiresAt > Date.now()) {
199
196
  return cached.result;
@@ -205,9 +202,9 @@ function getCachedResult(request) {
205
202
  }
206
203
  var DEFAULT_AUTONOMOUS_TTL_SECONDS = 60;
207
204
  var DEFAULT_STEP_UP_TTL_SECONDS = 300;
208
- function cacheResult(request, result, configuredTtl) {
205
+ function cacheResult(request, result, configuredTtl, counterpartyId) {
209
206
  const ttlSeconds = configuredTtl && configuredTtl > 0 ? configuredTtl : result.requiresStepUp ? DEFAULT_STEP_UP_TTL_SECONDS : DEFAULT_AUTONOMOUS_TTL_SECONDS;
210
- const key = getCacheKey(request);
207
+ const key = getCacheKey(request, counterpartyId);
211
208
  verificationCache.set(key, {
212
209
  result,
213
210
  expiresAt: Date.now() + ttlSeconds * 1e3
@@ -399,7 +396,7 @@ async function verify(config, request) {
399
396
  );
400
397
  }
401
398
  if (mergedConfig.cacheTtl !== 0) {
402
- const cached = getCachedResult(request);
399
+ const cached = getCachedResult(request, mergedConfig.counterpartyId);
403
400
  if (cached) {
404
401
  if (mergedConfig.debug) {
405
402
  console.log("[VerificationGateway] Returning cached result");
@@ -451,8 +448,8 @@ async function verify(config, request) {
451
448
  verifiedAt: /* @__PURE__ */ new Date(),
452
449
  // Extract sessionId so decisions can be recorded for denials too
453
450
  sessionId: apiResponse.sessionId,
454
- // v2.3.10 (defect #34, round-4): anonymous traffic has no session →
455
- // correlationId is the linking key for paired local_override events.
451
+ // Anonymous traffic has no session → correlationId is the per-attempt
452
+ // linking key (the sessionId-equivalent for anonymous callers).
456
453
  correlationId: apiResponse.correlationId,
457
454
  recommendation: apiResponse.recommendation,
458
455
  recommendationReasons: apiResponse.recommendationReasons
@@ -526,17 +523,14 @@ async function verify(config, request) {
526
523
  };
527
524
  } else if (result.recommendation === "step_up_required") {
528
525
  result.requiresStepUp = true;
529
- if (ACCESS_LEVEL_HIERARCHY[result.accessLevel] > ACCESS_LEVEL_HIERARCHY["read-only"]) {
530
- result.accessLevel = "read-only";
531
- }
532
526
  result.denialReasons = result.recommendationReasons || ["Step-up verification required"];
533
527
  }
534
528
  if (mergedConfig.cacheTtl !== 0 && result.recommendation !== "deny") {
535
- cacheResult(request, result, mergedConfig.cacheTtl);
529
+ cacheResult(request, result, mergedConfig.cacheTtl, mergedConfig.counterpartyId);
536
530
  }
537
531
  return result;
538
532
  }
539
- async function recordDecision(config, sessionId, decision, reason, override) {
533
+ async function recordDecision(config, sessionId, decision, reason) {
540
534
  const headers = { "Content-Type": "application/json" };
541
535
  if (config.apiKey) {
542
536
  headers["Authorization"] = `Bearer ${config.apiKey}`;
@@ -545,36 +539,7 @@ async function recordDecision(config, sessionId, decision, reason, override) {
545
539
  await fetch(`${config.apiBaseUrl}/agents/verify-access/${sessionId}/decision`, {
546
540
  method: "POST",
547
541
  headers,
548
- body: JSON.stringify({
549
- decision,
550
- reason,
551
- ...override && {
552
- overriddenBy: override.overriddenBy,
553
- toolName: override.toolName,
554
- requestedLevel: override.requestedLevel,
555
- grantedLevel: override.grantedLevel
556
- }
557
- })
558
- }).catch(() => {
559
- });
560
- }
561
- async function recordAnonymousLocalOverride(config, correlationId, override, reason) {
562
- const headers = { "Content-Type": "application/json" };
563
- if (config.apiKey) {
564
- headers["Authorization"] = `Bearer ${config.apiKey}`;
565
- headers["X-API-Key"] = config.apiKey;
566
- }
567
- await fetch(`${config.apiBaseUrl}/agents/verify-access/local-override`, {
568
- method: "POST",
569
- headers,
570
- body: JSON.stringify({
571
- correlationId,
572
- reason,
573
- overriddenBy: override.overriddenBy,
574
- toolName: override.toolName,
575
- requestedLevel: override.requestedLevel,
576
- grantedLevel: override.grantedLevel
577
- })
542
+ body: JSON.stringify({ decision, reason })
578
543
  }).catch(() => {
579
544
  });
580
545
  }
@@ -677,7 +642,10 @@ function mcpToPdlss(parsed, requestPath, headerPurpose, headerAction, toolGate)
677
642
  }
678
643
  let action;
679
644
  let actionSource;
680
- if (headerAction) {
645
+ if (toolGate?.action !== void 0) {
646
+ action = toolGate.action;
647
+ actionSource = "tool_gate";
648
+ } else if (headerAction) {
681
649
  action = headerAction;
682
650
  actionSource = "header";
683
651
  } else if (parsed.actionFromBody && parsed.actionSourceFromBody) {
@@ -788,7 +756,6 @@ function createMcpMiddleware(options) {
788
756
  return next();
789
757
  }
790
758
  req.mcpRequest = parsed;
791
- const wellKnownUrls = config.apiBaseUrl ? await getWellKnownUrls(config.apiBaseUrl).catch(() => void 0) : void 0;
792
759
  const headerRaw = req.headers["x-astra-id"] ?? req.headers["x-astra-agentid"];
793
760
  const headerAstraId = typeof headerRaw === "string" ? headerRaw : Array.isArray(headerRaw) ? headerRaw[0] : void 0;
794
761
  const bodyAstraId = parsed.agentIdFromBody;
@@ -825,7 +792,7 @@ function createMcpMiddleware(options) {
825
792
  return next();
826
793
  }
827
794
  }
828
- const { level: minAccessLevel, source: gateSource } = resolveMinAccessLevel(parsed, {
795
+ const { level: minAccessLevel } = resolveMinAccessLevel(parsed, {
829
796
  toolGates,
830
797
  methodGates
831
798
  });
@@ -851,7 +818,7 @@ function createMcpMiddleware(options) {
851
818
  req.path,
852
819
  headerPurpose,
853
820
  headerAction,
854
- gate ? { purpose: gate.purpose, resource: gate.resource } : void 0
821
+ gate ? { purpose: gate.purpose, action: gate.action, resource: gate.resource } : void 0
855
822
  );
856
823
  if (config.debug) {
857
824
  console.debug("[mcp-middleware] pdlss resolved", {
@@ -861,6 +828,23 @@ function createMcpMiddleware(options) {
861
828
  resolved_action: pdlss.action
862
829
  });
863
830
  }
831
+ if (!pdlss.purpose) {
832
+ const id = req.body?.id ?? null;
833
+ res.status(400).json({
834
+ jsonrpc: "2.0",
835
+ id,
836
+ error: {
837
+ code: -32602,
838
+ message: "PDLSS_PURPOSE_REQUIRED",
839
+ data: {
840
+ dimension: "pdlss.purpose",
841
+ detail: "This tool is access-gated but the call declared no PDLSS purpose. Supply a bare-category purpose via the X-Astra-Purpose header or params._meta.astrasync.purpose, or have the merchant set the tool\u2019s purpose in its toolGate config.",
842
+ resolvedAction: pdlss.action
843
+ }
844
+ }
845
+ });
846
+ return;
847
+ }
864
848
  const counterpartyUrl = config.counterpartyUrl || `${req.protocol}://${req.get("host")}${req.path}`;
865
849
  const shouldRecordDecisions = recordDecisions !== false;
866
850
  const result = await verify(config, {
@@ -884,7 +868,6 @@ function createMcpMiddleware(options) {
884
868
  });
885
869
  req.agentVerification = result;
886
870
  const sessionId = result.sessionId;
887
- const correlationId = result.correlationId;
888
871
  if (!result.identityVerified || !result.policyAllowed) {
889
872
  if (shouldRecordDecisions && sessionId) {
890
873
  recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
@@ -905,48 +888,6 @@ function createMcpMiddleware(options) {
905
888
  }
906
889
  return next();
907
890
  }
908
- if (!hasMinimumAccess(result.accessLevel, minAccessLevel)) {
909
- const insufficientFailure = {
910
- dimension: "access_level.insufficient",
911
- message: `Tool requires accessLevel '${minAccessLevel}'; agent has '${result.accessLevel}'.`,
912
- guidance: "Request elevated access via step-up verification (coming soon \u2014 ships this month). Step-up lets the agent owner approve a one-time elevation for this specific counterparty + purpose without changing the agent's baseline trust score."
913
- };
914
- result.failures = [...result.failures ?? [], insufficientFailure];
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
- }
923
- if (shouldRecordDecisions) {
924
- const overrideKind = gateSource === "toolGate" ? "toolGate" : gateSource === "methodGate" ? "methodGate" : "other";
925
- const override = {
926
- overriddenBy: overrideKind,
927
- ...parsed.toolName && { toolName: parsed.toolName },
928
- requestedLevel: minAccessLevel,
929
- grantedLevel: result.accessLevel
930
- };
931
- if (sessionId) {
932
- recordDecision(config, sessionId, "denied", result.denialReasons?.[0], override).catch(
933
- () => {
934
- }
935
- );
936
- } else if (correlationId) {
937
- recordAnonymousLocalOverride(
938
- config,
939
- correlationId,
940
- override,
941
- result.denialReasons?.[0]
942
- ).catch(() => {
943
- });
944
- }
945
- }
946
- dedupeFailures(result);
947
- onDenied(result, req, res);
948
- return;
949
- }
950
891
  if (effectiveAstraId) {
951
892
  res.setHeader(
952
893
  MCP_VERIFIED_HOP_HEADER,