Skip to content

guides

Intended Documentation

Verify Decision Tokens

Verify Ed25519 Authority Tokens locally with @intended/verify — pin the kid, validate claims, enforce the 300s TTL, and reject on the exact failure reasons the verifier returns.

Verify Authority Tokens#

The authorityDecisionToken returned by POST /intent is an Ed25519 JWT bound to exactly one intent, tenant, and execution target. Before the action runs, the executing system verifies the token locally — it never needs to call back to the Intended runtime. Verification is fail-closed: any failed check rejects the action.

This page uses @intended/verify's verifyToken, which is built on jose. The connector SDK's BaseAdapter performs the equivalent checks (plus single-use nonce consumption) automatically — see the Connector SDK.

Authority Token lifecycle
A token is minted on APPROVED as an Ed25519 JWT, carried to the executor, verified locally — kid pinned, signature checked, claims and expiry validated — then its nonce is consumed for single use. Replay, expiry, and kid mismatch are rejection states.

Minted on APPROVED, verified locally, consumed once. Kid mismatch, bad signature, claim mismatch, and expiry are all rejected.

Token claims#

The validated claim set (from AuthorityDecisionTokenClaimsSchema):

ClaimMeaning
intentIdThe intent this token authorizes (UUID).
tenantIdTenant scope.
adapterIdThe adapter permitted to consume the token.
adapterTargetThe specific target the adapter acts on.
targetSystemThe system the action targets.
proposedActionThe action that was approved.
decisionAlways APPROVED — only approvals mint tokens.
issuedAt / expiresAtISO-8601 validity window.
nonce≥32-character value enforcing single use.

At signing time the runtime also sets the registered JWT claims iss: "intended-authority", aud: <adapterId>, and integer iat / exp. The header is { "alg": "EdDSA", "typ": "JWT", "kid": "<tenant-key-id>" }kid is mandatory.

Local verification flow#

Decode the header and read kid

Read the kid from the JWT header and load the tenant's public key for that kid. Pinning kid is mandatory; an unpinned or unknown kid is rejected with KID_MISMATCH.

Verify the Ed25519 signature

Verify against the public key, restricted to the EdDSA algorithm, with the expected issuer and audience. A failed signature check returns SIGNATURE_VERIFICATION_FAILED.

Validate the claims

The verifier parses the claims against the schema, then asserts tenantId and adapterId match what you expect, the issuedAt/expiresAt window is well-formed, the token is not used before valid or past expiry (within clockSkewSeconds), and the TTL does not exceed maxTokenTtlSeconds.

Consume the nonce (connector SDK only)

verifyToken itself does not touch a datastore — it is pure verification. Single-use enforcement happens when the connector SDK's BaseAdapter consumes the nonce atomically. If you verify outside the connector SDK, you are responsible for replay protection.

Verifying in code#

verifyToken returns { valid, reason, claims, header }. On success reason is null and claims is the validated claim set; on failure valid is false and reason names the failure.

ts
import { verifyToken } from "@intended/verify";

const result = await verifyToken({
  token,                          // the authorityDecisionToken from POST /intent
  publicKeyPem,                   // tenant public key (SPKI PEM) for the token's kid
  expectedKid: kid,               // pin the key id
  expectedTenantId: "tenant_acme_prod",
  expectedAdapterId: "github-actions-adapter",
  expectedIssuer: "intended-authority",
  expectedAudience: "github-actions-adapter", // aud === adapterId
  maxTokenTtlSeconds: 300,        // 300s is the hard cap; a longer TTL is rejected
  clockSkewSeconds: 60,
});

if (!result.valid) {
  // result.reason is one of the documented failure reasons below
  throw new Error(result.reason ?? "TOKEN_SIGNATURE_INVALID");
}
// result.claims is the validated claim set; proceed with the action.

Failure reasons#

These are the exact reason values verifyToken returns. Treat every one as a hard reject.

ReasonCause
KID_MISMATCHToken kid does not match the pinned expectedKid.
SIGNATURE_VERIFICATION_FAILEDSignature, issuer, or audience check failed.
TOKEN_CLAIMS_INVALIDClaims fail schema validation.
TOKEN_TENANT_MISMATCHToken tenantIdexpectedTenantId.
TOKEN_ADAPTER_MISMATCHToken adapterIdexpectedAdapterId.
TOKEN_CLAIMS_INVALID_TIMEissuedAt / expiresAt not parseable.
TOKEN_TIME_WINDOW_INVALIDexpiresAtissuedAt.
TOKEN_USED_BEFORE_VALIDNow + skew is before issuedAt.
TOKEN_EXPIREDNow − skew is past expiresAt.
TOKEN_TTL_EXCEEDS_LIMITTTL exceeds maxTokenTtlSeconds (the 300s cap).

Warning

verifyToken is signature + claim verification only. It does not enforce single use — that requires consuming the nonce. Inside a connector, BaseAdapter does this for you; outside one, track and reject seen nonces yourself.

On-robot verification (Physical AI)#

For physical-AI runtimes, the Authority Token is verified at the robot before actuation lands, so a compromised network path or stale cloud session can never let an actuation through. The edge verifiers ship as language SDKs — Go (intended-go) and Python / ROS2 (intended-ros2) — that fetch the signer's JWKS, pin the kid, verify the Ed25519 signature, and check tenant / audience / issuer / expiry locally.

Note

Roadmap. The sub-50ms Rust edge verifier referenced for the hot path is not yet in the repo. The JWKS route at /.well-known/jwks.json currently serves the physical-AI signer using an ephemeral per-process dev keypair until the production signer GAs; treat published robot-verifier examples as the target shape, and confirm the audience your verifier expects matches the token's aud before relying on it in production.

python
from intended_ros2.token_verifier import AuthorityTokenVerifier, VerifierConfig

verifier = AuthorityTokenVerifier(
    VerifierConfig(
        jwks_url="https://api.intended.so/.well-known/jwks.json",
        expected_tenant_id="tenant-a",
        expected_audience="intended-edge-verifier",  # match the token's aud
        expected_issuer="intended-authority",
    )
)

result = verifier.verify(token, expected_action="navigate_to_pose")
if not result.allowed:
    return  # refuse the actuation; log result.reason
# proceed with motion

Next steps#

Verify Decision Tokens | Intended