npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@astrasyncai/verification-gateway

v5.16.0

Published

AstraSync KYA Platform SDK — counterparty verification gateway (verify incoming requests) + agent registration (register AI agents with the KYA backend).

Readme

@astrasyncai/verification-gateway

The AstraSync KYA Platform SDK. One package, two roles: register agents with the KYA backend, and verify agents at any counterparty type (API / MCP / website / agent-to-agent).

What's in this package?

As of v2.4.0 this is the only npm package you need for AstraSync integration. Pick the subpath that matches what you're doing:

| If you're… | Import from | Get | | ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | Registering an agent with the KYA backend | @astrasyncai/verification-gateway/registration | AstraSync class — register(), verify(), health(). The astrasync CLI is bundled too. | | Building an agent that calls out to other services (per-request credential injection) | @astrasyncai/verification-gateway/agent | AgentClient, ChallengeHandler, ownership validation, runtime PDLSS shape. | | Running an Express API that AI agents call into | @astrasyncai/verification-gateway/express | createMiddleware() — protect routes, attach verification context. | | Running a Next.js app that AI agents call into | @astrasyncai/verification-gateway/nextjs | createMiddleware(), verification-interstitial wiring. | | Running an MCP server that AI agents call into | @astrasyncai/verification-gateway/mcp | MCP tool gates + inner-hop dedupe headers. | | Doing direct verification (no framework) | @astrasyncai/verification-gateway/sdk | VerificationGatewayClient with retry / backoff / timeout. | | Parsing protocol-level credentials (HTTP / A2A / MCP / x402 / AP2 / MPP) | @astrasyncai/verification-gateway/transport | Cross-protocol credential extraction + injection. | | Evaluating PDLSS offline (local / hybrid mode) | @astrasyncai/verification-gateway/gateway | AstraSyncGateway (online / local / hybrid). | | Governing Claude Code with a local policy | @astrasyncai/verification-gateway/claude-code | PreToolUse-hook adapter; astrasync-guard install-claude-code sets it up. | | Receiving webhook callbacks | @astrasyncai/verification-gateway/webhooks | verifyAstraSyncWebhook(), signAstraSyncWebhook(). | | Building on the Trusted Agent Gateway edge machinery | .../edge-config, .../platform-signatures, .../metadata-capture | Dashboard-managed edge policy, platform fingerprint registry, shared header sanitiser — see Metadata capture. |

All subpaths talk to the same backend — POST /agents/verify-access for verification, POST /agents/register for registration. The contract is shared; the role is just which side of the handshake you're sitting on.

Before v2.4.0 the agent-registration half shipped as a separate package (@astrasyncai/agent-registration, briefly; @astrasyncai/sdk before that). It was merged in to stop the two-package version drift and to give partners one install instead of two. The legacy @astrasyncai/sdk package on npm is deprecated with a pointer to this package; @astrasyncai/agent-registration was never published.

Overview

The Verification Gateway provides a single, universal solution for verifying AI agents. One codebase, multiple deployment targets:

  • Express.js middleware - Protect API endpoints
  • Next.js middleware - Protect web applications with the verification interstitial
  • SDK functions - Direct verification for agent-to-agent or serverless

All verification flows through the same POST /agents/verify-access endpoint, ensuring consistent PDLSS (Permission, Duration, Limit, Scope, Self-instantiation) enforcement.

Installation

npm install @astrasyncai/verification-gateway

Quick Start

Express Middleware

import express from 'express';
import { createMiddleware } from '@astrasyncai/verification-gateway/express';

const app = express();

// v2.3.7+ — per-route policy lives in the AstraSync dashboard.
// The SDK fetches it on init via counterpartyId; do NOT pass routes here.
app.use(
  createMiddleware({
    apiBaseUrl: 'https://astrasync.ai/api',
    apiKey: process.env.ASTRASYNC_API_KEY,
    counterpartyId: 'ASTRAE-...', // your endpoint id from the dashboard
    setPassThroughHeader: true, // recommended for local-dev / staging
    // — surfaces pass-through mode when
    // no per-route policy is configured
  })
);

Next.js Middleware

// middleware.ts
import { createMiddleware } from '@astrasyncai/verification-gateway/nextjs';

// v2.3.7+ — per-route policy lives in the AstraSync dashboard.
// The SDK fetches it on init via counterpartyId; do NOT pass routes here.
export const middleware = createMiddleware({
  apiBaseUrl: 'https://api.astrasync.ai',
  apiKey: process.env.ASTRASYNC_API_KEY,
  counterpartyId: 'ASTRAE-...',
  showInterstitial: true,
});

export const config = {
  matcher: ['/api/:path*', '/dashboard/:path*'],
};

SDK (Direct Usage)

import { createClient } from '@astrasyncai/verification-gateway/sdk';

const gateway = createClient({
  apiBaseUrl: 'https://api.astrasync.ai',
});

// Verify another agent before interacting
const result = await gateway.verify({
  astraId: 'ASTRA-abc123',
  purpose: 'data-exchange',
});

// 5.0.0: the accessLevel band is gone — the decision is the two axes.
if (result.identityVerified && result.policyAllowed) {
  // Safe to interact with this agent
  console.log(`Trust score: ${result.agent?.trustScore}`);
}

Agent Registration

Use the SDK for all agent registration, whether you're a developer firing off a one-shot CLI call or an autonomous agent self-registering on first run. The SDK handles auth-mode routing for you: signature-authenticated callers get synchronous registration; API-key-only callers get an owner-approval handshake (server-side rule, not negotiable).

apiEndpoint — runtime-challenge URL

apiEndpoint is the URL where your agent's verification-gateway SDK is mounted to receive runtime challenges from counterparties. Optional but recommended — if you omit it, your agent declares no runtime-challenge support, which is included in the verification payload and may cause some counterparties to decline access requests.

Two registration response modes

The same register() call returns one of two shapes depending on auth context:

  • 201 active — synchronous: returned when the request is signed with a crypto keypair (privateKey configured) or when authenticated via email+password. Result: { status: 'active', agent }.
  • 202 pending_approval — API-key only: the SDK was authenticated with an API key but the request was not signed. The backend emails the account owner a "Sign In to Accept" link, fires a dashboard alert, and returns a tracking token. Result: { status: 'pending_approval', requestId, pollUrl, expiresAt }. The agent becomes active only after the owner approves.

This split enforces the platform rule that API-key registrations always require owner step-up — registering an agent autonomously with just an API key is not a sufficient authority signal on its own.

Pattern A — developer / long-running agent (block until approved)

Best for CLI tools, dev-machine first-run, and long-running services:

import {
  AstraSync,
  RegistrationDeniedError,
  RegistrationTimeoutError,
} from '@astrasyncai/verification-gateway/registration';

const sdk = new AstraSync({
  apiKey: process.env.ASTRASYNC_API_KEY,
  privateKey: process.env.ASTRASYNC_PRIVATE_KEY, // optional — when set, 201 sync path
});

try {
  const agent = await sdk.register({
    name: 'invoice-bot',
    description: 'Reconciles AP/AR across accounting systems.',
    agentType: 'autonomous',
    apiEndpoint: 'https://invoice-bot.example.com', // runtime-challenge URL
    model: { modelProvider: 'anthropic', modelName: 'claude-sonnet-4-5' },
    framework: { frameworkName: 'langchain', frameworkVersion: '0.3.0' },
    protocols: ['a2a', 'mcp'],
    pdlss: {
      purpose: {
        categories: ['accounting'],
        allowedActions: ['accounting.read', 'accounting.write'],
      },
      limits: { autonomousThreshold: 1_000, approvalThreshold: 10_000, currency: 'USD' },
      scope: { resources: ['xero.invoices', 'quickbooks.bills'] },
      selfInstantiation: { allowed: false },
    },
    // Blocking mode: poll until the request resolves.
    waitForApproval: true,
    timeoutMs: 10 * 60 * 1000, // 10 minutes default
    onPending: ({ ageMs }) =>
      console.log(`Awaiting owner approval (${(ageMs / 1000).toFixed(0)}s)…`),
  });
  console.log(`Agent registered: ${agent.astrasyncIdLevel1}`);
} catch (err) {
  if (err instanceof RegistrationDeniedError) {
    console.error('Owner denied:', err.reason);
  } else if (err instanceof RegistrationTimeoutError) {
    console.error(
      'Timed out — request is still active server-side; poll later via sdk.waitForApproval(requestId)'
    );
  } else {
    throw err;
  }
}

Pattern B — serverless / scheduled agent (non-blocking, exit and resume)

Best for Lambda / Cloud Functions / cron-driven self-registration where you can't hold the runtime open for minutes:

const sdk = new AstraSync({ apiKey: process.env.ASTRASYNC_API_KEY });

const result = await sdk.register({
  name: 'invoice-bot',
  apiEndpoint: 'https://invoice-bot.example.com',
  pdlss: { purpose: { categories: ['accounting'], allowedActions: ['accounting.read'] } },
});

if (result.status === 'pending_approval') {
  // Store the requestId in your durable storage (DynamoDB, KV, etc.)
  await store.set('astrasync.pendingRequestId', result.requestId);
  console.log(`Awaiting owner approval at ${result.pollUrl}; will resume on next scheduled run.`);
  return; // function exits — owner has time to approve
}

console.log(`Agent registered as ${result.agent.astrasyncIdLevel1}`);

On the next scheduled run, resume by polling:

const requestId = await store.get('astrasync.pendingRequestId');
const status = await sdk.pollRegistration(requestId);
if (status.state === 'approved') {
  await store.set('astrasync.agentId', status.agent.kyaAgentId);
  await store.delete('astrasync.pendingRequestId');
} else if (status.state === 'denied' || status.state === 'expired') {
  console.error(`Registration terminated: ${status.state}`);
}

CLI equivalent — same surface from a shell, also via npx:

npx astrasync register --name invoice-bot --agent-type autonomous \
  --model-provider anthropic --model-name claude-sonnet-4-5 \
  --framework-name langchain --framework-version 0.3.0 \
  --protocols a2a,mcp \
  --api-endpoint https://invoice-bot.example.com \
  --pdlss '{"purpose":{"categories":["accounting"],"allowedActions":["accounting.read"]}}'

The CLI uses the same response logic — it will print a "Sign in to approve" message and a poll URL when the response is 202 pending, and exit non-zero on deny/expire.

/registration is for one-shot onboarding — register an agent, look up its public profile, check API health. For per-request credential injection (attaching ASTRA-id, sessionId, PDLSS to outgoing HTTP / A2A / MCP calls from an already-registered agent), use /agent (AgentClient). The two roles are deliberately separate concerns: registration is identity, /agent is runtime.

A PDLSSConfig type exists in both /registration and /agent, with different shapes. /registration's PDLSSConfig is the boundary declaration submitted at register time; /agent's PDLSSConfig is the per-request runtime request shape. Disambiguate via the import path — the root export deliberately does not re-export either.

Inner-hop verification (cross-hop dedupe with X-Astra-Verified-Hop)

When an upstream AstraSync hop and an inner gateway-protected endpoint both verify the same agent in the same logical request, two verify-access calls fire — audit gets noisy and you pay a doubled round-trip. The X-Astra-Verified-Hop header marks the inner hop as already-verified by the upstream so the inner middleware skips the redundant call.

Two upstreams emit the marker automatically (SDK 4.3.0+):

  • the MCP middleware, on its outbound response (tool handlers calling inner REST hops forward it on outgoing fetches — worked example below);
  • the edge gateway (adapter-lambda / adapter-edge), toward the origin whenever its verify-access call produced identity (authenticate / authorize depth). An SDK-middleware origin behind the TAG then skips the double verify by setting trustVerifiedHop: true — at the default classify depth no marker is emitted and the origin verifies as usual.

trustVerifiedHop + verifiedHopMaxAgeMs are supported on the express, nextjs, and mcp middleware options (all default off). The inner middleware validates the marker via parseVerifiedHop + isVerifiedHopValidFor and skips a duplicate verify-access only if the marker is fresh AND matches the agent claimed on the inner hop. The edge gateway strips any inbound X-Astra-Verified-Hop on evaluated passes, so a viewer can never smuggle one through the TAG.

Trust model (read before enabling): the marker is UNSIGNED — a dedupe advisory, not proof. Enable trustVerifiedHop only when every network path to the inner hop crosses a stripping gateway (the TAG) or an equivalent trusted boundary; inside that boundary a forger could reduce verify-access from "every request" to "once per freshness window" for an agent id it already presents. This is the same trust model as the unsigned X-AstraSync-* attestation headers.

Worked example — upstream MCP → inner REST hop:

// === outer MCP server ===
import express from 'express';
import { createMcpMiddleware } from '@astrasyncai/verification-gateway/mcp';

const mcp = express();
// Behind a load balancer / CDN, set trust proxy to your real hop count so
// req.ip (the client IP recorded on verify-access) is the caller, not the
// proxy. The middleware never trusts a raw leftmost X-Forwarded-For over it.
mcp.set('trust proxy', 1);
mcp.use(express.json());
mcp.use(
  createMcpMiddleware({
    apiBaseUrl: 'https://astrasync.ai/api',
    apiKey: process.env.ASTRASYNC_API_KEY,
    counterpartyId: 'ASTRAE-mcp...',
    toolGates: { start_checkout: 'standard' },
  })
);

// MCP middleware sets X-Astra-Verified-Hop on the response automatically
// AND populates req.agentVerification. Tool handlers can read both.
mcp.post('/mcp', async (req, res) => {
  const v = req.agentVerification!;
  // Forward the verified-hop marker on outgoing inner-hop calls.
  // Build it from the same fields the middleware uses on the response.
  const { serializeVerifiedHop, MCP_VERIFIED_HOP_HEADER } =
    await import('@astrasyncai/verification-gateway/mcp');
  const hop = serializeVerifiedHop({
    astraId: v.agent!.astraId,
    sessionId: v.sessionId,
    checkedAt: Date.now(),
  });

  const inner = await fetch('http://internal/api/checkout/items', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Astra-Id': v.agent!.astraId, // identity marker
      [MCP_VERIFIED_HOP_HEADER]: hop, // verified-hop dedupe marker
    },
    body: JSON.stringify({ sku: 'sku_42' }),
  });
  res.status(inner.status).json(await inner.json());
});

// === inner REST hop (same fleet) ===
import { createMiddleware } from '@astrasyncai/verification-gateway/express';

const rest = express();
rest.use(
  createMiddleware({
    apiBaseUrl: 'https://astrasync.ai/api',
    apiKey: process.env.ASTRASYNC_API_KEY,
    counterpartyId: 'ASTRAE-rest...',
    trustVerifiedHop: true, // accept the dedupe marker
    verifiedHopMaxAgeMs: 60_000, // 60s default; tighten for high-stakes
  })
);
// Inner middleware reads X-Astra-Verified-Hop, validates via
// parseVerifiedHop + isVerifiedHopValidFor, and skips verify-access
// when the marker is fresh AND its astraId matches the X-Astra-Id on
// this request.

Important: the marker is NOT proof of identity by itself — pair it with X-Astra-Id so the inner middleware can verify the marker matches the claimed agent. The marker only gates the dedupe-skip decision.

Access Decision (5.0.0 — two axes, no bands)

The graded accessLevel band was removed in 5.0.0. A verification is now two explicit booleans:

| Axis | Question it answers | | ------------------ | -------------------------------------------------------- | | identityVerified | WHO — did the caller resolve to a registered agent? | | policyAllowed | WHAT — does this request fit the agent's declared PDLSS? |

Both true → proceed. Either false → failures[] says exactly which dimension failed and what fixes it. Value-gating (autonomous limit / hard limit) rides recommendation + stepUpApproval, not an access band.

Trust Levels

| Level | Score Range | | -------- | ----------- | | BRONZE | 0-39 | | SILVER | 40-59 | | GOLD | 60-79 | | PLATINUM | 80-100 |

UI Components

The package includes React components for displaying verification status:

import {
  VerificationInterstitial,
  TrustLevelBadge,
  GuidanceCard,
} from '@astrasyncai/verification-gateway/ui';

// Verification interstitial overlay (renamed from CommerceShield in 4.1.0;
// the old component/hook/props names still work as deprecated aliases)
<VerificationInterstitial
  visible={!verified}
  result={verificationResult}
  onRegister={() => window.location.href = '/register'}
  allowGuestAccess={true}
/>

// Trust level badge
<TrustLevelBadge level="GOLD" score={75} />

// Guidance card
<GuidanceCard guidance={verificationResult.guidance} />

Credential Extraction

Agents can provide credentials via:

  1. Headers (recommended):

    • X-Astra-Id: Agent ASTRA-ID
    • X-Api-Key: API key
    • Authorization: Bearer <jwt>: JWT token
  2. Query Parameters (fallback):

    • ?astraId=ASTRA-xxx
    • ?apiKey=xxx

Verification Response

interface VerificationResult {
  // 5.0.0: two explicit axes replaced the old verified/accessLevel band.
  identityVerified: boolean; // WHO: the caller resolved to a registered agent
  policyAllowed: boolean; // WHAT: the request fits the agent's PDLSS boundary

  agent?: {
    astraId: string;
    name: string;
    trustScore: number;
    trustLevel: 'BRONZE' | 'SILVER' | 'GOLD' | 'PLATINUM';
    blockchainAnchored: boolean;
  };

  developer?: {
    astradId: string;
    verified: boolean;
  };

  organization?: {
    name: string;
    verified: boolean;
    trustScore: number;
  };

  pdlss?: {
    purposeAllowed: boolean;
    withinDuration: boolean;
    withinLimits: boolean;
    scopeAllowed: boolean;
    selfInstantiationAllowed: boolean;
  };

  guidance?: {
    message: string;
    registrationUrl: string;
    documentationUrl: string;
    steps?: string[];
  };

  denialReasons?: string[];
}

Settlement Authorization

For direct-path merchants settling a priced cart, call authorizeSettlement() after pricing — the middleware only verifies identity/access, not the transaction value:

import { authorizeSettlement } from '@astrasyncai/verification-gateway';

const decision = await authorizeSettlement(config, {
  agentId: req.agentVerification.agent.astraId,
  value: cart.total, // YOUR authoritative priced total, never agent-supplied
  currency: 'USD',
});

if (!decision.authorized) {
  // decision.stepUpApproval?.pollUrl — if in the approval band, the owner can approve
  return res.status(402).json({ error: decision.reason, stepUpApproval: decision.stepUpApproval });
}
// Safe to settle

Step-Up Approval

When a transaction value is between the agent's Autonomous Limit and Hard Limit, verify-access returns stepUpApproval on the result:

interface StepUpApprovalInfo {
  approvalId: string; // Capability token (UUID)
  pollUrl: string; // GET /api/step-up-approvals/poll/:approvalId
  expiresAt: string; // ISO-8601 (decision window: 30 min, aligned to the intent mandate)
}

Poll the pollUrl (unauthenticated, rate-limited 60 req/min) to check if the owner approved. The getApprovalPollingInfo(result) helper extracts it from a VerificationResult.

Attempt chain (X-Astra-Attempt-Id, 5.8.0)

Every interaction chain carries an attempt id of the form att_<32 hex>. The AstraSync bridge stamps it on each call it delivers to your endpoint as the X-Astra-Attempt-Id request header, and the express / Next.js middleware automatically read the header and thread the id into verify-access (invalid values are dropped). Two effects:

  • One intent = one approval. A step-up the owner already approved for this chain passes every later verify-access hop that carries the same attempt id (the bridge pre-verify, your middleware's re-verify, and any re-drive after a delivery failure) — it is never asked for twice.
  • Idempotency key. Key your own idempotency/dedupe on the header value: a re-drive of the same chain arrives with the SAME id, so replays of an already-executed action can be detected venue-side.

No configuration is needed; direct (non-bridge) callers simply don't send the header and behaviour is unchanged.

Bridge-fronted endpoints: hold budgets

The bridge delivers your endpoint's response within an HTTP budget of 30 s by default (60 s maximum, configurable per route via responseBudgetMs in your registration's routes[]). stepUpMaxWaitMs: 0 (hold in-request until the approval decides) is designed for direct integrations and will outlive that budget — behind the bridge, either:

  • leave stepUpMaxWaitMs unset and return the 403 with stepUpApproval in-band (the bridge renders the approval card and re-drives after approval), or
  • bound the hold to well under the budget (~20 s) so the deny still reaches the bridge in time.

Either way the attempt chain above guarantees the post-approval re-drive passes without a second approval.

Checkout & settlement (astra-pay)

The first-party commerce rail: an agent shops under its ASTRA-id, holds NO payment credential, and the platform settles from the owner's saved instrument. Two phases discriminate quote from money movement:

  • commercePhase: 'quote' (or unphased) — verification only, never settles.
  • commercePhase: 'confirm' — the ONLY leg that can settle. Use client.confirmCheckout({ astraId, transactionValue, currency, checkoutSessionId, checkoutItems, counterpartyUrl }); checkoutSessionId is the per-cart idempotency key (one order row per session, normative).

Read result.settlementOutcome.status — never success alone:

| status | Meaning | Your action | | ------------------- | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | pending_merchant | 5.14.1: order recorded — YOU settle it | Charge the settlementToken (card) or redeem the settlement voucher (stablecoin), then reportSettlement() | | settled | Money moved (reported / webhook) | Fulfil / mark paid | | requires_approval | Held for human step-up — not a failure | Record as pending; a re-drive follows approval | | failed | Charge declined (failureCode) | Mark failed | | requires_action | 3DS/SCA needed on the saved card | Per your policy | | no_instrument | Policy passed, no chargeable card on file | Terminal on autonomous; NON-terminal after approval — the human's approval survives; re-drive the same session once a card exists | | anything else | A status newer than your SDK | Treat as PENDING, never failed (open union, 5.5.0) |

Every merchant settles (5.14.1 — AstraSync never charges): a purchase grant records the order and returns settlementOutcome: { status: 'pending_merchant', orderId } with what you settle:

  • Card, first-party storefront (our Stripe tenant): settlementToken (agent-blind { stripeCustomerId, stripePaymentMethodId, amountMinor, currency, sessionId, orderId, jti, statementSuffix, expiresAt }). Charge on your own integration with Stripe idempotency key voucher:<token.jti> and metadata: { jti, sessionId }, then client.reportSettlement({ orderId, status, amountMinor, currency, processorRef }).
  • Stablecoin, any merchant: the settlement voucher. Redeem it (the order moves to redeemed), move the funds, then client.reportSettlement({ orderId, status: 'settled', amountMinor, currency, txHash, chainId }).
  • The request's protocol carried the payment (ACP / MPP token, x402 payload, AP2 payment mandate): nothing is minted; settle with that credential and report back.

Amounts must match the order exactly; replays are safe. An order nobody settles expires 10 minutes after its artifact does; a late report still settles it. Never expose the token or voucher to the agent plane or logs.

Fulfilment PII (5.6.0): first-party confirm results carry a fulfilment: { email, emailSource } block — you ALWAYS get a fulfilment email for a first-party order. emailSource: 'agent_provided' means the agent passed a buyerEmail (on the bridge handoff body, the verify-access request, or your confirmCheckout({ buyerEmail }) call); emailSource: 'account' means the platform defaulted to the buyer's account email. Emails arrive canonicalized (trimmed, lower-cased). Precedence: an explicit handoff-body buyerEmail wins over fulfilment.email's account default. Store the email with the pending order and fulfil against it only once the order reaches settled / pending_merchant. All of it is transit-only — AstraSync stores nothing; treat fulfilment as merchant-only material (the bridge strips it from the agent plane). Shipping addresses never transit AstraSync: take them agent → your fulfilment endpoint directly, joined by sessionId.

Settlement Artifacts

On a clean merchant-mediated grant where the owner has a verified payment instrument, verify-access returns a settlement object (wire format v2, versioned via the ver: 2 claim — verify/redeem reject other versions):

interface SettlementArtifact {
  type: string; // e.g. "stablecoin_voucher"
  artifact: string; // JWS compact-serialised (ES256) — the authoritative, signed object
  binding: FiatSettlementBinding | StablecoinSettlementBinding; // display-only mirror
}

// Stablecoin voucher (v2): integer minor units, pinned settlement asset
interface StablecoinSettlementBinding {
  merchantId: string;
  amountMinor: number; // integer minor units — no floats anywhere
  assetDecimals: number; // scale for display: amountMinor / 10^assetDecimals
  currency: string; // e.g. "USDC"
  chainId: number; // the chain this voucher settles on
  tokenContract: string; // the token contract it settles in
  asset: string; // CAIP-19 asset id
  sessionId: string;
  singleUse: true;
  expiresAt: string;
}

// Fiat/processor artifacts (Stripe, Skyfire, PayPal): the processor is the
// unit authority, so the binding carries the major-unit pricing amount
interface FiatSettlementBinding {
  merchantId: string;
  amount: number;
  currency: string;
  sessionId: string;
  singleUse: true;
  expiresAt: string;
}

binding is display-only convenience. It mirrors the signed JWT claims but is not itself signed — render it, never settle from it. Decide from the server-verified payload returned by redeem.

Voucher lifecycle (server-to-server)

| Endpoint | Purpose | | ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | POST /api/wallets/voucher/verify | Verify signature/issuer and return the decoded payload — proves authenticity only, no spend | | POST /api/wallets/voucher/redeem | Atomic single-use spend: first caller gets 200 redeemed (plus order: { status: 'redeemed' } when the voucher funds a recorded order — 5.14.1, no charge happens here), replays get 409 already_redeemed (with first-redeemer forensics), revoked vouchers get 409 revoked | | GET /api/wallets/voucher/.well-known/jwks.json | Public JWKS for offline signature verification |

Redeem accepts optional expectedAmountMinor / expectedCurrency — mismatches are rejected (400 amount_mismatch / currency_mismatch) before the voucher is spent, so a mismatch never burns the voucher. Vouchers carry aud = your merchant id; declare merchantId on verify/redeem to have the audience check enforced (standard JWT libraries give you the same check free on offline verification).

Single-use is enforced server-side by an atomic claim on the voucher id — N of your nodes presenting the same voucher get exactly one 200 and N−1 409s, with no coordination between them.

What a successful redeem proves — and what it doesn't

A 200 from redeem proves, at that moment:

  • the voucher is authentic — signed by AstraSync, unexpired, format-valid;
  • the agent was authorised to commit this spend when the voucher was minted — it passed verification and the amount was within the owner's configured limits;
  • the voucher is bound to you (audience = your merchant id) and to the stated amount, currency, and settlement asset;
  • the spend is exactly-once — this voucher was never redeemed before, was not revoked, and can never be redeemed again.

It does not verify that funds exist in, or are reserved from, the owner's wallet. The voucher is a payment authorization, not a funds guarantee — today, funds assurance at settlement time comes from your own settlement rail, exactly as it does for any other payer.

Coming later: a settlement upgrade that makes redemption and funds transfer a single atomic on-chain event. A voucher will only redeem if the funds actually move — if the wallet can't cover it, the redemption fails and the voucher is not consumed — making "redeemed" and "funded" the same event. The wire format already carries what that needs (pinned chain, token contract, integer minor units), so no integration change is expected beyond opting in.

Configuration

interface GatewayConfig {
  // Required. Always include the /api path prefix — for prod use
  // 'https://astrasync.ai/api', for staging 'https://staging.astrasync.ai/api'.
  apiBaseUrl: string;

  // Optional
  apiKey?: string; // For authenticated requests
  cacheTtl?: number; // Cache duration in seconds (default: 300)
  debug?: boolean; // Enable debug logging

  // Counterparty attribution (v2.2.3+)
  counterpartyUrl?: string; // Sent with verify-access for analytics
  counterpartyType?: 'agent' | 'api' | 'mcp_server' | 'website' | 'other' | 'unknown';
  counterpartyId?: string; // Your ASTRAE-id (issued at endpoint registration);
  // forwarded on every verify-access call so the server attributes traffic
  // directly to this endpoint rather than resolving by URL.

  // Step-up hold-and-poll (v3.6.0+, OPT-IN). When stepUpMaxWaitMs is set, the
  // express/mcp/nextjs middleware HOLDS a step_up_required request, polls the
  // approval, and on 'approved' re-verifies once (cache-bypassed) so the
  // backend redeems the single-use approval — the owner approves in their
  // dashboard while the original request waits (MFA model). Unset = current
  // fail-closed behavior (immediate 403 with stepUpApproval polling info).
  //   0  → hold until the approval's expiresAt (+5s grace; 5-min fallback)
  //   >0 → hold at most this many milliseconds
  // CAUTION: Next.js edge runtimes enforce ~25s wall-clock budgets — use a
  // small value there (≤20000) or gate in a Node route handler instead.
  stepUpMaxWaitMs?: number;
  // Poll interval for the hold (default 3000ms, 250ms floor). The poll route
  // is rate-limited 60/min/IP: at the default interval ~3 concurrent holds
  // per merchant IP fit; excess polls degrade the hold to 'timeout'.
  stepUpPollIntervalMs?: number;

  // Init self-test (v2.2.3+) — fires a HEAD probe to verify-access on first
  // call and warns if apiBaseUrl is pointing at HTML (catches the marketing-
  // 404 case). Set true for tests where the extra request is undesirable.
  disableInitChecks?: boolean;

  // @deprecated — removed as functional config in v2.3.0; the accessLevel
  // band itself was removed in 5.0.0 (identityVerified + policyAllowed are
  // the decision axes). Setting these has no effect (a one-shot
  // console.warn fires). To gate access to your endpoint, configure
  // trust_score_requirement server-side via the /api/endpoints registration.
  minTrustScore?: number;
  minTrustScoreForFull?: number;
}

Verification interstitial

When an unverified agent visits a protected web page, the Next.js middleware serves the verification interstitial — an HTML overlay (user-visible title: "AstraSync Agent Verification") that displays:

  • Registration guidance
  • Steps to get verified
  • Link to documentation
  • Optional guest access

This creates a smooth experience for agents while maintaining security. Control it with showInterstitial (default true) on the Next.js middleware options; the React building blocks ship on the ./ui subpath (see UI Components).

Renamed in 4.1.0 — this feature was previously called "Commerce Shield". The old names still work as deprecated aliases and will be removed in the next major: showCommerceShield (option; showInterstitial wins when both are set), CommerceShield / useCommerceShield / CommerceShieldProps (from ./ui and the root barrel).

Trusted Agent Gateway (edge adapters)

The Trusted Agent Gateway (TAG) is AstraSync's verification gateway at the CDN edge: it classifies and (optionally) verifies every inbound agent request in front of your existing site — in observe mode (verify, classify, and record everything; block nothing) or enforce mode — with zero site changes. Two packages deploy it:

Both consume the same dashboard-managed policy (./edge-config), the same platform fingerprint registry (./platform-signatures), and the same header sanitiser (./metadata-capture) as this SDK — one classification everywhere.

What each integration captures (canonical capture matrix)

Header-level capture (ObservedMetadata.headers etc.) is identical everywhere: automatic on every verify for the edge adapters and the express / nextjs / mcp SDK adapters (no config, no opt-out). What differs is the connection layer — and the deciding variable is who operates the CDN in front of your origin:

| Integration | Connection layer | TLS fingerprint (JA3/JA4) | | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Edge adapter (adapter-lambda, adapter-edge) | Full — native platform signals (IP, ASN, geo, TLS, HTTP version, device class, header structure) | CloudFront: JA3 + JA4 (2.2.0+, via the template's origin request policy). Cloudflare Workers: Bot Management zones only. Vercel: JA4 with BotID. Fastly: none | | SDK behind a CDN you control (your own Cloudflare / Fastly / Vercel / CloudFront distribution) | Derived — geo/ASN/TLS promoted from the CDN's injected headers (deriveConnectionFromHeaders) | Cloudflare: JA3 + JA4 (cf-ja3-hash / cf-ja4, plan-dependent). CloudFront: JA3 + JA4 — add the CloudFront-Viewer-JA3/JA4-Fingerprint headers to your origin request policy | | SDK behind a platform-managed CDN (Railway, Render, Fly, Heroku, …) | IP only — platform CDNs forward x-forwarded-for but NOT their provider-specific geo/TLS headers to customer origins | None — no fingerprint header reaches your origin | | SDK, no CDN | Absent (IP from x-forwarded-for when a proxy sets it) | None |

"Behind Fastly" ≠ "receives Fastly's headers": platform-managed CDNs are operated by the platform, and they do not forward Fastly-Client-IP-style headers to your origin. If connection-layer signals or fingerprints matter to you on a managed platform, deploy the edge adapter in front (bucket 1) — that is the whole point of the TAG.

Note: runCommercePipeline (on the ./transport subpath) is the server-side orchestrator of commerce-protocol verification — it runs inside the AstraSync verify-access service. The edge adapters forward raw commerce artifacts to verify-access and never bundle or call the pipeline themselves.

Which EdgeConfig fields each integration consumes

The dashboard-managed EdgeConfig drives the edge posture. The observe/enforce × depth posture is edge-only by design: SDK adapters enforce via your dashboard route policy (fetched automatically on startup), not via EdgeConfig — the SDK consumes only the sampling block, which governs its anonymous traffic beacon.

| EdgeConfig field | Edge adapters (adapter-lambda, adapter-edge) | SDK adapters (express / nextjs / mcp) | | ------------------------------ | ------------------------------------------------ | ------------------------------------------------- | | mode (observe / enforce) | Yes — the deployment posture | — (enforcement comes from dashboard route policy) | | depth | Yes — classify / authenticate / authorize | — | | pathRules | Yes — per-path mode/depth/skip overrides | — | | sampling.anonymousBeaconRate | Yes — anonymous-bot beacon sampling | Yes — governs the SDK anonymous beacon | | failurePosture | Yes — always fail-open in v1 | — |

Metadata capture

Three subpath exports back the capture machinery (all edge-safe — no Node built-ins — and also re-exported flat from the package root):

./metadata-capture

The ONE sanitiser deciding what is safe to store from an inbound agent's request. Used identically by the edge adapters, the SDK adapters, and the backend.

  • sanitizeHeaders(raw) → { headers, apiKeyFormat?, platformHeaders? } — drops genuine secrets (cookies, signatures, AstraSync credential headers), reduces credential headers to a safe format prefix (e.g. sk-ant-api03 — a platform signal, never the key), keeps everything else verbatim, and enforces size caps (MAX_HEADERS, MAX_HEADER_VALUE_BYTES, MAX_TOTAL_BYTES).
  • extractApiKeyFormat(value) — the safe key-format prefix of a credential header value, or undefined for unrecognised/opaque tokens.
  • extractPlatformHeaders(sanitized) — the platform-signal subset (openai-organization, anthropic-version, x-goog-user-project, …) of an already-sanitised map.
  • deriveConnectionFromHeaders(headers) — promotes CDN-injected headers (Cloudflare / Fastly / Vercel / CloudFront geo, client-IP, TLS-fingerprint, and header-structure headers — incl. cf-ja3-hash / cf-ja4 and cloudfront-viewer-ja3-fingerprint / -ja4-fingerprint from 4.3.0) into the connection block, so SDK-only deployments behind a CDN you control still get connection-layer attribution (see the capture matrix for the platform-managed-CDN caveat). Values are truncated to MAX_CONNECTION_VALUE_CHARS (256 — the platform bound). Native edge signals remain authoritative; this is the SDK-level fallback.
  • buildSdkObservedMetadata(rawHeaders) — the one builder the express / nextjs / mcp adapters share: sanitised headers + apiKeyFormat + platformHeaders + derived connection + schemaVersion.
  • CAPTURE_SCHEMA_VERSION — the capture-semantics version stamped on every ObservedMetadata (currently 1). Capture semantics are frozen within a major version: schemaVersion bumps only on a semantic change to what is kept/dropped/derived, never for additive signals. The verify body's sdkVersion disambiguates builds.
  • Types: ObservedMetadata ({ headers, apiKeyFormat?, platformHeaders?, connection?, schemaVersion? } — connection is populated natively by edge adapters and via CDN-header derivation by SDK adapters), SanitizeHeadersResult.

./platform-signatures

The shared registry of known platform-agent fingerprints (Claude / ChatGPT / Gemini / Cursor / Goose / Perplexity) — the single source of truth for backend and edge classification.

  • PLATFORM_AGENT_SIGNATURES — the seed signature set (UA patterns, agent-card patterns, API-key-format patterns, header patterns).
  • detectPlatformFingerprint(input) → { vendor, fingerprintKey, displayName } | undefined — first-match-wins detection over the seed set.
  • matchPlatformSignature(signatures, input) — the same matcher over a caller-supplied signature set (for merged/dynamic registries).

./edge-config

The per-endpoint policy a Trusted Agent Gateway deployment fetches from the AstraSync dashboard.

  • getEdgeConfig(config, counterpartyId) — cached fetch (TTL + stale-while-revalidate). Fail posture is asymmetric by design: an unreachable backend can never turn enforcement ON, and a config stale for >24h automatically degrades any enforce back to observe.
  • fetchEdgeConfig(...) — one uncached fetch (ETag/304 aware); matchEdgePathRule(rules, path) — first-match-wins *-glob path rules; degradeToObserve(config); isEdgeConfig(value); DEFAULT_EDGE_CONFIG (observe-only, classify depth).
  • Types: EdgeConfig ({ version, mode, depth, pathRules, sampling, failurePosture }), EdgeMode ('observe' | 'enforce'), EdgeVerificationDepth ('classify' | 'authenticate' | 'authorize'), EdgePathRule.

Two baseUrl conventions — same package, different clients

This package ships two clients. They use slightly different URL conventions; this is intentional but worth pinning down:

| Client | Field | Expected value | Example | | ---------------------------------------------------------------------------- | -------------------------- | ----------------------- | -------------------------- | | Verification gateway (createMiddleware, createMcpMiddleware, verify()) | GatewayConfig.apiBaseUrl | Origin + /api | https://astrasync.ai/api | | Registration SDK (new AstraSync(...)) | AstraSyncConfig.baseUrl | Bare origin (no /api) | https://astrasync.ai |

Why the split: the gateway derives multiple endpoints from apiBaseUrl (verify-access, fetchRoutes, recordDecision, etc.), all under /api/*. The registration SDK prepends /api/agents/... per call and would double the path if baseUrl already ended with /api.

As of v2.4.2, the registration SDK tolerates a trailing /api on baseUrl — it strips the suffix and emits a one-time console warning so you can fix the source. Pass silent: true in AstraSyncConfig to suppress the warning (e.g. in tests).

Staging vs production

Both environments share the same URL shape (origin + /api for the verify gateway; bare origin for the registration SDK). Promotion is a single hostname swap:

| Environment | Verify gateway (apiBaseUrl) | Registration SDK (baseUrl) | | ----------- | ---------------------------------- | ------------------------------ | | Staging | https://staging.astrasync.ai/api | https://staging.astrasync.ai | | Production | https://astrasync.ai/api | https://astrasync.ai |

Path shape, request bodies, response schemas, error envelopes — all identical. Swap staging.astrasync.ai ↔ astrasync.ai, nothing else.

Header semantics — X-Astra-Gateway-Mode

The SDK sets X-Astra-Gateway-Mode on every response that's been through the gateway middleware. The value describes the GATE state — not whether the request succeeded end-to-end.

| Value | Meaning | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | enforced | The gateway evaluated policy on this request. Set on denial responses (defaultOnDenied, defaultMcpDenied). | | unenforced | The gateway skipped policy evaluation for this request. The X-Astra-Gateway-Reason header explains why (mcp-tier-none, non-jsonrpc-body, no-policy, no-match, route-none). The downstream handler may still return any status. |

Pre-v2.4.2 used the value pass-through — renamed in v2.4.2 to disambiguate "gate skipped" from "request allowed".

Local Guard for Claude Code

Native governance for Claude Code via its PreToolUse hook (subpath ./claude-code, shipped in v3.7.0). Every tool call — file reads/writes, shell commands, web fetches, and optionally MCP tools — is evaluated against a local PDLSS policy before it runs. No proxy, no cloud dependency.

npm install -g @astrasyncai/verification-gateway

# User-level install (~/.claude + ~/.astrasync); add --project for repo-scoped
astrasync-guard install-claude-code

The installer registers the astrasync-claude-hook binary in Claude Code's settings.json and creates .astrasync/policy.yaml (the PDLSS policy — re-read on every tool call) plus .astrasync/guard.json (guard settings). Flags: --project (install into the current repo; a project policy overrides the user-level one), --wizard (interactive policy builder), --active (start enforcing immediately), --policy <path> (use an existing policy file).

  • Posture — installs default to "posture": "passive": decisions are evaluated and written to JSONL traces under .astrasync/traces/, nothing is blocked. Flip to "active" in guard.json to enforce: DENY blocks the tool call with a reason (permissionDecision: "deny"), MANUAL_REVIEW routes to Claude Code's own permission prompt ("ask"), and ALLOW emits nothing — the guard only tightens, it can never bypass Claude Code's native permission system.
  • Safe by default — a machine with no policy file is a silent no-op; a corrupt guard.json falls back to enforcing defaults rather than silently loosening the guard. ASTRASYNC_GUARD_DISABLE=1 turns the hook off; ASTRASYNC_GUARD_FAIL_OPEN=1 allows on internal hook errors (default is fail-closed in active posture).
  • MCP governance — set "governMcp": true to govern MCP tools too; mcp__{server}__{tool} maps to purpose {server}.{tool} in the policy.
  • Uninstall — astrasync-guard uninstall-claude-code removes the hook; policy and traces are left in place.

Changelog

v4.1.0 — Trusted Agent Gateway naming + MCP metadata capture

  • The Next.js unverified-agent overlay is now the verification interstitial: showInterstitial option, VerificationInterstitial / useVerificationInterstitial / VerificationInterstitialProps. The old showCommerceShield / CommerceShield / useCommerceShield / CommerceShieldProps names keep working as deprecated aliases until the next major (showInterstitial wins when both options are set).
  • The MCP middleware now captures header-level ObservedMetadata on every verify-access call (parity with the express/nextjs adapters).
  • The edge product is the Trusted Agent Gateway (TAG) — see the new README sections above.

v3.8.0 — Settlement voucher wire format v2

  • BREAKING type change: SettlementArtifact.binding is now a union — StablecoinSettlementBinding (v2: amountMinor integer minor units + assetDecimals, pinned chainId/tokenContract/CAIP-19 asset) or FiatSettlementBinding (processor artifacts keep major-unit amount). The float amount field is gone from stablecoin bindings. Matches the server's ver: 2 wire format, which redeems/verifies reject other versions of.
  • Settlement Artifacts docs rewritten: voucher lifecycle endpoints (verify / redeem / JWKS), expected-value gates on redeem, revocation semantics, and an explicit guarantee boundary — what a successful redeem proves and what it doesn't (redeem proves the authorization is live and single-use; it does not check or move funds).

v5.16.0 — ChallengeHandler auto-include removed

  • ChallengeHandler answers only for counterparties registered as pending — the 3.7.1 auto-include let anyone presenting an agent's ASTRA-id pass its runtime challenge. Pass challengeHandler to AgentClient and fetch() registers each call while it is in flight; otherwise call registerPending() / removePending() around each interaction.

v3.7.1 — ChallengeHandler auto-include (removed in 5.16.0)

  • ChallengeHandler auto-includes the incoming counterpartyId/counterpartyUrl in the challenge response when no counterparties were pre-registered via registerPending() — the correct default for agents that never call it. Pre-registered whitelists behave exactly as before.
  • Runtime-challenge docs: auto-include default + double-appended-path warning.

v3.7.0 — AstraSync Local Guard for Claude Code

  • ./claude-code subpath export — Layer 4 Local Guard adapter for Claude Code's PreToolUse hook (see the Local Guard section above). Strictly tightening: DENY → deny, MANUAL_REVIEW → ask, ALLOW → no output.
  • astrasync-claude-hook binary + astrasync-guard install-claude-code / uninstall-claude-code installer commands.

v3.6.0 — Step-up hold-and-poll

  • Opt-in stepUpMaxWaitMs / stepUpPollIntervalMs on GatewayConfig: middleware holds a step_up_required request, polls the approval, and on approved re-verifies cache-bypassed so the backend redeems the single-use approval. Non-approved outcomes deny with stepUpOutcome (denied | expired | consumed | timeout) and an X-Astra-Step-Up header. OFF by default.
  • awaitStepUpApproval() / pollStepUpStatus() exported for direct use.
  • reportUnregisteredAttempt sends the configured counterpartyId (anon-lead attribution is id-first) and omits a null counterpartyUrl.

v3.5.0 — Partner feedback round

  • extractTransactionValue option on Express middleware — opt-in upfront value extraction so PDLSS limit evaluation runs at middleware time (authorizeSettlement() remains the required fail-closed settlement gate).
  • Settlement architecture: merchant-mediated grants are voucher-primary — only crypto wallets mint settlement artifacts; fiat instruments log a diagnostic and do not mint.

v3.4.0 — Type alignment for LMAX settlement + step-up approval

  • StepUpApprovalInfo and SettlementArtifact interfaces added to VerificationResult
  • stepUpApproval surfaced in Express/MCP/Next.js adapter deny response bodies
  • SettlementDecision carries stepUpApproval on step-up denials via authorizeSettlement()
  • getApprovalPollingInfo() simplified — typed field, no more unknown casts
  • Attestation.checkedAt (required) — merchant freshness gate timestamp
  • VERSION constant updated to 3.4.0

v3.3.0 — Direct-path value enforcement

  • authorizeSettlement(config, { agentId, value, currency }) — fail-closed settlement gate for direct-path merchants
  • restrictions surfaces approvalThreshold (enforced per-tx) and maxPerPeriod (not yet enforced)
  • ASCII-safe agent-facing strings

v3.2.1 — Platform-agent go-live readiness

  • Canonical PDLSS limits terminology (autonomousThreshold, approvalThreshold)
  • Step-up/approval fail-closed in Express/MCP/Next.js adapters via approval-gate.ts

v3.2.0 — Commerce observability

  • Access-level band no longer gates (informational only); requiresStepUp carries the signal
  • Trust score redacted from agent-facing responses
  • Cross-merchant cache key fix

v3.1.0 — Canonical PDLSS vocabulary (Bug 14)

  • Two-axis purpose/action chains with dotted action tokens
  • Route send-mapping for tool→semantic-action translation

v2.4.6 — Round-14 partner integration testing

⚠️ BREAKING CHANGE — endpointUrl → counterpartyUrl on POST /api/endpoints AND PUT /api/endpoints/{id}

The body field renamed to align with every other surface (verify-access, dashboard policy, SDK config — all use counterparty*). Applies to BOTH create (POST) and update (PUT) verbs:

  • POST /api/endpoints — request body uses counterpartyUrl (was endpointUrl).
  • PUT /api/endpoints/{id} — same. The strict-mode validator on both verbs returns a clean 400 unrecognized_keys naming counterpartyUrl as the expected key when partners send the old field name.
  • Response shape (GET /api/endpoints/{id}) also renamed for full symmetry — partners receive counterpartyUrl on read AND send counterpartyUrl on write. DB column endpoint_url stays as the internal join key; the service layer maps the public name to the DB column.

Migration: if your code posts endpointUrl to POST /api/endpoints OR PUT /api/endpoints/{id}, rename to counterpartyUrl. If your code reads .endpointUrl from GET responses, rename to .counterpartyUrl. No legacy alias is shipped — clean break per the feedback_hard_break_no_legacy_shim discipline.

Other round-14 items:

  • Item 1 — F8 runtime-challenge gate moved from SEND to RECOMMENDATION (agents.routes.ts). Round-12 placed the gate at the SEND (if (data.enableRuntimeChallenge && counterparty.requiresRuntimeChallenge)) which suppressed the challenge entirely on F8=optional, losing the trust-scoring data the result feeds. Round-14 moves the gate to the recommendation outcome:

    • challenge ALWAYS fires when enableRuntimeChallenge: true (captures trust-scoring data for every call; the result lands on the response body's runtimeChallenge block regardless of F8 setting)
    • failure / timeout only escalates recommendation (→ deny / step_up_required) when the endpoint declared requiresRuntimeChallenge: true

    Extracted into a pure helper deriveRuntimeChallengeRecommendation so the 6-quadrant truth table is unit-testable without the full verify-access stack. See /docs/agent-access/runtime-challenge for the partner-facing contract page (new in this round).

  • New /docs/agent-access/runtime-challenge page documenting the agent-side challenge contract — request shape, response shape, HTTP status semantics, signing posture (unsigned v1; DPoP RFC 9449 on the roadmap as the cryptographic upgrade path), timing, replay semantics. Headlines the ChallengeHandler drop-in from @astrasyncai/verification-gateway/agent; curl is the wire-spec fallback.

  • New /docs/mcp-integration "Purpose + action precedence" section documenting the 4-tier chain (header → _meta → arguments → default) that applies to both purpose and action. Closes the round-13 SDK-README- only documentation gap.

  • /docs/agent-access revisions: section 4b expanded to a dedicated "Headers an integrating agent must send" section covering X-Astra-Id, X-Astra-Purpose, X-Astra-Action (the last is new partner-facing documentation for the round-13 R13-2 header). Section 5 inverted tokenGuidance text corrected. New section 5b "What verify-access tells you" annotates the full grant payload with cross-links to the SDK's exported TypeScript types (VerificationResult, TokenGuidance, EnhancedVerificationResult).

  • /docs/merchants additions: worked endpoint POST + PUT examples using the new counterpartyUrl name; per-protocol terminal-status table with explicit external-contract preamble (the literals come from each protocol's published spec, not from AstraSync's choice) + auth/capture two-step callout for agent-pay and TAP; A2A JSON-RPC worked example.

  • F14 Health Check tooltip reframed as descriptive-only metadata ("active probe on roadmap") — the storage + serializer + dashboard UI exist but the active probe pipeline isn't implemented yet (deferred to a focused future round). Sets partner expectations correctly while the pipeline lands.

v2.4.5 — Round-13 partner integration testing

⚠️ BREAKING CHANGE — pdlss_immutable → agent_immutable

The 409 response from PUT /api/agents/:id (post-mint mutation attempt) now returns error: 'agent_immutable' instead of error: 'pdlss_immutable'. The scope also widened: round-12 rejected only the subset { pdlss, model, framework, agentType, apiEndpoint }; round-13 rejects ANY field except agentStatus (the only allowed lifecycle transition post-mint).

Migration: if your code catches pdlss_immutable, update to agent_immutable. No legacy alias is shipped — clean break prevents permanent shim cruft. The new shape:

{
  "success": false,
  "error": "agent_immutable",
  "message": "Agents are immutable post-approval. ... Attempted immutable fields: <list>. ...",
  "immutableFields": ["name", "description", ...]
}

Why: agents become immutable at approval + mint per the trust-chain invariant. Pre-mint owner edits flow through POST /agents/request-registration/:requestId/approve (dashboard-only, accepts a full edit body). Post-mint, the only allowed transition is agentStatus (e.g. dashboard retire button). For configuration changes, use the upgrade flow (Agent dashboard -> Upgrade, or POST /agents/:id/upgrade from a dashboard session) — it mints a successor version with a new ASTRA-id and retires the current one.

Other round-13 items:

  • R13-1 + R13-2 — MCP middleware: symmetric precedence chain for purpose and action. Canonical resolution (documented ONCE, applies to both):

    1. X-Astra-<concept> HTTP header
    2. params._meta.astrasync.<concept> body field
    3. params.arguments.<concept> body field
    4. Transport-layer default:
      • purpose → 'mcp_invoke'
      • action → '<method>:<toolName>' (or '<method>' alone)

    Round-12 F19 shipped purpose with header → _meta → default; this round closes the params.arguments.purpose fallback gap AND ships action with the same full chain in one round (not staggered) to pre-empt the parallel "I set action in arguments and it didn't take" support tickets. Resource string stays mcp:tool/<name> regardless.

    mcpToPdlss(parsed, headerPurpose, headerAction) signature. McpPdlssMapping.purposeSource now 'header' | 'meta' | 'tool_argument' | 'default_mcp_invoke' (round-12 narrower 'header' | 'tool_argument' | 'default_mcp_invoke' widened to split meta from tool_argument). New companion actionSource: 'header' | 'meta' | 'tool_argument' | 'transport_layer'.

  • R13-5 — MCP evaluateAlwaysIfCredentialed parity with F9. Flag moved from ExpressMiddlewareOptions to GatewayConfig so both adapters inherit. MCP middleware now mirrors the express F9 pattern: route-none + flag-on + credentialed → run verify-access for the audit trail, populate req.agentVerification, then proceed without gates (X-Astra-Gateway-Mode: enforced, Reason: evaluated-not-enforced). Closes the round-12 deferral.

  • F14 closure — sdkVersion body field on verify-access. Replaces round-12's User-Agent regex extraction which silently failed because Node's undici fetch doesn't ship a usable User-Agent header. The SDK now sets body.sdkVersion = SDK_VERSION (sourced from packages/verification-gateway/src/version.ts, bumped alongside package.json on every release). Backend reads from the body field and runs the same forward-only auto-pop into kya_counterparty.sdk_version. Works in Node, browser, and behind CDNs uniformly.

  • R13-4 — Branded TypeScript types (compile-time protection against the recurring UUID / public-id string-confusion bug class — round-7 #46, round-11 F1, round-12 F15). New CounterpartyUuid, AgentUuid, OwnerUuid, CounterpartyAstraeId, AgentAstraId, OwnerAstradId branded types in the backend at apps/backend/src/types/branded-ids.ts. Zero runtime cost; affects only compile-time assignment comp