001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.rest.server.mcp.v20260728;
018
019import java.security.Principal;
020import java.util.*;
021
022import org.apache.juneau.commons.concurrent.*;
023
024/**
025 * Pluggable SPI for sealing/unsealing the opaque {@code requestState} continuation token (MCP {@code 2026-07-28}
026 * SEP-2322 Multi-Round-Trip Requests).
027 *
028 * <p>
029 * Mirrors the "pluggable SPI + built-in default" shape sub-project E established for the client-side
030 * {@code McpResponseCache} (not on this module's classpath): this interface plus {@link AeadRequestStateCodec}
031 * (the built-in default) is the server-side, seal/unseal counterpart.
032 *
033 * <p>
034 * Implementations must treat any verification failure (tamper, AAD mismatch, or an implementation-specific
035 * integrity check) as an {@link Optional#empty()} return from {@link #unseal(String, String)} &mdash; never a
036 * thrown exception. Expiry is checked by the caller ({@code McpRevision}) against
037 * {@link McpRequestState#expiresAtMs()}, not by the codec itself, since expiry is a dispatcher-level policy (see
038 * {@link McpMrtrConfig}), not a codec integrity concern.
039 *
040 * <p>
041 * <b>Trust model &mdash; {@code requestState} is a bearer token.</b> The sealed token is a replayable credential:
042 * anyone who holds it can resume the paused operation until it expires, and it carries no caller identity. A
043 * codec (and every call site) must treat it accordingly &mdash; transport only over authenticated TLS, and
044 * <b>never log</b> a {@code requestState}. <b>Multi-use within TTL by default:</b> a captured token can be
045 * replayed any number of times within its TTL unless an operator configures a {@link ReplayCache} (see
046 * {@link McpMrtrConfig#setReplayCache(ReplayCache)}) to enforce single-use &mdash; the built-in
047 * {@link InMemoryReplayCache} is per-process only, so cross-node single-use requires a shared implementation. A
048 * handler must still write idempotent resume side effects unless it knows a single-use {@link ReplayCache} is in
049 * effect; the max-rounds cap bounds chain depth, not replay count (see {@link McpMrtrConfig}).
050 *
051 * <p>
052 * <b>Canonical AAD format.</b> The dispatcher binds each token to the request that produced it by passing a
053 * single canonical AAD string: {@code method + '\u0000' + protocolVersion + '\u0000' + target} &mdash; the
054 * JSON-RPC method name, the negotiated protocol version, and the operation target (the tool {@code name} for
055 * {@code tools/call}, the prompt {@code name} for {@code prompts/get}, or the resource {@code uri} for
056 * {@code resources/read}; empty for any other method), all NUL ({@code U+0000})-separated (see
057 * {@code McpRevision#aad}). NUL is chosen because it can never appear in a method name, a protocol-version
058 * literal, or the trailing target field, so the concatenation is unambiguous. The trailing target field binds
059 * the token to the specific operation it paused against, not merely the method, so a token minted while paused
060 * on one tool/prompt/resource cannot be resumed against a different one of the same kind. An implementation
061 * only needs to treat the AAD as opaque bytes; the format is documented here so seal and unseal call sites (and
062 * any custom codec) agree on exactly what is authenticated.
063 *
064 * <p>
065 * <b>Authenticated principal.</b> Both {@link #seal(McpRequestState, String, Principal)} and
066 * {@link #unseal(String, String, Principal)} receive the caller's authenticated {@link Principal} &mdash; the same
067 * identity the resource-server layer establishes (see {@code McpResourceServerSupport#principal}). This is the
068 * <i>seam</i> a hardened codec uses to fold the caller identity into its authenticated data, so a
069 * {@code requestState} minted for principal A cannot be resumed by principal B. The built-in
070 * {@link AeadRequestStateCodec} binds exactly that: it folds a canonical, deterministic {@code iss|sub} identity
071 * (see {@link AeadRequestStateCodec#principalIdentity(Principal)}) into its AEAD authenticated data, so a
072 * mismatched principal fails the GCM tag check and {@link #unseal} returns {@link Optional#empty()}. The
073 * dispatcher guarantees the principal is delivered to the codec at both seal and unseal; a custom codec
074 * remains free to bind a different identity attribute (subject claim, issuer+subject, full claim set) or none at
075 * all. The principal is <b>nullable</b>: when RS auth is disabled or the caller is anonymous it is <jk>null</jk>,
076 * and every codec must seal/unseal cleanly (no NPE) in that case. The two-argument
077 * {@link #seal(McpRequestState, String)} / {@link #unseal(String, String)} convenience overloads simply pass a
078 * <jk>null</jk> (no-principal) identity.
079 */
080public interface RequestStateCodec {
081
082   /**
083    * Seals a payload into an opaque token, exposing the authenticated caller identity to the codec.
084    *
085    * @param state The payload to seal. Must not be <jk>null</jk>.
086    * @param aad Additional authenticated data (the dispatcher passes the canonical
087    *    {@code method + '\u0000' + protocolVersion + '\u0000' + target} form &mdash; see the class Javadoc). Must
088    *    not be <jk>null</jk>.
089    * @param principal The authenticated caller (see the class Javadoc). May be <jk>null</jk> for an anonymous
090    *    caller or when resource-server auth is disabled.
091    * @return The opaque token. Never <jk>null</jk>.
092    */
093   String seal(McpRequestState state, String aad, Principal principal);
094
095   /**
096    * Unseals a token, verifying integrity against the supplied AAD, with the authenticated caller identity
097    * available to the codec.
098    *
099    * <p>
100    * <b>The {@code token} is arbitrary, untrusted client input.</b> It arrives verbatim from the follow-up
101    * request's {@code requestState} field, so an implementation must assume nothing about its length or content:
102    * it must not allocate buffers sized from the raw token, and it must not throw &mdash; any malformed, oversized,
103    * tampered, or AAD-mismatched input must return {@link Optional#empty()} (see the class Javadoc contract).
104    *
105    * @param token The token previously returned by {@link #seal(McpRequestState, String, Principal)}. Must not be
106    *    <jk>null</jk>.
107    * @param aad The AAD to verify against. Must match the value passed to
108    *    {@link #seal(McpRequestState, String, Principal)} exactly, or verification fails.
109    * @param principal The authenticated caller (see the class Javadoc). May be <jk>null</jk> for an anonymous
110    *    caller or when resource-server auth is disabled. A codec that binds the principal must fail
111    *    verification when it differs from the sealing principal.
112    * @return The original payload, or {@link Optional#empty()} if verification fails for any reason (tamper,
113    *    AAD mismatch, malformed token, principal mismatch).
114    */
115   Optional<McpRequestState> unseal(String token, String aad, Principal principal);
116
117   /**
118    * No-principal convenience overload of {@link #seal(McpRequestState, String, Principal)} (anonymous caller).
119    *
120    * @param state The payload to seal. Must not be <jk>null</jk>.
121    * @param aad Additional authenticated data. Must not be <jk>null</jk>.
122    * @return The opaque token. Never <jk>null</jk>.
123    */
124   default String seal(McpRequestState state, String aad) {
125      return seal(state, aad, null);
126   }
127
128   /**
129    * No-principal convenience overload of {@link #unseal(String, String, Principal)} (anonymous caller).
130    *
131    * @param token The token previously returned by {@link #seal(McpRequestState, String)}. Must not be <jk>null</jk>.
132    * @param aad The AAD to verify against.
133    * @return The original payload, or {@link Optional#empty()} if verification fails for any reason.
134    */
135   default Optional<McpRequestState> unseal(String token, String aad) {
136      return unseal(token, aad, null);
137   }
138}