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

v4.0.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(), Commerce Shield 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(). |

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 Commerce Shield
  • 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-...',
  showCommerceShield: 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',
});

if (result.verified && result.accessLevel !== 'none') {
  // 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: { stepUpThreshold: 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 (MCP → REST dedupe with X-Astra-Verified-Hop)

When an MCP tool calls an inner REST endpoint that ALSO runs the verification gateway, two verify-access calls fire for the same agent in the same logical request — 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 a redundant verify-access call.

The MCP middleware emits the marker automatically on its outbound response. Tool handlers calling inner REST hops need to forward it on outgoing fetches; the inner middleware reads it via parseVerifiedHop + isVerifiedHopValidFor and skips a duplicate verify-access if the marker is fresh AND matches the agent claimed on the inner hop.

Worked example — upstream MCP → inner REST hop:

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

const mcp = express();
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 Levels

| Level | Description | | ----------- | --------------------------------- | | none | No credentials provided | | guidance | Commerce Shield overlay shown | | read-only | Can browse, no mutations | | standard | Normal access per PDLSS | | full | Full access for high-trust agents | | internal | Organization member access |

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 { CommerceShield, TrustLevelBadge, GuidanceCard } from '@astrasyncai/verification-gateway/ui';

// Commerce Shield overlay
<CommerceShield
  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 {
  verified: boolean;
  accessLevel: 'none' | 'guidance' | 'read-only' | 'standard' | 'full' | 'internal';

  agent?: {
    astraId: string;
    name: string;
    trustScore: number;
    trustLevel: 'BRONZE' | 'SILVER' | 'GOLD' | 'PLATINUM';
    blockchainVerified: 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, 5-minute TTL
}

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

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, 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
  defaultAccessLevel?: string; // Default: 'guidance'
  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. Server is the
  // single source of truth for access-level decisions; the SDK reads
  // access.accessLevel from the response verbatim. 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;
}

Commerce Shield

When an unverified agent visits a protected page, the Commerce Shield overlay displays:

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

This creates a smooth experience for agents while maintaining security.

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.aiastrasync.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.
  • Uninstallastrasync-guard uninstall-claude-code removes the hook; policy and traces are left in place.

Changelog

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).

v3.7.1 — ChallengeHandler auto-include

  • 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 — endpointUrlcounterpartyUrl 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_immutableagent_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 (coming soon) or retire-and-re-register.

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 compatibility. Scope intentionally narrow — only the conversion-point function signatures.

v2.4.4 — Round-12 partner integration testing

  • F9ExpressMiddlewareOptions.evaluateAlwaysIfCredentialed: when true + credentials present + route-none, the middleware calls verify-access for the audit trail + req.agentVerification population, then proceeds without enforcement. Default false preserves existing behaviour. Use for tiered-response rendering on routes that grant public access but want caller identity visible to the handler.
  • F12defaultOnDenied / defaultMcpDenied synthesise access_level.insufficient failure entry on accessLevel-below-route + trust-score-below-route denials. Guidance text references the step-up verification flow ("coming soon — ships this month") only. Prior denials carried INSUFFICIENT_ACCESS with empty failures[] / denialReasons[] arrays.
  • F16register() now passes the API response's warnings[] through verbatim on both 201 (sync) and 202 (pending-approval) paths. Pre-fix the SDK silently dropped backend advisories like no_callback_endpoint. RegisterResult + PendingRegistrationResponse + RegistrationResponse types extended.
  • F19 — MCP middleware purpose pass-through. The hardcoded purpose: 'mcp_invoke' is now a fallback; resolution precedence is X-Astra-Purpose header → params._meta.astrasync.purpose'mcp_invoke' default. Adds invocationProtocol: 'mcp' to the verify-access body so transport is marked separately from intent. Debug-level purpose_source log line per call for adoption tracking + support triage.
  • mcpToPdlss signature extended to accept optional headerPurpose + toolArgumentPurpose args; return type gains purposeSource: 'header' | 'tool_argument' | 'default_mcp_invoke'.

v2.4.3 — Round-11 partner integration testing

  • PollRegistrationResult.astraId — the polling response now surfaces the canonical ASTRA-* id once the registration request is approved. Pre-fix partners only got the owner-private UUID from the register flow and had no programmatic path to the canonical id (had to go to the dashboard). Backend changes that drive this: the GET /api/agents/request-registration/{requestId} handler looks up kyaAgent.astrasyncIdLevel1 on approval and includes it in the response body.
  • No SDK code changes — pure type widening. Existing 2.4.2 callers see the same state / agent / reason shape plus the new optional astraId field. TypeScript surfaces the new field at every sdk.pollRegistration(...) call site.
  • Pairs with backend round-11 F2: the public verify-access surface now refuses UUID-form agentId with a 400 INVALID_AGENT_ID. After F3 partners can discover the canonical astraId post-approval; F2 is what they pass to verify-access from there.

v2.4.2 — Round-10 partner re-test alignment

  • baseUrl normalization (AstraSyncConfig): trailing /api is stripped + warned. Removes a class of NOT_FOUND bugs when partners pass the verify-gateway-style URL to the registration client.
  • Denial responses surface failures[] + correlationId (defaultOnDenied, defaultMcpDenied): partners can now render per-dimension UX and tie a denial back to a server log line. Previously only the first denialReason was surfaced.
  • API-error fallback (createGuidanceResponse): synthesised stubs for transient 5xx / network failures now carry verify_access.api_error in failures[] instead of the misleading "register your agent" template.
  • X-Astra-Gateway-Mode rename: pass-throughunenforced. The new enforced value lands on denial paths. See the header semantics table above.
  • silent config flag (AstraSyncConfig): suppresses one-time SDK warnings (baseUrl strip, deprecation notices).

v2.4.1 — Round-9 docs/SDK alignment

See git log for details.

License

MIT