@astrasyncai/verification-gateway 3.1.0 → 3.2.1

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 (79) 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 +46 -61
  6. package/dist/adapters/express.js.map +1 -1
  7. package/dist/adapters/express.mjs +46 -61
  8. package/dist/adapters/express.mjs.map +1 -1
  9. package/dist/adapters/mcp.d.mts +12 -7
  10. package/dist/adapters/mcp.d.ts +12 -7
  11. package/dist/adapters/mcp.js +60 -99
  12. package/dist/adapters/mcp.js.map +1 -1
  13. package/dist/adapters/mcp.mjs +60 -99
  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 +37 -30
  18. package/dist/adapters/nextjs.js.map +1 -1
  19. package/dist/adapters/nextjs.mjs +37 -30
  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/browser/background.js +18 -21
  30. package/dist/browser/background.js.map +1 -1
  31. package/dist/browser/background.mjs +18 -21
  32. package/dist/browser/background.mjs.map +1 -1
  33. package/dist/browser/browser-adapter.d.mts +2 -2
  34. package/dist/browser/browser-adapter.d.ts +2 -2
  35. package/dist/cli/index.d.mts +2 -2
  36. package/dist/cli/index.d.ts +2 -2
  37. package/dist/cursor/cursor-adapter.d.mts +2 -2
  38. package/dist/cursor/cursor-adapter.d.ts +2 -2
  39. package/dist/cursor/extension.d.mts +2 -2
  40. package/dist/cursor/extension.d.ts +2 -2
  41. package/dist/cursor/extension.js +18 -21
  42. package/dist/cursor/extension.js.map +1 -1
  43. package/dist/cursor/extension.mjs +18 -21
  44. package/dist/cursor/extension.mjs.map +1 -1
  45. package/dist/{express-DavQ76oF.d.ts → express-BowlMHQF.d.ts} +1 -1
  46. package/dist/{express-DFVBlXr_.d.mts → express-CeoSdOAZ.d.mts} +1 -1
  47. package/dist/gateway/gateway.d.mts +2 -2
  48. package/dist/gateway/gateway.d.ts +2 -2
  49. package/dist/gateway/gateway.js +18 -21
  50. package/dist/gateway/gateway.js.map +1 -1
  51. package/dist/gateway/gateway.mjs +18 -21
  52. package/dist/gateway/gateway.mjs.map +1 -1
  53. package/dist/git-trigger/git-hooks.d.mts +2 -2
  54. package/dist/git-trigger/git-hooks.d.ts +2 -2
  55. package/dist/{index-BhL2R65s.d.mts → index-B51W8gn8.d.mts} +1 -1
  56. package/dist/{index-BhEgEiJL.d.ts → index-DBmlycVm.d.ts} +1 -1
  57. package/dist/{index-BVxantdv.d.mts → index-DtGziFEm.d.mts} +1 -1
  58. package/dist/{index-Dk2nIA4w.d.ts → index-DzXXBuLm.d.ts} +1 -1
  59. package/dist/index.d.mts +7 -7
  60. package/dist/index.d.ts +7 -7
  61. package/dist/index.js +87 -122
  62. package/dist/index.js.map +1 -1
  63. package/dist/index.mjs +87 -122
  64. package/dist/index.mjs.map +1 -1
  65. package/dist/local-evaluator/evaluator.d.mts +2 -2
  66. package/dist/local-evaluator/evaluator.d.ts +2 -2
  67. package/dist/{nextjs-D-maqrNz.d.mts → nextjs-BW1rzr1I.d.mts} +1 -1
  68. package/dist/{nextjs-BXLH1hJj.d.ts → nextjs-V_K0qlAQ.d.ts} +1 -1
  69. package/dist/{sdk-767LaEP8.d.mts → sdk-ZYgI7G9f.d.ts} +14 -3
  70. package/dist/{sdk-K8IgssHI.d.ts → sdk-e5jg7sqW.d.mts} +14 -3
  71. package/dist/transport/index.d.mts +2 -2
  72. package/dist/transport/index.d.ts +2 -2
  73. package/dist/{types-CyFwZ_Yu.d.mts → types-BNiLZY0i.d.mts} +1 -1
  74. package/dist/{types-WIRp_BP_.d.ts → types-DJi-u3fz.d.ts} +1 -1
  75. package/dist/{types-Cuh7ELfr.d.mts → types-rFh4VMH4.d.mts} +5 -2
  76. package/dist/{types-Cuh7ELfr.d.ts → types-rFh4VMH4.d.ts} +5 -2
  77. package/dist/ui/index.d.mts +1 -1
  78. package/dist/ui/index.d.ts +1 -1
  79. 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-Cuh7ELfr.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
@@ -264,16 +264,21 @@ interface McpMiddlewareOptions extends GatewayConfig {
264
264
  * Per-tool gating for `tools/call` invocations. Tools not listed inherit
265
265
  * the default tier from `mcpRiskTier` (`tools/call` → `'standard'`).
266
266
  *
267
- * 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`**:
268
274
  * ```typescript
269
275
  * toolGates: {
270
- * browse_catalog: 'read-only', // shorthand
271
- * list_products: { minAccessLevel: 'none', // full shape
272
- * purpose: 'shopping',
276
+ * browse_catalog: 'read-only', // shorthand — purpose must come
277
+ * // from a header / _meta, else 400
278
+ * list_products: { purpose: 'shopping', // object form (recommended)
273
279
  * action: 'shopping.search',
274
280
  * resource: '/api/catalog' },
275
- * start_checkout: { minAccessLevel: 'standard',
276
- * purpose: 'shopping',
281
+ * start_checkout: { purpose: 'shopping',
277
282
  * action: 'shopping.purchase',
278
283
  * resource: '/api/checkout/*' },
279
284
  * }
@@ -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-Cuh7ELfr.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
@@ -264,16 +264,21 @@ interface McpMiddlewareOptions extends GatewayConfig {
264
264
  * Per-tool gating for `tools/call` invocations. Tools not listed inherit
265
265
  * the default tier from `mcpRiskTier` (`tools/call` → `'standard'`).
266
266
  *
267
- * 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`**:
268
274
  * ```typescript
269
275
  * toolGates: {
270
- * browse_catalog: 'read-only', // shorthand
271
- * list_products: { minAccessLevel: 'none', // full shape
272
- * purpose: 'shopping',
276
+ * browse_catalog: 'read-only', // shorthand — purpose must come
277
+ * // from a header / _meta, else 400
278
+ * list_products: { purpose: 'shopping', // object form (recommended)
273
279
  * action: 'shopping.search',
274
280
  * resource: '/api/catalog' },
275
- * start_checkout: { minAccessLevel: 'standard',
276
- * purpose: 'shopping',
281
+ * start_checkout: { purpose: 'shopping',
277
282
  * action: 'shopping.purchase',
278
283
  * resource: '/api/checkout/*' },
279
284
  * }
@@ -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.1.0";
43
+ var SDK_VERSION = "3.2.1";
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,38 +539,22 @@ 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
- })
542
+ body: JSON.stringify({ decision, reason })
558
543
  }).catch(() => {
559
544
  });
560
545
  }
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
- })
578
- }).catch(() => {
579
- });
546
+
547
+ // src/adapters/approval-gate.ts
548
+ var APPROVAL_REASON = "Transaction is above the autonomous limit and requires human approval, which is not yet available \u2014 it cannot be completed automatically.";
549
+ function requiresHumanApproval(result) {
550
+ return result.requiresStepUp === true || result.requiresApproval === true;
551
+ }
552
+ function annotateApprovalRequired(result) {
553
+ result.failures = [
554
+ ...result.failures ?? [],
555
+ { dimension: "commerce.intent.approval_required", message: APPROVAL_REASON }
556
+ ];
557
+ result.denialReasons = [APPROVAL_REASON, ...result.denialReasons ?? []];
580
558
  }
581
559
 
582
560
  // src/transport/mcp-server.ts
@@ -791,7 +769,6 @@ function createMcpMiddleware(options) {
791
769
  return next();
792
770
  }
793
771
  req.mcpRequest = parsed;
794
- const wellKnownUrls = config.apiBaseUrl ? await getWellKnownUrls(config.apiBaseUrl).catch(() => void 0) : void 0;
795
772
  const headerRaw = req.headers["x-astra-id"] ?? req.headers["x-astra-agentid"];
796
773
  const headerAstraId = typeof headerRaw === "string" ? headerRaw : Array.isArray(headerRaw) ? headerRaw[0] : void 0;
797
774
  const bodyAstraId = parsed.agentIdFromBody;
@@ -828,7 +805,7 @@ function createMcpMiddleware(options) {
828
805
  return next();
829
806
  }
830
807
  }
831
- const { level: minAccessLevel, source: gateSource } = resolveMinAccessLevel(parsed, {
808
+ const { level: minAccessLevel } = resolveMinAccessLevel(parsed, {
832
809
  toolGates,
833
810
  methodGates
834
811
  });
@@ -864,6 +841,23 @@ function createMcpMiddleware(options) {
864
841
  resolved_action: pdlss.action
865
842
  });
866
843
  }
844
+ if (!pdlss.purpose) {
845
+ const id = req.body?.id ?? null;
846
+ res.status(400).json({
847
+ jsonrpc: "2.0",
848
+ id,
849
+ error: {
850
+ code: -32602,
851
+ message: "PDLSS_PURPOSE_REQUIRED",
852
+ data: {
853
+ dimension: "pdlss.purpose",
854
+ 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.",
855
+ resolvedAction: pdlss.action
856
+ }
857
+ }
858
+ });
859
+ return;
860
+ }
867
861
  const counterpartyUrl = config.counterpartyUrl || `${req.protocol}://${req.get("host")}${req.path}`;
868
862
  const shouldRecordDecisions = recordDecisions !== false;
869
863
  const result = await verify(config, {
@@ -887,7 +881,6 @@ function createMcpMiddleware(options) {
887
881
  });
888
882
  req.agentVerification = result;
889
883
  const sessionId = result.sessionId;
890
- const correlationId = result.correlationId;
891
884
  if (!result.identityVerified || !result.policyAllowed) {
892
885
  if (shouldRecordDecisions && sessionId) {
893
886
  recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
@@ -897,6 +890,16 @@ function createMcpMiddleware(options) {
897
890
  onDenied(result, req, res);
898
891
  return;
899
892
  }
893
+ if (requiresHumanApproval(result)) {
894
+ annotateApprovalRequired(result);
895
+ if (shouldRecordDecisions && sessionId) {
896
+ recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
897
+ });
898
+ }
899
+ dedupeFailures(result);
900
+ onDenied(result, req, res);
901
+ return;
902
+ }
900
903
  if (!shouldEnforce) {
901
904
  if (config.setPassThroughHeader) {
902
905
  res.setHeader("X-Astra-Gateway-Mode", "enforced");
@@ -908,48 +911,6 @@ function createMcpMiddleware(options) {
908
911
  }
909
912
  return next();
910
913
  }
911
- if (!hasMinimumAccess(result.accessLevel, minAccessLevel)) {
912
- const insufficientFailure = {
913
- dimension: "access_level.insufficient",
914
- message: `Tool requires accessLevel '${minAccessLevel}'; agent has '${result.accessLevel}'.`,
915
- 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."
916
- };
917
- result.failures = [...result.failures ?? [], insufficientFailure];
918
- result.denialReasons = [...result.denialReasons ?? [], insufficientFailure.message];
919
- if (!result.guidance && wellKnownUrls) {
920
- result.guidance = {
921
- message: insufficientFailure.message,
922
- registrationUrl: wellKnownUrls.registrationUrl,
923
- documentationUrl: wellKnownUrls.documentationUrl
924
- };
925
- }
926
- if (shouldRecordDecisions) {
927
- const overrideKind = gateSource === "toolGate" ? "toolGate" : gateSource === "methodGate" ? "methodGate" : "other";
928
- const override = {
929
- overriddenBy: overrideKind,
930
- ...parsed.toolName && { toolName: parsed.toolName },
931
- requestedLevel: minAccessLevel,
932
- grantedLevel: result.accessLevel
933
- };
934
- if (sessionId) {
935
- recordDecision(config, sessionId, "denied", result.denialReasons?.[0], override).catch(
936
- () => {
937
- }
938
- );
939
- } else if (correlationId) {
940
- recordAnonymousLocalOverride(
941
- config,
942
- correlationId,
943
- override,
944
- result.denialReasons?.[0]
945
- ).catch(() => {
946
- });
947
- }
948
- }
949
- dedupeFailures(result);
950
- onDenied(result, req, res);
951
- return;
952
- }
953
914
  if (effectiveAstraId) {
954
915
  res.setHeader(
955
916
  MCP_VERIFIED_HOP_HEADER,