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}