@astrasyncai/verification-gateway 5.18.1 → 5.20.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 (129) hide show
  1. package/CHANGELOG.md +232 -0
  2. package/README.md +217 -4
  3. package/dist/adapter-interface/interface.d.mts +2 -2
  4. package/dist/adapter-interface/interface.d.ts +2 -2
  5. package/dist/adapters/express.d.mts +9 -1
  6. package/dist/adapters/express.d.ts +9 -1
  7. package/dist/adapters/express.js +160 -52
  8. package/dist/adapters/express.js.map +1 -1
  9. package/dist/adapters/express.mjs +160 -52
  10. package/dist/adapters/express.mjs.map +1 -1
  11. package/dist/adapters/http-pdlss.d.mts +7 -1
  12. package/dist/adapters/http-pdlss.d.ts +7 -1
  13. package/dist/adapters/http-pdlss.js +18 -2
  14. package/dist/adapters/http-pdlss.js.map +1 -1
  15. package/dist/adapters/http-pdlss.mjs +16 -1
  16. package/dist/adapters/http-pdlss.mjs.map +1 -1
  17. package/dist/adapters/kit-express.d.mts +93 -1
  18. package/dist/adapters/kit-express.d.ts +93 -1
  19. package/dist/adapters/kit-express.js +181 -2
  20. package/dist/adapters/kit-express.js.map +1 -1
  21. package/dist/adapters/kit-express.mjs +180 -2
  22. package/dist/adapters/kit-express.mjs.map +1 -1
  23. package/dist/adapters/mcp.d.mts +102 -9
  24. package/dist/adapters/mcp.d.ts +102 -9
  25. package/dist/adapters/mcp.js +242 -19
  26. package/dist/adapters/mcp.js.map +1 -1
  27. package/dist/adapters/mcp.mjs +240 -19
  28. package/dist/adapters/mcp.mjs.map +1 -1
  29. package/dist/adapters/nextjs.d.mts +1 -1
  30. package/dist/adapters/nextjs.d.ts +1 -1
  31. package/dist/adapters/nextjs.js +97 -14
  32. package/dist/adapters/nextjs.js.map +1 -1
  33. package/dist/adapters/nextjs.mjs +97 -14
  34. package/dist/adapters/nextjs.mjs.map +1 -1
  35. package/dist/adapters/sdk.d.mts +101 -2
  36. package/dist/adapters/sdk.d.ts +101 -2
  37. package/dist/adapters/sdk.js +199 -12
  38. package/dist/adapters/sdk.js.map +1 -1
  39. package/dist/adapters/sdk.mjs +199 -12
  40. package/dist/adapters/sdk.mjs.map +1 -1
  41. package/dist/agent/index.d.mts +1 -1
  42. package/dist/agent/index.d.ts +1 -1
  43. package/dist/agent/index.js +1 -1
  44. package/dist/agent/index.js.map +1 -1
  45. package/dist/agent/index.mjs +1 -1
  46. package/dist/agent/index.mjs.map +1 -1
  47. package/dist/bin/astrasync-claude-hook.js +94 -16
  48. package/dist/bin/astrasync-codex-hook.js +94 -16
  49. package/dist/bin/astrasync-guard.js +94 -16
  50. package/dist/bin/astrasync.js +127 -16
  51. package/dist/browser/background.js +94 -16
  52. package/dist/browser/background.js.map +1 -1
  53. package/dist/browser/background.mjs +94 -16
  54. package/dist/browser/background.mjs.map +1 -1
  55. package/dist/cli/index.js +1 -1
  56. package/dist/cli/index.js.map +1 -1
  57. package/dist/cli/index.mjs +1 -1
  58. package/dist/cli/index.mjs.map +1 -1
  59. package/dist/codex/index.js +94 -16
  60. package/dist/codex/index.js.map +1 -1
  61. package/dist/codex/index.mjs +94 -16
  62. package/dist/codex/index.mjs.map +1 -1
  63. package/dist/cursor/extension.js +94 -16
  64. package/dist/cursor/extension.js.map +1 -1
  65. package/dist/cursor/extension.mjs +94 -16
  66. package/dist/cursor/extension.mjs.map +1 -1
  67. package/dist/edge-config.d.mts +1 -1
  68. package/dist/edge-config.d.ts +1 -1
  69. package/dist/edge-config.js +1 -1
  70. package/dist/edge-config.js.map +1 -1
  71. package/dist/edge-config.mjs +1 -1
  72. package/dist/edge-config.mjs.map +1 -1
  73. package/dist/edge-core/index.d.mts +1 -1
  74. package/dist/edge-core/index.d.ts +1 -1
  75. package/dist/edge-core/index.js +69 -12
  76. package/dist/edge-core/index.js.map +1 -1
  77. package/dist/edge-core/index.mjs +69 -12
  78. package/dist/edge-core/index.mjs.map +1 -1
  79. package/dist/gateway/gateway.d.mts +2 -2
  80. package/dist/gateway/gateway.d.ts +2 -2
  81. package/dist/gateway/gateway.js +94 -16
  82. package/dist/gateway/gateway.js.map +1 -1
  83. package/dist/gateway/gateway.mjs +94 -16
  84. package/dist/gateway/gateway.mjs.map +1 -1
  85. package/dist/git-trigger/git-hooks.d.mts +1 -1
  86. package/dist/git-trigger/git-hooks.d.ts +1 -1
  87. package/dist/index.d.mts +706 -34
  88. package/dist/index.d.ts +706 -34
  89. package/dist/index.js +1391 -205
  90. package/dist/index.js.map +1 -1
  91. package/dist/index.mjs +1374 -195
  92. package/dist/index.mjs.map +1 -1
  93. package/dist/local-evaluator/evaluator.d.mts +2 -2
  94. package/dist/local-evaluator/evaluator.d.ts +2 -2
  95. package/dist/registration/index.d.mts +11 -2
  96. package/dist/registration/index.d.ts +11 -2
  97. package/dist/registration/index.js +1 -1
  98. package/dist/registration/index.js.map +1 -1
  99. package/dist/registration/index.mjs +1 -1
  100. package/dist/registration/index.mjs.map +1 -1
  101. package/dist/transport/index.d.mts +1 -1
  102. package/dist/transport/index.d.ts +1 -1
  103. package/dist/transport/index.js +3 -2
  104. package/dist/transport/index.js.map +1 -1
  105. package/dist/transport/index.mjs +3 -2
  106. package/dist/transport/index.mjs.map +1 -1
  107. package/dist/{types-BRz2U0Pn.d.ts → types-B_HLhZ9n.d.ts} +1 -1
  108. package/dist/{types-BfILnheI.d.mts → types-CU17R4Nr.d.mts} +11 -0
  109. package/dist/{types-BfILnheI.d.ts → types-CU17R4Nr.d.ts} +11 -0
  110. package/dist/{types-Cns4l8gX.d.mts → types-CxjqmPl-.d.mts} +115 -13
  111. package/dist/{types-Cns4l8gX.d.ts → types-CxjqmPl-.d.ts} +115 -13
  112. package/dist/{types-3ObBpsnf.d.mts → types-D37aSQ9c.d.mts} +106 -3
  113. package/dist/{types-CfBpm3w5.d.mts → types-Djpz-L0w.d.mts} +1 -1
  114. package/dist/{types-BoxZEYPS.d.ts → types-Dn8tXnei.d.ts} +106 -3
  115. package/dist/ui/index.d.mts +1 -1
  116. package/dist/ui/index.d.ts +1 -1
  117. package/dist/verify.d.mts +50 -4
  118. package/dist/verify.d.ts +50 -4
  119. package/dist/verify.js +93 -14
  120. package/dist/verify.js.map +1 -1
  121. package/dist/verify.mjs +91 -14
  122. package/dist/verify.mjs.map +1 -1
  123. package/dist/webhooks.d.mts +16 -3
  124. package/dist/webhooks.d.ts +16 -3
  125. package/dist/webhooks.js +24 -13
  126. package/dist/webhooks.js.map +1 -1
  127. package/dist/webhooks.mjs +24 -13
  128. package/dist/webhooks.mjs.map +1 -1
  129. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  import { Request, Response, RequestHandler } from 'express';
2
- import { d as VerificationResult, G as GatewayConfig } from '../types-Cns4l8gX.js';
2
+ import { e as VerificationResult, G as GatewayConfig } from '../types-CxjqmPl-.js';
3
3
 
4
4
  /**
5
5
  * X-Astra-Verified-Hop — cross-hop verify-access dedupe marker.
@@ -378,23 +378,116 @@ interface McpMiddlewareOptions extends GatewayConfig {
378
378
  recordDecisions?: boolean;
379
379
  /** Forward runtime challenge (default `true`). */
380
380
  enableRuntimeChallenge?: boolean;
381
+ /**
382
+ * 5.19.0: how a dashboard tool rule combines with a code `toolGates`
383
+ * entry for the same tool. With `apiKey` + `counterpartyId` set, the
384
+ * middleware fetches the endpoint's tool rules (`{tool, purpose, action,
385
+ * resource?, observe?}`, edited in the AstraSync dashboard; refreshed every
386
+ * `routesRefreshMs`) and turns each into a gate for that tool.
387
+ *
388
+ * - `'code'` (default): a dashboard rule can TIGHTEN a code gate but never
389
+ * loosen it. For a tool gated in code (object form), a dashboard
390
+ * `observe` rule, or one with no purpose, is ignored; a dashboard rule
391
+ * whose purpose or action differs is evaluated IN ADDITION — both must
392
+ * pass. Tools with no code gate (or a code `'observe'`) follow the
393
+ * dashboard rule.
394
+ * - `'dashboard'`: the dashboard rule wins outright for every tool it
395
+ * names (including `observe`).
396
+ *
397
+ * Rules flagged `legacy: true` (path rules stored on an MCP endpoint before
398
+ * tool rules existed) are ignored. The middleware sends its code-gated tool
399
+ * names on the policy fetch (`?codeGates=`) so the dashboard can mark them.
400
+ */
401
+ toolGatePrecedence?: 'code' | 'dashboard';
402
+ /** 5.19.0: refresh interval (ms) for the dashboard tool rules. Default 5 minutes. */
403
+ routesRefreshMs?: number;
404
+ /**
405
+ * 5.20.0: tools and JSON-RPC methods that pass through with NO verify-access
406
+ * call at all — not even the audit evaluation `evaluateAlwaysIfCredentialed`
407
+ * adds for credentialed callers. Applies only while the call is not
408
+ * enforced (an `'observe'` gate, or a handshake/introspection method with
409
+ * no `methodGates` entry); a dashboard rule that enforces a listed tool
410
+ * still verifies.
411
+ *
412
+ * Use it for:
413
+ * - `methods`: handshake and introspection (`initialize`, `tools/list`,
414
+ * `resources/list`, `resources/read`) — nothing to attribute, and a
415
+ * backend round trip slows the host's first card fetch;
416
+ * - `tools`: observe tools whose handler calls verify-access itself, so
417
+ * the gate's evaluation would be a second backend verify for the same
418
+ * call (the MCP bridge measured ~3 backend calls per `verify_agent`).
419
+ *
420
+ * Listed calls leave `req.agentVerification` unset.
421
+ */
422
+ skipEvaluation?: {
423
+ tools?: readonly string[];
424
+ methods?: readonly string[];
425
+ };
381
426
  /**
382
427
  * Posture when the MCP middleware itself throws an internal error.
383
- * Default `'open'` for backward compatibility in SDK 2.4.13. Shadow logs
384
- * record what WOULD be denied with `failOnError: 'closed'` so merchants
385
- * can grep correlationId for impact analysis during the observation
386
- * window. Default flips to `'closed'` in a follow-up release.
387
428
  *
388
- * 5.12.0: `'closed'` answers the retryable verification-unavailable
389
- * JSON-RPC error (HTTP 503 + Retry-After, code -32003) — never an
390
- * identity-shaped 401.
429
+ * 5.19.0 (behaviour change): default `'closed'` — an internal throw answers
430
+ * the retryable verification-unavailable JSON-RPC error (HTTP 503 +
431
+ * Retry-After, code -32003; never an identity-shaped 401) and the tool
432
+ * handler is not called. `'open'` (pass the call through unverified) is
433
+ * an explicit opt-in. The `[SHADOW] would-have-denied` log line is still
434
+ * written on every throw, with the correlationId.
391
435
  */
392
436
  failOnError?: 'open' | 'closed';
393
437
  }
438
+ /** 5.19.0: a dashboard tool rule as the MCP middleware evaluates it. */
439
+ interface DashboardToolRule {
440
+ tool: string;
441
+ purpose?: string;
442
+ action?: string;
443
+ resource?: string;
444
+ observe?: boolean;
445
+ }
446
+ /** The usable tool rules in a fetched policy (tool key set, not legacy), by tool name. */
447
+ declare function dashboardToolRules(routes: ReadonlyArray<{
448
+ tool?: string;
449
+ legacy?: boolean;
450
+ purpose?: string;
451
+ action?: string;
452
+ resource?: string;
453
+ observe?: boolean;
454
+ }>): Map<string, DashboardToolRule>;
455
+ type GateSource = 'toolGate' | 'methodGate' | 'dashboard' | 'tier' | 'discovery_default';
456
+ /**
457
+ * Resolve whether a parsed MCP request is enforced, and the gate config to
458
+ * apply. SDK 5.0.0 removed the access-level band — a gate's only decision is
459
+ * enforce vs observe.
460
+ *
461
+ * Precedence:
462
+ * 1. Non-tools/call without `methodGates` entry → pass through
463
+ * (`discovery_default`). Discovery is open — agents must list tools
464
+ * before invoking them.
465
+ * 2. `methodGates[method]` — per-method override (`'observe'` or a config)
466
+ * 3. tools/call: the code `toolGates[toolName]` and the dashboard tool rule
467
+ * (5.19.0) — see `toolGatePrecedence`. With the default `'code'`, a
468
+ * dashboard rule only tightens a code gate: it is ignored when it is
469
+ * `observe` or has no purpose, and evaluated as an `additional` gate
470
+ * (both must pass) when its purpose/action differ.
471
+ * 4. `mcpDefaultEnforced` — built-in default (tools/call → enforced)
472
+ *
473
+ * The `source` field is logged for forensics / activity-feed attribution.
474
+ */
475
+ declare function resolveGate(parsed: ParsedMcpRequest, opts: {
476
+ toolGates?: Record<string, ToolGate>;
477
+ methodGates?: Record<string, ToolGate>;
478
+ dashboardRules?: Map<string, DashboardToolRule>;
479
+ toolGatePrecedence?: 'code' | 'dashboard';
480
+ }): {
481
+ enforce: boolean;
482
+ config?: ToolGateConfig;
483
+ /** 5.19.0: a dashboard gate evaluated in addition to `config` (both must pass). */
484
+ additional?: ToolGateConfig;
485
+ source: GateSource;
486
+ };
394
487
  /**
395
488
  * Create the MCP middleware. Attach AFTER `express.json()` — the body must
396
489
  * already be a parsed object.
397
490
  */
398
491
  declare function createMcpMiddleware(options: McpMiddlewareOptions): RequestHandler;
399
492
 
400
- export { MCP_VERIFIED_HOP_HEADER, type McpMiddlewareOptions, type ParsedMcpRequest, type ToolGate, type ToolGateConfig, createMcpMiddleware, isVerifiedHopValidFor, mcpDefaultEnforced, mcpToPdlss, parseMcpJsonRpc, parseVerifiedHop, serializeVerifiedHop };
493
+ export { type DashboardToolRule, MCP_VERIFIED_HOP_HEADER, type McpMiddlewareOptions, type ParsedMcpRequest, type ToolGate, type ToolGateConfig, createMcpMiddleware, dashboardToolRules, isVerifiedHopValidFor, mcpDefaultEnforced, mcpToPdlss, parseMcpJsonRpc, parseVerifiedHop, resolveGate, serializeVerifiedHop };
@@ -22,17 +22,19 @@ var mcp_exports = {};
22
22
  __export(mcp_exports, {
23
23
  MCP_VERIFIED_HOP_HEADER: () => MCP_VERIFIED_HOP_HEADER,
24
24
  createMcpMiddleware: () => createMcpMiddleware,
25
+ dashboardToolRules: () => dashboardToolRules,
25
26
  isVerifiedHopValidFor: () => isVerifiedHopValidFor,
26
27
  mcpDefaultEnforced: () => mcpDefaultEnforced,
27
28
  mcpToPdlss: () => mcpToPdlss,
28
29
  parseMcpJsonRpc: () => parseMcpJsonRpc,
29
30
  parseVerifiedHop: () => parseVerifiedHop,
31
+ resolveGate: () => resolveGate,
30
32
  serializeVerifiedHop: () => serializeVerifiedHop
31
33
  });
32
34
  module.exports = __toCommonJS(mcp_exports);
33
35
 
34
36
  // src/version.ts
35
- var SDK_VERSION = "5.18.1";
37
+ var SDK_VERSION = "5.20.0";
36
38
 
37
39
  // src/http.ts
38
40
  var SDK_USER_AGENT = `astrasync-sdk/${SDK_VERSION}`;
@@ -274,7 +276,12 @@ function getCacheKey(request, counterpartyId) {
274
276
  // its start_checkout quote was served the cached quote-leg grant (no
275
277
  // settlementOutcome) and never reached the backend. Same F-A1-07 bug class.
276
278
  request.commercePhase || "",
277
- request.checkoutSessionId || ""
279
+ request.checkoutSessionId || "",
280
+ // 5.19.0: a route_config purpose claim is checked server-side against
281
+ // the stored rule for this method + path, so it changes the verdict.
282
+ request.purposeSource || "",
283
+ request.requestMethod || "",
284
+ request.requestPath || ""
278
285
  ].join("|");
279
286
  }
280
287
  function getCachedResult(request, counterpartyId) {
@@ -466,6 +473,9 @@ async function callVerifyAccessAPI(config, request) {
466
473
  if (requestData.enableRuntimeChallenge)
467
474
  body.enableRuntimeChallenge = requestData.enableRuntimeChallenge;
468
475
  if (requestData.routePolicy) body.routePolicy = requestData.routePolicy;
476
+ if (config.actingProof?.connectorToken) {
477
+ body.actingProof = { connectorToken: config.actingProof.connectorToken };
478
+ }
469
479
  if (requestData.approvalHeldByRoute) body.approvalHeldByRoute = true;
470
480
  if (requestData.createSession) body.createSession = requestData.createSession;
471
481
  if (requestData.durationRequired) body.durationRequired = requestData.durationRequired;
@@ -482,6 +492,11 @@ async function callVerifyAccessAPI(config, request) {
482
492
  if (requestData.buyerEmail) body.buyerEmail = requestData.buyerEmail;
483
493
  if (requestData.commerceArtifacts) body.commerceArtifacts = requestData.commerceArtifacts;
484
494
  if (requestData.attemptId) body.attemptId = requestData.attemptId;
495
+ if (requestData.purposeSource) body.purposeSource = requestData.purposeSource;
496
+ if (requestData.agentArtifacts) body.agentArtifacts = requestData.agentArtifacts;
497
+ if (requestData.metadata) body.metadata = requestData.metadata;
498
+ if (requestData.requestMethod) body.requestMethod = requestData.requestMethod;
499
+ if (requestData.requestPath) body.requestPath = requestData.requestPath;
485
500
  if (requestData.considerationSet) body.considerationSet = requestData.considerationSet;
486
501
  body.sdkVersion = SDK_VERSION;
487
502
  if (requestData.callerMetadata || requestData.clientIp || requestData.userAgent) {
@@ -628,22 +643,46 @@ async function verify(config, request, options) {
628
643
  "[VerificationGateway] minTrustScore / minTrustScoreForFull are deprecated in v2.3.0 and have no effect. Server is now the single source of truth for access decisions (verified ID + runtime challenge + PDLSS + trust score). To gate access to an endpoint, configure the endpoint's trust_score_requirement server-side."
629
644
  );
630
645
  }
631
- const uncacheable = Boolean(request.commerceArtifacts);
646
+ const uncacheable = Boolean(request.commerceArtifacts) || Boolean(mergedConfig.actingProof);
632
647
  if (mergedConfig.cacheTtl !== 0 && !options?.bypassCache && !uncacheable) {
633
648
  const cached = getCachedResult(request, mergedConfig.counterpartyId);
634
649
  if (cached) {
635
650
  if (mergedConfig.debug) {
636
651
  console.log("[VerificationGateway] Returning cached result");
637
652
  }
638
- const hit = { ...cached };
639
- if (request.attemptId) {
640
- hit.attemptId = request.attemptId;
641
- } else {
642
- delete hit.attemptId;
643
- }
644
- return hit;
653
+ return sharedVerdict(cached, request);
654
+ }
655
+ }
656
+ if (mergedConfig.cacheTtl !== 0 && !options?.bypassCache && !uncacheable) {
657
+ const key = getCacheKey(request, mergedConfig.counterpartyId);
658
+ const pending = inflightVerifies.get(key);
659
+ if (pending) return sharedVerdict(await pending, request);
660
+ const flight = verifyViaApi(mergedConfig, request, urls, uncacheable);
661
+ inflightVerifies.set(key, flight);
662
+ try {
663
+ return await flight;
664
+ } finally {
665
+ inflightVerifies.delete(key);
645
666
  }
646
667
  }
668
+ return verifyViaApi(mergedConfig, request, urls, uncacheable);
669
+ }
670
+ var inflightVerifies = /* @__PURE__ */ new Map();
671
+ var sharedVerdicts = /* @__PURE__ */ new WeakSet();
672
+ function isSharedVerdict(result) {
673
+ return sharedVerdicts.has(result);
674
+ }
675
+ function sharedVerdict(source, request) {
676
+ const hit = { ...source };
677
+ if (request.attemptId) {
678
+ hit.attemptId = request.attemptId;
679
+ } else {
680
+ delete hit.attemptId;
681
+ }
682
+ sharedVerdicts.add(hit);
683
+ return hit;
684
+ }
685
+ async function verifyViaApi(mergedConfig, request, urls, uncacheable) {
647
686
  const enrichedRequest = { ...request };
648
687
  if (!enrichedRequest.counterpartyUrl && mergedConfig.counterpartyUrl) {
649
688
  enrichedRequest.counterpartyUrl = mergedConfig.counterpartyUrl;
@@ -671,9 +710,18 @@ async function verify(config, request, options) {
671
710
  if (!apiResponse.access?.allowed) {
672
711
  const aggregatedFailures = apiResponse.access?.failures;
673
712
  const idVerifiedFromBackend = apiResponse.verificationContext?.idVerified === true && !aggregatedFailures?.some((f) => REJECTED_IDENTITY_DIMENSIONS.has(f.dimension));
713
+ const denyAgent = idVerifiedFromBackend && apiResponse.agent ? {
714
+ astraId: apiResponse.agent.astraId,
715
+ name: apiResponse.agent.name,
716
+ trustScore: apiResponse.agent.trustScore,
717
+ trustLevel: getTrustLevel(apiResponse.agent.trustScore),
718
+ blockchainAnchored: apiResponse.agent.blockchainStatus === "anchored",
719
+ status: apiResponse.agent.agentStatus
720
+ } : void 0;
674
721
  const result2 = {
675
722
  identityVerified: idVerifiedFromBackend,
676
723
  policyAllowed: false,
724
+ ...denyAgent && { agent: denyAgent },
677
725
  // 3.6.0: the server now emits top-level denialReasons on every deny
678
726
  // path — prefer it verbatim; the failure-message reconstruction remains
679
727
  // as the fallback for older backends.
@@ -713,7 +761,9 @@ async function verify(config, request, options) {
713
761
  fulfilment: apiResponse.fulfilment,
714
762
  mandates: apiResponse.mandates,
715
763
  kyaToken: apiResponse.kyaToken,
716
- aep: apiResponse.aep
764
+ aep: apiResponse.aep,
765
+ // 5.20.0 (round 29.1): non-fatal integration warnings (e.g. events_url_missing).
766
+ warnings: apiResponse.warnings
717
767
  };
718
768
  return result2;
719
769
  }
@@ -781,7 +831,9 @@ async function verify(config, request, options) {
781
831
  // 5.15.0: merchant-lane AP2 mandates + KYAPay identity token.
782
832
  mandates: apiResponse.mandates,
783
833
  kyaToken: apiResponse.kyaToken,
784
- aep: apiResponse.aep
834
+ aep: apiResponse.aep,
835
+ // 5.20.0 (round 29.1): non-fatal integration warnings (e.g. events_url_missing).
836
+ warnings: apiResponse.warnings
785
837
  };
786
838
  if (result.recommendation === "deny") {
787
839
  result.policyAllowed = false;
@@ -807,8 +859,18 @@ async function verify(config, request, options) {
807
859
  cacheResult(request, result, mergedConfig.cacheTtl, mergedConfig.counterpartyId);
808
860
  }
809
861
  }
862
+ notifyVerificationListeners(request, result);
810
863
  return result;
811
864
  }
865
+ var verificationListeners = /* @__PURE__ */ new Set();
866
+ function notifyVerificationListeners(request, result) {
867
+ for (const listener of verificationListeners) {
868
+ try {
869
+ listener(result, request);
870
+ } catch {
871
+ }
872
+ }
873
+ }
812
874
  async function recordDecision(config, sessionId, decision, reason) {
813
875
  const headers = { "Content-Type": "application/json" };
814
876
  if (config.apiKey) {
@@ -822,6 +884,42 @@ async function recordDecision(config, sessionId, decision, reason) {
822
884
  }).catch(() => {
823
885
  });
824
886
  }
887
+ var refusedPolicyReads = /* @__PURE__ */ new Set();
888
+ async function fetchRoutes(config, counterpartyId, options = {}) {
889
+ if (!counterpartyId) return null;
890
+ const headers = { "Content-Type": "application/json" };
891
+ if (config.apiKey) {
892
+ headers["Authorization"] = `Bearer ${config.apiKey}`;
893
+ headers["X-API-Key"] = config.apiKey;
894
+ }
895
+ try {
896
+ headers["X-AstraSync-SDK-Version"] = SDK_VERSION;
897
+ const response = await sdkFetch(
898
+ `${config.apiBaseUrl}/endpoints/${encodeURIComponent(counterpartyId)}/routes` + (options.codeGates?.length ? `?codeGates=${options.codeGates.map(encodeURIComponent).join(",")}` : ""),
899
+ { method: "GET", headers }
900
+ );
901
+ if (!response.ok) {
902
+ if ((response.status === 401 || response.status === 403 || response.status === 404) && !refusedPolicyReads.has(counterpartyId)) {
903
+ refusedPolicyReads.add(counterpartyId);
904
+ console.error(
905
+ `[VerificationGateway] the configured API key cannot read the route policy for ${counterpartyId} (HTTP ${response.status}). The key belongs to an account that does not own this endpoint, or the counterpartyId is wrong. The gateway is in PASS-THROUGH mode: no agent is being verified.`
906
+ );
907
+ }
908
+ return null;
909
+ }
910
+ const body = await response.json();
911
+ const type = body.data?.counterpartyType;
912
+ if (typeof type === "string" && options.onEndpointType) {
913
+ try {
914
+ options.onEndpointType(type);
915
+ } catch {
916
+ }
917
+ }
918
+ return body.data?.routes ?? [];
919
+ } catch {
920
+ return null;
921
+ }
922
+ }
825
923
 
826
924
  // src/metadata-capture.ts
827
925
  var CAPTURE_SCHEMA_VERSION = 1;
@@ -1144,6 +1242,42 @@ async function awaitStepUpApproval(config, approval) {
1144
1242
  return "timeout";
1145
1243
  }
1146
1244
 
1245
+ // src/adapters/routes-cache.ts
1246
+ var DEFAULT_ROUTES_REFRESH_MS = 5 * 60 * 1e3;
1247
+ function createRoutesCache(config, opts = {}) {
1248
+ const refreshMs = opts.refreshMs ?? DEFAULT_ROUTES_REFRESH_MS;
1249
+ let cached = [];
1250
+ let lastFetchAt = 0;
1251
+ let refreshing = null;
1252
+ async function refresh() {
1253
+ if (!config.counterpartyId) {
1254
+ opts.onNoCounterparty?.();
1255
+ return;
1256
+ }
1257
+ const fetched = await fetchRoutes(config, config.counterpartyId, opts.fetchOptions);
1258
+ if (fetched) {
1259
+ cached = fetched;
1260
+ lastFetchAt = Date.now();
1261
+ opts.onFetched?.(fetched);
1262
+ }
1263
+ }
1264
+ function start() {
1265
+ refreshing = refresh().catch(() => void 0).finally(() => {
1266
+ refreshing = null;
1267
+ });
1268
+ }
1269
+ start();
1270
+ return {
1271
+ async current() {
1272
+ if (refreshing) await refreshing.catch(() => {
1273
+ });
1274
+ if (config.counterpartyId && Date.now() - lastFetchAt > refreshMs) start();
1275
+ return cached;
1276
+ },
1277
+ peek: () => cached
1278
+ };
1279
+ }
1280
+
1147
1281
  // src/verified-hop.ts
1148
1282
  var MCP_VERIFIED_HOP_HEADER = "X-Astra-Verified-Hop";
1149
1283
  var MCP_VERIFIED_HOP_MAX_AGE_MS = 6e4;
@@ -1364,6 +1498,21 @@ function defaultMcpDenied(result, req, res) {
1364
1498
  }
1365
1499
  });
1366
1500
  }
1501
+ function dashboardToolRules(routes) {
1502
+ const out = /* @__PURE__ */ new Map();
1503
+ for (const r of routes) {
1504
+ if (typeof r.tool !== "string" || !r.tool || r.legacy === true) continue;
1505
+ if (out.has(r.tool)) continue;
1506
+ out.set(r.tool, {
1507
+ tool: r.tool,
1508
+ ...r.purpose && { purpose: r.purpose },
1509
+ ...r.action && { action: r.action },
1510
+ ...r.resource && { resource: r.resource },
1511
+ ...r.observe === true && { observe: true }
1512
+ });
1513
+ }
1514
+ return out;
1515
+ }
1367
1516
  function resolveGate(parsed, opts) {
1368
1517
  if (!parsed.toolName) {
1369
1518
  const methodGate = opts.methodGates?.[parsed.method];
@@ -1373,8 +1522,30 @@ function resolveGate(parsed, opts) {
1373
1522
  return { enforce: false, source: "discovery_default" };
1374
1523
  }
1375
1524
  const toolGate = opts.toolGates?.[parsed.toolName];
1525
+ const rule = opts.dashboardRules?.get(parsed.toolName);
1526
+ const fromRule = (r) => r.observe ? { enforce: false, source: "dashboard" } : {
1527
+ enforce: true,
1528
+ config: {
1529
+ ...r.purpose && { purpose: r.purpose },
1530
+ ...r.action && { action: r.action },
1531
+ ...r.resource && { resource: r.resource }
1532
+ },
1533
+ source: "dashboard"
1534
+ };
1535
+ const codeGated = toolGate !== void 0 && toolGate !== "observe";
1536
+ if (rule && (opts.toolGatePrecedence === "dashboard" || !codeGated)) return fromRule(rule);
1376
1537
  if (toolGate !== void 0) {
1377
- return toolGate === "observe" ? { enforce: false, source: "toolGate" } : { enforce: true, config: toolGate, source: "toolGate" };
1538
+ if (toolGate === "observe") return { enforce: false, source: "toolGate" };
1539
+ if (rule && !rule.observe && rule.purpose && (rule.purpose !== toolGate.purpose || rule.action && rule.action !== toolGate.action)) {
1540
+ const extra = fromRule(rule).config;
1541
+ return {
1542
+ enforce: true,
1543
+ config: toolGate,
1544
+ additional: { ...extra, ...!extra.resource && toolGate.resource && { resource: toolGate.resource } },
1545
+ source: "toolGate"
1546
+ };
1547
+ }
1548
+ return { enforce: true, config: toolGate, source: "toolGate" };
1378
1549
  }
1379
1550
  return { enforce: mcpDefaultEnforced(parsed), source: "tier" };
1380
1551
  }
@@ -1389,13 +1560,34 @@ function createMcpMiddleware(options) {
1389
1560
  verifiedHopMaxAgeMs,
1390
1561
  recordDecisions,
1391
1562
  enableRuntimeChallenge = true,
1392
- failOnError = "open",
1563
+ failOnError = "closed",
1564
+ toolGatePrecedence = "code",
1565
+ routesRefreshMs,
1566
+ skipEvaluation,
1393
1567
  ...config
1394
1568
  } = options;
1569
+ const skipTools = new Set(skipEvaluation?.tools ?? []);
1570
+ const skipMethods = new Set(skipEvaluation?.methods ?? []);
1395
1571
  if (config.apiBaseUrl) {
1396
1572
  prefetchWellKnown(config.apiBaseUrl).catch(() => {
1397
1573
  });
1398
1574
  }
1575
+ const codeGatedTools = Object.entries(toolGates ?? {}).filter(([, g]) => g !== "observe").map(([name]) => name);
1576
+ let warnedEndpointType = false;
1577
+ const onEndpointType = (counterpartyType) => {
1578
+ if (counterpartyType === "mcp_server" || warnedEndpointType) return;
1579
+ warnedEndpointType = true;
1580
+ console.warn(
1581
+ `[VerificationGateway/MCP] counterpartyId ${config.counterpartyId} is a '${counterpartyType}' endpoint, not an 'mcp_server' endpoint. Dashboard tool rules are read from an mcp_server endpoint only: register a separate MCP endpoint for this server (same owner) and use its id here. See https://astrasync.ai/docs/mcp-integration#tool-rules.`
1582
+ );
1583
+ };
1584
+ const rulesCache = config.apiKey && config.counterpartyId ? createRoutesCache(config, {
1585
+ ...routesRefreshMs !== void 0 && { refreshMs: routesRefreshMs },
1586
+ fetchOptions: {
1587
+ ...codeGatedTools.length && { codeGates: codeGatedTools },
1588
+ onEndpointType
1589
+ }
1590
+ }) : void 0;
1399
1591
  return async (req, res, next) => {
1400
1592
  try {
1401
1593
  if (skip) return next();
@@ -1444,10 +1636,19 @@ function createMcpMiddleware(options) {
1444
1636
  return next();
1445
1637
  }
1446
1638
  }
1639
+ if (!parsed.toolName && skipMethods.has(parsed.method) && methodGates?.[parsed.method] === void 0) {
1640
+ return next();
1641
+ }
1642
+ const dashboardRules = rulesCache ? dashboardToolRules(await rulesCache.current()) : void 0;
1447
1643
  const gateDecision = resolveGate(parsed, {
1448
1644
  toolGates,
1449
- methodGates
1645
+ methodGates,
1646
+ dashboardRules,
1647
+ toolGatePrecedence
1450
1648
  });
1649
+ if (!gateDecision.enforce && (parsed.toolName && skipTools.has(parsed.toolName) || !parsed.toolName && skipMethods.has(parsed.method))) {
1650
+ return next();
1651
+ }
1451
1652
  const credentials = extractCredentials(
1452
1653
  req.headers,
1453
1654
  req.query
@@ -1504,8 +1705,14 @@ function createMcpMiddleware(options) {
1504
1705
  const forwardedFor = req.headers["x-forwarded-for"];
1505
1706
  const forwardedForStr = Array.isArray(forwardedFor) ? forwardedFor.join(", ") : forwardedFor;
1506
1707
  const sourceIp = req.ip ?? (forwardedForStr ? forwardedForStr.split(",")[0].trim() : void 0);
1708
+ const attemptRaw = readSingleHeader(req.headers["x-astra-attempt-id"]);
1709
+ const metaAttempt = req.body?.params?._meta?.astrasync?.attemptId;
1710
+ const attemptId = [attemptRaw, metaAttempt].find(
1711
+ (v) => typeof v === "string" && /^att_[0-9a-f]{32}$/.test(v)
1712
+ );
1507
1713
  const verificationRequest = {
1508
1714
  credentials,
1715
+ ...attemptId && { attemptId },
1509
1716
  purpose: pdlss.purpose,
1510
1717
  action: pdlss.action,
1511
1718
  resource: pdlss.resource,
@@ -1544,6 +1751,20 @@ function createMcpMiddleware(options) {
1544
1751
  result.stepUpOutcome = outcome;
1545
1752
  }
1546
1753
  }
1754
+ if (shouldEnforce && gateDecision.additional?.purpose && result.policyAllowed && !requiresHumanApproval(result)) {
1755
+ const extra = mcpToPdlss(parsed, req.path, void 0, void 0, gateDecision.additional);
1756
+ const extraResult = await verify(config, {
1757
+ ...verificationRequest,
1758
+ purpose: extra.purpose,
1759
+ action: extra.action,
1760
+ resource: extra.resource
1761
+ });
1762
+ if (!extraResult.policyAllowed || requiresHumanApproval(extraResult)) {
1763
+ result = extraResult;
1764
+ req.agentVerification = result;
1765
+ sessionId = result.sessionId;
1766
+ }
1767
+ }
1547
1768
  if (!result.policyAllowed && isVerificationUnavailable(result) && (!shouldEnforce || isReadOnlyMcpMethod(parsed) && !credentials.astraId && !credentials.jwt)) {
1548
1769
  res.setHeader(VERIFICATION_STATUS_HEADER, "unavailable");
1549
1770
  return next();
@@ -1553,14 +1774,14 @@ function createMcpMiddleware(options) {
1553
1774
  res.setHeader("X-Astra-Gateway-Mode", "enforced");
1554
1775
  res.setHeader("X-Astra-Gateway-Reason", "evaluated-not-enforced");
1555
1776
  }
1556
- if (shouldRecordDecisions && sessionId) {
1777
+ if (shouldRecordDecisions && sessionId && !isSharedVerdict(result)) {
1557
1778
  recordDecision(config, sessionId, "granted").catch(() => {
1558
1779
  });
1559
1780
  }
1560
1781
  return next();
1561
1782
  }
1562
1783
  if (!result.policyAllowed) {
1563
- if (shouldRecordDecisions && sessionId) {
1784
+ if (shouldRecordDecisions && sessionId && !isSharedVerdict(result)) {
1564
1785
  recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
1565
1786
  });
1566
1787
  }
@@ -1570,7 +1791,7 @@ function createMcpMiddleware(options) {
1570
1791
  }
1571
1792
  if (requiresHumanApproval(result)) {
1572
1793
  annotateApprovalRequired(result);
1573
- if (shouldRecordDecisions && sessionId) {
1794
+ if (shouldRecordDecisions && sessionId && !isSharedVerdict(result)) {
1574
1795
  recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
1575
1796
  });
1576
1797
  }
@@ -1588,7 +1809,7 @@ function createMcpMiddleware(options) {
1588
1809
  })
1589
1810
  );
1590
1811
  }
1591
- if (shouldRecordDecisions && sessionId) {
1812
+ if (shouldRecordDecisions && sessionId && !isSharedVerdict(result)) {
1592
1813
  recordDecision(config, sessionId, "granted").catch(() => {
1593
1814
  });
1594
1815
  }
@@ -1629,11 +1850,13 @@ function createMcpMiddleware(options) {
1629
1850
  0 && (module.exports = {
1630
1851
  MCP_VERIFIED_HOP_HEADER,
1631
1852
  createMcpMiddleware,
1853
+ dashboardToolRules,
1632
1854
  isVerifiedHopValidFor,
1633
1855
  mcpDefaultEnforced,
1634
1856
  mcpToPdlss,
1635
1857
  parseMcpJsonRpc,
1636
1858
  parseVerifiedHop,
1859
+ resolveGate,
1637
1860
  serializeVerifiedHop
1638
1861
  });
1639
1862
  //# sourceMappingURL=mcp.js.map