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;
023
024import static java.util.Objects.requireNonNull;
025
026/**
027 * Secret-free deployment-comparison fingerprint for the active MCP
028 * trace-correlation configuration.
029 * <p>
030 * This value is operational metadata only, not an authentication or token
031 * derivation input.
032 *
033 * @author <a href="https://www.revetkn.com">Mark Allen</a>
034 */
035@ThreadSafe
036public final class McpTraceCorrelationFingerprint {
037        @NonNull
038        private final String value;
039
040        @NonNull
041        static McpTraceCorrelationFingerprint fromValue(
042                        @NonNull String value) {
043                return new McpTraceCorrelationFingerprint(value);
044        }
045
046        private McpTraceCorrelationFingerprint(@NonNull String value) {
047                requireNonNull(value);
048                if (!isCanonicalSha256Base64Url(value))
049                        throw new IllegalArgumentException(
050                                        "Trace-correlation fingerprint must be a canonical unpadded Base64URL SHA-256 value.");
051                this.value = value;
052        }
053
054        /** @return unpadded Base64URL fingerprint value */
055        @NonNull
056        public String getValue() {
057                return this.value;
058        }
059
060        /** @return whether this value has the same encoded fingerprint */
061        @Override
062        public boolean equals(@Nullable Object other) {
063                if (this == other)
064                        return true;
065                if (!(other instanceof McpTraceCorrelationFingerprint fingerprint))
066                        return false;
067                return this.value.equals(fingerprint.value);
068        }
069
070        /** @return value-based hash code */
071        @Override
072        public int hashCode() {
073                return this.value.hashCode();
074        }
075
076        /** @return the unpadded Base64URL fingerprint value */
077        @Override
078        @NonNull
079        public String toString() {
080                return this.value;
081        }
082
083        private static boolean isCanonicalSha256Base64Url(@NonNull String value) {
084                if (value.length() != 43)
085                        return false;
086                for (int index = 0; index < value.length(); ++index) {
087                        char character = value.charAt(index);
088                        if (!((character >= 'A' && character <= 'Z')
089                                        || (character >= 'a' && character <= 'z')
090                                        || (character >= '0' && character <= '9')
091                                        || character == '-' || character == '_'))
092                                return false;
093                }
094                char finalCharacter = value.charAt(value.length() - 1);
095                return "AEIMQUYcgkosw048".indexOf(finalCharacter) >= 0;
096        }
097}