001/*
002 * Copyright 2022-2026 Revetware LLC.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package com.soklet;
018
019import org.jspecify.annotations.NonNull;
020import org.jspecify.annotations.Nullable;
021
022import javax.annotation.concurrent.ThreadSafe;
023import java.util.Arrays;
024
025import static java.util.Objects.requireNonNull;
026
027/**
028 * Immutable framework-supplied context for an application request-state
029 * protector.
030 * <p>
031 * Associated-data bytes are the exact versioned canonical binding that Soklet
032 * can independently reconstruct for the original operation and its retry. A
033 * custom protector must authenticate these bytes exactly. Neither they nor the
034 * other values in this context are suitable for logs or metric dimensions.
035 *
036 * @author <a href="https://www.revetkn.com">Mark Allen</a>
037 */
038@ThreadSafe
039public final class McpRequestStateProtectionContext {
040        @NonNull
041        private final String endpointPath;
042        @NonNull
043        private final McpProtocolVersion protocolVersion;
044        @NonNull
045        private final String jsonRpcMethod;
046        private final byte @NonNull [] associatedData;
047
048        /**
049         * Creates a protection context from its components. This factory is useful
050         * for exercising application-provided request-state protectors in isolation;
051         * Soklet constructs the authoritative contexts used for live requests.
052         *
053         * @param endpointPath normalized endpoint path
054         * @param protocolVersion validated MCP protocol version
055         * @param jsonRpcMethod MCP JSON-RPC method
056         * @param associatedData canonical associated data, defensively copied
057         * @return immutable request-state protection context
058         * @throws NullPointerException if any argument is null
059         */
060        @NonNull
061        public static McpRequestStateProtectionContext fromComponents(
062                        @NonNull String endpointPath, @NonNull McpProtocolVersion protocolVersion,
063                        @NonNull String jsonRpcMethod,
064                        byte @NonNull [] associatedData) {
065                return new McpRequestStateProtectionContext(endpointPath, protocolVersion,
066                                jsonRpcMethod, associatedData);
067        }
068
069        McpRequestStateProtectionContext(@NonNull String endpointPath,
070                        @NonNull McpProtocolVersion protocolVersion, @NonNull String jsonRpcMethod,
071                        byte @NonNull [] associatedData) {
072                this.endpointPath = requireNonNull(endpointPath);
073                this.protocolVersion = requireNonNull(protocolVersion);
074                this.jsonRpcMethod = requireNonNull(jsonRpcMethod);
075                this.associatedData = requireNonNull(associatedData).clone();
076        }
077
078        /** @return normalized endpoint path */
079        @NonNull
080        public String getEndpointPath() {
081                return this.endpointPath;
082        }
083
084        /** @return validated MCP protocol version supplied on this request */
085        @NonNull
086        public McpProtocolVersion getProtocolVersion() {
087                return this.protocolVersion;
088        }
089
090        /** @return MCP JSON-RPC method */
091        @NonNull
092        public String getJsonRpcMethod() {
093                return this.jsonRpcMethod;
094        }
095
096        /** @return defensive copy of the canonical associated data */
097        public byte @NonNull [] getAssociatedData() {
098                return this.associatedData.clone();
099        }
100
101        /**
102         * Compares the exact canonical protection inputs, including associated-data
103         * bytes by content.
104         *
105         * @return whether every protection-context property is structurally equal
106         */
107        @Override
108        public boolean equals(@Nullable Object other) {
109                if (this == other)
110                        return true;
111                if (!(other instanceof McpRequestStateProtectionContext context))
112                        return false;
113                return this.endpointPath.equals(context.endpointPath)
114                                && this.protocolVersion.equals(context.protocolVersion)
115                                && this.jsonRpcMethod.equals(context.jsonRpcMethod)
116                                && Arrays.equals(this.associatedData, context.associatedData);
117        }
118
119        /** @return structural protection-context hash code */
120        @Override
121        public int hashCode() {
122                int result = this.endpointPath.hashCode();
123                result = 31 * result + this.protocolVersion.hashCode();
124                result = 31 * result + this.jsonRpcMethod.hashCode();
125                result = 31 * result + Arrays.hashCode(this.associatedData);
126                return result;
127        }
128}